@adea-ai/ui 0.128.2 → 0.128.4

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.
Files changed (138) hide show
  1. package/dist/components/theme/appearance-panel.js +1 -1
  2. package/dist/components/theme/index.js +1 -1
  3. package/dist/components/theme/lazy.d.ts +15 -0
  4. package/dist/components/theme/lazy.d.ts.map +1 -0
  5. package/dist/components/theme/lazy.js +9 -0
  6. package/dist/components/theme/theme-catalogue-source.d.ts +15 -0
  7. package/dist/components/theme/theme-catalogue-source.d.ts.map +1 -0
  8. package/dist/components/theme/theme-catalogue-source.js +51 -0
  9. package/dist/components/theme/theme-catalogue-source.js.map +1 -0
  10. package/dist/components/theme/theme-document-core.d.ts +123 -0
  11. package/dist/components/theme/theme-document-core.d.ts.map +1 -0
  12. package/dist/components/theme/{theme-document.js → theme-document-core.js} +12 -12
  13. package/dist/components/theme/theme-document-core.js.map +1 -0
  14. package/dist/components/theme/theme-document.d.ts +8 -107
  15. package/dist/components/theme/theme-document.d.ts.map +1 -1
  16. package/dist/components/theme/theme-persistence.d.ts +1 -1
  17. package/dist/components/theme/theme-persistence.d.ts.map +1 -1
  18. package/dist/components/theme/theme-persistence.js +9 -6
  19. package/dist/components/theme/theme-persistence.js.map +1 -1
  20. package/dist/components/theme/theme-provider-core.d.ts +19 -0
  21. package/dist/components/theme/theme-provider-core.d.ts.map +1 -0
  22. package/dist/components/theme/theme-provider-core.js +28 -0
  23. package/dist/components/theme/theme-provider-core.js.map +1 -0
  24. package/dist/components/theme/theme-provider-lazy.d.ts +28 -0
  25. package/dist/components/theme/theme-provider-lazy.d.ts.map +1 -0
  26. package/dist/components/theme/theme-provider-lazy.js +36 -0
  27. package/dist/components/theme/theme-provider-lazy.js.map +1 -0
  28. package/dist/components/theme/theme-provider-value.d.ts +175 -0
  29. package/dist/components/theme/theme-provider-value.d.ts.map +1 -0
  30. package/dist/components/theme/theme-provider-value.js +200 -0
  31. package/dist/components/theme/theme-provider-value.js.map +1 -0
  32. package/dist/components/theme/theme-provider.d.ts +11 -136
  33. package/dist/components/theme/theme-provider.d.ts.map +1 -1
  34. package/dist/components/theme/theme-provider.js +27 -149
  35. package/dist/components/theme/theme-provider.js.map +1 -1
  36. package/dist/components/theme/theme-script.js +4 -3
  37. package/dist/components/theme/theme-script.js.map +1 -1
  38. package/dist/components/theme/theme-state-core.d.ts +248 -0
  39. package/dist/components/theme/theme-state-core.d.ts.map +1 -0
  40. package/dist/components/theme/{theme-state.js → theme-state-core.js} +8 -7
  41. package/dist/components/theme/theme-state-core.js.map +1 -0
  42. package/dist/components/theme/theme-state.d.ts +8 -214
  43. package/dist/components/theme/theme-state.d.ts.map +1 -1
  44. package/dist/components/theme/theme-toggle.js.map +1 -1
  45. package/dist/index.js +8 -4
  46. package/dist/lib/accent-presets.d.ts +35 -0
  47. package/dist/lib/accent-presets.d.ts.map +1 -0
  48. package/dist/lib/accent-presets.js +20 -0
  49. package/dist/lib/accent-presets.js.map +1 -0
  50. package/dist/lib/theme-catalogue.d.ts +3 -0
  51. package/dist/lib/theme-catalogue.d.ts.map +1 -0
  52. package/dist/lib/theme-catalogue.js +22 -0
  53. package/dist/lib/theme-catalogue.js.map +1 -0
  54. package/dist/lib/theme-css.d.ts +43 -0
  55. package/dist/lib/theme-css.d.ts.map +1 -0
  56. package/dist/lib/theme-css.js +167 -0
  57. package/dist/lib/theme-css.js.map +1 -0
  58. package/dist/lib/theme-defaults.d.ts +46 -0
  59. package/dist/lib/theme-defaults.d.ts.map +1 -0
  60. package/dist/lib/theme-defaults.generated.d.ts +13 -0
  61. package/dist/lib/theme-defaults.generated.d.ts.map +1 -0
  62. package/dist/lib/theme-defaults.generated.js +318 -0
  63. package/dist/lib/theme-defaults.generated.js.map +1 -0
  64. package/dist/lib/theme-defaults.js +47 -0
  65. package/dist/lib/theme-defaults.js.map +1 -0
  66. package/dist/lib/theme-engine-light.d.ts +3 -0
  67. package/dist/lib/theme-engine-light.d.ts.map +1 -0
  68. package/dist/lib/theme-engine-light.js +75 -0
  69. package/dist/lib/theme-engine-light.js.map +1 -0
  70. package/dist/lib/theme-engine.d.ts +69 -0
  71. package/dist/lib/theme-engine.d.ts.map +1 -0
  72. package/dist/lib/theme-engine.js +33 -0
  73. package/dist/lib/theme-engine.js.map +1 -0
  74. package/dist/lib/theme-records.d.ts +39 -0
  75. package/dist/lib/theme-records.d.ts.map +1 -0
  76. package/dist/lib/theme-records.js +178 -0
  77. package/dist/lib/theme-records.js.map +1 -0
  78. package/dist/lib/themes.d.ts +16 -60
  79. package/dist/lib/themes.d.ts.map +1 -1
  80. package/dist/lib/themes.js +23 -322
  81. package/dist/lib/themes.js.map +1 -1
  82. package/dist/lib/tokens.d.ts +1 -25
  83. package/dist/lib/tokens.d.ts.map +1 -1
  84. package/dist/lib/tokens.js +1 -24
  85. package/dist/lib/tokens.js.map +1 -1
  86. package/dist/r/lib.json +40 -0
  87. package/dist/r/registry.json +75 -0
  88. package/dist/r/src/components/theme/lazy.ts +35 -0
  89. package/dist/r/src/components/theme/theme-catalogue-source.ts +59 -0
  90. package/dist/r/src/components/theme/theme-document-core.ts +262 -0
  91. package/dist/r/src/components/theme/theme-document.ts +23 -240
  92. package/dist/r/src/components/theme/theme-persistence.ts +14 -6
  93. package/dist/r/src/components/theme/theme-provider-core.tsx +40 -0
  94. package/dist/r/src/components/theme/theme-provider-lazy.tsx +34 -0
  95. package/dist/r/src/components/theme/theme-provider-value.ts +445 -0
  96. package/dist/r/src/components/theme/theme-provider.tsx +23 -346
  97. package/dist/r/src/components/theme/theme-script.ts +3 -3
  98. package/dist/r/src/components/theme/theme-state-core.ts +364 -0
  99. package/dist/r/src/components/theme/theme-state.ts +10 -329
  100. package/dist/r/src/components/theme/theme-toggle.tsx +1 -1
  101. package/dist/r/src/lib/accent-presets.ts +54 -0
  102. package/dist/r/src/lib/theme-catalogue.ts +26 -0
  103. package/dist/r/src/lib/theme-css.ts +201 -0
  104. package/dist/r/src/lib/theme-defaults.generated.ts +319 -0
  105. package/dist/r/src/lib/theme-defaults.ts +79 -0
  106. package/dist/r/src/lib/theme-engine-light.ts +117 -0
  107. package/dist/r/src/lib/theme-engine.ts +111 -0
  108. package/dist/r/src/lib/theme-records.ts +205 -0
  109. package/dist/r/src/lib/themes.ts +30 -375
  110. package/dist/r/src/lib/tokens.ts +1 -35
  111. package/dist/r/theming.json +35 -0
  112. package/package.json +10 -2
  113. package/registry.json +75 -0
  114. package/src/components/theme/lazy.ts +35 -0
  115. package/src/components/theme/theme-catalogue-source.ts +59 -0
  116. package/src/components/theme/theme-document-core.ts +262 -0
  117. package/src/components/theme/theme-document.ts +23 -240
  118. package/src/components/theme/theme-persistence.ts +14 -6
  119. package/src/components/theme/theme-provider-core.tsx +40 -0
  120. package/src/components/theme/theme-provider-lazy.tsx +34 -0
  121. package/src/components/theme/theme-provider-value.ts +445 -0
  122. package/src/components/theme/theme-provider.tsx +23 -346
  123. package/src/components/theme/theme-script.ts +3 -3
  124. package/src/components/theme/theme-state-core.ts +364 -0
  125. package/src/components/theme/theme-state.ts +10 -329
  126. package/src/components/theme/theme-toggle.tsx +1 -1
  127. package/src/lib/accent-presets.ts +54 -0
  128. package/src/lib/theme-catalogue.ts +26 -0
  129. package/src/lib/theme-css.ts +201 -0
  130. package/src/lib/theme-defaults.generated.ts +319 -0
  131. package/src/lib/theme-defaults.ts +79 -0
  132. package/src/lib/theme-engine-light.ts +117 -0
  133. package/src/lib/theme-engine.ts +111 -0
  134. package/src/lib/theme-records.ts +205 -0
  135. package/src/lib/themes.ts +30 -375
  136. package/src/lib/tokens.ts +1 -35
  137. package/dist/components/theme/theme-document.js.map +0 -1
  138. package/dist/components/theme/theme-state.js.map +0 -1
