Skip to content

Commit 7df85ea

Browse files
authored
✨ feat: adding preset, dark mode service improvements, prevent SSR flickering (#384)
1 parent b4040e3 commit 7df85ea

File tree

21 files changed

+445
-76
lines changed

21 files changed

+445
-76
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,5 @@ Thumbs.db
4747
.github/instructions/nx.instructions.md
4848

4949
.history/
50+
cline_docs
51+
.clinerules

apps/web/prerender-routes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/docs
33
/docs/components
44
/blocks
5+
/themes
56
/colors
67
/docs/introduction
78
/docs/components

apps/web/project.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
},
4040
"configurations": {
4141
"production": {
42+
"optimization": {
43+
"styles": {
44+
"inlineCritical": false
45+
}
46+
},
4247
"budgets": [
4348
{
4449
"type": "initial",

apps/web/public/r/dark-mode.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"files": [
55
{
66
"name": "dark-mode.ts",
7-
"content": "import { isPlatformBrowser, DOCUMENT } from '@angular/common';\nimport { Injectable, type OnDestroy, PLATFORM_ID, computed, inject, signal } from '@angular/core';\n\nexport enum EDarkModes {\n LIGHT = 'light',\n DARK = 'dark',\n SYSTEM = 'system',\n}\nexport type DarkModeOptions = EDarkModes.LIGHT | EDarkModes.DARK | EDarkModes.SYSTEM;\n\n@Injectable({\n providedIn: 'root',\n})\nexport class ZardDarkMode implements OnDestroy {\n private readonly document = inject(DOCUMENT);\n\n private static readonly STORAGE_KEY = 'theme';\n private handleThemeChange = (event: MediaQueryListEvent) => this.updateThemeMode(event.matches);\n private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n private readonly themeSignal = signal<DarkModeOptions>(EDarkModes.SYSTEM);\n private darkModeQuery?: MediaQueryList;\n\n readonly theme = this.themeSignal.asReadonly();\n\n readonly themeMode = computed(() => {\n if (this.themeSignal() === EDarkModes.SYSTEM) {\n return this.isDarkMode() ? EDarkModes.DARK : EDarkModes.LIGHT;\n }\n return this.themeSignal();\n });\n\n ngOnDestroy(): void {\n this.handleSystemChanges(false);\n }\n\n toggleTheme(targetMode?: DarkModeOptions): void {\n if (!this.isBrowser) {\n return;\n }\n\n if (targetMode) {\n this.applyTheme(targetMode);\n } else {\n const next = this.themeMode() === EDarkModes.DARK ? EDarkModes.LIGHT : EDarkModes.DARK;\n this.applyTheme(next);\n }\n }\n\n getCurrentTheme(): DarkModeOptions {\n return this.themeSignal();\n }\n\n private applyTheme(theme: DarkModeOptions): void {\n if (!this.isBrowser) {\n return;\n }\n\n localStorage.setItem(ZardDarkMode.STORAGE_KEY, theme);\n this.themeSignal.set(theme);\n // whenever we apply theme call listener removal\n this.handleSystemChanges(false);\n\n this.darkModeQuery ??= this.getDarkModeQuery();\n this.updateThemeMode(this.isDarkMode());\n if (theme === EDarkModes.SYSTEM) {\n this.handleSystemChanges(true);\n }\n }\n\n private getStoredTheme(): DarkModeOptions | undefined {\n if (!this.isBrowser) {\n return undefined;\n }\n\n const value = localStorage.getItem(ZardDarkMode.STORAGE_KEY);\n if (value === EDarkModes.LIGHT || value === EDarkModes.DARK || value === EDarkModes.SYSTEM) {\n return value;\n }\n return undefined;\n }\n\n private getThemeMode(isDarkMode: boolean): EDarkModes.LIGHT | EDarkModes.DARK {\n return isDarkMode ? EDarkModes.DARK : EDarkModes.LIGHT;\n }\n\n private updateThemeMode(isDarkMode: boolean): void {\n const themeMode = this.getThemeMode(isDarkMode);\n const html = this.document.documentElement;\n html.classList.toggle('dark', isDarkMode);\n html.setAttribute('data-theme', themeMode);\n html.style.colorScheme = themeMode;\n }\n\n private getDarkModeQuery(): MediaQueryList | undefined {\n if (!this.isBrowser) {\n return;\n }\n return this.document.defaultView?.matchMedia('(prefers-color-scheme: dark)');\n }\n\n private isDarkMode(): boolean {\n if (!this.isBrowser) {\n return false;\n }\n\n const isSystemDarkMode = this.darkModeQuery?.matches ?? false;\n const stored = localStorage.getItem(ZardDarkMode.STORAGE_KEY);\n return stored === EDarkModes.DARK || (stored === EDarkModes.SYSTEM && isSystemDarkMode);\n }\n\n private handleSystemChanges(addListener: boolean): void {\n if (addListener) {\n this.darkModeQuery?.addEventListener('change', this.handleThemeChange);\n } else {\n this.darkModeQuery?.removeEventListener('change', this.handleThemeChange);\n }\n }\n}\n"
7+
"content": "import { MediaMatcher } from '@angular/cdk/layout';\nimport { isPlatformBrowser, DOCUMENT } from '@angular/common';\nimport { DestroyRef, Injectable, PLATFORM_ID, computed, effect, inject, signal } from '@angular/core';\n\nexport enum EDarkModes {\n LIGHT = 'light',\n DARK = 'dark',\n SYSTEM = 'system',\n}\nexport type DarkModeOptions = EDarkModes.LIGHT | EDarkModes.DARK | EDarkModes.SYSTEM;\n\n@Injectable({\n providedIn: 'root',\n})\nexport class ZardDarkMode {\n private readonly document = inject(DOCUMENT);\n private readonly destroyRef = inject(DestroyRef);\n\n private static readonly STORAGE_KEY = 'theme';\n private handleThemeChange = (event: MediaQueryListEvent) => this.updateThemeMode(event.matches, EDarkModes.SYSTEM);\n private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n private readonly themeSignal = signal<DarkModeOptions>(EDarkModes.SYSTEM);\n private _query?: MediaQueryList;\n private initialized = false;\n\n constructor() {\n if (this.isBrowser) {\n this._query = inject(MediaMatcher).matchMedia('(prefers-color-scheme: dark)');\n this.destroyRef.onDestroy(() => this.handleSystemChanges(false));\n\n effect(() => {\n const theme = this.themeSignal();\n const isDarkMode = this.isDarkModeActive(theme);\n this.updateThemeMode(isDarkMode, theme);\n });\n }\n }\n\n readonly themeMode = computed(() => {\n const currentTheme = this.themeSignal();\n if (currentTheme === EDarkModes.SYSTEM) {\n return this.isDarkModeActive(currentTheme) ? EDarkModes.DARK : EDarkModes.LIGHT;\n }\n return currentTheme;\n });\n\n init() {\n if (!this.initialized && this.isBrowser) {\n this.initializeTheme();\n }\n }\n\n toggleTheme(targetMode?: DarkModeOptions): void {\n if (!this.isBrowser) {\n return;\n }\n\n if (targetMode) {\n this.applyTheme(targetMode);\n } else {\n const next = this.themeMode() === EDarkModes.DARK ? EDarkModes.LIGHT : EDarkModes.DARK;\n this.applyTheme(next);\n }\n }\n\n /**\n * Returns a ReadonlySignal<\"light\" | \"dark\" | \"system\"> that cannot be mutated externally.\n * Call currentTheme() to access the value or use it directly in templates where signals are supported.\n * @example service.currentTheme() // returns \"light\", \"dark\", or \"system\"\n */\n get currentTheme() {\n return this.themeSignal.asReadonly();\n }\n\n private get query(): MediaQueryList {\n if (!this.isBrowser || !this._query) {\n throw new Error('Cannot access media query on server');\n }\n return this._query;\n }\n\n private initializeTheme(): void {\n const storedTheme = this.getStoredTheme();\n if (storedTheme) {\n this.themeSignal.set(storedTheme);\n }\n\n if (!storedTheme || storedTheme === EDarkModes.SYSTEM) {\n this.handleSystemChanges();\n }\n this.initialized = true;\n }\n\n private applyTheme(theme: DarkModeOptions): void {\n if (!this.isBrowser) {\n return;\n }\n\n try {\n localStorage.setItem(ZardDarkMode.STORAGE_KEY, theme);\n } catch (error) {\n console.warn('Failed to save theme to localStorage:', error);\n }\n this.themeSignal.set(theme);\n\n if (theme === EDarkModes.SYSTEM) {\n this.handleSystemChanges();\n } else {\n this.handleSystemChanges(false);\n }\n }\n\n private getStoredTheme(): DarkModeOptions | undefined {\n if (!this.isBrowser) {\n return undefined;\n }\n\n try {\n const value = localStorage.getItem(ZardDarkMode.STORAGE_KEY);\n if (value === EDarkModes.LIGHT || value === EDarkModes.DARK || value === EDarkModes.SYSTEM) {\n return value;\n }\n } catch (error) {\n console.warn('Failed to read theme from localStorage:', error);\n }\n return undefined;\n }\n\n private updateThemeMode(isDarkMode: boolean, themeMode: EDarkModes): void {\n const html = this.document.documentElement;\n html.classList.toggle('dark', isDarkMode);\n html.setAttribute('data-theme', themeMode);\n }\n\n private isDarkModeActive(currentTheme: DarkModeOptions): boolean {\n if (!this.isBrowser) {\n return false;\n }\n\n return currentTheme === EDarkModes.DARK || (currentTheme === EDarkModes.SYSTEM && this.query.matches);\n }\n\n private handleSystemChanges(addListener = true): void {\n try {\n if (addListener) {\n this.query.addEventListener('change', this.handleThemeChange);\n } else {\n this.query.removeEventListener('change', this.handleThemeChange);\n }\n } catch (error) {\n console.warn('Failed to manage media query event listener:', error);\n }\n }\n}\n"
88
}
99
],
1010
"basePath": "services"

apps/web/src/app/app.config.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import { provideHttpClient, withFetch } from '@angular/common/http';
2-
import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
2+
import {
3+
ApplicationConfig,
4+
importProvidersFrom,
5+
inject,
6+
provideAppInitializer,
7+
provideZoneChangeDetection,
8+
} from '@angular/core';
39
import { BrowserModule, provideClientHydration, withEventReplay } from '@angular/platform-browser';
410
import { provideRouter, withInMemoryScrolling } from '@angular/router';
511

612
import { provideZard } from '@zard/core/provider/providezard';
13+
import { ZardDarkMode } from '@zard/services/dark-mode';
714

815
import { appRoutes } from './app.routes';
916

@@ -21,5 +28,6 @@ export const appConfig: ApplicationConfig = {
2128
),
2229
provideHttpClient(withFetch()),
2330
provideZard(),
31+
provideAppInitializer(() => inject(ZardDarkMode).init()),
2432
],
2533
};

apps/web/src/app/core/layouts/documentation/documentation.layout.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export class DocumentationLayout {
3434
private readonly destroyRef = inject(DestroyRef);
3535
readonly isDevEnv = !environment.production;
3636

37-
private readonly themeSignal = signal<DarkModeOptions>(this.darkModeService.getCurrentTheme());
37+
private readonly themeSignal = signal<DarkModeOptions>(this.darkModeService.currentTheme());
3838
readonly currentTheme = computed(() => this.themeSignal());
3939

4040
constructor() {
@@ -44,7 +44,7 @@ export class DocumentationLayout {
4444
}
4545

4646
const observer = new MutationObserver(() => {
47-
const newTheme = this.darkModeService.getCurrentTheme();
47+
const newTheme = this.darkModeService.currentTheme();
4848
if (newTheme !== this.themeSignal()) {
4949
this.themeSignal.set(newTheme);
5050
}

apps/web/src/app/domain/components/block-preview/block-preview.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class BlockPreviewComponent {
4848
console.error('Erro ao carregar imagem:', {
4949
src: img.src,
5050
block: this.block()?.id,
51-
theme: this.darkModeService.getCurrentTheme(),
51+
theme: this.darkModeService.currentTheme(),
5252
currentImage: this.currentImage(),
5353
});
5454
}

apps/web/src/app/domain/pages/dark-mode/dark-mode.page.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,14 @@ <h2 class="text-xl font-bold tracking-tight sm:text-2xl lg:text-3xl">Interactive
8585

8686
<z-card class="p-6">
8787
<div class="mb-6 flex items-center justify-between">
88+
@let theme = darkModeService.currentTheme();
8889
<div>
8990
<h3 class="text-lg font-semibold">Theme Control</h3>
9091
<p class="text-muted-foreground text-sm">
9192
Current theme:
92-
<strong>{{ darkModeService.theme() }}</strong>
93+
<strong>{{ theme }}</strong>
9394
</p>
9495
</div>
95-
@let theme = currentTheme();
9696
<z-button-group>
9797
<button
9898
z-button

apps/web/src/app/domain/pages/dark-mode/dark-mode.page.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ export class DarkmodePage implements OnInit {
3737
private readonly seoService = inject(SeoService);
3838

3939
protected readonly darkModeService = inject(ZardDarkMode);
40-
protected readonly currentTheme = this.darkModeService.theme;
4140
protected readonly EDarkModes = EDarkModes;
4241

4342
readonly navigationConfig: NavigationConfig = {

apps/web/src/app/domain/pages/themes/components/theme-sidebar/theme-sidebar.component.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,15 @@ <h2 class="text-foreground text-base font-semibold tracking-tight">Customize</h2
7373
<z-accordion zType="multiple" [zDefaultValue]="['base']" class="space-y-1">
7474
@for (group of colorGroups; track group.key) {
7575
<z-accordion-item [zValue]="group.key" [zTitle]="group.label">
76-
<div class="space-y-4 py-3">
77-
@for (colorKey of group.colors; track colorKey) {
76+
@for (colorKey of group.colors; track colorKey) {
77+
<div class="py-3">
7878
<app-color-picker-field
7979
[colorKey]="colorKey"
8080
[value]="currentColors()[colorKey]"
8181
(valueChange)="onColorChange(colorKey, $event)"
8282
/>
83-
}
84-
</div>
83+
</div>
84+
}
8585
</z-accordion-item>
8686
}
8787
</z-accordion>

0 commit comments

Comments
 (0)