@fuzdev/fuz_ui 0.222.1 → 0.223.0

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.
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { color_schemes, type ColorScheme } from '@fuzdev/fuz_css/theme.ts';
2
+ import { color_schemes, type ColorSchemePreference } from '@fuzdev/fuz_css/theme.ts';
3
3
  import type { OmitStrict } from '@fuzdev/fuz_util/types.ts';
4
4
  import type { SvelteHTMLElements } from 'svelte/elements';
5
5
 
@@ -12,7 +12,7 @@
12
12
  value = get_theme_state(),
13
13
  ...rest
14
14
  }: OmitStrict<SvelteHTMLElements['menu'], 'title' | 'children' | 'onselect'> & {
15
- value?: { color_scheme: ColorScheme };
15
+ value?: { color_scheme: ColorSchemePreference };
16
16
  } = $props();
17
17
  </script>
18
18
 
@@ -1,9 +1,9 @@
1
- import { type ColorScheme } from '@fuzdev/fuz_css/theme.ts';
1
+ import { type ColorSchemePreference } from '@fuzdev/fuz_css/theme.ts';
2
2
  import type { OmitStrict } from '@fuzdev/fuz_util/types.ts';
3
3
  import type { SvelteHTMLElements } from 'svelte/elements';
4
4
  type $$ComponentProps = OmitStrict<SvelteHTMLElements['menu'], 'title' | 'children' | 'onselect'> & {
5
5
  value?: {
6
- color_scheme: ColorScheme;
6
+ color_scheme: ColorSchemePreference;
7
7
  };
8
8
  };
9
9
  declare const ColorSchemeInput: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -1 +1 @@
1
- {"version":3,"file":"ColorSchemeInput.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ColorSchemeInput.svelte"],"names":[],"mappings":"AAGA,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC3E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAKzD,KAAK,gBAAgB,GAAI,UAAU,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,UAAU,CAAC,GAAG;IACpG,KAAK,CAAC,EAAE;QAAE,YAAY,EAAE,WAAW,CAAA;KAAE,CAAC;CACtC,CAAC;AAsBH,QAAA,MAAM,gBAAgB,sDAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"ColorSchemeInput.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ColorSchemeInput.svelte"],"names":[],"mappings":"AAGA,OAAO,EAAiB,KAAK,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACrF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAKzD,KAAK,gBAAgB,GAAI,UAAU,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,UAAU,CAAC,GAAG;IACpG,KAAK,CAAC,EAAE;QAAE,YAAY,EAAE,qBAAqB,CAAA;KAAE,CAAC;CAChD,CAAC;AAsBH,QAAA,MAAM,gBAAgB,sDAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -63,11 +63,11 @@
63
63
  contextmenu.select(entry);
64
64
  }}
65
65
  >
66
- <div class="content">
67
- <div class="icon">
66
+ <div class="menuitem_content">
67
+ <div class="menuitem_icon">
68
68
  <ContextmenuIcon {icon} />
69
69
  </div>
70
- <div class="title">{@render children()}</div>
70
+ <div class="menuitem_title">{@render children()}</div>
71
71
  {#if pending}<PendingAnimation />{:else if error_message}⚠️{/if}
72
72
  </div>
73
73
  </li>
@@ -88,15 +88,15 @@
88
88
  contextmenu.select(entry);
89
89
  }}
90
90
  >
91
- <div class="content">
92
- <div class="icon">
91
+ <div class="menuitem_content">
92
+ <div class="menuitem_icon">
93
93
  {#if icon}
94
94
  <ContextmenuIcon {icon} />
95
95
  {:else}
96
96
  <Svg data={external ? icon_external_link : icon_link} />
97
97
  {/if}
98
98
  </div>
99
- <div class="title">
99
+ <div class="menuitem_title">
100
100
  <span class="text">
101
101
  {#if children}{@render children()}{:else}{text}{/if}
102
102
  </span>
@@ -89,11 +89,11 @@
89
89
  }}
90
90
  aria-expanded={selected}
91
91
  >
92
- <div class="content">
93
- <div class="icon">
92
+ <div class="menuitem_content">
93
+ <div class="menuitem_icon">
94
94
  <ContextmenuIcon {icon} />
95
95
  </div>
96
- <div class="title">{@render children()}</div>
96
+ <div class="menuitem_title">{@render children()}</div>
97
97
  </div>
98
98
  <div class="chevron" aria-hidden="true"></div>
99
99
  </div>
@@ -3,10 +3,14 @@
3
3
  </script>
4
4
 
5
5
  <script lang="ts">
6
- import { onMount, type Snippet } from 'svelte';
7
- import { render_theme_style } from '@fuzdev/fuz_css/theme.ts';
6
+ import { onMount, untrack, type Snippet } from 'svelte';
7
+ import {
8
+ render_theme_style,
9
+ to_theme_stance,
10
+ type ResolvedTheme
11
+ } from '@fuzdev/fuz_css/theme.ts';
8
12
  import type { Theme } from '@fuzdev/fuz_css/variable.ts';
9
- import { DEFAULT_THEME } from '@fuzdev/fuz_css/themes.ts';
13
+ import { DEFAULT_THEME, default_themes } from '@fuzdev/fuz_css/themes.ts';
10
14
  import { DEV } from 'esm-env';
11
15
 
