@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.
- package/dist/ColorSchemeInput.svelte +2 -2
- package/dist/ColorSchemeInput.svelte.d.ts +2 -2
- package/dist/ColorSchemeInput.svelte.d.ts.map +1 -1
- package/dist/ContextmenuEntry.svelte +3 -3
- package/dist/ContextmenuLinkEntry.svelte +3 -3
- package/dist/ContextmenuSubmenu.svelte +3 -3
- package/dist/ThemeRoot.svelte +79 -12
- package/dist/ThemeRoot.svelte.d.ts +21 -1
- package/dist/ThemeRoot.svelte.d.ts.map +1 -1
- package/dist/declaration.svelte.d.ts +2 -2
- package/dist/declaration.svelte.js +2 -2
- package/dist/projects.js +1 -1
- package/dist/theme_state.svelte.d.ts +46 -8
- package/dist/theme_state.svelte.d.ts.map +1 -1
- package/dist/theme_state.svelte.js +87 -5
- package/package.json +3 -3
- package/src/lib/declaration.svelte.ts +2 -2
- package/src/lib/projects.ts +1 -1
- package/src/lib/theme_state.svelte.ts +118 -14
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { color_schemes, type
|
|
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:
|
|
15
|
+
value?: { color_scheme: ColorSchemePreference };
|
|
16
16
|
} = $props();
|
|
17
17
|
</script>
|
|
18
18
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { type
|
|
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:
|
|
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,
|
|
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="
|
|
67
|
-
<div class="
|
|
66
|
+
<div class="menuitem_content">
|
|
67
|
+
<div class="menuitem_icon">
|
|
68
68
|
<ContextmenuIcon {icon} />
|
|
69
69
|
</div>
|
|
70
|
-
<div class="
|
|
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="
|
|
92
|
-
<div class="
|
|
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="
|
|
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="
|
|
93
|
-
<div class="
|
|
92
|
+
<div class="menuitem_content">
|
|
93
|
+
<div class="menuitem_icon">
|
|
94
94
|
<ContextmenuIcon {icon} />
|
|
95
95
|
</div>
|
|
96
|
-
<div class="
|
|
96
|
+
<div class="menuitem_title">{@render children()}</div>
|
|
97
97
|
</div>
|
|
98
98
|
<div class="chevron" aria-hidden="true"></div>
|
|
99
99
|
</div>
|
package/dist/ThemeRoot.svelte
CHANGED
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<script lang="ts">
|
|
6
|
-
import { onMount, type Snippet } from 'svelte';
|
|
7
|
-
import {
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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 =
|
|
103
|
-
* `type Foo =
|
|
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 =
|
|
190
|
-
* `type Foo =
|
|
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,
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
23
|
-
export declare const load_color_scheme: (fallback?:
|
|
24
|
-
|
|
25
|
-
|
|
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,
|
|
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
|
-
|
|
42
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
248
|
-
* `type Foo =
|
|
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 {
|
package/src/lib/projects.ts
CHANGED
|
@@ -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,
|
|
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 {
|
|
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 {
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
68
|
+
fallback: ColorSchemePreference = 'auto',
|
|
60
69
|
key = COLOR_SCHEME_STORAGE_KEY
|
|
61
|
-
):
|
|
70
|
+
): ColorSchemePreference => load_from_storage(key, false, parse_color_scheme) ?? fallback;
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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):
|
|
72
|
-
color_schemes.includes(value as any) ? (value as
|
|
175
|
+
const parse_color_scheme = (value: unknown): ColorSchemePreference | null =>
|
|
176
|
+
color_schemes.includes(value as any) ? (value as ColorSchemePreference) : null;
|