@noxlovette/material 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,20 +9,15 @@ It manages the Material 3 Dynamic Theme, dark mode, and icon providers.
9
9
  import { app } from './theme';
10
10
  import type { AppProps } from './types';
11
11
  import { ModeWatcher } from 'mode-watcher';
12
+ import ThemeScript from '../../../utils/ThemeScript.svelte';
12
13
 
13
- let {
14
- children,
15
- class: className,
16
- iconProviderProps = {},
17
- themeConfig,
18
- isDark,
19
- ...restProps
20
- }: AppProps = $props();
14
+ let { children, class: className, iconProviderProps = {}, ...restProps }: AppProps = $props();
21
15
 
22
- const baseCls = $derived(app({ class: [className, isDark ? 'dark' : ''] }));
16
+ const baseCls = $derived(app({ class: className }));
23
17
  </script>
24
18
 
25
- <Theme config={themeConfig} {isDark} />
19
+ <ThemeScript />
20
+ <Theme />
26
21
  <MaterialSymbolsProvider {...iconProviderProps} />
27
22
  <ModeWatcher />
28
23
  <Tooltip.Provider>
@@ -1,5 +1,4 @@
1
1
  import type { MaterialSymbolsProviderProps } from '../../../utils/index.js';
2
- import type { ThemeConfig } from '../../../utils/theme.svelte.js';
3
2
  import type { DivAttrs } from '../../../utils/types.js';
4
3
  import type { Snippet } from 'svelte';