12
16
  import {
@@ -26,9 +30,11 @@
26
30
  save_color_scheme = default_save_color_scheme,
27
31
  load_theme = default_load_theme,
28
32
  save_theme = default_save_theme,
33
+ themes = default_themes,
34
+ resolve_theme,
29
35
  theme_fallback,
30
36
  theme_state = new ThemeState({
31
- theme: load_theme(theme_fallback),
37
+ theme: load_theme(themes, theme_fallback),
32
38
  color_scheme: load_color_scheme()
33
39
  }),
34
40
  children
@@ -38,10 +44,29 @@
38
44
  save_color_scheme?: typeof default_save_color_scheme;
39
45
  load_theme?: typeof default_load_theme;
40
46
  save_theme?: typeof default_save_theme;
47
+ /**
48
+ * The app's theme registry. Themes in it persist by name, so a later load
49
+ * takes the registry's current version.
50
+ * @default default_themes
51
+ */
52
+ themes?: Array<Theme>;
53
+ /**
54
+ * Resolves a theme's derived data for rendering. Inject
55
+ * `resolve_theme_stance` from `@fuzdev/fuz_css/theme_stance.ts` when the app
56
+ * ships a single-scheme (stanced) theme, which renders its stance mirror
57
+ * only through it. Construction throws when `themes` or `theme_fallback`
58
+ * holds a stanced theme and this is absent.
59
+ */
60
+ resolve_theme?: (theme: Theme) => ResolvedTheme;
61
+ /**
62
+ * The theme used when storage holds none. When absent, `load_theme` falls
63
+ * back to the first of `themes`, or `DEFAULT_THEME` when `themes` is empty.
64
+ */
41
65
  theme_fallback?: Theme | undefined;
42
66
  /**
43
67
  * A reactive class containing the selected theme and color scheme.
44
- * Defaults to the first default theme.
68
+ * Defaults to a `ThemeState` over `load_theme(themes, theme_fallback)` and
69
+ * the saved color scheme.
45
70
  */
46
71
  theme_state?: ThemeState;
47
72
  children: Snippet<
@@ -69,14 +94,52 @@
69
94
  * @module
70
95
  */
71
96
 
97
+ // The app's own stanced themes fail loudly at construction without a resolver -
98
+ // they'd render without their mirror, the wrong appearance in one color scheme.
99
+ // The props are read once, like `theme_state`'s default.
100
+ untrack(() => {
101
+ if (!resolve_theme) {
102
+ for (const [theme, prop] of [
103
+ ...themes.map((t) => [t, 'themes'] as const),
104
+ ...(theme_fallback ? [[theme_fallback, 'theme_fallback'] as const] : [])
105
+ ]) {
106
+ const stance = to_theme_stance(theme.scheme);
107
+ if (stance !== null) {
108
+ throw Error(
109
+ `ThemeRoot: the theme "${theme.name}" in \`${prop}\` has the "${stance}" scheme stance` +
110
+ ' and no `resolve_theme` is injected - pass `resolve_theme={resolve_theme_stance}`' +
111
+ ' from `@fuzdev/fuz_css/theme_stance.ts`'
112
+ );
113
+ }
114
+ }
115
+ }
116
+ });
117
+
72
118
  theme_state_context.set(() => theme_state);
73
119
 
74
- const selected_theme_name = $derived(theme_state.theme.name);
75
- const style = $derived(
76
- selected_theme_name === DEFAULT_THEME.name // TODO @many proper equality check, won't work when we allow editing, need an id or unique names and a deep equality check
77
- ? null
78
- : render_theme_style(theme_state.theme)
79
- );
120
+ let logged_unresolved = false;
121
+
122
+ const style = $derived.by(() => {
123
+ const { theme } = theme_state;
124
+ // the bundled CSS already renders the default theme, compared by reference
125
+ // because a custom theme may share its name
126
+ if (theme === DEFAULT_THEME) return null;
127
+ if (resolve_theme) {
128
+ const resolved = resolve_theme(theme);
129
+ return render_theme_style(resolved.theme, resolved) || null;
130
+ }
131
+ // a stanced theme the app didn't ship, like a custom one restored from
132
+ // storage, renders without its mirror rather than throwing on saved data
133
+ if (!logged_unresolved && to_theme_stance(theme.scheme) !== null) {
134
+ logged_unresolved = true;
135
+ // eslint-disable-next-line no-console -- a lost mirror is a bug the app should see
136
+ console.error(
137
+ `ThemeRoot: the theme "${theme.name}" has a scheme stance and no \`resolve_theme\`` +
138
+ ' is injected, so it renders without its stance mirror'
139
+ );
140
+ }
141
+ return render_theme_style(theme) || null;
142
+ });
80
143
  const theme_style_html = $derived(style ? `<style>${style}</style>` : null);
81
144
 
82
145
  effect_with_count((count) => {
@@ -92,7 +155,11 @@
92
155
  effect_with_count((count) => {
93
156
  const v = theme_state.theme;
94
157
  if (count === 1) return;
95
- save_theme(v);
158
+ // a new `themes` array alone doesn't re-save an unchanged theme
159
+ save_theme(
160
+ v,
161
+ untrack(() => themes)
162
+ );
96
163
  });
97
164
 
98
165
  // TODO @many @html making me nervous
@@ -1,4 +1,5 @@
1
1
  import { type Snippet } from 'svelte';
2
+ import { type ResolvedTheme } from '@fuzdev/fuz_css/theme.ts';
2
3
  import type { Theme } from '@fuzdev/fuz_css/variable.ts';
3
4
  import { load_color_scheme as default_load_color_scheme, save_color_scheme as default_save_color_scheme, sync_color_scheme as default_sync_color_scheme, save_theme as default_save_theme, load_theme as default_load_theme, ThemeState } from './theme_state.svelte.js';
4
5
  type $$ComponentProps = {
@@ -7,10 +8,29 @@ type $$ComponentProps = {
7
8
  save_color_scheme?: typeof default_save_color_scheme;
8
9
  load_theme?: typeof default_load_theme;
9
10
  save_theme?: typeof default_save_theme;
11
+ /**
12
+ * The app's theme registry. Themes in it persist by name, so a later load
13
+ * takes the registry's current version.
14
+ * @default default_themes
15
+ */
16
+ themes?: Array<Theme>;
17
+ /**
18
+ * Resolves a theme's derived data for rendering. Inject
19
+ * `resolve_theme_stance` from `@fuzdev/fuz_css/theme_stance.ts` when the app
20
+ * ships a single-scheme (stanced) theme, which renders its stance mirror
21
+ * only through it. Construction throws when `themes` or `theme_fallback`
22
+ * holds a stanced theme and this is absent.
23
+ */
24
+ resolve_theme?: (theme: Theme) => ResolvedTheme;
25
+ /**
26
+ * The theme used when storage holds none. When absent, `load_theme` falls
27
+ * back to the first of `themes`, or `DEFAULT_THEME` when `themes` is empty.
28
+ */
10
29
  theme_fallback?: Theme | undefined;
11
30
  /**
12
31
  * A reactive class containing the selected theme and color scheme.
13
- * Defaults to the first default theme.
32
+ * Defaults to a `ThemeState` over `load_theme(themes, theme_fallback)` and
33
+ * the saved color scheme.
14
34
  */
15
35
  theme_state?: ThemeState;
16
36
  children: Snippet<[
@@ -1 +1 @@
1
- {"version":3,"file":"ThemeRoot.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ThemeRoot.svelte"],"names":[],"mappings":"AAMA,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAE/C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAIzD,OAAO,EACL,iBAAiB,IAAI,yBAAyB,EAC9C,iBAAiB,IAAI,yBAAyB,EAC9C,iBAAiB,IAAI,yBAAyB,EAC9C,UAAU,IAAI,kBAAkB,EAChC,UAAU,IAAI,kBAAkB,EAEhC,UAAU,EACV,MAAM,yBAAyB,CAAC;AAGjC,KAAK,gBAAgB,GAAI;IACxB,iBAAiB,CAAC,EAAE,OAAO,yBAAyB,CAAC;IACrD,iBAAiB,CAAC,EAAE,OAAO,yBAAyB,CAAC;IACrD,iBAAiB,CAAC,EAAE,OAAO,yBAAyB,CAAC;IACrD,UAAU,CAAC,EAAE,OAAO,kBAAkB,CAAC;IACvC,UAAU,CAAC,EAAE,OAAO,kBAAkB,CAAC;IACvC,cAAc,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;IACnC;;;OAGG;IACH,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,QAAQ,EAAE,OAAO,CAChB;QAAC,WAAW,EAAE,UAAU;QAAE,KAAK,EAAE,MAAM,GAAG,IAAI;QAAE,gBAAgB,EAAE,MAAM,GAAG,IAAI;KAAC,CAChF,CAAC;CACF,CAAC;AAqFH,QAAA,MAAM,SAAS,sDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"ThemeRoot.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ThemeRoot.svelte"],"names":[],"mappings":"AAMA,OAAO,EAAoB,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AACxD,OAAO,EAGL,KAAK,aAAa,EAClB,MAAM,0BAA0B,CAAC;AACnC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAIzD,OAAO,EACL,iBAAiB,IAAI,yBAAyB,EAC9C,iBAAiB,IAAI,yBAAyB,EAC9C,iBAAiB,IAAI,yBAAyB,EAC9C,UAAU,IAAI,kBAAkB,EAChC,UAAU,IAAI,kBAAkB,EAEhC,UAAU,EACV,MAAM,yBAAyB,CAAC;AAGjC,KAAK,gBAAgB,GAAI;IACxB,iBAAiB,CAAC,EAAE,OAAO,yBAAyB,CAAC;IACrD,iBAAiB,CAAC,EAAE,OAAO,yBAAyB,CAAC;IACrD,iBAAiB,CAAC,EAAE,OAAO,yBAAyB,CAAC;IACrD,UAAU,CAAC,EAAE,OAAO,kBAAkB,CAAC;IACvC,UAAU,CAAC,EAAE,OAAO,kBAAkB,CAAC;IACvC;;;;OAIG;IACH,MAAM,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IACtB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,aAAa,CAAC;IAChD;;;OAGG;IACH,cAAc,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;IACnC;;;;OAIG;IACH,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,QAAQ,EAAE,OAAO,CAChB;QAAC,WAAW,EAAE,UAAU;QAAE,KAAK,EAAE,MAAM,GAAG,IAAI;QAAE,gBAAgB,EAAE,MAAM,GAAG,IAAI;KAAC,CAChF,CAAC;CACF,CAAC;AAiIH,QAAA,MAAM,SAAS,sDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -99,8 +99,8 @@ export declare class Declaration {
99
99
  get external_types(): Array<string> | undefined;
100
100
  /**
101
101
  * Whether the exported name also carries a value meaning — a merged
102
- * value+type symbol like `const Foo = z.strictObject({...})` beside
103
- * `type Foo = z.infer<typeof Foo>`, where the type wins the declaration slot.
102
+ * value+type symbol like `const Foo = object({...})` beside
103
+ * `type Foo = Infer<typeof Foo>`, where the type wins the declaration slot.
104
104
  * Present on `type` and `interface` kinds.
105
105
  */
106
106
  get merged_value(): boolean | undefined;
@@ -186,8 +186,8 @@ export class Declaration {
186
186
  }
187
187
  /**
188
188
  * Whether the exported name also carries a value meaning — a merged
189
- * value+type symbol like `const Foo = z.strictObject({...})` beside
190
- * `type Foo = z.infer<typeof Foo>`, where the type wins the declaration slot.
189
+ * value+type symbol like `const Foo = object({...})` beside
190
+ * `type Foo = Infer<typeof Foo>`, where the type wins the declaration slot.
191
191
  * Present on `type` and `interface` kinds.
192
192
  */
193
193
  get merged_value() {
package/dist/projects.js CHANGED
@@ -11,7 +11,7 @@ export const project_items = [
11
11
  { name: 'fuz_template', url: 'https://template.fuz.dev/', logo: logo_fuz_template, description: 'starter template for SvelteKit web apps, Rust native tools, or both', glyph: '❄', color_class: 'color_h_50' },
12
12
  { name: 'fuz_blog', url: 'https://blog.fuz.dev/', logo: logo_fuz_blog, description: 'blog software from scratch with SvelteKit', glyph: '🖊️', color_class: 'color_e_50' },
13
13
  { name: 'fuz_code', url: 'https://code.fuz.dev/', logo: logo_fuz_code, description: 'syntax styling utilities and components for TypeScript, Svelte, Markdown, and more', glyph: '🎨', color_class: 'color_g_50' },
14
- { name: 'mdz', url: 'https://mdz.fuz.dev/', logo: logo_mdz, description: 'a strict markdown dialect built for streaming, Svelte authoring, docs websites, and untrusted content', glyph: '📝', color_class: 'color_d_50' },
14
+ { name: 'mdz', url: 'https://mdz.fuz.dev/', logo: logo_mdz, description: 'a strict markdown dialect built for streaming, web authoring, docs, and untrusted content', glyph: '📝', color_class: 'color_d_50' },
15
15
  { name: 'fuz_repos', url: 'https://repos.fuz.dev/', logo: logo_fuz_repos, description: 'a tool for managing many repos', glyph: '🪄', color_class: 'color_a_50' },
16
16
  { name: 'svelte-docinfo', url: 'https://svelte-docinfo.fuz.dev/', logo: logo_svelte_docinfo, description: 'static analysis for TypeScript and Svelte', glyph: '📜', color_class: 'color_i_50' },
17
17
  { name: 'fuz_util', url: 'https://util.fuz.dev/', logo: logo_fuz_util, description: 'utility belt for JS', glyph: '🦕', color_class: 'color_f_50' },
@@ -1,13 +1,13 @@
1
- import { type ColorScheme } from '@fuzdev/fuz_css/theme.ts';
1
+ import { type ColorSchemePreference } from '@fuzdev/fuz_css/theme.ts';
2
2
  import { type Theme } from '@fuzdev/fuz_css/variable.ts';
3
3
  export interface ThemeStateJson {
4
4
  theme: Theme;
5
- color_scheme: ColorScheme;
5
+ color_scheme: ColorSchemePreference;
6
6
  }
7
7
  export type ThemeStateOptions = Partial<ThemeStateJson>;
8
8
  export declare class ThemeState {
9
9
  theme: Theme;
10
- color_scheme: ColorScheme;
10
+ color_scheme: ColorSchemePreference;
11
11
  constructor(options?: ThemeStateOptions);
12
12
  toJSON(): ThemeStateJson;
13
13
  }
@@ -16,11 +16,49 @@ export declare const theme_state_context: {
16
16
  get_maybe: () => (() => ThemeState) | undefined;
17
17
  set: (value: () => ThemeState) => () => ThemeState;
18
18
  };
19
- export declare const sync_color_scheme: (color_scheme: ColorScheme | null) => void;
19
+ export declare const sync_color_scheme: (color_scheme: ColorSchemePreference | null) => void;
20
20
  export declare const COLOR_SCHEME_STORAGE_KEY = "fuz:color-scheme";
21
21
  export declare const THEME_STORAGE_KEY = "fuz:theme";
22
- export declare const save_color_scheme: (color_scheme: ColorScheme | null, key?: string) => void;
23
- export declare const load_color_scheme: (fallback?: ColorScheme, key?: string) => ColorScheme;
24
- export declare const save_theme: (theme: Theme | null, key?: string) => void;
25
- export declare const load_theme: (fallback?: Theme, key?: string) => Theme;
22
+ export declare const save_color_scheme: (color_scheme: ColorSchemePreference | null, key?: string) => void;
23
+ export declare const load_color_scheme: (fallback?: ColorSchemePreference, key?: string) => ColorSchemePreference;
24
+ /**
25
+ * Saves a theme to storage: a theme in the `themes` registry by its name, a
26
+ * registry theme composed with a contrast modifier as `{name, contrast}`, and
27
+ * any other theme whole, trimmed to its authored fields (`name`, `summary`,
28
+ * `scheme`, `variables`). Saving by reference means a later load takes the
29
+ * registry's current version, recomposed. Both matches are by name, so a theme
30
+ * sharing a registry theme's name saves as that registry theme, and one named
31
+ * like a composition (`ledger (high contrast)`) saves as that composition.
32
+ *
33
+ * A composition assigned at runtime, like `theme_state.theme =
34
+ * compose_themes(ledger_theme, contrast_modifiers.high)`, takes the
35
+ * `{name, contrast}` path.
36
+ *
37
+ * @param theme - the theme to save, or `null` to remove the saved one
38
+ * @param themes - the app's theme registry
39
+ * @param key - the storage key
40
+ */
41
+ export declare const save_theme: (theme: Theme | null, themes?: Array<Theme>, key?: string) => void;
42
+ /**
43
+ * Loads the saved theme from storage, falling back when there's none or it
44
+ * doesn't resolve to a theme. A saved name loads the registry theme of that
45
+ * name, `{name, contrast}` the registry theme composed with that contrast
46
+ * modifier, an object named like a registry theme the registry theme (so a
47
+ * whole-object save from before themes saved by name loads the current
48
+ * version), and any other object its authored fields when they parse as a
49
+ * `Theme`.
50
+ *
51
+ * @param themes - the app's theme registry
52
+ * @param fallback - the theme to use when storage holds none
53
+ * @param key - the storage key
54
+ */
55
+ export declare const load_theme: (themes?: Array<Theme>, fallback?: Theme, key?: string) => Theme;
56
+ /**
57
+ * Resolves a value from storage to a theme, the parsing half of `load_theme`.
58
+ *
59
+ * @param value - the stored value, parsed from JSON
60
+ * @param themes - the app's theme registry
61
+ * @returns the theme, or `null` when the value doesn't resolve to one
62
+ */
63
+ export declare const parse_saved_theme: (value: unknown, themes: Array<Theme>) => Theme | null;
26
64
  //# sourceMappingURL=theme_state.svelte.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"theme_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/theme_state.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC3E,OAAO,EAAe,KAAK,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAOtE,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,KAAK,CAAC;IACb,YAAY,EAAE,WAAW,CAAC;CAC1B;AAED,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;AAExD,qBAAa,UAAU;IACtB,KAAK,EAAE,KAAK,CAAiB;IAC7B,YAAY,EAAE,WAAW,CAAiB;gBAE9B,OAAO,CAAC,EAAE,iBAAiB;IAUvC,MAAM,IAAI,cAAc;CAMxB;AACD,eAAO,MAAM,mBAAmB;2CAAwB,UAAU;4BAAV,UAAU;uBAAV,UAAU,WAAV,UAAU;CAAG,CAAC;AAEtE,eAAO,MAAM,iBAAiB,GAAI,cAAc,WAAW,GAAG,IAAI,KAAG,IAOpE,CAAC;AAEF,eAAO,MAAM,wBAAwB,qBAAqB,CAAC;AAC3D,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAE7C,eAAO,MAAM,iBAAiB,GAC7B,cAAc,WAAW,GAAG,IAAI,EAChC,YAA8B,KAC5B,IAEF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC7B,WAAU,WAAoB,EAC9B,YAA8B,KAC5B,WAA4E,CAAC;AAEhF,eAAO,MAAM,UAAU,GAAI,OAAO,KAAK,GAAG,IAAI,EAAE,YAAuB,KAAG,IAEzE,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,WAAU,KAA0B,EAAE,YAAuB,KAAG,KACrC,CAAC"}
1
+ {"version":3,"file":"theme_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/theme_state.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,KAAK,qBAAqB,EAC1B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAe,KAAK,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAYtE,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,KAAK,CAAC;IACb,YAAY,EAAE,qBAAqB,CAAC;CACpC;AAED,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;AAExD,qBAAa,UAAU;IACtB,KAAK,EAAE,KAAK,CAAiB;IAC7B,YAAY,EAAE,qBAAqB,CAAiB;gBAExC,OAAO,CAAC,EAAE,iBAAiB;IAUvC,MAAM,IAAI,cAAc;CAMxB;AACD,eAAO,MAAM,mBAAmB;2CAAwB,UAAU;4BAAV,UAAU;uBAAV,UAAU,WAAV,UAAU;CAAG,CAAC;AAEtE,eAAO,MAAM,iBAAiB,GAAI,cAAc,qBAAqB,GAAG,IAAI,KAAG,IAO9E,CAAC;AAEF,eAAO,MAAM,wBAAwB,qBAAqB,CAAC;AAC3D,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAE7C,eAAO,MAAM,iBAAiB,GAC7B,cAAc,qBAAqB,GAAG,IAAI,EAC1C,YAA8B,KAC5B,IAEF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC7B,WAAU,qBAA8B,EACxC,YAA8B,KAC5B,qBAAsF,CAAC;AAE1F;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,UAAU,GACtB,OAAO,KAAK,GAAG,IAAI,EACnB,SAAQ,KAAK,CAAC,KAAK,CAAkB,EACrC,YAAuB,KACrB,IAEF,CAAC;AA2BF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU,GACtB,SAAQ,KAAK,CAAC,KAAK,CAAkB,EACrC,WAAU,KAAkC,EAC5C,YAAuB,KACrB,KAA8F,CAAC;AAElG;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC,KAAK,CAAC,KAAG,KAAK,GAAG,IAahF,CAAC"}
@@ -1,6 +1,6 @@
1
- import { color_schemes } from '@fuzdev/fuz_css/theme.ts';
1
+ import { color_schemes, compose_themes } from '@fuzdev/fuz_css/theme.ts';
2
2
  import { parse_theme } from '@fuzdev/fuz_css/variable.ts';
3
- import { default_themes } from '@fuzdev/fuz_css/themes.ts';
3
+ import { contrast_modifiers, default_themes, DEFAULT_THEME } from '@fuzdev/fuz_css/themes.ts';
4
4
  import { BROWSER } from 'esm-env';
5
5
  import { create_context } from './context_helpers.js';
6
6
  import { load_from_storage, save_to_storage } from './storage.js';
@@ -38,9 +38,91 @@ export const save_color_scheme = (color_scheme, key = COLOR_SCHEME_STORAGE_KEY)
38
38
  save_to_storage(key, color_scheme);
39
39
  };
40
40
  export const load_color_scheme = (fallback = 'auto', key = COLOR_SCHEME_STORAGE_KEY) => load_from_storage(key, false, parse_color_scheme) ?? fallback;
41
- export const save_theme = (theme, key = THEME_STORAGE_KEY) => {
42
- save_to_storage(key, theme, true);
41
+ /**
42
+ * Saves a theme to storage: a theme in the `themes` registry by its name, a
43
+ * registry theme composed with a contrast modifier as `{name, contrast}`, and
44
+ * any other theme whole, trimmed to its authored fields (`name`, `summary`,
45
+ * `scheme`, `variables`). Saving by reference means a later load takes the
46
+ * registry's current version, recomposed. Both matches are by name, so a theme
47
+ * sharing a registry theme's name saves as that registry theme, and one named
48
+ * like a composition (`ledger (high contrast)`) saves as that composition.
49
+ *
50
+ * A composition assigned at runtime, like `theme_state.theme =
51
+ * compose_themes(ledger_theme, contrast_modifiers.high)`, takes the
52
+ * `{name, contrast}` path.
53
+ *
54
+ * @param theme - the theme to save, or `null` to remove the saved one
55
+ * @param themes - the app's theme registry
56
+ * @param key - the storage key
57
+ */
58
+ export const save_theme = (theme, themes = default_themes, key = THEME_STORAGE_KEY) => {
59
+ save_to_storage(key, theme === null ? null : to_saved_theme(theme, themes), true);
60
+ };
61
+ const CONTRASTS = ['low', 'high'];
62
+ /**
63
+ * The value `save_theme` stores for a non-null theme.
64
+ */
65
+ const to_saved_theme = (theme, themes) => {
66
+ if (themes.some((t) => t.name === theme.name))
67
+ return theme.name;
68
+ // recomposing keeps the naming rule in `compose_themes`, and costs one
69
+ // composition per registry theme and modifier, only when saving
70
+ for (const t of themes) {
71
+ for (const contrast of CONTRASTS) {
72
+ if (compose_themes(t, contrast_modifiers[contrast]).name === theme.name) {
73
+ return { name: t.name, contrast };
74
+ }
75
+ }
76
+ }
77
+ return trim_theme(theme);
78
+ };
79
+ /**
80
+ * Loads the saved theme from storage, falling back when there's none or it
81
+ * doesn't resolve to a theme. A saved name loads the registry theme of that
82
+ * name, `{name, contrast}` the registry theme composed with that contrast
83
+ * modifier, an object named like a registry theme the registry theme (so a
84
+ * whole-object save from before themes saved by name loads the current
85
+ * version), and any other object its authored fields when they parse as a
86
+ * `Theme`.
87
+ *
88
+ * @param themes - the app's theme registry
89
+ * @param fallback - the theme to use when storage holds none
90
+ * @param key - the storage key
91
+ */
92
+ export const load_theme = (themes = default_themes, fallback = themes[0] ?? DEFAULT_THEME, key = THEME_STORAGE_KEY) => load_from_storage(key, true, (value) => parse_saved_theme(value, themes)) ?? fallback;
93
+ /**
94
+ * Resolves a value from storage to a theme, the parsing half of `load_theme`.
95
+ *
96
+ * @param value - the stored value, parsed from JSON
97
+ * @param themes - the app's theme registry
98
+ * @returns the theme, or `null` when the value doesn't resolve to one
99
+ */
100
+ export const parse_saved_theme = (value, themes) => {
101
+ if (typeof value === 'string') {
102
+ return themes.find((t) => t.name === value) ?? null;
103
+ }
104
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
105
+ return null;
106
+ const { name, contrast } = value;
107
+ const registry_theme = typeof name === 'string' ? themes.find((t) => t.name === name) : undefined;
108
+ if (registry_theme) {
109
+ return contrast === 'low' || contrast === 'high'
110
+ ? compose_themes(registry_theme, contrast_modifiers[contrast])
111
+ : registry_theme;
112
+ }
113
+ return parse_theme(trim_theme(value));
114
+ };
115
+ /**
116
+ * Copies a theme's authored fields, dropping anything else it carries.
117
+ */
118
+ const trim_theme = (theme) => {
119
+ const trimmed = { name: theme.name };
120
+ if (theme.summary !== undefined)
121
+ trimmed.summary = theme.summary;
122
+ if (theme.scheme !== undefined)
123
+ trimmed.scheme = theme.scheme;
124
+ trimmed.variables = theme.variables;
125
+ return trimmed;
43
126
  };
44
- export const load_theme = (fallback = default_themes[0], key = THEME_STORAGE_KEY) => load_from_storage(key, true, parse_theme) ?? fallback;
45
127
  // TODO move to fuz_css
46
128
  const parse_color_scheme = (value) => color_schemes.includes(value) ? value : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_ui",
3
- "version": "0.222.1",
3
+ "version": "0.223.0",
4
4
  "description": "Svelte component library and frontend helpers",
5
5
  "tagline": "friendly user zystem",
6
6
  "glyph": "🧶",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@fuzdev/fuz_code": ">=0.56.0",
41
- "@fuzdev/fuz_css": ">=0.72.0",
41
+ "@fuzdev/fuz_css": ">=0.74.0",
42
42
  "@fuzdev/fuz_util": ">=0.79.0",
43
43
  "@fuzdev/gro": ">=0.223.0",
44
44
  "@fuzdev/mdz": ">=0.7.0",
@@ -64,7 +64,7 @@
64
64
  "@changesets/changelog-git": "^0.2.1",
65
65
  "@fuzdev/blake3-wasm": "^0.2.0",
66
66
  "@fuzdev/fuz_code": "^0.56.0",
67
- "@fuzdev/fuz_css": "^0.72.0",
67
+ "@fuzdev/fuz_css": "^0.74.0",
68
68
  "@fuzdev/fuz_schema": "^0.5.0",
69
69
  "@fuzdev/fuz_util": "^0.79.0",
70
70
  "@fuzdev/gro": "^0.224.0",
@@ -244,8 +244,8 @@ export class Declaration {
244
244
 
245
245
  /**
246
246
  * Whether the exported name also carries a value meaning — a merged
247
- * value+type symbol like `const Foo = z.strictObject({...})` beside
248
- * `type Foo = z.infer<typeof Foo>`, where the type wins the declaration slot.
247
+ * value+type symbol like `const Foo = object({...})` beside
248
+ * `type Foo = Infer<typeof Foo>`, where the type wins the declaration slot.
249
249
  * Present on `type` and `interface` kinds.
250
250
  */
251
251
  get merged_value(): boolean | undefined {
@@ -37,7 +37,7 @@ export const project_items: Array<ProjectItem> = [
37
37
  {name: 'fuz_template', url: 'https://template.fuz.dev/', logo: logo_fuz_template, description: 'starter template for SvelteKit web apps, Rust native tools, or both', glyph: '❄', color_class: 'color_h_50'},
38
38
  {name: 'fuz_blog', url: 'https://blog.fuz.dev/', logo: logo_fuz_blog, description: 'blog software from scratch with SvelteKit', glyph: '🖊️', color_class: 'color_e_50'},
39
39
  {name: 'fuz_code', url: 'https://code.fuz.dev/', logo: logo_fuz_code, description: 'syntax styling utilities and components for TypeScript, Svelte, Markdown, and more', glyph: '🎨', color_class: 'color_g_50'},
40
- {name: 'mdz', url: 'https://mdz.fuz.dev/', logo: logo_mdz, description: 'a strict markdown dialect built for streaming, Svelte authoring, docs websites, and untrusted content', glyph: '📝', color_class: 'color_d_50'},
40
+ {name: 'mdz', url: 'https://mdz.fuz.dev/', logo: logo_mdz, description: 'a strict markdown dialect built for streaming, web authoring, docs, and untrusted content', glyph: '📝', color_class: 'color_d_50'},
41
41
  {name: 'fuz_repos', url: 'https://repos.fuz.dev/', logo: logo_fuz_repos, description: 'a tool for managing many repos', glyph: '🪄', color_class: 'color_a_50'},
42
42
  {name: 'svelte-docinfo', url: 'https://svelte-docinfo.fuz.dev/', logo: logo_svelte_docinfo, description: 'static analysis for TypeScript and Svelte', glyph: '📜', color_class: 'color_i_50'},
43
43
  {name: 'fuz_util', url: 'https://util.fuz.dev/', logo: logo_fuz_util, description: 'utility belt for JS', glyph: '🦕', color_class: 'color_f_50'},
@@ -1,6 +1,15 @@
1
- import { color_schemes, type ColorScheme } from '@fuzdev/fuz_css/theme.ts';
1
+ import {
2
+ color_schemes,
3
+ compose_themes,
4
+ type ColorSchemePreference
5
+ } from '@fuzdev/fuz_css/theme.ts';
2
6
  import { parse_theme, type Theme } from '@fuzdev/fuz_css/variable.ts';
3
- import { default_themes } from '@fuzdev/fuz_css/themes.ts';
7
+ import {
8
+ contrast_modifiers,
9
+ default_themes,
10
+ DEFAULT_THEME,
11
+ type ContrastModifiers
12
+ } from '@fuzdev/fuz_css/themes.ts';
4
13
  import { BROWSER } from 'esm-env';
5
14
 
6
15
  import { create_context } from './context_helpers.ts';
@@ -8,14 +17,14 @@ import { load_from_storage, save_to_storage } from './storage.ts';
8
17
 
9
18
  export interface ThemeStateJson {
10
19
  theme: Theme;
11
- color_scheme: ColorScheme;
20
+ color_scheme: ColorSchemePreference;
12
21
  }
13
22
 
14
23
  export type ThemeStateOptions = Partial<ThemeStateJson>;
15
24
 
16
25
  export class ThemeState {
17
26
  theme: Theme = $state.raw()!;
18
- color_scheme: ColorScheme = $state.raw()!;
27
+ color_scheme: ColorSchemePreference = $state.raw()!;
19
28
 
20
29
  constructor(options?: ThemeStateOptions) {
21
30
  const theme = options?.theme ?? default_themes[0]!;
@@ -36,7 +45,7 @@ export class ThemeState {
36
45
  }
37
46
  export const theme_state_context = create_context<() => ThemeState>();
38
47
 
39
- export const sync_color_scheme = (color_scheme: ColorScheme | null): void => {
48
+ export const sync_color_scheme = (color_scheme: ColorSchemePreference | null): void => {
40
49
  if (!BROWSER) return;
41
50
  const dark =
42
51
  color_scheme === 'dark' ||
@@ -49,24 +58,119 @@ export const COLOR_SCHEME_STORAGE_KEY = 'fuz:color-scheme';
49
58
  export const THEME_STORAGE_KEY = 'fuz:theme';
50
59
 
51
60
  export const save_color_scheme = (
52
- color_scheme: ColorScheme | null,
61
+ color_scheme: ColorSchemePreference | null,
53
62
  key = COLOR_SCHEME_STORAGE_KEY
54
63
  ): void => {
55
64
  save_to_storage(key, color_scheme);
56
65
  };
57
66
 
58
67
  export const load_color_scheme = (
59
- fallback: ColorScheme = 'auto',
68
+ fallback: ColorSchemePreference = 'auto',
60
69
  key = COLOR_SCHEME_STORAGE_KEY
61
- ): ColorScheme => load_from_storage(key, false, parse_color_scheme) ?? fallback;
70
+ ): ColorSchemePreference => load_from_storage(key, false, parse_color_scheme) ?? fallback;
62
71
 
63
- export const save_theme = (theme: Theme | null, key = THEME_STORAGE_KEY): void => {
64
- save_to_storage(key, theme, true);
72
+ /**
73
+ * Saves a theme to storage: a theme in the `themes` registry by its name, a
74
+ * registry theme composed with a contrast modifier as `{name, contrast}`, and
75
+ * any other theme whole, trimmed to its authored fields (`name`, `summary`,
76
+ * `scheme`, `variables`). Saving by reference means a later load takes the
77
+ * registry's current version, recomposed. Both matches are by name, so a theme
78
+ * sharing a registry theme's name saves as that registry theme, and one named
79
+ * like a composition (`ledger (high contrast)`) saves as that composition.
80
+ *
81
+ * A composition assigned at runtime, like `theme_state.theme =
82
+ * compose_themes(ledger_theme, contrast_modifiers.high)`, takes the
83
+ * `{name, contrast}` path.
84
+ *
85
+ * @param theme - the theme to save, or `null` to remove the saved one
86
+ * @param themes - the app's theme registry
87
+ * @param key - the storage key
88
+ */
89
+ export const save_theme = (
90
+ theme: Theme | null,
91
+ themes: Array<Theme> = default_themes,
92
+ key = THEME_STORAGE_KEY
93
+ ): void => {
94
+ save_to_storage(key, theme === null ? null : to_saved_theme(theme, themes), true);
65
95
  };
66
96
 
67
- export const load_theme = (fallback: Theme = default_themes[0]!, key = THEME_STORAGE_KEY): Theme =>
68
- load_from_storage(key, true, parse_theme) ?? fallback;
97
+ const CONTRASTS = ['low', 'high'] as const satisfies ReadonlyArray<keyof ContrastModifiers>;
98
+
99
+ /**
100
+ * The value `save_theme` stores for a non-null theme.
101
+ */
102
+ const to_saved_theme = (theme: Theme, themes: Array<Theme>): SavedTheme => {
103
+ if (themes.some((t) => t.name === theme.name)) return theme.name;
104
+ // recomposing keeps the naming rule in `compose_themes`, and costs one
105
+ // composition per registry theme and modifier, only when saving
106
+ for (const t of themes) {
107
+ for (const contrast of CONTRASTS) {
108
+ if (compose_themes(t, contrast_modifiers[contrast]).name === theme.name) {
109
+ return { name: t.name, contrast };
110
+ }
111
+ }
112
+ }
113
+ return trim_theme(theme);
114
+ };
115
+
116
+ /**
117
+ * A theme as `save_theme` stores it: a registry theme's name, a registry
118
+ * theme's name with a contrast modifier, or a whole trimmed theme.
119
+ */
120
+ type SavedTheme = string | { name: string; contrast: keyof ContrastModifiers } | Theme;
121
+
122
+ /**
123
+ * Loads the saved theme from storage, falling back when there's none or it
124
+ * doesn't resolve to a theme. A saved name loads the registry theme of that
125
+ * name, `{name, contrast}` the registry theme composed with that contrast
126
+ * modifier, an object named like a registry theme the registry theme (so a
127
+ * whole-object save from before themes saved by name loads the current
128
+ * version), and any other object its authored fields when they parse as a
129
+ * `Theme`.
130
+ *
131
+ * @param themes - the app's theme registry
132
+ * @param fallback - the theme to use when storage holds none
133
+ * @param key - the storage key
134
+ */
135
+ export const load_theme = (
136
+ themes: Array<Theme> = default_themes,
137
+ fallback: Theme = themes[0] ?? DEFAULT_THEME,
138
+ key = THEME_STORAGE_KEY
139
+ ): Theme => load_from_storage(key, true, (value) => parse_saved_theme(value, themes)) ?? fallback;
140
+
141
+ /**
142
+ * Resolves a value from storage to a theme, the parsing half of `load_theme`.
143
+ *
144
+ * @param value - the stored value, parsed from JSON
145
+ * @param themes - the app's theme registry
146
+ * @returns the theme, or `null` when the value doesn't resolve to one
147
+ */
148
+ export const parse_saved_theme = (value: unknown, themes: Array<Theme>): Theme | null => {
149
+ if (typeof value === 'string') {
150
+ return themes.find((t) => t.name === value) ?? null;
151
+ }
152
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return null;
153
+ const { name, contrast } = value as Record<string, unknown>;
154
+ const registry_theme = typeof name === 'string' ? themes.find((t) => t.name === name) : undefined;
155
+ if (registry_theme) {
156
+ return contrast === 'low' || contrast === 'high'
157
+ ? compose_themes(registry_theme, contrast_modifiers[contrast])
158
+ : registry_theme;
159
+ }
160
+ return parse_theme(trim_theme(value as Theme));
161
+ };
162
+
163
+ /**
164
+ * Copies a theme's authored fields, dropping anything else it carries.
165
+ */
166
+ const trim_theme = (theme: Theme): Theme => {
167
+ const trimmed: Record<string, unknown> = { name: theme.name };
168
+ if (theme.summary !== undefined) trimmed.summary = theme.summary;
169
+ if (theme.scheme !== undefined) trimmed.scheme = theme.scheme;
170
+ trimmed.variables = theme.variables;
171
+ return trimmed as Theme;
172
+ };
69
173
 
70
174
  // TODO move to fuz_css
71
- const parse_color_scheme = (value: unknown): ColorScheme | null =>
72
- color_schemes.includes(value as any) ? (value as ColorScheme) : null;
175
+ const parse_color_scheme = (value: unknown): ColorSchemePreference | null =>
176
+ color_schemes.includes(value as any) ? (value as ColorSchemePreference) : null;