@adea-ai/ui 0.125.0 → 0.128.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/NOTICE +40 -0
- package/dist/components/theme/index.d.ts +1 -1
- package/dist/components/theme/index.d.ts.map +1 -1
- package/dist/components/theme/index.js +4 -3
- package/dist/components/theme/theme-document.d.ts +37 -2
- package/dist/components/theme/theme-document.d.ts.map +1 -1
- package/dist/components/theme/theme-document.js +23 -3
- package/dist/components/theme/theme-document.js.map +1 -1
- package/dist/components/theme/theme-persistence.d.ts +87 -0
- package/dist/components/theme/theme-persistence.d.ts.map +1 -1
- package/dist/components/theme/theme-persistence.js +63 -2
- package/dist/components/theme/theme-persistence.js.map +1 -1
- package/dist/components/theme/theme-provider.d.ts +55 -41
- package/dist/components/theme/theme-provider.d.ts.map +1 -1
- package/dist/components/theme/theme-provider.js +59 -52
- package/dist/components/theme/theme-provider.js.map +1 -1
- package/dist/components/theme/theme-script.d.ts +100 -0
- package/dist/components/theme/theme-script.d.ts.map +1 -0
- package/dist/components/theme/theme-script.js +57 -0
- package/dist/components/theme/theme-script.js.map +1 -0
- package/dist/components/theme/theme-state.d.ts +58 -0
- package/dist/components/theme/theme-state.d.ts.map +1 -1
- package/dist/components/theme/theme-state.js +22 -1
- package/dist/components/theme/theme-state.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/lib/font-zoom.d.ts +25 -0
- package/dist/lib/font-zoom.d.ts.map +1 -0
- package/dist/lib/font-zoom.js +45 -0
- package/dist/lib/font-zoom.js.map +1 -0
- package/dist/lib/preference-store.d.ts +126 -0
- package/dist/lib/preference-store.d.ts.map +1 -0
- package/dist/lib/preference-store.js +126 -0
- package/dist/lib/preference-store.js.map +1 -0
- package/dist/lib/theme-surface.d.ts +71 -0
- package/dist/lib/theme-surface.d.ts.map +1 -0
- package/dist/lib/theme-surface.js +62 -0
- package/dist/lib/theme-surface.js.map +1 -0
- package/dist/lib/themes.d.ts +57 -1
- package/dist/lib/themes.d.ts.map +1 -1
- package/dist/lib/themes.js +81 -4
- package/dist/lib/themes.js.map +1 -1
- package/dist/lib/tokens.d.ts.map +1 -1
- package/dist/lib/tokens.js +15 -0
- package/dist/lib/tokens.js.map +1 -1
- package/dist/r/lib.json +15 -0
- package/dist/r/registry.json +26 -1
- package/dist/r/src/components/theme/index.ts +7 -0
- package/dist/r/src/components/theme/theme-document.ts +77 -2
- package/dist/r/src/components/theme/theme-persistence.ts +167 -0
- package/dist/r/src/components/theme/theme-provider.tsx +132 -50
- package/dist/r/src/components/theme/theme-script.ts +201 -0
- package/dist/r/src/components/theme/theme-state.ts +72 -0
- package/dist/r/src/lib/font-zoom.ts +74 -0
- package/dist/r/src/lib/preference-store.ts +216 -0
- package/dist/r/src/lib/theme-surface.ts +110 -0
- package/dist/r/src/lib/themes.ts +115 -2
- package/dist/r/src/lib/tokens.ts +17 -0
- package/dist/r/src/styles/theme-variants.css +3010 -0
- package/dist/r/src/styles/theme.css +38 -0
- package/dist/r/theme.json +6 -1
- package/dist/r/theming.json +5 -0
- package/package.json +2 -1
- package/registry.json +26 -1
- package/src/components/theme/index.ts +7 -0
- package/src/components/theme/theme-document.ts +77 -2
- package/src/components/theme/theme-persistence.ts +167 -0
- package/src/components/theme/theme-provider.tsx +132 -50
- package/src/components/theme/theme-script.ts +201 -0
- package/src/components/theme/theme-state.ts +72 -0
- package/src/index.ts +29 -0
- package/src/lib/font-zoom.ts +74 -0
- package/src/lib/preference-store.ts +216 -0
- package/src/lib/theme-surface.ts +110 -0
- package/src/lib/themes.ts +115 -2
- package/src/lib/tokens.ts +17 -0
- package/src/styles/theme-variants.css +3010 -0
- package/src/styles/theme.css +38 -0
package/dist/NOTICE
CHANGED
|
@@ -336,6 +336,46 @@ provider are Adea behaviour that port retains.
|
|
|
336
336
|
Zeron's MIT notice, reproduced in full under "Appearance editor translated from
|
|
337
337
|
Zeron" above, applies to the translated portion.
|
|
338
338
|
|
|
339
|
+
## Surface policy and font-zoom chords translated from Zeron
|
|
340
|
+
|
|
341
|
+
`packages/ui/src/lib/theme-surface.ts` and `packages/ui/src/lib/font-zoom.ts`
|
|
342
|
+
are moved from Adea's accepted port of Zeron (https://github.com/zeronsh/zeron),
|
|
343
|
+
`packages/ui/src/components/appearance.ts` and `packages/ui/src/components/font-zoom.ts`,
|
|
344
|
+
so the shared provider and Adea run one implementation. The first carries Zeron's
|
|
345
|
+
`SurfacePreference.resolve` as that port extends it: the translucent capability
|
|
346
|
+
gate (a request the host cannot render paints the tokenized frosted surface) and
|
|
347
|
+
the reduced-transparency policy (the operating system's setting or the user's own
|
|
348
|
+
forces the opaque surface), plus the opacity each surface paints. The second maps
|
|
349
|
+
the platform zoom chords (Cmd or Ctrl with `=`, `+`, `-`, `0`) onto the three text
|
|
350
|
+
sizes. The frost rule and `--surface-*` tokens in `packages/ui/src/styles/theme.css`
|
|
351
|
+
are Adea's, written for that port, and introduce no further upstream source.
|
|
352
|
+
|
|
353
|
+
Copyright (c) 2026 Wing
|
|
354
|
+
Licensed under the MIT License.
|
|
355
|
+
|
|
356
|
+
Both files retain that header. Zeron's MIT notice, reproduced in full under
|
|
357
|
+
"Appearance editor translated from Zeron" above, applies to the translated portion.
|
|
358
|
+
|
|
359
|
+
## Preference store recovery translated from Zeron
|
|
360
|
+
|
|
361
|
+
`packages/ui/src/lib/preference-store.ts` is moved from Adea's accepted port of
|
|
362
|
+
Zeron (https://github.com/zeronsh/zeron), `packages/ui/src/components/appearance.ts`,
|
|
363
|
+
so the shared provider and Adea run one implementation. It carries the recovery
|
|
364
|
+
behaviour of that port: an unreadable or unsupported stored document is quarantined
|
|
365
|
+
byte for byte into a recovery envelope at read time, before any write can touch the
|
|
366
|
+
main key; the legacy single-key value is read only while the main key is absent and
|
|
367
|
+
is never deleted; and blocked storage reads as nothing stored. The envelope shape is
|
|
368
|
+
unchanged. The no-flash boot script in `packages/ui/src/components/theme/theme-script.ts`
|
|
369
|
+
is rewritten from that port's `appearanceThemeScript` to read the same document, and
|
|
370
|
+
introduces no further upstream source.
|
|
371
|
+
|
|
372
|
+
Copyright (c) 2026 Wing
|
|
373
|
+
Licensed under the MIT License.
|
|
374
|
+
|
|
375
|
+
`preference-store.ts` retains that header. Zeron's MIT notice, reproduced in full
|
|
376
|
+
under "Appearance editor translated from Zeron" above, applies to the translated
|
|
377
|
+
portion.
|
|
378
|
+
|
|
339
379
|
## Strict binary layout extracted from the accepted Adea donor port
|
|
340
380
|
|
|
341
381
|
`packages/ui/src/components/layout/split-layout/model.ts` is extracted from
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { AppearancePanel, type AppearancePanelProps } from './appearance-panel';
|
|
2
2
|
export { ThemePicker, ThemePreview, ThemeSwatch, type ThemePickerProps, type ThemePreviewProps, type ThemeSwatchProps, } from './theme-picker';
|
|
3
|
-
export { ThemeProvider, defaultThemeSelection, inlineScriptLiteral, localStoragePersistence, resolvedDensity, themeScript, useOptionalTheme, useTheme, type ThemeContextValue, type ThemeDensity, type ThemePersistence, type ThemeProviderProps, type ThemeSelection, } from './theme-provider';
|
|
3
|
+
export { ThemeProvider, createVersionedPersistence, defaultThemeSelection, inlineScriptLiteral, localStoragePersistence, resolvedDensity, themeScript, useOptionalTheme, useTheme, type EditorRolesMode, type ThemeContextValue, type ThemeDensity, type ThemePersistence, type ThemeProviderProps, type ThemeScriptOptions, type ThemeSelection, type ThemeStorageSchema, type ThemeSurface, type VersionedCodec, type VersionedPersistenceOptions, } from './theme-provider';
|
|
4
4
|
export { ThemeToggle, type ThemeToggleProps } from './theme-toggle';
|
|
5
5
|
export { ThemeModeToggle, type ThemeModeToggleProps } from './theme-mode-toggle';
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/theme/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAC/E,OAAO,EACL,WAAW,EACX,YAAY,EACZ,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACL,aAAa,EACb,qBAAqB,EACrB,mBAAmB,EACnB,uBAAuB,EACvB,eAAe,EACf,WAAW,EACX,gBAAgB,EAChB,QAAQ,EACR,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/theme/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAC/E,OAAO,EACL,WAAW,EACX,YAAY,EACZ,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACL,aAAa,EACb,0BAA0B,EAC1B,qBAAqB,EACrB,mBAAmB,EACnB,uBAAuB,EACvB,eAAe,EACf,WAAW,EACX,gBAAgB,EAChB,QAAQ,EACR,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,2BAA2B,GACjC,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACnE,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,qBAAqB,CAAA"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ThemePicker, ThemePreview, ThemeSwatch } from "./theme-picker.js";
|
|
2
2
|
import { useOptionalTheme, useTheme } from "./theme-context.js";
|
|
3
3
|
import { defaultThemeSelection, resolvedDensity } from "./theme-state.js";
|
|
4
|
-
import { localStoragePersistence } from "./theme-persistence.js";
|
|
5
|
-
import {
|
|
4
|
+
import { createVersionedPersistence, localStoragePersistence } from "./theme-persistence.js";
|
|
5
|
+
import { inlineScriptLiteral, themeScript } from "./theme-script.js";
|
|
6
|
+
import { ThemeProvider } from "./theme-provider.js";
|
|
6
7
|
import { AppearancePanel } from "./appearance-panel.js";
|
|
7
8
|
import { ThemeToggle } from "./theme-toggle.js";
|
|
8
9
|
import { ThemeModeToggle } from "./theme-mode-toggle.js";
|
|
9
|
-
export { AppearancePanel, ThemeModeToggle, ThemePicker, ThemePreview, ThemeProvider, ThemeSwatch, ThemeToggle, defaultThemeSelection, inlineScriptLiteral, localStoragePersistence, resolvedDensity, themeScript, useOptionalTheme, useTheme };
|
|
10
|
+
export { AppearancePanel, ThemeModeToggle, ThemePicker, ThemePreview, ThemeProvider, ThemeSwatch, ThemeToggle, createVersionedPersistence, defaultThemeSelection, inlineScriptLiteral, localStoragePersistence, resolvedDensity, themeScript, useOptionalTheme, useTheme };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type ThemeAppearance, type ThemeVariant } from '#lib/themes';
|
|
1
|
+
import { type EditorRolesMode, type ThemeAppearance, type ThemeVariant } from '#lib/themes';
|
|
2
|
+
import { type SurfaceState } from '#lib/theme-surface';
|
|
2
3
|
import { type ThemeSelection } from './theme-state';
|
|
3
4
|
/**
|
|
4
5
|
* The document half of the theme provider: the single writer of `<html>`.
|
|
@@ -12,6 +13,16 @@ export type ThemeDocumentState = {
|
|
|
12
13
|
selection: ThemeSelection;
|
|
13
14
|
appearance: ThemeAppearance;
|
|
14
15
|
variant: ThemeVariant;
|
|
16
|
+
/**
|
|
17
|
+
* The variant whose terminal palette is pinned over the interface variant's.
|
|
18
|
+
* Absent, the terminal follows the interface theme.
|
|
19
|
+
*/
|
|
20
|
+
terminalVariant?: ThemeVariant;
|
|
21
|
+
/**
|
|
22
|
+
* The resolved surface axis. Absent, the axis is off: no attribute and no
|
|
23
|
+
* `--surface-alpha` is written, and any this writer wrote before is removed.
|
|
24
|
+
*/
|
|
25
|
+
surface?: SurfaceState;
|
|
15
26
|
};
|
|
16
27
|
export type ThemeDocumentOptions = {
|
|
17
28
|
/**
|
|
@@ -26,10 +37,34 @@ export type ThemeDocumentOptions = {
|
|
|
26
37
|
* only a custom `#rrggbb` is held to the default floor of 3:1.
|
|
27
38
|
*/
|
|
28
39
|
accentContrastFloor?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Where a catalogue variant's palette comes from. `inline` (the default) writes
|
|
42
|
+
* every role onto the root. `stylesheet` writes none for a catalogue variant,
|
|
43
|
+
* because the host loads a stylesheet that carries a `[data-theme]` block for it;
|
|
44
|
+
* a variant the catalogue does not hold (a host's imported theme) is still written
|
|
45
|
+
* inline, since no stylesheet can know it. The accent, the terminal pin, the
|
|
46
|
+
* surface alpha and the fonts are written the same in both.
|
|
47
|
+
*
|
|
48
|
+
* Switching from an imported variant back to a catalogue one removes what the
|
|
49
|
+
* imported one wrote, so nothing inline outlives it to beat the stylesheet.
|
|
50
|
+
*/
|
|
51
|
+
palette?: 'inline' | 'stylesheet';
|
|
52
|
+
/**
|
|
53
|
+
* The `--editor-*` projection. `syntax` (the default) is the catalogue's quiet
|
|
54
|
+
* syntax roles, which is what `theme-variants.css` carries; `floor` holds every
|
|
55
|
+
* editor role to 4.5:1 on the canvas, as hex.
|
|
56
|
+
*
|
|
57
|
+
* With `palette: 'stylesheet'`, `floor` is the one palette write a catalogue
|
|
58
|
+
* variant still gets: the stylesheet is generated for the default projection, so
|
|
59
|
+
* the sixteen `--editor-*` properties are written inline over it. The rest of the
|
|
60
|
+
* palette stays in the stylesheet.
|
|
61
|
+
*/
|
|
62
|
+
editorRoles?: EditorRolesMode;
|
|
29
63
|
};
|
|
30
64
|
/**
|
|
31
65
|
* The custom properties a state paints: the variant's roles, with the accent
|
|
32
|
-
* selection laid over them,
|
|
66
|
+
* selection laid over them, the pinned terminal palette over that, the surface
|
|
67
|
+
* alpha, then `transformTokens`.
|
|
33
68
|
*
|
|
34
69
|
* The accent is applied *here* rather than left to the `[data-accent]` blocks in
|
|
35
70
|
* `theme.css`, because the properties are written inline and an inline declaration
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme-document.d.ts","sourceRoot":"","sources":["../../../src/components/theme/theme-document.ts"],"names":[],"mappings":"AAIA,OAAO,
|
|
1
|
+
{"version":3,"file":"theme-document.d.ts","sourceRoot":"","sources":["../../../src/components/theme/theme-document.ts"],"names":[],"mappings":"AAIA,OAAO,EAKL,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,YAAY,EAClB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACpE,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAA;AAEpE;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,IAAI,CAC1B,WAAW,EACX,WAAW,GAAG,SAAS,GAAG,OAAO,GAAG,iBAAiB,GAAG,cAAc,CACvE,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,mFAAmF;IACnF,SAAS,EAAE,cAAc,CAAA;IACzB,UAAU,EAAE,eAAe,CAAA;IAC3B,OAAO,EAAE,YAAY,CAAA;IACrB;;;OAGG;IACH,eAAe,CAAC,EAAE,YAAY,CAAA;IAC9B;;;OAGG;IACH,OAAO,CAAC,EAAE,YAAY,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5E;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAA;IACjC;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,eAAe,CAAA;CAC9B,CAAA;AAmBD;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,kBAAkB,EACzB,OAAO,GAAE,oBAAyB,GACjC;IAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAqBvD;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,kBAAkB,EACzB,OAAO,GAAE,oBAAyB,EAClC,QAAQ,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,GAC7B,WAAW,CAAC,MAAM,CAAC,CAiDrB;AAED,gEAAgE;AAChE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE;QAAE,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;KAAE,CAAA;IAC7C,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG;QAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;QAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,MAAM,IAAI,IAAI,CAAA;KACf,CAAA;CACF,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,kBAAkB,EACvB,KAAK,EAAE,MAAM,IAAI,EACjB,QAAQ,GAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAsB,GACzD,MAAM,IAAI,CAqBZ"}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { isCustomAccent } from "../../lib/accent-roles.js";
|
|
2
|
-
import { accentVariables, themeCssVariables } from "../../lib/themes.js";
|
|
2
|
+
import { accentVariables, isCatalogueVariant, terminalCssVariables, themeCssVariables } from "../../lib/themes.js";
|
|
3
3
|
import { applyAppearanceFontSettings, normalizeAppearanceEditorFontSettings } from "../../lib/appearance-font-settings.js";
|
|
4
|
+
import { surfaceAlpha } from "../../lib/theme-surface.js";
|
|
4
5
|
import { resolvedDensity } from "./theme-state.js";
|
|
5
6
|
//#region src/components/theme/theme-document.ts
|
|
6
7
|
/**
|
|
8
|
+
* What a catalogue variant still writes inline when its palette is the stylesheet's:
|
|
9
|
+
* nothing, unless the editor projection is not the one the stylesheet was generated
|
|
10
|
+
* for.
|
|
11
|
+
*/
|
|
12
|
+
function stylesheetOverrides(variant, editorRoles) {
|
|
13
|
+
if (editorRoles !== "floor") return {};
|
|
14
|
+
return Object.fromEntries(Object.entries(themeCssVariables(variant, { editor: "floor" })).filter(([name]) => name.startsWith("--editor-")));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
7
17
|
* The custom properties a state paints: the variant's roles, with the accent
|
|
8
|
-
* selection laid over them,
|
|
18
|
+
* selection laid over them, the pinned terminal palette over that, the surface
|
|
19
|
+
* alpha, then `transformTokens`.
|
|
9
20
|
*
|
|
10
21
|
* The accent is applied *here* rather than left to the `[data-accent]` blocks in
|
|
11
22
|
* `theme.css`, because the properties are written inline and an inline declaration
|
|
@@ -15,13 +26,15 @@ import { resolvedDensity } from "./theme-state.js";
|
|
|
15
26
|
* set, the rules were present, and nothing changed on screen.
|
|
16
27
|
*/
|
|
17
28
|
function themeDocumentTokens(state, options = {}) {
|
|
18
|
-
const properties = { ...themeCssVariables(state.variant) };
|
|
29
|
+
const properties = options.palette === "stylesheet" && isCatalogueVariant(state.variant) ? stylesheetOverrides(state.variant, options.editorRoles) : { ...themeCssVariables(state.variant, { editor: options.editorRoles }) };
|
|
19
30
|
const accent = accentVariables(state.selection.accent, state.appearance, state.selection, {
|
|
20
31
|
background: state.variant.colors.background,
|
|
21
32
|
floor: options.accentContrastFloor,
|
|
22
33
|
variantId: state.variant.id
|
|
23
34
|
});
|
|
24
35
|
if (accent) Object.assign(properties, accent);
|
|
36
|
+
if (state.terminalVariant) Object.assign(properties, terminalCssVariables(state.terminalVariant));
|
|
37
|
+
if (state.surface) properties["--surface-alpha"] = surfaceAlpha[state.surface.surface];
|
|
25
38
|
return {
|
|
26
39
|
tokens: options.transformTokens ? options.transformTokens(properties) : properties,
|
|
27
40
|
accented: accent !== void 0
|
|
@@ -48,6 +61,13 @@ function applyThemeToRoot(root, state, options = {}, previous) {
|
|
|
48
61
|
root.style.colorScheme = state.appearance;
|
|
49
62
|
if (!accented) root.removeAttribute("data-accent");
|
|
50
63
|
else root.dataset["accent"] = isCustomAccent(state.selection.accent) ? "custom" : state.selection.accent;
|
|
64
|
+
if (state.surface) {
|
|
65
|
+
root.dataset["surface"] = state.surface.surface;
|
|
66
|
+
root.dataset["reduceTransparency"] = state.surface.reduceTransparencyActive ? "true" : "false";
|
|
67
|
+
} else {
|
|
68
|
+
root.removeAttribute("data-surface");
|
|
69
|
+
root.removeAttribute("data-reduce-transparency");
|
|
70
|
+
}
|
|
51
71
|
applyAppearanceFontSettings(root, normalizeAppearanceEditorFontSettings(state.selection.fonts, state.selection.font).settings);
|
|
52
72
|
if (resolvedDensity(state.selection) === "comfortable") root.removeAttribute("data-density");
|
|
53
73
|
else root.dataset["density"] = resolvedDensity(state.selection);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme-document.js","names":[],"sources":["../../../src/components/theme/theme-document.ts"],"sourcesContent":["import {\n applyAppearanceFontSettings,\n normalizeAppearanceEditorFontSettings,\n} from '#lib/appearance-font-settings'\nimport {\n accentVariables,\n themeCssVariables,\n type ThemeAppearance,\n type ThemeVariant,\n} from '#lib/themes'\nimport { isCustomAccent } from '#lib/accent-roles'\nimport { resolvedDensity, type ThemeSelection } from './theme-state'\n\n/**\n * The document half of the theme provider: the single writer of `<html>`.\n *\n * Written against the few members of an element it uses, so the rules can be tested\n * on a stand-in and the provider's effect stays a one-line call.\n */\nexport type ThemeRoot = Pick<\n HTMLElement,\n 'classList' | 'dataset' | 'style' | 'removeAttribute' | 'setAttribute'\n>\n\nexport type ThemeDocumentState = {\n /** What the document shows: the committed selection, or a preview laid over it. */\n selection: ThemeSelection\n appearance: ThemeAppearance\n variant: ThemeVariant\n}\n\nexport type ThemeDocumentOptions = {\n /**\n * Rewrites the custom properties just before they are written: the variant's\n * roles with the accent laid over them. It sees every property of the pass and\n * must return the full map; a name it drops is not written.\n */\n transformTokens?: (tokens: Record<string, string>) => Record<string, string>\n /**\n * The contrast an accent is held to against the variant's background. Absent, a\n * preset is painted verbatim (the catalogue measured it for its own themes) and\n * only a custom `#rrggbb` is held to the default floor of 3:1.\n */\n accentContrastFloor?: number\n}\n\n/**\n * The custom properties a state paints: the variant's roles, with the accent\n * selection laid over them, then `transformTokens`.\n *\n * The accent is applied *here* rather than left to the `[data-accent]` blocks in\n * `theme.css`, because the properties are written inline and an inline declaration\n * outranks every stylesheet selector however specific it is. The blocks still exist\n * and are still correct — they are what styles the window before this runs — but\n * they cannot win from here, so leaving the accent to them meant the attribute was\n * set, the rules were present, and nothing changed on screen.\n */\nexport function themeDocumentTokens(\n state: ThemeDocumentState,\n options: ThemeDocumentOptions = {}\n): { tokens: Record<string, string>; accented: boolean } {\n const properties = { ...themeCssVariables(state.variant) }\n const accent = accentVariables(state.selection.accent, state.appearance, state.selection, {\n background: state.variant.colors.background,\n floor: options.accentContrastFloor,\n variantId: state.variant.id,\n })\n if (accent) Object.assign(properties, accent)\n return {\n tokens: options.transformTokens ? options.transformTokens(properties) : properties,\n accented: accent !== undefined,\n }\n}\n\n/**\n * Write a state onto a root and report which custom properties it owns.\n *\n * Pass the previous call's return as `previous` and any palette property that pass\n * wrote and this one does not is removed. For the catalogue the two sets are the\n * same, so nothing is ever removed; it matters for a host variant that carries\n * `tokens` the catalogue does not, which would otherwise stay inline, beating the\n * stylesheet, after the user switched away from it.\n */\nexport function applyThemeToRoot(\n root: ThemeRoot,\n state: ThemeDocumentState,\n options: ThemeDocumentOptions = {},\n previous?: ReadonlySet<string>\n): ReadonlySet<string> {\n const { tokens, accented } = themeDocumentTokens(state, options)\n\n for (const [name, value] of Object.entries(tokens)) {\n root.style.setProperty(name, value)\n }\n if (previous) {\n for (const name of previous) {\n if (!(name in tokens)) root.style.removeProperty(name)\n }\n }\n\n root.classList.toggle('dark', state.appearance === 'dark')\n root.dataset['theme'] = state.variant.id\n root.dataset['appearance'] = state.appearance\n root.style.colorScheme = state.appearance\n\n // A custom colour has no stylesheet block to select, and its value is the\n // attribute's worst possible content, so it is reported as `custom`.\n if (!accented) root.removeAttribute('data-accent')\n else\n root.dataset['accent'] = isCustomAccent(state.selection.accent)\n ? 'custom'\n : state.selection.accent\n\n applyAppearanceFontSettings(\n root,\n normalizeAppearanceEditorFontSettings(state.selection.fonts, state.selection.font).settings\n )\n\n // Density is a pure token shift, so unlike the colour axes there is nothing to\n // compute: the attribute selects a block in `theme.css`. It is applied here for\n // the same reason as the others — this is the one place that knows what the\n // current selection is, and it is the only writer of these attributes.\n if (resolvedDensity(state.selection) === 'comfortable') root.removeAttribute('data-density')\n else root.dataset['density'] = resolvedDensity(state.selection)\n\n return new Set(Object.keys(tokens))\n}\n\n/** What {@link applyWithoutTransitions} needs of a document. */\nexport type TransitionDocument = {\n head: { appendChild(node: unknown): unknown }\n createElement(tag: 'style'): {\n textContent: string | null\n offsetHeight?: number\n remove(): void\n }\n}\n\n/**\n * Run `apply` with every CSS transition switched off for the frame that carries\n * it, so a theme switch cannot animate page-wide.\n *\n * Without this, flipping the custom properties on `<html>` starts a transition on\n * every element that declares one, and each fades from the old palette to the new\n * over its own duration: a theme change that looks like a rendering fault. The\n * rule is removed two frames later, after the browser has committed the new styles\n * with transitions off, and `restore` runs it early (the provider calls it when it\n * is disposed mid-switch).\n *\n * `schedule` is the frame source; it defaults to `requestAnimationFrame` and falls\n * back to running at once where there is none, which leaves transitions on rather\n * than off.\n */\nexport function applyWithoutTransitions(\n doc: TransitionDocument,\n apply: () => void,\n schedule: (callback: () => void) => void = defaultSchedule\n): () => void {\n const style = doc.createElement('style')\n style.textContent = '*{transition:none !important}'\n doc.head.appendChild(style)\n // Reading a layout property forces the browser to commit the rule before the\n // properties change; without it the two can be batched into one style recalc.\n void style.offsetHeight\n let removed = false\n const restore = () => {\n if (removed) return\n removed = true\n style.remove()\n }\n try {\n apply()\n } catch (error) {\n restore()\n throw error\n }\n schedule(() => schedule(restore))\n return restore\n}\n\nfunction defaultSchedule(callback: () => void): void {\n if (typeof requestAnimationFrame === 'function') requestAnimationFrame(callback)\n else callback()\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAyDA,SAAgB,oBACd,OACA,UAAgC,CAAC,GACsB;CACvD,MAAM,aAAa,EAAE,GAAG,kBAAkB,MAAM,OAAO,EAAE;CACzD,MAAM,SAAS,gBAAgB,MAAM,UAAU,QAAQ,MAAM,YAAY,MAAM,WAAW;EACxF,YAAY,MAAM,QAAQ,OAAO;EACjC,OAAO,QAAQ;EACf,WAAW,MAAM,QAAQ;CAC3B,CAAC;CACD,IAAI,QAAQ,OAAO,OAAO,YAAY,MAAM;CAC5C,OAAO;EACL,QAAQ,QAAQ,kBAAkB,QAAQ,gBAAgB,UAAU,IAAI;EACxE,UAAU,WAAW,KAAA;CACvB;AACF;;;;;;;;;;AAWA,SAAgB,iBACd,MACA,OACA,UAAgC,CAAC,GACjC,UACqB;CACrB,MAAM,EAAE,QAAQ,aAAa,oBAAoB,OAAO,OAAO;CAE/D,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,MAAM,GAC/C,KAAK,MAAM,YAAY,MAAM,KAAK;CAEpC,IAAI,UACG;OAAA,MAAM,QAAQ,UACjB,IAAI,EAAE,QAAQ,SAAS,KAAK,MAAM,eAAe,IAAI;CAAA;CAIzD,KAAK,UAAU,OAAO,QAAQ,MAAM,eAAe,MAAM;CACzD,KAAK,QAAQ,WAAW,MAAM,QAAQ;CACtC,KAAK,QAAQ,gBAAgB,MAAM;CACnC,KAAK,MAAM,cAAc,MAAM;CAI/B,IAAI,CAAC,UAAU,KAAK,gBAAgB,aAAa;MAE/C,KAAK,QAAQ,YAAY,eAAe,MAAM,UAAU,MAAM,IAC1D,WACA,MAAM,UAAU;CAEtB,4BACE,MACA,sCAAsC,MAAM,UAAU,OAAO,MAAM,UAAU,IAAI,CAAC,CAAC,QACrF;CAMA,IAAI,gBAAgB,MAAM,SAAS,MAAM,eAAe,KAAK,gBAAgB,cAAc;MACtF,KAAK,QAAQ,aAAa,gBAAgB,MAAM,SAAS;CAE9D,OAAO,IAAI,IAAI,OAAO,KAAK,MAAM,CAAC;AACpC;;;;;;;;;;;;;;;;AA2BA,SAAgB,wBACd,KACA,OACA,WAA2C,iBAC/B;CACZ,MAAM,QAAQ,IAAI,cAAc,OAAO;CACvC,MAAM,cAAc;CACpB,IAAI,KAAK,YAAY,KAAK;CAG1B,MAAW;CACX,IAAI,UAAU;CACd,MAAM,gBAAgB;EACpB,IAAI,SAAS;EACb,UAAU;EACV,MAAM,OAAO;CACf;CACA,IAAI;EACF,MAAM;CACR,SAAS,OAAO;EACd,QAAQ;EACR,MAAM;CACR;CACA,eAAe,SAAS,OAAO,CAAC;CAChC,OAAO;AACT;AAEA,SAAS,gBAAgB,UAA4B;CACnD,IAAI,OAAO,0BAA0B,YAAY,sBAAsB,QAAQ;MAC1E,SAAS;AAChB"}
|
|
1
|
+
{"version":3,"file":"theme-document.js","names":[],"sources":["../../../src/components/theme/theme-document.ts"],"sourcesContent":["import {\n applyAppearanceFontSettings,\n normalizeAppearanceEditorFontSettings,\n} from '#lib/appearance-font-settings'\nimport {\n accentVariables,\n isCatalogueVariant,\n terminalCssVariables,\n themeCssVariables,\n type EditorRolesMode,\n type ThemeAppearance,\n type ThemeVariant,\n} from '#lib/themes'\nimport { isCustomAccent } from '#lib/accent-roles'\nimport { surfaceAlpha, type SurfaceState } from '#lib/theme-surface'\nimport { resolvedDensity, type ThemeSelection } from './theme-state'\n\n/**\n * The document half of the theme provider: the single writer of `<html>`.\n *\n * Written against the few members of an element it uses, so the rules can be tested\n * on a stand-in and the provider's effect stays a one-line call.\n */\nexport type ThemeRoot = Pick<\n HTMLElement,\n 'classList' | 'dataset' | 'style' | 'removeAttribute' | 'setAttribute'\n>\n\nexport type ThemeDocumentState = {\n /** What the document shows: the committed selection, or a preview laid over it. */\n selection: ThemeSelection\n appearance: ThemeAppearance\n variant: ThemeVariant\n /**\n * The variant whose terminal palette is pinned over the interface variant's.\n * Absent, the terminal follows the interface theme.\n */\n terminalVariant?: ThemeVariant\n /**\n * The resolved surface axis. Absent, the axis is off: no attribute and no\n * `--surface-alpha` is written, and any this writer wrote before is removed.\n */\n surface?: SurfaceState\n}\n\nexport type ThemeDocumentOptions = {\n /**\n * Rewrites the custom properties just before they are written: the variant's\n * roles with the accent laid over them. It sees every property of the pass and\n * must return the full map; a name it drops is not written.\n */\n transformTokens?: (tokens: Record<string, string>) => Record<string, string>\n /**\n * The contrast an accent is held to against the variant's background. Absent, a\n * preset is painted verbatim (the catalogue measured it for its own themes) and\n * only a custom `#rrggbb` is held to the default floor of 3:1.\n */\n accentContrastFloor?: number\n /**\n * Where a catalogue variant's palette comes from. `inline` (the default) writes\n * every role onto the root. `stylesheet` writes none for a catalogue variant,\n * because the host loads a stylesheet that carries a `[data-theme]` block for it;\n * a variant the catalogue does not hold (a host's imported theme) is still written\n * inline, since no stylesheet can know it. The accent, the terminal pin, the\n * surface alpha and the fonts are written the same in both.\n *\n * Switching from an imported variant back to a catalogue one removes what the\n * imported one wrote, so nothing inline outlives it to beat the stylesheet.\n */\n palette?: 'inline' | 'stylesheet'\n /**\n * The `--editor-*` projection. `syntax` (the default) is the catalogue's quiet\n * syntax roles, which is what `theme-variants.css` carries; `floor` holds every\n * editor role to 4.5:1 on the canvas, as hex.\n *\n * With `palette: 'stylesheet'`, `floor` is the one palette write a catalogue\n * variant still gets: the stylesheet is generated for the default projection, so\n * the sixteen `--editor-*` properties are written inline over it. The rest of the\n * palette stays in the stylesheet.\n */\n editorRoles?: EditorRolesMode\n}\n\n/**\n * What a catalogue variant still writes inline when its palette is the stylesheet's:\n * nothing, unless the editor projection is not the one the stylesheet was generated\n * for.\n */\nfunction stylesheetOverrides(\n variant: ThemeVariant,\n editorRoles: EditorRolesMode | undefined\n): Record<string, string> {\n if (editorRoles !== 'floor') return {}\n return Object.fromEntries(\n Object.entries(themeCssVariables(variant, { editor: 'floor' })).filter(([name]) =>\n name.startsWith('--editor-')\n )\n )\n}\n\n/**\n * The custom properties a state paints: the variant's roles, with the accent\n * selection laid over them, the pinned terminal palette over that, the surface\n * alpha, then `transformTokens`.\n *\n * The accent is applied *here* rather than left to the `[data-accent]` blocks in\n * `theme.css`, because the properties are written inline and an inline declaration\n * outranks every stylesheet selector however specific it is. The blocks still exist\n * and are still correct — they are what styles the window before this runs — but\n * they cannot win from here, so leaving the accent to them meant the attribute was\n * set, the rules were present, and nothing changed on screen.\n */\nexport function themeDocumentTokens(\n state: ThemeDocumentState,\n options: ThemeDocumentOptions = {}\n): { tokens: Record<string, string>; accented: boolean } {\n const fromStylesheet = options.palette === 'stylesheet' && isCatalogueVariant(state.variant)\n const properties = fromStylesheet\n ? stylesheetOverrides(state.variant, options.editorRoles)\n : { ...themeCssVariables(state.variant, { editor: options.editorRoles }) }\n const accent = accentVariables(state.selection.accent, state.appearance, state.selection, {\n background: state.variant.colors.background,\n floor: options.accentContrastFloor,\n variantId: state.variant.id,\n })\n if (accent) Object.assign(properties, accent)\n // Written after the variant and the accent, so it wins over the interface\n // variant's own terminal ramp (and over the stylesheet's, which it outranks as an\n // inline declaration). Dropping the pin needs no removal of its own: the ramp is\n // then rewritten by the variant, or swept back to the stylesheet.\n if (state.terminalVariant) Object.assign(properties, terminalCssVariables(state.terminalVariant))\n if (state.surface) properties['--surface-alpha'] = surfaceAlpha[state.surface.surface]\n return {\n tokens: options.transformTokens ? options.transformTokens(properties) : properties,\n accented: accent !== undefined,\n }\n}\n\n/**\n * Write a state onto a root and report which custom properties it owns.\n *\n * Pass the previous call's return as `previous` and any palette property that pass\n * wrote and this one does not is removed. For the catalogue the two sets are the\n * same, so nothing is ever removed; it matters for a host variant that carries\n * `tokens` the catalogue does not, which would otherwise stay inline, beating the\n * stylesheet, after the user switched away from it.\n */\nexport function applyThemeToRoot(\n root: ThemeRoot,\n state: ThemeDocumentState,\n options: ThemeDocumentOptions = {},\n previous?: ReadonlySet<string>\n): ReadonlySet<string> {\n const { tokens, accented } = themeDocumentTokens(state, options)\n\n for (const [name, value] of Object.entries(tokens)) {\n root.style.setProperty(name, value)\n }\n if (previous) {\n for (const name of previous) {\n if (!(name in tokens)) root.style.removeProperty(name)\n }\n }\n\n root.classList.toggle('dark', state.appearance === 'dark')\n root.dataset['theme'] = state.variant.id\n root.dataset['appearance'] = state.appearance\n root.style.colorScheme = state.appearance\n\n // A custom colour has no stylesheet block to select, and its value is the\n // attribute's worst possible content, so it is reported as `custom`.\n if (!accented) root.removeAttribute('data-accent')\n else\n root.dataset['accent'] = isCustomAccent(state.selection.accent)\n ? 'custom'\n : state.selection.accent\n\n // The surface axis is opt-in, so its absence removes rather than defaults: a host\n // that never enables it keeps an `<html>` with neither attribute, and one that\n // turns it off leaves nothing behind. (`--surface-alpha` is swept with the tokens.)\n if (state.surface) {\n root.dataset['surface'] = state.surface.surface\n root.dataset['reduceTransparency'] = state.surface.reduceTransparencyActive ? 'true' : 'false'\n } else {\n root.removeAttribute('data-surface')\n root.removeAttribute('data-reduce-transparency')\n }\n\n applyAppearanceFontSettings(\n root,\n normalizeAppearanceEditorFontSettings(state.selection.fonts, state.selection.font).settings\n )\n\n // Density is a pure token shift, so unlike the colour axes there is nothing to\n // compute: the attribute selects a block in `theme.css`. It is applied here for\n // the same reason as the others — this is the one place that knows what the\n // current selection is, and it is the only writer of these attributes.\n if (resolvedDensity(state.selection) === 'comfortable') root.removeAttribute('data-density')\n else root.dataset['density'] = resolvedDensity(state.selection)\n\n return new Set(Object.keys(tokens))\n}\n\n/** What {@link applyWithoutTransitions} needs of a document. */\nexport type TransitionDocument = {\n head: { appendChild(node: unknown): unknown }\n createElement(tag: 'style'): {\n textContent: string | null\n offsetHeight?: number\n remove(): void\n }\n}\n\n/**\n * Run `apply` with every CSS transition switched off for the frame that carries\n * it, so a theme switch cannot animate page-wide.\n *\n * Without this, flipping the custom properties on `<html>` starts a transition on\n * every element that declares one, and each fades from the old palette to the new\n * over its own duration: a theme change that looks like a rendering fault. The\n * rule is removed two frames later, after the browser has committed the new styles\n * with transitions off, and `restore` runs it early (the provider calls it when it\n * is disposed mid-switch).\n *\n * `schedule` is the frame source; it defaults to `requestAnimationFrame` and falls\n * back to running at once where there is none, which leaves transitions on rather\n * than off.\n */\nexport function applyWithoutTransitions(\n doc: TransitionDocument,\n apply: () => void,\n schedule: (callback: () => void) => void = defaultSchedule\n): () => void {\n const style = doc.createElement('style')\n style.textContent = '*{transition:none !important}'\n doc.head.appendChild(style)\n // Reading a layout property forces the browser to commit the rule before the\n // properties change; without it the two can be batched into one style recalc.\n void style.offsetHeight\n let removed = false\n const restore = () => {\n if (removed) return\n removed = true\n style.remove()\n }\n try {\n apply()\n } catch (error) {\n restore()\n throw error\n }\n schedule(() => schedule(restore))\n return restore\n}\n\nfunction defaultSchedule(callback: () => void): void {\n if (typeof requestAnimationFrame === 'function') requestAnimationFrame(callback)\n else callback()\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PreferenceStorage } from '#lib/preference-store';
|
|
1
2
|
import { type ThemeSelection } from './theme-state';
|
|
2
3
|
/**
|
|
3
4
|
* Where a selection is kept between sessions.
|
|
@@ -47,4 +48,90 @@ export declare function localStoragePersistence(key: string | (() => string)): T
|
|
|
47
48
|
export declare function readPersisted(persistence: ThemePersistence): Partial<ThemeSelection> | undefined;
|
|
48
49
|
/** Hand a selection to the store; a store that throws loses the preference, not the switch. */
|
|
49
50
|
export declare function writePersisted(persistence: ThemePersistence, next: ThemeSelection): void;
|
|
51
|
+
/**
|
|
52
|
+
* Where a host's versioned preference document lives. One schema describes the
|
|
53
|
+
* document to both halves of the system: {@link createVersionedPersistence} reads and
|
|
54
|
+
* writes it, and `themeScript(options)` reads it before first paint, so the two
|
|
55
|
+
* cannot disagree about a key or a version.
|
|
56
|
+
*/
|
|
57
|
+
export type ThemeStorageSchema = {
|
|
58
|
+
/** The storage key of the document. */
|
|
59
|
+
key: string;
|
|
60
|
+
/**
|
|
61
|
+
* The only top-level `version` read. Absent, the document is unversioned. A record
|
|
62
|
+
* of any other version is quarantined (see `recoveryKey`), not interpreted.
|
|
63
|
+
*/
|
|
64
|
+
version?: number;
|
|
65
|
+
/**
|
|
66
|
+
* The document field that holds the appearance preference (`system`, `light`,
|
|
67
|
+
* `dark`). Read by `themeScript`; the persistence leaves field names to the codec.
|
|
68
|
+
* Default `appearance`.
|
|
69
|
+
*/
|
|
70
|
+
appearanceField?: 'appearance' | 'mode';
|
|
71
|
+
/**
|
|
72
|
+
* An older single-value key (`light` | `dark` | `system`) read only while `key` is
|
|
73
|
+
* absent, and never deleted. The persistence hands it to the codec's `fromLegacy`.
|
|
74
|
+
*/
|
|
75
|
+
legacyAppearanceKey?: string;
|
|
76
|
+
/**
|
|
77
|
+
* The stored library of imported themes, for `themeScript` only: an imported
|
|
78
|
+
* theme's id and verbatim tokens, so the first paint can use the right variant. The
|
|
79
|
+
* persistence neither reads nor writes it.
|
|
80
|
+
*/
|
|
81
|
+
libraryKey?: string;
|
|
82
|
+
/** Where an unreadable document is kept. Default `${key}.recovery`. */
|
|
83
|
+
recoveryKey?: string;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* How a host's document maps to and from the shared {@link ThemeSelection}.
|
|
87
|
+
*
|
|
88
|
+
* The codec is the product schema: it decides which fields a document has, how an
|
|
89
|
+
* unknown value is corrected, and what a fresh install looks like. The engine decides
|
|
90
|
+
* everything about *storage*: what is quarantined, when the legacy key is read, and
|
|
91
|
+
* that nothing is written until `setSelection` asks.
|
|
92
|
+
*/
|
|
93
|
+
export type VersionedCodec<Doc> = {
|
|
94
|
+
/**
|
|
95
|
+
* A readable record (the right JSON shape and version) as a selection patch. Fields
|
|
96
|
+
* it leaves `undefined` fall back to the provider's defaults. It may keep an id the
|
|
97
|
+
* catalogue lacks; the provider resolves it, it does not rewrite it.
|
|
98
|
+
*/
|
|
99
|
+
decode(record: Readonly<Record<string, unknown>>): Partial<ThemeSelection>;
|
|
100
|
+
/**
|
|
101
|
+
* The legacy key's value (never `null`) as a selection patch, or `undefined` for a
|
|
102
|
+
* value it does not recognise. Called only when the main key is absent.
|
|
103
|
+
*/
|
|
104
|
+
fromLegacy?(value: string): Partial<ThemeSelection> | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* What a store with nothing readable answers: a fresh install, an unreadable
|
|
107
|
+
* document, a blocked store. Absent, the provider's own defaults apply.
|
|
108
|
+
*/
|
|
109
|
+
defaults?(): Partial<ThemeSelection> | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* The document to store for a selection. `previous` is the record last read or
|
|
112
|
+
* written, so a codec can carry a field the selection does not model.
|
|
113
|
+
*/
|
|
114
|
+
encode(next: ThemeSelection, previous?: Readonly<Record<string, unknown>>): Doc;
|
|
115
|
+
};
|
|
116
|
+
/** Seams for the storage and the clock; both default to the real ones. */
|
|
117
|
+
export type VersionedPersistenceOptions = {
|
|
118
|
+
/** The store, or nothing. Called per access; default is `localStorage` (blocked-safe). */
|
|
119
|
+
storage?: () => PreferenceStorage | undefined;
|
|
120
|
+
/** The clock for the recovery envelope's `capturedAt`. */
|
|
121
|
+
now?: () => Date;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* A {@link ThemePersistence} over a versioned document and a host codec.
|
|
125
|
+
*
|
|
126
|
+
* `read` follows the engine's rules (`lib/preference-store.ts`): the main key first;
|
|
127
|
+
* an unreadable or unsupported document is quarantined byte for byte into the
|
|
128
|
+
* recovery key and the codec's `defaults` answer; the legacy key only when the main
|
|
129
|
+
* key is absent. It never writes the main key, so the first write is a user's
|
|
130
|
+
* decision. `write` stores `codec.encode(next, previous)` as JSON and swallows a
|
|
131
|
+
* failure.
|
|
132
|
+
*
|
|
133
|
+
* Neither method throws. The stored bytes are exactly what the codec encodes, so a
|
|
134
|
+
* host keeps its keys, field names and bytes by keeping its codec.
|
|
135
|
+
*/
|
|
136
|
+
export declare function createVersionedPersistence<Doc>(schema: ThemeStorageSchema, codec: VersionedCodec<Doc>, options?: VersionedPersistenceOptions): ThemePersistence;
|
|
50
137
|
//# sourceMappingURL=theme-persistence.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme-persistence.d.ts","sourceRoot":"","sources":["../../../src/components/theme/theme-persistence.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"theme-persistence.d.ts","sourceRoot":"","sources":["../../../src/components/theme/theme-persistence.ts"],"names":[],"mappings":"AAKA,OAAO,EAML,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAA0C,KAAK,cAAc,EAAE,MAAM,eAAe,CAAA;AAE3F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,IAAI,OAAO,CAAC,cAAc,CAAC,GAAG,SAAS,CAAA;IAC3C,KAAK,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAAA;CAClC,CAAA;AAyED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,GAAG,gBAAgB,CAatF;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,CAAC,GAAG,SAAS,CAMhG;AAED,+FAA+F;AAC/F,wBAAgB,cAAc,CAAC,WAAW,EAAE,gBAAgB,EAAE,IAAI,EAAE,cAAc,GAAG,IAAI,CAOxF;AAED;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,uCAAuC;IACvC,GAAG,EAAE,MAAM,CAAA;IACX;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,eAAe,CAAC,EAAE,YAAY,GAAG,MAAM,CAAA;IACvC;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,CAAC,GAAG,IAAI;IAChC;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;IAC1E;;;OAGG;IACH,UAAU,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,GAAG,SAAS,CAAA;IAC/D;;;OAGG;IACH,QAAQ,CAAC,IAAI,OAAO,CAAC,cAAc,CAAC,GAAG,SAAS,CAAA;IAChD;;;OAGG;IACH,MAAM,CAAC,IAAI,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,GAAG,CAAA;CAChF,CAAA;AAED,0EAA0E;AAC1E,MAAM,MAAM,2BAA2B,GAAG;IACxC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,iBAAiB,GAAG,SAAS,CAAA;IAC7C,0DAA0D;IAC1D,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;CACjB,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAC5C,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,cAAc,CAAC,GAAG,CAAC,EAC1B,OAAO,GAAE,2BAAgC,GACxC,gBAAgB,CAqDlB"}
|
|
@@ -3,7 +3,9 @@ import { normalizeCustomAccent } from "../../lib/accent-roles.js";
|
|
|
3
3
|
import { isThemeAccentId } from "../../lib/theme-accents.js";
|
|
4
4
|
import { defaultDarkThemeId, defaultLightThemeId, themeById } from "../../lib/themes.js";
|
|
5
5
|
import { normalizeAppearanceEditorFontSettings } from "../../lib/appearance-font-settings.js";
|
|
6
|
+
import { normalizeSurface } from "../../lib/theme-surface.js";
|
|
6
7
|
import { defaultThemeSelection, resolvedDensity } from "./theme-state.js";
|
|
8
|
+
import { defaultPreferenceStorage, readLegacyValue, readVersionedRecord, recoveryKeyFor, writeVersionedRecord } from "../../lib/preference-store.js";
|
|
7
9
|
//#region src/components/theme/theme-persistence.ts
|
|
8
10
|
/**
|
|
9
11
|
* The preference store, or nothing.
|
|
@@ -31,7 +33,10 @@ function readSelection(storageKey) {
|
|
|
31
33
|
accent: typeof parsed.accent === "string" && (accentPresets.some((preset) => preset.id === parsed.accent) || isThemeAccentId(parsed.accent)) ? parsed.accent : normalizeCustomAccent(parsed.accent) ?? defaultThemeSelection.accent,
|
|
32
34
|
font: fonts.ui.family,
|
|
33
35
|
fonts,
|
|
34
|
-
density: resolvedDensity(parsed)
|
|
36
|
+
density: resolvedDensity(parsed),
|
|
37
|
+
terminalThemeId: typeof parsed.terminalThemeId === "string" && parsed.terminalThemeId.trim() !== "" ? parsed.terminalThemeId : void 0,
|
|
38
|
+
surface: normalizeSurface(parsed.surface),
|
|
39
|
+
reduceTransparency: typeof parsed.reduceTransparency === "boolean" ? parsed.reduceTransparency : void 0
|
|
35
40
|
};
|
|
36
41
|
} catch {
|
|
37
42
|
return defaultThemeSelection;
|
|
@@ -81,7 +86,63 @@ function writePersisted(persistence, next) {
|
|
|
81
86
|
persistence.write(next);
|
|
82
87
|
} catch {}
|
|
83
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* A {@link ThemePersistence} over a versioned document and a host codec.
|
|
91
|
+
*
|
|
92
|
+
* `read` follows the engine's rules (`lib/preference-store.ts`): the main key first;
|
|
93
|
+
* an unreadable or unsupported document is quarantined byte for byte into the
|
|
94
|
+
* recovery key and the codec's `defaults` answer; the legacy key only when the main
|
|
95
|
+
* key is absent. It never writes the main key, so the first write is a user's
|
|
96
|
+
* decision. `write` stores `codec.encode(next, previous)` as JSON and swallows a
|
|
97
|
+
* failure.
|
|
98
|
+
*
|
|
99
|
+
* Neither method throws. The stored bytes are exactly what the codec encodes, so a
|
|
100
|
+
* host keeps its keys, field names and bytes by keeping its codec.
|
|
101
|
+
*/
|
|
102
|
+
function createVersionedPersistence(schema, codec, options = {}) {
|
|
103
|
+
const storageOf = options.storage ?? defaultPreferenceStorage;
|
|
104
|
+
const recordSchema = {
|
|
105
|
+
key: schema.key,
|
|
106
|
+
version: schema.version,
|
|
107
|
+
recoveryKey: recoveryKeyFor(schema)
|
|
108
|
+
};
|
|
109
|
+
let previous;
|
|
110
|
+
const fallback = () => {
|
|
111
|
+
try {
|
|
112
|
+
return codec.defaults?.();
|
|
113
|
+
} catch {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
read() {
|
|
119
|
+
const storage = storageOf();
|
|
120
|
+
if (!storage) return fallback();
|
|
121
|
+
try {
|
|
122
|
+
const found = readVersionedRecord(storage, recordSchema, options.now);
|
|
123
|
+
if (found.status === "record") {
|
|
124
|
+
previous = found.record;
|
|
125
|
+
return codec.decode(found.record);
|
|
126
|
+
}
|
|
127
|
+
if (found.status === "absent" && schema.legacyAppearanceKey && codec.fromLegacy) {
|
|
128
|
+
const legacy = readLegacyValue(storage, schema.legacyAppearanceKey);
|
|
129
|
+
const migrated = legacy === null ? void 0 : codec.fromLegacy(legacy);
|
|
130
|
+
if (migrated) return migrated;
|
|
131
|
+
}
|
|
132
|
+
} catch {}
|
|
133
|
+
return fallback();
|
|
134
|
+
},
|
|
135
|
+
write(next) {
|
|
136
|
+
const storage = storageOf();
|
|
137
|
+
if (!storage) return;
|
|
138
|
+
try {
|
|
139
|
+
const document = codec.encode(next, previous);
|
|
140
|
+
if (writeVersionedRecord(storage, schema.key, document)) previous = typeof document === "object" && document !== null ? document : void 0;
|
|
141
|
+
} catch {}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
84
145
|
//#endregion
|
|
85
|
-
export { localStoragePersistence, readPersisted, writePersisted };
|
|
146
|
+
export { createVersionedPersistence, localStoragePersistence, readPersisted, writePersisted };
|
|
86
147
|
|
|
87
148
|
//# sourceMappingURL=theme-persistence.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme-persistence.js","names":[],"sources":["../../../src/components/theme/theme-persistence.ts"],"sourcesContent":["import { normalizeCustomAccent } from '#lib/accent-roles'\nimport { accentPresets } from '#lib/tokens'\nimport { normalizeAppearanceEditorFontSettings } from '#lib/appearance-font-settings'\nimport { defaultDarkThemeId, defaultLightThemeId, isThemeAccentId, themeById } from '#lib/themes'\nimport { defaultThemeSelection, resolvedDensity, type ThemeSelection } from './theme-state'\n\n/**\n * Where a selection is kept between sessions.\n *\n * The provider never touches storage itself; it asks a `ThemePersistence`. The\n * default is {@link localStoragePersistence}, which is what the provider always did.\n * A host that stores the preference in a different shape, under a versioned\n * envelope, or somewhere that is not `localStorage` supplies its own and the\n * resolution and application rules stay the shared ones.\n *\n * The contract is deliberately narrow, so that an implementation cannot make the\n * provider less safe than it was:\n *\n * - `read` is synchronous and runs once, when the provider is created. It may\n * repair or quarantine what it finds, and it must not throw: the provider\n * treats a throw as \"nothing stored\" and starts from the defaults.\n * - `write` runs only from `setSelection`. Never on mount, never from `preview`,\n * never as a side effect of reading, so the first write to a store is always a\n * user's decision. A throw is swallowed, because a full or blocked store must\n * not break the switch.\n */\nexport type ThemePersistence = {\n read(): Partial<ThemeSelection> | undefined\n write(next: ThemeSelection): void\n}\n\n/**\n * The preference store, or nothing.\n *\n * Only ever called inside a `try`: where storage is blocked — a sandboxed iframe, a\n * privacy mode, a denied site-data permission — merely *reading* the `localStorage`\n * global throws (\"Access is denied\", `SecurityError`), so even the `typeof` guard\n * has to sit inside the `try` rather than in front of it. Without a store the\n * preference lives only in the provider's signal for the session.\n */\nfunction preferenceStore(): Storage | undefined {\n return typeof localStorage === 'undefined' ? undefined : localStorage\n}\n\n/** The stored preference, validated against the catalogue. */\nfunction readSelection(storageKey: string): ThemeSelection {\n try {\n const raw = preferenceStore()?.getItem(storageKey)\n if (!raw) return defaultThemeSelection\n const parsed = JSON.parse(raw) as Partial<ThemeSelection>\n const fonts = normalizeAppearanceEditorFontSettings(parsed.fonts, parsed.font).settings\n return {\n appearance:\n parsed.appearance === 'light' ||\n parsed.appearance === 'dark' ||\n parsed.appearance === 'system'\n ? parsed.appearance\n : defaultThemeSelection.appearance,\n // An unknown id would leave the document with no theme at all, so a stale\n // preference from an older catalogue falls back rather than applying nothing.\n lightThemeId:\n themeById(parsed.lightThemeId ?? '')?.appearance === 'light'\n ? (parsed.lightThemeId as string)\n : defaultLightThemeId,\n darkThemeId:\n themeById(parsed.darkThemeId ?? '')?.appearance === 'dark'\n ? (parsed.darkThemeId as string)\n : defaultDarkThemeId,\n // A theme accent is kept whenever it names a slot, even one the stored pair\n // cannot offer: the id is role-shaped so it survives a theme switch, and an\n // unoffered slot already resolves to the theme's own primary when applied. A\n // custom `#rrggbb` is kept in its one stored spelling; its readability is\n // settled when it is painted, not when it is read.\n accent:\n typeof parsed.accent === 'string' &&\n (accentPresets.some((preset) => preset.id === parsed.accent) ||\n isThemeAccentId(parsed.accent))\n ? parsed.accent\n : (normalizeCustomAccent(parsed.accent) ?? defaultThemeSelection.accent),\n font: fonts.ui.family,\n fonts,\n // Every axis has to be read back here, or it is lost twice: the provider's\n // document effect removes the `data-density` that `themeScript` set for the first paint, and\n // the next `setSelection` rewrites the store without it.\n density: resolvedDensity(parsed as ThemeSelection),\n }\n } catch {\n return defaultThemeSelection\n }\n}\n\n/**\n * The default persistence: one JSON document in `localStorage` under `key`.\n *\n * `read` is the provider's original `readSelection`, moved here unchanged — every\n * stored id is validated against the catalogue, so a stale preference falls back to\n * a default rather than reaching the document. `key` may be a function so a\n * provider whose `storageKey` prop changes writes to the key it is now given.\n *\n * Blocked, full or absent storage is survivable in both directions: a read answers\n * with the defaults and a write is dropped, so the preference simply lasts the\n * session.\n */\nexport function localStoragePersistence(key: string | (() => string)): ThemePersistence {\n const keyOf = () => (typeof key === 'function' ? key() : key)\n return {\n read: () => readSelection(keyOf()),\n write(next) {\n try {\n preferenceStore()?.setItem(keyOf(), JSON.stringify(next))\n } catch {\n // A full, blocked or unavailable store must not break the switch: the\n // preference simply does not survive the reload.\n }\n },\n }\n}\n\n/**\n * What a persistence knows, or nothing.\n *\n * The seam's `read` must not throw, and the provider does not rely on that: a host\n * persistence is code the design system has not seen, and a theme provider that\n * fails to mount because a store misbehaved has turned a lost preference into a\n * blank application.\n */\nexport function readPersisted(persistence: ThemePersistence): Partial<ThemeSelection> | undefined {\n try {\n return persistence.read()\n } catch {\n return undefined\n }\n}\n\n/** Hand a selection to the store; a store that throws loses the preference, not the switch. */\nexport function writePersisted(persistence: ThemePersistence, next: ThemeSelection): void {\n try {\n persistence.write(next)\n } catch {\n // Same contract as a full or blocked `localStorage`: the choice holds for the\n // session and does not survive the reload.\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAwCA,SAAS,kBAAuC;CAC9C,OAAO,OAAO,iBAAiB,cAAc,KAAA,IAAY;AAC3D;;AAGA,SAAS,cAAc,YAAoC;CACzD,IAAI;EACF,MAAM,MAAM,gBAAgB,CAAC,EAAE,QAAQ,UAAU;EACjD,IAAI,CAAC,KAAK,OAAO;EACjB,MAAM,SAAS,KAAK,MAAM,GAAG;EAC7B,MAAM,QAAQ,sCAAsC,OAAO,OAAO,OAAO,IAAI,CAAC,CAAC;EAC/E,OAAO;GACL,YACE,OAAO,eAAe,WACtB,OAAO,eAAe,UACtB,OAAO,eAAe,WAClB,OAAO,aACP,sBAAsB;GAG5B,cACE,UAAU,OAAO,gBAAgB,EAAE,CAAC,EAAE,eAAe,UAChD,OAAO,eACR;GACN,aACE,UAAU,OAAO,eAAe,EAAE,CAAC,EAAE,eAAe,SAC/C,OAAO,cACR;GAMN,QACE,OAAO,OAAO,WAAW,aACxB,cAAc,MAAM,WAAW,OAAO,OAAO,OAAO,MAAM,KACzD,gBAAgB,OAAO,MAAM,KAC3B,OAAO,SACN,sBAAsB,OAAO,MAAM,KAAK,sBAAsB;GACrE,MAAM,MAAM,GAAG;GACf;GAIA,SAAS,gBAAgB,MAAwB;EACnD;CACF,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;;;AAcA,SAAgB,wBAAwB,KAAgD;CACtF,MAAM,cAAe,OAAO,QAAQ,aAAa,IAAI,IAAI;CACzD,OAAO;EACL,YAAY,cAAc,MAAM,CAAC;EACjC,MAAM,MAAM;GACV,IAAI;IACF,gBAAgB,CAAC,EAAE,QAAQ,MAAM,GAAG,KAAK,UAAU,IAAI,CAAC;GAC1D,QAAQ,CAGR;EACF;CACF;AACF;;;;;;;;;AAUA,SAAgB,cAAc,aAAoE;CAChG,IAAI;EACF,OAAO,YAAY,KAAK;CAC1B,QAAQ;EACN;CACF;AACF;;AAGA,SAAgB,eAAe,aAA+B,MAA4B;CACxF,IAAI;EACF,YAAY,MAAM,IAAI;CACxB,QAAQ,CAGR;AACF"}
|
|
1
|
+
{"version":3,"file":"theme-persistence.js","names":[],"sources":["../../../src/components/theme/theme-persistence.ts"],"sourcesContent":["import { normalizeCustomAccent } from '#lib/accent-roles'\nimport { accentPresets } from '#lib/tokens'\nimport { normalizeAppearanceEditorFontSettings } from '#lib/appearance-font-settings'\nimport { defaultDarkThemeId, defaultLightThemeId, isThemeAccentId, themeById } from '#lib/themes'\nimport { normalizeSurface } from '#lib/theme-surface'\nimport {\n defaultPreferenceStorage,\n readLegacyValue,\n readVersionedRecord,\n recoveryKeyFor,\n writeVersionedRecord,\n type PreferenceStorage,\n} from '#lib/preference-store'\nimport { defaultThemeSelection, resolvedDensity, type ThemeSelection } from './theme-state'\n\n/**\n * Where a selection is kept between sessions.\n *\n * The provider never touches storage itself; it asks a `ThemePersistence`. The\n * default is {@link localStoragePersistence}, which is what the provider always did.\n * A host that stores the preference in a different shape, under a versioned\n * envelope, or somewhere that is not `localStorage` supplies its own and the\n * resolution and application rules stay the shared ones.\n *\n * The contract is deliberately narrow, so that an implementation cannot make the\n * provider less safe than it was:\n *\n * - `read` is synchronous and runs once, when the provider is created. It may\n * repair or quarantine what it finds, and it must not throw: the provider\n * treats a throw as \"nothing stored\" and starts from the defaults.\n * - `write` runs only from `setSelection`. Never on mount, never from `preview`,\n * never as a side effect of reading, so the first write to a store is always a\n * user's decision. A throw is swallowed, because a full or blocked store must\n * not break the switch.\n */\nexport type ThemePersistence = {\n read(): Partial<ThemeSelection> | undefined\n write(next: ThemeSelection): void\n}\n\n/**\n * The preference store, or nothing.\n *\n * Only ever called inside a `try`: where storage is blocked — a sandboxed iframe, a\n * privacy mode, a denied site-data permission — merely *reading* the `localStorage`\n * global throws (\"Access is denied\", `SecurityError`), so even the `typeof` guard\n * has to sit inside the `try` rather than in front of it. Without a store the\n * preference lives only in the provider's signal for the session.\n */\nfunction preferenceStore(): Storage | undefined {\n return typeof localStorage === 'undefined' ? undefined : localStorage\n}\n\n/** The stored preference, validated against the catalogue. */\nfunction readSelection(storageKey: string): ThemeSelection {\n try {\n const raw = preferenceStore()?.getItem(storageKey)\n if (!raw) return defaultThemeSelection\n const parsed = JSON.parse(raw) as Partial<ThemeSelection>\n const fonts = normalizeAppearanceEditorFontSettings(parsed.fonts, parsed.font).settings\n return {\n appearance:\n parsed.appearance === 'light' ||\n parsed.appearance === 'dark' ||\n parsed.appearance === 'system'\n ? parsed.appearance\n : defaultThemeSelection.appearance,\n // An unknown id would leave the document with no theme at all, so a stale\n // preference from an older catalogue falls back rather than applying nothing.\n lightThemeId:\n themeById(parsed.lightThemeId ?? '')?.appearance === 'light'\n ? (parsed.lightThemeId as string)\n : defaultLightThemeId,\n darkThemeId:\n themeById(parsed.darkThemeId ?? '')?.appearance === 'dark'\n ? (parsed.darkThemeId as string)\n : defaultDarkThemeId,\n // A theme accent is kept whenever it names a slot, even one the stored pair\n // cannot offer: the id is role-shaped so it survives a theme switch, and an\n // unoffered slot already resolves to the theme's own primary when applied. A\n // custom `#rrggbb` is kept in its one stored spelling; its readability is\n // settled when it is painted, not when it is read.\n accent:\n typeof parsed.accent === 'string' &&\n (accentPresets.some((preset) => preset.id === parsed.accent) ||\n isThemeAccentId(parsed.accent))\n ? parsed.accent\n : (normalizeCustomAccent(parsed.accent) ?? defaultThemeSelection.accent),\n font: fonts.ui.family,\n fonts,\n // Every axis has to be read back here, or it is lost twice: the provider's\n // document effect removes the `data-density` that `themeScript` set for the first paint, and\n // the next `setSelection` rewrites the store without it.\n density: resolvedDensity(parsed as ThemeSelection),\n // The newer axes are read back only when the store holds a usable value and are\n // otherwise left `undefined`, never defaulted: `JSON.stringify` drops them, so a\n // consumer that never sets one writes the bytes it always did. An id is kept as\n // stored, known or not, for the same reason a theme id the registry lacks is.\n terminalThemeId:\n typeof parsed.terminalThemeId === 'string' && parsed.terminalThemeId.trim() !== ''\n ? parsed.terminalThemeId\n : undefined,\n surface: normalizeSurface(parsed.surface),\n reduceTransparency:\n typeof parsed.reduceTransparency === 'boolean' ? parsed.reduceTransparency : undefined,\n }\n } catch {\n return defaultThemeSelection\n }\n}\n\n/**\n * The default persistence: one JSON document in `localStorage` under `key`.\n *\n * `read` is the provider's original `readSelection`, moved here unchanged — every\n * stored id is validated against the catalogue, so a stale preference falls back to\n * a default rather than reaching the document. `key` may be a function so a\n * provider whose `storageKey` prop changes writes to the key it is now given.\n *\n * Blocked, full or absent storage is survivable in both directions: a read answers\n * with the defaults and a write is dropped, so the preference simply lasts the\n * session.\n */\nexport function localStoragePersistence(key: string | (() => string)): ThemePersistence {\n const keyOf = () => (typeof key === 'function' ? key() : key)\n return {\n read: () => readSelection(keyOf()),\n write(next) {\n try {\n preferenceStore()?.setItem(keyOf(), JSON.stringify(next))\n } catch {\n // A full, blocked or unavailable store must not break the switch: the\n // preference simply does not survive the reload.\n }\n },\n }\n}\n\n/**\n * What a persistence knows, or nothing.\n *\n * The seam's `read` must not throw, and the provider does not rely on that: a host\n * persistence is code the design system has not seen, and a theme provider that\n * fails to mount because a store misbehaved has turned a lost preference into a\n * blank application.\n */\nexport function readPersisted(persistence: ThemePersistence): Partial<ThemeSelection> | undefined {\n try {\n return persistence.read()\n } catch {\n return undefined\n }\n}\n\n/** Hand a selection to the store; a store that throws loses the preference, not the switch. */\nexport function writePersisted(persistence: ThemePersistence, next: ThemeSelection): void {\n try {\n persistence.write(next)\n } catch {\n // Same contract as a full or blocked `localStorage`: the choice holds for the\n // session and does not survive the reload.\n }\n}\n\n/**\n * Where a host's versioned preference document lives. One schema describes the\n * document to both halves of the system: {@link createVersionedPersistence} reads and\n * writes it, and `themeScript(options)` reads it before first paint, so the two\n * cannot disagree about a key or a version.\n */\nexport type ThemeStorageSchema = {\n /** The storage key of the document. */\n key: string\n /**\n * The only top-level `version` read. Absent, the document is unversioned. A record\n * of any other version is quarantined (see `recoveryKey`), not interpreted.\n */\n version?: number\n /**\n * The document field that holds the appearance preference (`system`, `light`,\n * `dark`). Read by `themeScript`; the persistence leaves field names to the codec.\n * Default `appearance`.\n */\n appearanceField?: 'appearance' | 'mode'\n /**\n * An older single-value key (`light` | `dark` | `system`) read only while `key` is\n * absent, and never deleted. The persistence hands it to the codec's `fromLegacy`.\n */\n legacyAppearanceKey?: string\n /**\n * The stored library of imported themes, for `themeScript` only: an imported\n * theme's id and verbatim tokens, so the first paint can use the right variant. The\n * persistence neither reads nor writes it.\n */\n libraryKey?: string\n /** Where an unreadable document is kept. Default `${key}.recovery`. */\n recoveryKey?: string\n}\n\n/**\n * How a host's document maps to and from the shared {@link ThemeSelection}.\n *\n * The codec is the product schema: it decides which fields a document has, how an\n * unknown value is corrected, and what a fresh install looks like. The engine decides\n * everything about *storage*: what is quarantined, when the legacy key is read, and\n * that nothing is written until `setSelection` asks.\n */\nexport type VersionedCodec<Doc> = {\n /**\n * A readable record (the right JSON shape and version) as a selection patch. Fields\n * it leaves `undefined` fall back to the provider's defaults. It may keep an id the\n * catalogue lacks; the provider resolves it, it does not rewrite it.\n */\n decode(record: Readonly<Record<string, unknown>>): Partial<ThemeSelection>\n /**\n * The legacy key's value (never `null`) as a selection patch, or `undefined` for a\n * value it does not recognise. Called only when the main key is absent.\n */\n fromLegacy?(value: string): Partial<ThemeSelection> | undefined\n /**\n * What a store with nothing readable answers: a fresh install, an unreadable\n * document, a blocked store. Absent, the provider's own defaults apply.\n */\n defaults?(): Partial<ThemeSelection> | undefined\n /**\n * The document to store for a selection. `previous` is the record last read or\n * written, so a codec can carry a field the selection does not model.\n */\n encode(next: ThemeSelection, previous?: Readonly<Record<string, unknown>>): Doc\n}\n\n/** Seams for the storage and the clock; both default to the real ones. */\nexport type VersionedPersistenceOptions = {\n /** The store, or nothing. Called per access; default is `localStorage` (blocked-safe). */\n storage?: () => PreferenceStorage | undefined\n /** The clock for the recovery envelope's `capturedAt`. */\n now?: () => Date\n}\n\n/**\n * A {@link ThemePersistence} over a versioned document and a host codec.\n *\n * `read` follows the engine's rules (`lib/preference-store.ts`): the main key first;\n * an unreadable or unsupported document is quarantined byte for byte into the\n * recovery key and the codec's `defaults` answer; the legacy key only when the main\n * key is absent. It never writes the main key, so the first write is a user's\n * decision. `write` stores `codec.encode(next, previous)` as JSON and swallows a\n * failure.\n *\n * Neither method throws. The stored bytes are exactly what the codec encodes, so a\n * host keeps its keys, field names and bytes by keeping its codec.\n */\nexport function createVersionedPersistence<Doc>(\n schema: ThemeStorageSchema,\n codec: VersionedCodec<Doc>,\n options: VersionedPersistenceOptions = {}\n): ThemePersistence {\n const storageOf = options.storage ?? defaultPreferenceStorage\n const recordSchema = {\n key: schema.key,\n version: schema.version,\n recoveryKey: recoveryKeyFor(schema),\n }\n let previous: Readonly<Record<string, unknown>> | undefined\n\n const fallback = () => {\n try {\n return codec.defaults?.()\n } catch {\n return undefined\n }\n }\n\n return {\n read() {\n const storage = storageOf()\n if (!storage) return fallback()\n try {\n const found = readVersionedRecord(storage, recordSchema, options.now)\n if (found.status === 'record') {\n previous = found.record\n return codec.decode(found.record)\n }\n if (found.status === 'absent' && schema.legacyAppearanceKey && codec.fromLegacy) {\n const legacy = readLegacyValue(storage, schema.legacyAppearanceKey)\n const migrated = legacy === null ? undefined : codec.fromLegacy(legacy)\n if (migrated) return migrated\n }\n } catch {\n // A codec that throws on a record it cannot read is an unreadable store.\n }\n return fallback()\n },\n write(next) {\n const storage = storageOf()\n if (!storage) return\n try {\n const document = codec.encode(next, previous)\n if (writeVersionedRecord(storage, schema.key, document)) {\n previous =\n typeof document === 'object' && document !== null\n ? (document as Readonly<Record<string, unknown>>)\n : undefined\n }\n } catch {\n // An encoder that throws loses the preference, not the switch.\n }\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { JSX } from 'solid-js';
|
|
2
|
-
import { type ThemeVariant } from '#lib/themes';
|
|
2
|
+
import { type EditorRolesMode, type ThemeVariant } from '#lib/themes';
|
|
3
3
|
import { type ThemePersistence } from './theme-persistence';
|
|
4
4
|
import { type ThemeSelection } from './theme-state';
|
|
5
5
|
export { useOptionalTheme, useTheme } from './theme-context';
|
|
6
|
-
export { localStoragePersistence, type ThemePersistence } from './theme-persistence';
|
|
6
|
+
export { createVersionedPersistence, localStoragePersistence, type ThemePersistence, type ThemeStorageSchema, type VersionedCodec, type VersionedPersistenceOptions, } from './theme-persistence';
|
|
7
|
+
export type { ThemeSurface } from '#lib/theme-surface';
|
|
8
|
+
export type { EditorRolesMode } from '#lib/themes';
|
|
9
|
+
export { inlineScriptLiteral, themeScript, type ThemeScriptOptions } from './theme-script';
|
|
7
10
|
export { defaultThemeSelection, resolvedDensity, type ThemeContextValue, type ThemeDensity, type ThemeSelection, } from './theme-state';
|
|
8
11
|
/**
|
|
9
12
|
* ThemeProvider.
|
|
@@ -90,45 +93,56 @@ export type ThemeProviderProps = {
|
|
|
90
93
|
* own themes the floor changes no preset.
|
|
91
94
|
*/
|
|
92
95
|
accentContrastFloor?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Turns the surface axis on: `selection.surface` and `selection.reduceTransparency`
|
|
98
|
+
* are resolved against the operating system's Reduce Transparency setting and the
|
|
99
|
+
* host's capability, and `data-surface`, `data-reduce-transparency` and
|
|
100
|
+
* `--surface-alpha` are written to `<html>` (the frost rule in `theme.css` reads
|
|
101
|
+
* them). Off by default: a provider that never opts in writes none of them, adds no
|
|
102
|
+
* media-query listener, and stores nothing new.
|
|
103
|
+
*
|
|
104
|
+
* An object also states the host's capability. `nativeTranslucency` is whether the
|
|
105
|
+
* window can render the OS's own translucency; without it a `translucent`
|
|
106
|
+
* preference paints the tokenized `frosted` surface instead of pretending to
|
|
107
|
+
* vibrancy. A function is read inside the provider's reactive scope, for a
|
|
108
|
+
* capability that is learned after mount.
|
|
109
|
+
*
|
|
110
|
+
* A selection that already carries a `surface` turns the axis on for itself, so a
|
|
111
|
+
* host that stores the field gets the same document without the prop.
|
|
112
|
+
*/
|
|
113
|
+
surface?: boolean | {
|
|
114
|
+
nativeTranslucency?: boolean | (() => boolean);
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Where a catalogue variant's palette comes from. `inline` (the default) writes
|
|
118
|
+
* every role onto `<html>`. `stylesheet` leaves a catalogue variant to the host's
|
|
119
|
+
* `[data-theme]` stylesheet and writes inline only what no stylesheet can know: a
|
|
120
|
+
* host's own `themes`, the accent, the pinned terminal palette, the surface alpha
|
|
121
|
+
* and the fonts. Switching from a host variant back to a catalogue one removes the
|
|
122
|
+
* properties the host variant wrote.
|
|
123
|
+
*/
|
|
124
|
+
palette?: 'inline' | 'stylesheet';
|
|
125
|
+
/**
|
|
126
|
+
* The projection behind the `--editor-*` ramp. `syntax` (the default) is the
|
|
127
|
+
* catalogue's quiet syntax roles, a comment among them deliberately below
|
|
128
|
+
* body-text contrast. `floor` holds all sixteen roles to 4.5:1 on the canvas, as
|
|
129
|
+
* hex, for a host that renders code as content (a diff, a file view) rather than
|
|
130
|
+
* as decoration.
|
|
131
|
+
*
|
|
132
|
+
* Applies to catalogue variants; a host's own `themes` keep their `editor` and
|
|
133
|
+
* their `tokens`. With `palette="stylesheet"` it is the one palette property a
|
|
134
|
+
* catalogue variant still writes inline, because `theme-variants.css` is generated
|
|
135
|
+
* for `syntax`. Absent, nothing changes.
|
|
136
|
+
*/
|
|
137
|
+
editorRoles?: EditorRolesMode;
|
|
138
|
+
/**
|
|
139
|
+
* Bind the platform zoom chords (Cmd or Ctrl with `=`, `+`, `-`, `0`) to the three
|
|
140
|
+
* text sizes instead of the browser's page zoom: stepping them together is zoom
|
|
141
|
+
* inside the persisted type system. Off by default: it adds a capturing `keydown`
|
|
142
|
+
* listener to `window`, and an event already handled elsewhere is left alone.
|
|
143
|
+
* `zoomFonts` on the context does the same from a menu or a host's own binding.
|
|
144
|
+
*/
|
|
145
|
+
fontZoom?: boolean;
|
|
93
146
|
};
|
|
94
147
|
export declare function ThemeProvider(props: ThemeProviderProps): JSX.Element;
|
|
95
|
-
/**
|
|
96
|
-
* A string as a JavaScript literal that is also safe inside an inline `<script>`.
|
|
97
|
-
*
|
|
98
|
-
* `JSON.stringify` produces a correct JavaScript literal, and it is **not** safe
|
|
99
|
-
* here: it leaves `<` and `>` alone, so a value containing `</script>` ends the tag
|
|
100
|
-
* early and everything after it is parsed as HTML. That is the classic inline-script
|
|
101
|
-
* injection, and it is a real one even though the value is a developer-supplied prop
|
|
102
|
-
* rather than user input — a prop can come from configuration, and a key that happens
|
|
103
|
-
* to contain a closing tag should not be able to execute.
|
|
104
|
-
*
|
|
105
|
-
* The escape is also correct JavaScript: `\u003c` is `<` to the parser, so the value
|
|
106
|
-
* is byte-identical at runtime and inert to the HTML tokenizer. `\u2028` and `\u2029`
|
|
107
|
-
* are escaped for the neighbouring reason — they are legal inside a JSON string and
|
|
108
|
-
* were, before ES2019, line terminators to a JavaScript parser.
|
|
109
|
-
*
|
|
110
|
-
* CodeQL flags the unescaped form as `js/bad-code-sanitization`, which is how this
|
|
111
|
-
* was found; `tests/theme-script.test.ts` pins it so it cannot come back.
|
|
112
|
-
*/
|
|
113
|
-
export declare function inlineScriptLiteral(value: string): string;
|
|
114
|
-
/**
|
|
115
|
-
* The no-flash script.
|
|
116
|
-
*
|
|
117
|
-
* Rendered inline in `<head>` before any stylesheet that matters. Without it a dark
|
|
118
|
-
* preference paints the light theme first and then swaps, which is the flash every
|
|
119
|
-
* themed application has to solve and the one thing a provider cannot do from
|
|
120
|
-
* inside the body.
|
|
121
|
-
*
|
|
122
|
-
* Deliberately tiny and dependency-free: it reads the stored preference, resolves
|
|
123
|
-
* `system`, and sets the class. It does not apply the variant's roles — the default
|
|
124
|
-
* theme in `theme.css` covers the first paint, and the provider writes the rest on
|
|
125
|
-
* mount.
|
|
126
|
-
*
|
|
127
|
-
* The script reads two values out of `localStorage` and writes them to `dataset`,
|
|
128
|
-
* which is a *property* assignment rather than code: a stored accent or font id can
|
|
129
|
-
* set an attribute and cannot execute. The one value that reaches the source as text
|
|
130
|
-
* is `storageKey`, and it goes through {@link inlineScriptLiteral} for the reason
|
|
131
|
-
* documented there.
|
|
132
|
-
*/
|
|
133
|
-
export declare function themeScript(storageKey?: string): string;
|
|
134
148
|
//# sourceMappingURL=theme-provider.d.ts.map
|