5
4
  export type AppProps = DivAttrs & {
@@ -9,17 +8,4 @@ export type AppProps = DivAttrs & {
9
8
  class?: string;
10
9
  /** Props to pass to the material symbols provider */
11
10
  iconProviderProps?: Partial<MaterialSymbolsProviderProps>;
12
- /**
13
- * Optional CSS generated on the server to prevent FOUC.
14
- */
15
- ssrThemeCSS?: string;
16
- /**
17
- * Optional theme configuration passed from the server.
18
- * If provided, the App component will generate the theme CSS automatically.
19
- */
20
- themeConfig?: ThemeConfig;
21
- /**
22
- * Optional dark mode state passed from the server to prevent flashing.
23
- */
24
- isDark?: boolean;
25
11
  };
@@ -19,6 +19,7 @@ Navigation rails provide access to primary destinations in apps when using table
19
19
  rounded = false,
20
20
  expandable = true,
21
21
  fab,
22
+ railFooter,
22
23
  collapsed = $bindable(true),
23
24
  anchor = 'viewport',
24
25
  railTop = 0,
@@ -60,6 +61,12 @@ Navigation rails provide access to primary destinations in apps when using table
60
61
  {@render children?.()}
61
62
  </NavigationMenu.List>
62
63
  </NavigationMenu.Root>
64
+
65
+ {#if railFooter}
66
+ <div class="mt-auto flex flex-col items-center pb-2">
67
+ {@render railFooter()}
68
+ </div>
69
+ {/if}
63
70
  </div>
64
71
 
65
72
  <style>
@@ -35,6 +35,8 @@ export type RailProps = RailVariants & HTMLAttributes<HTMLDivElement> & {
35
35
  expandable?: boolean;
36
36
  /** An optional Floating Action Button snippet. */
37
37
  fab?: Snippet;
38
+ /** An optional snippet rendered at the bottom of the rail (e.g. avatar/settings). */
39
+ railFooter?: Snippet;
38
40
  /** Whether the rail is currently collapsed. */
39
41
  collapsed?: boolean;
40
42
  /** Where the rail should be anchored. */
@@ -1,47 +1,23 @@
1
1
  <script lang="ts">
2
- import { generateThemeCSS, isDarkScheme, themeState, type ThemeConfig } from './theme.svelte.js';
2
+ import { generateThemeCSS, isDarkScheme, themeState, STORAGE_KEY } from './theme.svelte.js';
3
3
 
4
- interface Props {
5
- /**
6
- * Initial configuration for the theme.
7
- * Useful for SSR when reading from cookies.
8
- */
9
- config?: Partial<ThemeConfig>;
10
- /**
11
- * Optional dark mode state passed from the server to prevent flashing.
12
- * If provided, it overrides the auto-detection.
13
- */
14
- isDark?: boolean;
15
- /**
16
- * Whether to persist changes to localStorage and cookies.
17
- * Defaults to true.
18
- */
19
- persist?: boolean;
20
- }
21
-
22
- let { config, isDark: isDarkProp, persist = true }: Props = $props();
23
-
24
- // Sync config to global state
25
- // We use an effect to keep it in sync if the prop changes,
26
- // but also run it during initialization for SSR.
27
- function syncConfig(c?: Partial<ThemeConfig>) {
28
- if (!c) return;
29
- if (c.sourceColor) themeState.sourceColor = c.sourceColor;
30
- if (c.scheme) themeState.scheme = c.scheme;
31
- if (c.contrast) themeState.contrast = c.contrast;
32
- if (c.variant) themeState.variant = c.variant;
33
- }
34
-
35
- // Run immediately for SSR
36
- syncConfig(config);
37
-
38
- // Keep in sync if prop updates on client
4
+ // Restore from localStorage on mount
39
5
  $effect(() => {
40
- syncConfig(config);
6
+ const saved = localStorage.getItem(STORAGE_KEY);
7
+ if (!saved) return;
8
+ try {
9
+ const parsed = JSON.parse(saved);
10
+ if (parsed.sourceColor) themeState.sourceColor = parsed.sourceColor;
11
+ if (parsed.scheme) themeState.scheme = parsed.scheme;
12
+ if (parsed.contrast) themeState.contrast = parsed.contrast;
13
+ if (parsed.variant) themeState.variant = parsed.variant;
14
+ } catch {
15
+ localStorage.removeItem(STORAGE_KEY);
16
+ }
41
17
  });
42
18
 
43
- // Calculate final dark mode state
44
- let finalIsDark = $state(isDarkProp !== undefined ? isDarkProp : isDarkScheme(themeState.scheme));
19
+ // Track dark mode, including system preference changes
20
+ let finalIsDark = $state(isDarkScheme(themeState.scheme));
45
21
 
46
22
  $effect(() => {
47
23
  if (themeState.scheme !== 'system') {
@@ -61,21 +37,20 @@
61
37
 
62
38
  const themeStyles = $derived(generateThemeCSS(themeState, finalIsDark));
63
39
 
64
- // Persistence effect
40
+ // Persist to localStorage on any change
65
41
  $effect(() => {
66
- if (!persist) return;
67
-
68
- const configStr = JSON.stringify({
69
- sourceColor: themeState.sourceColor,
70
- scheme: themeState.scheme,
71
- contrast: themeState.contrast,
72
- variant: themeState.variant
73
- });
74
-
75
- localStorage.setItem('ogonek-m3-theme-config', configStr);
42
+ localStorage.setItem(
43
+ STORAGE_KEY,
44
+ JSON.stringify({
45
+ sourceColor: themeState.sourceColor,
46
+ scheme: themeState.scheme,
47
+ contrast: themeState.contrast,
48
+ variant: themeState.variant
49
+ })
50
+ );
76
51
  });
77
52
 
78
- // Dark mode class effect
53
+ // Sync dark class on <html>
79
54
  $effect(() => {
80
55
  if (finalIsDark) {
81
56
  document.documentElement.classList.add('dark');
@@ -0,0 +1,17 @@
1
+ <!--
2
+ @component
3
+ Injects the theme initialisation script into <head> before first paint,
4
+ preventing a flash of unstyled/wrong-scheme content.
5
+
6
+ Add this to your root +layout.svelte (or any layout that wraps the whole app):
7
+ @example
8
+ <ThemeScript />
9
+ <App>...</App>
10
+ -->
11
+ <script lang="ts">
12
+ import { THEME_INIT_SCRIPT } from './theme.svelte.js';
13
+ </script>
14
+
15
+ <svelte:head>
16
+ {@html `<script>${THEME_INIT_SCRIPT}</script>`}
17
+ </svelte:head>
@@ -0,0 +1,27 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ /**
15
+ * Injects the theme initialisation script into <head> before first paint,
16
+ * preventing a flash of unstyled/wrong-scheme content.
17
+ *
18
+ * Add this to your root +layout.svelte (or any layout that wraps the whole app):
19
+ * @example
20
+ * <ThemeScript />
21
+ * <App>...</App>
22
+ */
23
+ declare const ThemeScript: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
24
+ [evt: string]: CustomEvent<any>;
25
+ }, {}, {}, string>;
26
+ type ThemeScript = InstanceType<typeof ThemeScript>;
27
+ export default ThemeScript;
@@ -5,3 +5,4 @@ export * from './ui.js';
5
5
  export type { ColorScheme, ContrastMode, ThemeConfig, ThemeVariant } from './theme.svelte.js';
6
6
  export { default as Layer } from './Layer.svelte';
7
7
  export { default as Theme } from './Theme.svelte';
8
+ export { default as ThemeScript } from './ThemeScript.svelte';
@@ -4,3 +4,4 @@ export * from './types.js';
4
4
  export * from './ui.js';
5
5
  export { default as Layer } from './Layer.svelte';
6
6
  export { default as Theme } from './Theme.svelte';
7
+ export { default as ThemeScript } from './ThemeScript.svelte';
@@ -8,6 +8,17 @@ export interface ThemeConfig {
8
8
  variant: ThemeVariant;
9
9
  }
10
10
  export declare const DEFAULT_CONFIG: ThemeConfig;
11
+ export declare const STORAGE_KEY = "ogonek-m3-theme-config";
12
+ /**
13
+ * Inline script that reads the saved theme from localStorage and applies the
14
+ * `dark` class to <html> before first paint, preventing a flash of unstyled content.
15
+ *
16
+ * Inject this into your app.html <head> (or use the <ThemeScript /> component):
17
+ * @example
18
+ * // app.html
19
+ * <script>{THEME_INIT_SCRIPT}</script>
20
+ */
21
+ export declare const THEME_INIT_SCRIPT = "try {\n const saved = JSON.parse(localStorage.getItem('ogonek-m3-theme-config') || '{}');\n const scheme = saved.scheme || 'system';\n const dark = scheme === 'dark' || (scheme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);\n if (dark) document.documentElement.classList.add('dark');\n} catch (e) {}";
11
22
  /**
12
23
  * Global reactive theme state.
13
24
  * Changes to this state will automatically update the theme if the <Theme /> component is mounted.
@@ -13,6 +13,22 @@ const SCHEME_VARIANTS = {
13
13
  'fruit-salad': SchemeFruitSalad,
14
14
  rainbow: SchemeRainbow
15
15
  };
16
+ export const STORAGE_KEY = 'ogonek-m3-theme-config';
17
+ /**
18
+ * Inline script that reads the saved theme from localStorage and applies the
19
+ * `dark` class to <html> before first paint, preventing a flash of unstyled content.
20
+ *
21
+ * Inject this into your app.html <head> (or use the <ThemeScript /> component):
22
+ * @example
23
+ * // app.html
24
+ * <script>{THEME_INIT_SCRIPT}</script>
25
+ */
26
+ export const THEME_INIT_SCRIPT = `try {
27
+ const saved = JSON.parse(localStorage.getItem('${STORAGE_KEY}') || '{}');
28
+ const scheme = saved.scheme || 'system';
29
+ const dark = scheme === 'dark' || (scheme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
30
+ if (dark) document.documentElement.classList.add('dark');
31
+ } catch (e) {}`;
16
32
  // --- State ---
17
33
  /**
18
34
  * Global reactive theme state.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noxlovette/material",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {