Skip to content

Commit 4d9440b

Browse files
committed
✨ feat: fixing flickering for ssr
1 parent 6007e05 commit 4d9440b

File tree

8 files changed

+37
-15
lines changed

8 files changed

+37
-15
lines changed

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 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 init() {\n if (this.isBrowser) {\n this.darkModeQuery = this.getDarkModeQuery();\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 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.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 initializeTheme(): void {\n const storedTheme = this.getStoredTheme();\n if (storedTheme) {\n this.themeSignal.set(storedTheme);\n }\n\n // Initialize theme based on current system preferences if no stored theme or if system mode\n if (!storedTheme || storedTheme === EDarkModes.SYSTEM) {\n this.updateThemeMode(this.isDarkMode());\n // Start listening for system changes if we're using system mode (either explicitly or by default)\n this.handleSystemChanges(true);\n } else {\n this.updateThemeMode(storedTheme === EDarkModes.DARK);\n }\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 = this.themeSignal();\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, 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 private readonly query = inject(MediaMatcher).matchMedia('(prefers-color-scheme: dark)');\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\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 constructor() {\n if (this.isBrowser) {\n this.destroyRef.onDestroy(() => this.handleSystemChanges(false));\n }\n }\n\n init() {\n if (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 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.updateThemeMode(this.isDarkModeActive(EDarkModes.SYSTEM));\n this.handleSystemChanges();\n } else {\n this.updateThemeMode(storedTheme === EDarkModes.DARK);\n }\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\n this.updateThemeMode(this.isDarkModeActive(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 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.classList.toggle('dark-theme', isDarkMode);\n html.setAttribute('data-theme', themeMode);\n html.style.colorScheme = 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 if (addListener) {\n this.query.addEventListener('change', this.handleThemeChange);\n } else {\n this.query.removeEventListener('change', this.handleThemeChange);\n }\n }\n}\n"
88
}
99
],
1010
"basePath": "services"

apps/web/src/index.html

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,8 @@
22
<html lang="en" class="layout-fixed">
33
<head>
44
<meta charset="utf-8" />
5-
<link rel="preconnect" href="https://fonts.googleapis.com" />
6-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
7-
<link
8-
href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&family=Geist:wght@100..900&display=swap"
9-
rel="stylesheet"
10-
/>
5+
<base href="/" />
6+
<title>Zard UI - The @shadcn/ui Alternative for Angular</title>
117

128
<link rel="icon" href="/site/favicon.svg" type="image/svg+xml" />
139
<link rel="apple-touch-icon" href="/site/apple-touch-icon.png" />
@@ -74,15 +70,11 @@
7470
const isDark = theme === 'dark' || (isSystem && prefersDark);
7571
const resolvedTheme = isDark ? 'dark' : 'light';
7672
html.classList.toggle('dark', isDark);
77-
html.classList.toggle('dark-theme', isDark);
7873
html.setAttribute('data-theme', theme ?? 'system');
7974
html.style.colorScheme = resolvedTheme;
8075
} catch (_) {}
8176
})();
8277
</script>
83-
84-
<base href="/" />
85-
<title>Zard UI - The @shadcn/ui Alternative for Angular</title>
8678
</head>
8779

8880
<body

apps/web/src/styles.css

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
@import 'tailwindcss';
2+
@import '@fontsource-variable/geist-mono';
3+
@import '@fontsource-variable/geist';
24

35
@plugin 'tailwindcss-animate';
46

@@ -9,8 +11,8 @@
911
--header-height: calc(var(--spacing) * 14);
1012
--footer-height: calc(var(--spacing) * 14);
1113
--sidebar-width: 16rem;
12-
--font-geist-sans: 'Geist', sans-serif;
13-
--font-geist-mono: 'Geist Mono', monospace;
14+
--font-geist-sans: 'Geist Variable', sans-serif;
15+
--font-geist-mono: 'Geist Mono Variable', monospace;
1416
--radius: 0.625rem;
1517
--background: oklch(1 0 0);
1618
--foreground: oklch(0.145 0 0);
@@ -208,7 +210,7 @@
208210
}
209211

210212
@utility container {
211-
@apply 3xl:max-w-screen-2xl mx-auto max-w-[1400px] px-4 lg:px-8;
213+
@apply 3xl:max-w-screen-2xl mx-auto max-w-350 px-4 lg:px-8;
212214
}
213215

214216
@layer utilities {

libs/zard/src/lib/shared/services/dark-mode.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ describe('ZardDarkMode - System Appearance Change Detection', () => {
113113

114114
const newService = TestBed.inject(ZardDarkMode);
115115
newService.init();
116+
TestBed.tick();
116117

117118
expect(newService.themeMode()).toBe(EDarkModes.DARK);
118119
});

package-lock.json

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
"@angular/router": "20.3.15",
3939
"@angular/ssr": "20.3.13",
4040
"@antfu/ni": "^26.2.0",
41+
"@fontsource-variable/geist": "^5.2.8",
42+
"@fontsource-variable/geist-mono": "^5.2.7",
4143
"@shikijs/rehype": "^3.20.0",
4244
"@tailwindcss/postcss": "^4.1.18",
4345
"chalk": "^5.6.2",

packages/cli/src/commands/add/dark-mode-setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { existsSync } from 'fs';
33
import * as fsPromises from 'fs/promises';
44
import * as path from 'path';
55

6-
const DARK_MODE_IMPORT = "import { ZardDarkMode } from '../../services/dark-mode';";
6+
const DARK_MODE_IMPORT = "import { ZardDarkMode } from '@/shared/services/dark-mode';";
77
const DARK_MODE_INITIALIZER = 'provideAppInitializer(() => inject(ZardDarkMode).init())';
88

99
export async function updateProvideZardWithDarkMode(

0 commit comments

Comments
 (0)