@@ -0,0 +1,445 @@
1
+ import type { JSX } from 'solid-js'
2
+ import { createEffect, createMemo, createSignal, onCleanup, splitProps, untrack } from 'solid-js'
3
+ import { fontZoomShortcut, type FontZoomIntent } from '#lib/font-zoom'
4
+ import { REDUCED_TRANSPARENCY_QUERY, resolveSurfaceState } from '#lib/theme-surface'
5
+ import { CatalogueRequired, needsCatalogue, type ThemeEngine } from '#lib/theme-engine'
6
+ import type { EditorRolesMode, ThemeAppearance, ThemeVariant } from '#lib/themes'
7
+ import { applyThemeToRootWith, applyWithoutTransitions } from './theme-document-core'
8
+ import {
9
+ localStoragePersistence,
10
+ readPersisted,
11
+ writePersisted,
12
+ type ThemePersistence,
13
+ } from './theme-persistence'
14
+ import {
15
+ accentRolesForStateWith,
16
+ initialThemeSelection,
17
+ mergeRegistry,
18
+ mergeThemeSelection,
19
+ resolveTerminalVariant,
20
+ resolveThemeState,
21
+ zoomedFontSettings,
22
+ type ThemeContextValue,
23
+ type ThemeSelection,
24
+ } from './theme-state-core'
25
+
26
+ /**
27
+ * ThemeProvider.
28
+ *
29
+ * Owns the appearance preference and applies it to the document. One provider near
30
+ * the root; everything below reads it.
31
+ *
32
+ * ## What a preference is
33
+ *
34
+ * Four independent choices (`ThemeSelection`), and keeping them independent is the
35
+ * point: appearance, a theme per appearance, an accent and fonts. They compose: a
36
+ * user who wants Nord with a violet accent and the system font should not need
37
+ * someone to author that combination.
38
+ *
39
+ * ## Where the rules live
40
+ *
41
+ * This component is wiring. What a selection merges to and resolves to is
42
+ * `theme-state.ts`; what lands on `<html>` is `theme-document.ts`; where the
43
+ * preference is kept is `theme-persistence.ts`. Each is plain code a test can call,
44
+ * which is the reason none of it is inlined here.
45
+ *
46
+ * ## Why the variants are applied as inline properties
47
+ *
48
+ * The catalogue is data, and a theme is applied by writing its roles onto
49
+ * `<html>` as custom properties. That is what makes an imported palette work
50
+ * without a hand-written `[data-theme]` block per theme, and it is why a settings
51
+ * UI can show a preview by reading the same object rather than by loading a
52
+ * stylesheet. `theme.css` keeps the default theme's values so the first paint is
53
+ * correct before any script runs.
54
+ */
55
+ export type ThemeProviderProps = {
56
+ children?: JSX.Element
57
+ /** Where the preference is persisted. Distinct per application. */
58
+ storageKey?: string
59
+ /** Start from this instead of the stored preference. */
60
+ initial?: Partial<ThemeSelection>
61
+ /**
62
+ * The selection, owned by the caller.
63
+ *
64
+ * For a consumer that keeps the state itself — a settings store, or a workshop
65
+ * whose selection lives in Storybook globals — rather than handing it to this
66
+ * provider's signal. Each change lands here and the document follows; nothing is
67
+ * written to the store, because the caller is the one who persists. A picker
68
+ * below the provider still works, and its change is the caller's to read back.
69
+ *
70
+ * Absent, the provider is the whole owner: it reads `storageKey` on mount,
71
+ * seeds from `initial`, and persists every `setSelection`.
72
+ */
73
+ selection?: Partial<ThemeSelection>
74
+ /**
75
+ * Where the preference is kept, in place of `localStorage` under `storageKey`.
76
+ *
77
+ * Read once when the provider is created, so changing the prop later does not
78
+ * move the store. `storageKey` is ignored when this is given.
79
+ */
80
+ persistence?: ThemePersistence
81
+ /**
82
+ * Variants the host adds to the built-in catalogue — themes a user imported.
83
+ *
84
+ * A function is read inside the provider's reactive scope, so one that reads a
85
+ * signal keeps the picker and the document current when the set changes. A
86
+ * stored id that names no variant, built in or added, paints the default of its
87
+ * appearance and stays in the selection untouched.
88
+ */
89
+ themes?: readonly ThemeVariant[] | (() => readonly ThemeVariant[])
90
+ /**
91
+ * Rewrites the custom properties of each pass before they are written to
92
+ * `<html>`: the variant's roles with the accent over them. `tokensAsHex` from
93
+ * `lib/themes` is the one the system ships.
94
+ */
95
+ transformTokens?: (tokens: Record<string, string>) => Record<string, string>
96
+ /**
97
+ * Switch CSS transitions off for the frame that carries a change, so a theme
98
+ * switch cannot animate the whole page. Off by default: it adds a `<style>`
99
+ * element to `<head>` for two frames on every change.
100
+ */
101
+ disableTransitionOnChange?: boolean
102
+ /**
103
+ * The contrast ratio an accent is held to against the variant's background,
104
+ * repaired to the nearest passing colour when it falls short. Absent, presets
105
+ * are painted verbatim — the catalogue measured them for its own themes — and
106
+ * only a custom `#rrggbb` is held to 3:1. A host that adds themes the catalogue
107
+ * never measured passes 3 to hold the presets to it as well; for the catalogue's
108
+ * own themes the floor changes no preset.
109
+ */
110
+ accentContrastFloor?: number
111
+ /**
112
+ * Turns the surface axis on: `selection.surface` and `selection.reduceTransparency`
113
+ * are resolved against the operating system's Reduce Transparency setting and the
114
+ * host's capability, and `data-surface`, `data-reduce-transparency` and
115
+ * `--surface-alpha` are written to `<html>` (the frost rule in `theme.css` reads
116
+ * them). Off by default: a provider that never opts in writes none of them, adds no
117
+ * media-query listener, and stores nothing new.
118
+ *
119
+ * An object also states the host's capability. `nativeTranslucency` is whether the
120
+ * window can render the OS's own translucency; without it a `translucent`
121
+ * preference paints the tokenized `frosted` surface instead of pretending to
122
+ * vibrancy. A function is read inside the provider's reactive scope, for a
123
+ * capability that is learned after mount.
124
+ *
125
+ * A selection that already carries a `surface` turns the axis on for itself, so a
126
+ * host that stores the field gets the same document without the prop.
127
+ */
128
+ surface?: boolean | { nativeTranslucency?: boolean | (() => boolean) }
129
+ /**
130
+ * Where a catalogue variant's palette comes from. `inline` (the default) writes
131
+ * every role onto `<html>`. `stylesheet` leaves a catalogue variant to the host's
132
+ * `[data-theme]` stylesheet and writes inline only what no stylesheet can know: a
133
+ * host's own `themes`, the accent, the pinned terminal palette, the surface alpha
134
+ * and the fonts. Switching from a host variant back to a catalogue one removes the
135
+ * properties the host variant wrote.
136
+ */
137
+ palette?: 'inline' | 'stylesheet'
138
+ /**
139
+ * The projection behind the `--editor-*` ramp. `syntax` (the default) is the
140
+ * catalogue's quiet syntax roles, a comment among them deliberately below
141
+ * body-text contrast. `floor` holds all sixteen roles to 4.5:1 on the canvas, as
142
+ * hex, for a host that renders code as content (a diff, a file view) rather than
143
+ * as decoration.
144
+ *
145
+ * Applies to catalogue variants; a host's own `themes` keep their `editor` and
146
+ * their `tokens`. With `palette="stylesheet"` it is the one palette property a
147
+ * catalogue variant still writes inline, because `theme-variants.css` is generated
148
+ * for `syntax`. Absent, nothing changes.
149
+ */
150
+ editorRoles?: EditorRolesMode
151
+ /**
152
+ * Bind the platform zoom chords (Cmd or Ctrl with `=`, `+`, `-`, `0`) to the three
153
+ * text sizes instead of the browser's page zoom: stepping them together is zoom
154
+ * inside the persisted type system. Off by default: it adds a capturing `keydown`
155
+ * listener to `window`, and an event already handled elsewhere is left alone.
156
+ * `zoomFonts` on the context does the same from a menu or a host's own binding.
157
+ */
158
+ fontZoom?: boolean
159
+ }
160
+
161
+ /**
162
+ * Where the provider's catalogue questions are answered, and whether the whole
163
+ * catalogue is available yet.
164
+ *
165
+ * The eager provider's source is the full catalogue and is always loaded. The lazy
166
+ * provider's source starts on the default pair that ships with it and swaps to the
167
+ * full catalogue when {@link ThemeCatalogueSource.load} resolves. Both read the same
168
+ * way in the provider: `engine()` is reactive, so everything derived from it follows
169
+ * the swap.
170
+ */
171
+ export type ThemeCatalogueSource = {
172
+ /** The engine answering catalogue questions now. Changes once, when loading completes. */
173
+ engine: () => ThemeEngine
174
+ /** True when `engine()` is the full catalogue. */
175
+ loaded: () => boolean
176
+ /** True when loading failed; the provider then paints what it can without the rest. */
177
+ failed: () => boolean
178
+ /** Start loading the full catalogue (idempotent). Resolves when `loaded()` is true. */
179
+ load: () => Promise<void>
180
+ }
181
+
182
+ /** The operating system's Reduce Transparency query, where there is a window to ask. */
183
+ function reducedTransparencyMedia(): MediaQueryList | undefined {
184
+ return typeof window !== 'undefined' && typeof window.matchMedia === 'function'
185
+ ? window.matchMedia(REDUCED_TRANSPARENCY_QUERY)
186
+ : undefined
187
+ }
188
+
189
+ /**
190
+ * Everything the provider is, as a value: the context object, with its effects and
191
+ * listeners created in the caller's reactive scope.
192
+ *
193
+ * Parameterised by where its catalogue comes from: `ThemeProvider`
194
+ * (`theme-provider.tsx`) and the lazy `ThemeProvider` (`theme-provider-lazy.tsx`) wrap
195
+ * this with a source. It imports no catalogue code or data, which is what lets the lazy
196
+ * one keep them out of a consumer's start-up chunk. It is not JSX so that the unit lane,
197
+ * which has no Solid compiler, can drive it directly.
198
+ */
199
+ export function createThemeContextValue(
200
+ props: ThemeProviderProps & { catalogue: ThemeCatalogueSource }
201
+ ): ThemeContextValue {
202
+ const [local] = splitProps(props, [
203
+ 'catalogue',
204
+ 'storageKey',
205
+ 'initial',
206
+ 'selection',
207
+ 'persistence',
208
+ 'themes',
209
+ 'transformTokens',
210
+ 'disableTransitionOnChange',
211
+ 'accentContrastFloor',
212
+ 'surface',
213
+ 'palette',
214
+ 'editorRoles',
215
+ 'fontZoom',
216
+ ])
217
+ const storageKey = () => local.storageKey ?? 'adea-appearance'
218
+ const persistence = local.persistence ?? localStoragePersistence(storageKey)
219
+
220
+ // Read before the first resolution and before the store is, so a persistence whose
221
+ // answer depends on the extras (an id it only accepts if the theme exists) sees
222
+ // them.
223
+ const extraThemes = createMemo(() =>
224
+ typeof local.themes === 'function' ? local.themes() : (local.themes ?? [])
225
+ )
226
+ const source = () => local.catalogue
227
+ const engine = () => source().engine()
228
+ const registry = createMemo(() => mergeRegistry(engine().builtinThemes, extraThemes()))
229
+
230
+ const [selection, setSelectionState] = createSignal<ThemeSelection>(
231
+ initialThemeSelection(readPersisted(persistence), local.initial)
232
+ )
233
+
234
+ // The controlled half of the contract. A patch from the owner replaces the
235
+ // matching axes; the stored preference is untouched, since the owner is the one
236
+ // that persists. Reading `local.selection` — a prop getter, not a destructured
237
+ // value — is what keeps this subscribed for the provider's lifetime.
238
+ createEffect(() => {
239
+ const incoming = local.selection
240
+ if (incoming) setSelectionState((current) => mergeThemeSelection(current, incoming))
241
+ })
242
+
243
+ const [systemAppearance, setSystemAppearance] = createSignal<ThemeAppearance>('light')
244
+
245
+ if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
246
+ const query = window.matchMedia('(prefers-color-scheme: dark)')
247
+ setSystemAppearance(query.matches ? 'dark' : 'light')
248
+ const onChange = (event: MediaQueryListEvent) =>
249
+ setSystemAppearance(event.matches ? 'dark' : 'light')
250
+ query.addEventListener('change', onChange)
251
+ onCleanup(() => query.removeEventListener('change', onChange))
252
+ }
253
+
254
+ // What the stored preference resolves to. `selection()`, `variant()` and
255
+ // `resolvedAppearance()` answer from this and never from a preview, so a picker
256
+ // that previews a draft still reads back what is committed.
257
+ const committed = createMemo(() => resolveThemeState(selection(), systemAppearance(), registry()))
258
+ const resolvedAppearance = createMemo<ThemeAppearance>(() => committed().appearance)
259
+ const variant = createMemo<ThemeVariant>(() => committed().variant)
260
+
261
+ const [draft, setDraft] = createSignal<Partial<ThemeSelection> | undefined>()
262
+ const shownSelection = createMemo(() => {
263
+ const preview = draft()
264
+ return preview ? mergeThemeSelection(selection(), preview) : selection()
265
+ })
266
+ const shown = createMemo(() =>
267
+ draft() ? resolveThemeState(shownSelection(), systemAppearance(), registry()) : committed()
268
+ )
269
+
270
+ // The surface axis. Off, none of this reads the operating system: the media query
271
+ // is subscribed only while the axis is on, so a provider that never opts in adds no
272
+ // listener and no repaint.
273
+ const surfaceOn = (candidate: ThemeSelection) =>
274
+ local.surface === true || typeof local.surface === 'object' || candidate.surface !== undefined
275
+ const nativeTranslucency = () => {
276
+ const capability = typeof local.surface === 'object' ? local.surface.nativeTranslucency : false
277
+ return (typeof capability === 'function' ? capability() : capability) === true
278
+ }
279
+ const [osReducedTransparency, setOsReducedTransparency] = createSignal(
280
+ surfaceOn(untrack(shownSelection)) ? (reducedTransparencyMedia()?.matches ?? false) : false
281
+ )
282
+ const axisOn = createMemo(() => surfaceOn(shownSelection()))
283
+ createEffect(() => {
284
+ const query = axisOn() ? reducedTransparencyMedia() : undefined
285
+ setOsReducedTransparency(query?.matches ?? false)
286
+ if (!query) return
287
+ const onChange = (event: MediaQueryListEvent) => setOsReducedTransparency(event.matches)
288
+ query.addEventListener('change', onChange)
289
+ onCleanup(() => query.removeEventListener('change', onChange))
290
+ })
291
+ const surfaceStateOf = (candidate: ThemeSelection) =>
292
+ resolveSurfaceState(candidate.surface, {
293
+ osReducedTransparency: osReducedTransparency(),
294
+ userReducedTransparency: candidate.reduceTransparency === true,
295
+ nativeTranslucency: nativeTranslucency(),
296
+ })
297
+ const committedSurface = createMemo(() =>
298
+ surfaceOn(selection()) ? surfaceStateOf(selection()) : undefined
299
+ )
300
+
301
+ // The catalogue questions the selection asks. A selection that names a catalogue theme
302
+ // beyond the shipped default pair cannot be answered, painted or previewed until the
303
+ // whole catalogue has loaded; asking for it is the only side effect.
304
+ const waitingFor = (candidate: ThemeSelection, appearance: ThemeAppearance) =>
305
+ !source().loaded() && !source().failed() && needsCatalogue(candidate, appearance)
306
+ createEffect(() => {
307
+ if (
308
+ needsCatalogue(selection(), committed().appearance) ||
309
+ needsCatalogue(shownSelection(), shown().appearance)
310
+ )
311
+ void source().load()
312
+ })
313
+
314
+ let written: ReadonlySet<string> | undefined
315
+ let restoreTransitions: (() => void) | undefined
316
+ createEffect(() => {
317
+ if (typeof document === 'undefined') return
318
+ const current = engine()
319
+ const display = shown()
320
+ // Until the catalogue arrives the document keeps what is already on it — the
321
+ // stylesheet's theme, set before first paint by `themeScript` — instead of
322
+ // flashing the default for a theme that is only not loaded yet.
323
+ if (waitingFor(shownSelection(), display.appearance)) return
324
+ const state = {
325
+ selection: shownSelection(),
326
+ appearance: display.appearance,
327
+ variant: display.variant,
328
+ terminalVariant: resolveTerminalVariant(shownSelection().terminalThemeId, registry()),
329
+ surface: axisOn() ? surfaceStateOf(shownSelection()) : undefined,
330
+ }
331
+ const options = {
332
+ transformTokens: local.transformTokens,
333
+ accentContrastFloor: local.accentContrastFloor,
334
+ palette: local.palette,
335
+ editorRoles: local.editorRoles,
336
+ }
337
+ const apply = () => {
338
+ try {
339
+ written = applyThemeToRootWith(current, document.documentElement, state, options, written)
340
+ } catch (error) {
341
+ // The catalogue failed to load and the selection needs it: leave the document
342
+ // as it is rather than half-painting it.
343
+ if (!(error instanceof CatalogueRequired)) throw error
344
+ }
345
+ }
346
+ if (local.disableTransitionOnChange) {
347
+ restoreTransitions?.()
348
+ restoreTransitions = applyWithoutTransitions(document, apply)
349
+ } else {
350
+ apply()
351
+ }
352
+ })
353
+ onCleanup(() => restoreTransitions?.())
354
+
355
+ const themeAccents = createMemo(() => {
356
+ try {
357
+ return engine().themeAccentsFor(selection().lightThemeId, selection().darkThemeId)
358
+ } catch (error) {
359
+ if (!(error instanceof CatalogueRequired)) throw error
360
+ return []
361
+ }
362
+ })
363
+
364
+ const zoomFonts = (intent: FontZoomIntent) =>
365
+ value.setSelection({ fonts: zoomedFontSettings(selection(), intent) })
366
+
367
+ // The platform zoom chords drive the three text sizes, captured like the other
368
+ // global chords; an event something else already handled never double-steps.
369
+ const onKeyDown = (event: KeyboardEvent) => {
370
+ if (event.defaultPrevented) return
371
+ const intent = fontZoomShortcut(event)
372
+ if (!intent) return
373
+ event.preventDefault()
374
+ zoomFonts(intent)
375
+ }
376
+ createEffect(() => {
377
+ if (!local.fontZoom || typeof window === 'undefined') return
378
+ window.addEventListener('keydown', onKeyDown, { capture: true })
379
+ onCleanup(() => window.removeEventListener('keydown', onKeyDown, { capture: true }))
380
+ })
381
+
382
+ const value: ThemeContextValue = {
383
+ selection,
384
+ resolvedAppearance,
385
+ variant,
386
+ get themes() {
387
+ return engine().builtinThemes
388
+ },
389
+ themeAccents,
390
+ allThemes: registry,
391
+ preview(next) {
392
+ setDraft(next ? { ...next } : undefined)
393
+ },
394
+ previewing: () => draft() !== undefined,
395
+ accentRolesFor(accent, variantId) {
396
+ const current = engine()
397
+ const state = {
398
+ selection: selection(),
399
+ variant: variant(),
400
+ registry: registry(),
401
+ accentContrastFloor: local.accentContrastFloor,
402
+ }
403
+ try {
404
+ return accentRolesForStateWith(current, accent, variantId, state)
405
+ } catch (error) {
406
+ if (!(error instanceof CatalogueRequired)) throw error
407
+ // Needs the whole catalogue to measure the tint exactly. Ask for it, and answer
408
+ // meanwhile without the theme's record: the same four colours, the appearance's
409
+ // own tint strength. Reading `engine()` above re-runs a reactive caller on arrival.
410
+ void source().load()
411
+ try {
412
+ return accentRolesForStateWith(
413
+ {
414
+ accentRolesForSelection: (id, appearance, pair, options) =>
415
+ current.accentRolesForSelection(id, appearance, pair, {
416
+ ...options,
417
+ variantId: undefined,
418
+ }),
419
+ },
420
+ accent,
421
+ variantId,
422
+ state
423
+ )
424
+ } catch (inner) {
425
+ if (!(inner instanceof CatalogueRequired)) throw inner
426
+ return undefined
427
+ }
428
+ }
429
+ },
430
+ catalogueReady: () => source().loaded(),
431
+ loadCatalogue: () => source().load(),
432
+ effectiveSurface: () => committedSurface()?.surface ?? 'opaque',
433
+ reduceTransparencyActive: () => committedSurface()?.reduceTransparencyActive ?? false,
434
+ zoomFonts,
435
+ setSelection(patch) {
436
+ const next = mergeThemeSelection(selection(), patch)
437
+ setSelectionState(next)
438
+ // The committed selection is now what should be showing.
439
+ setDraft(undefined)
440
+ writePersisted(persistence, next)
441
+ },
442
+ }
443
+
444
+ return value
445
+ }