@adea-ai/ui 0.128.2 → 0.128.3

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 (132) 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 +7 -4
  46. package/dist/lib/accent-presets.d.ts +26 -0
  47. package/dist/lib/accent-presets.d.ts.map +1 -0
  48. package/dist/lib/accent-presets.js +11 -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/themes.d.ts +14 -48
  75. package/dist/lib/themes.d.ts.map +1 -1
  76. package/dist/lib/themes.js +21 -163
  77. package/dist/lib/themes.js.map +1 -1
  78. package/dist/lib/tokens.d.ts +1 -25
  79. package/dist/lib/tokens.d.ts.map +1 -1
  80. package/dist/lib/tokens.js +1 -24
  81. package/dist/lib/tokens.js.map +1 -1
  82. package/dist/r/lib.json +35 -0
  83. package/dist/r/registry.json +70 -0
  84. package/dist/r/src/components/theme/lazy.ts +35 -0
  85. package/dist/r/src/components/theme/theme-catalogue-source.ts +59 -0
  86. package/dist/r/src/components/theme/theme-document-core.ts +262 -0
  87. package/dist/r/src/components/theme/theme-document.ts +23 -240
  88. package/dist/r/src/components/theme/theme-persistence.ts +14 -6
  89. package/dist/r/src/components/theme/theme-provider-core.tsx +40 -0
  90. package/dist/r/src/components/theme/theme-provider-lazy.tsx +34 -0
  91. package/dist/r/src/components/theme/theme-provider-value.ts +445 -0
  92. package/dist/r/src/components/theme/theme-provider.tsx +23 -346
  93. package/dist/r/src/components/theme/theme-script.ts +3 -3
  94. package/dist/r/src/components/theme/theme-state-core.ts +364 -0
  95. package/dist/r/src/components/theme/theme-state.ts +10 -329
  96. package/dist/r/src/components/theme/theme-toggle.tsx +1 -1
  97. package/dist/r/src/lib/accent-presets.ts +43 -0
  98. package/dist/r/src/lib/theme-catalogue.ts +26 -0
  99. package/dist/r/src/lib/theme-css.ts +201 -0
  100. package/dist/r/src/lib/theme-defaults.generated.ts +319 -0
  101. package/dist/r/src/lib/theme-defaults.ts +79 -0
  102. package/dist/r/src/lib/theme-engine-light.ts +117 -0
  103. package/dist/r/src/lib/theme-engine.ts +111 -0
  104. package/dist/r/src/lib/themes.ts +27 -191
  105. package/dist/r/src/lib/tokens.ts +1 -35
  106. package/dist/r/theming.json +35 -0
  107. package/package.json +10 -2
  108. package/registry.json +70 -0
  109. package/src/components/theme/lazy.ts +35 -0
  110. package/src/components/theme/theme-catalogue-source.ts +59 -0
  111. package/src/components/theme/theme-document-core.ts +262 -0
  112. package/src/components/theme/theme-document.ts +23 -240
  113. package/src/components/theme/theme-persistence.ts +14 -6
  114. package/src/components/theme/theme-provider-core.tsx +40 -0
  115. package/src/components/theme/theme-provider-lazy.tsx +34 -0
  116. package/src/components/theme/theme-provider-value.ts +445 -0
  117. package/src/components/theme/theme-provider.tsx +23 -346
  118. package/src/components/theme/theme-script.ts +3 -3
  119. package/src/components/theme/theme-state-core.ts +364 -0
  120. package/src/components/theme/theme-state.ts +10 -329
  121. package/src/components/theme/theme-toggle.tsx +1 -1
  122. package/src/lib/accent-presets.ts +43 -0
  123. package/src/lib/theme-catalogue.ts +26 -0
  124. package/src/lib/theme-css.ts +201 -0
  125. package/src/lib/theme-defaults.generated.ts +319 -0
  126. package/src/lib/theme-defaults.ts +79 -0
  127. package/src/lib/theme-engine-light.ts +117 -0
  128. package/src/lib/theme-engine.ts +111 -0
  129. package/src/lib/themes.ts +27 -191
  130. package/src/lib/tokens.ts +1 -35
  131. package/dist/components/theme/theme-document.js.map +0 -1
  132. package/dist/components/theme/theme-state.js.map +0 -1
@@ -1,326 +1,17 @@
1
- import {
2
- DEFAULT_APPEARANCE_EDITOR_FONT_SETTINGS,
3
- normalizeAppearanceEditorFontSettings,
4
- type AppearanceEditorFontSettings,
5
- } from '../../lib/appearance-font-settings'
6
- import type { AccentPreset } from '@adea-ai/themes'
7
- import { normalizeCustomAccent, type AccentRoles } from '../../lib/accent-roles'
8
- import { stepAppearanceFontSizes, type FontZoomIntent } from '../../lib/font-zoom'
9
- import type { ThemeSurface } from '../../lib/theme-surface'
10
- import {
11
- accentRolesForSelection,
12
- defaultDarkThemeId,
13
- defaultLightThemeId,
14
- resolveVariant,
15
- type ThemeAppearance,
16
- type ThemeVariant,
17
- } from '../../lib/themes'
1
+ import { catalogueThemeEngine } from '../../lib/theme-catalogue'
2
+ import type { AccentRoles } from '../../lib/accent-roles'
3
+ import type { ThemeVariant } from '../../lib/themes'
4
+ import { accentRolesForStateWith, type ThemeSelection } from './theme-state-core'
18
5
 
19
- /**
20
- * The pure half of the theme provider: what a selection is, how two selections
21
- * merge, and what a selection resolves to.
22
- *
23
- * Nothing here touches the document, storage or a signal, so each rule can be
24
- * tested by calling it. `ThemeProvider` owns the reactivity and wires these to it;
25
- * `theme-document.ts` owns the writes; `theme-persistence.ts` owns the store.
26
- */
27
-
28
- /**
29
- * Four independent choices, and keeping them independent is the point:
30
- *
31
- * - **appearance** — light, dark, or follow the system.
32
- * - **a theme per appearance** — so a user can run Catppuccin Mocha by night and
33
- * Solarized Light by day without re-choosing when the system flips.
34
- * - **an accent** — adea's six presets, or one of the accents the selected theme
35
- * pair carries itself (`ansi-blue`…), which re-colour the interactive roles on
36
- * top of whichever theme is active.
37
- * - **fonts** — separate family and size choices for UI, content and code.
38
- */
39
- export type ThemeSelection = {
40
- /** `system` follows `prefers-color-scheme` and updates when it changes. */
41
- appearance: ThemeAppearance | 'system'
42
- /** The variant to use when the resolved appearance is light. */
43
- lightThemeId: string
44
- /** The variant to use when the resolved appearance is dark. */
45
- darkThemeId: string
46
- /**
47
- * An `accentPresets` id, a theme accent id from `themeAccents()` (`ansi-blue`…),
48
- * `theme` for the variant's own primary, or a custom colour as `#rrggbb`. A theme
49
- * accent the current pair does not offer falls back to the variant's own primary.
50
- *
51
- * A custom colour is stored lower case and is raised to the contrast floor when it
52
- * is painted, never when it is stored: the selection keeps what the user chose.
53
- * Anything else that is not an id is dropped at paint, not rejected here.
54
- */
55
- accent: string
56
- /** UI-family alias for consumers of the original single-font preference. */
57
- font: string
58
- /** UI, content and code font settings. Omitted legacy records are migrated from `font`. */
59
- fonts?: AppearanceEditorFontSettings
60
- /**
61
- * `comfortable` is the system's default geometry; `compact` moves the control
62
- * ladder down one rung.
63
- *
64
- * Optional, and defaulted on read, so a preference stored before density existed
65
- * keeps working and `ThemeSelection` stays assignable without it.
66
- */
67
- density?: ThemeDensity
68
- /**
69
- * The terminal either follows the interface theme (`theme`, or absent) or is pinned
70
- * to another theme's terminal palette by that theme's id. The pin is written over
71
- * the interface variant's own `--terminal-*` properties, so a light interface can
72
- * carry a dark terminal.
73
- *
74
- * An id the registry does not hold paints the interface palette and stays in the
75
- * selection, the same rule as an unknown theme id.
76
- *
77
- * Optional and not defaulted: see {@link defaultThemeSelection}.
78
- */
79
- terminalThemeId?: string
80
- /**
81
- * How opaque elevated panels are. Written to `<html>` only when the provider's
82
- * `surface` prop is on or this is set; absent, a host paints no surface axis at
83
- * all. Resolved against the OS and the host's capability before it is painted —
84
- * see `resolveSurface` — so the value stored is a preference, never the outcome.
85
- */
86
- surface?: ThemeSurface
87
- /**
88
- * The user's own Reduce Transparency switch, which forces the `opaque` surface the
89
- * same way the operating system's setting does.
90
- */
91
- reduceTransparency?: boolean
92
- }
93
-
94
- /**
95
- * The two density rungs.
96
- *
97
- * `comfortable` is the geometry in `theme.css`, so it is the absence of an
98
- * attribute rather than a value: a document that ships no `data-density` at all
99
- * renders correctly, which is what keeps this additive.
100
- */
101
- export type ThemeDensity = 'comfortable' | 'compact'
102
-
103
- /**
104
- * Deliberately lists only the fields every consumer has always had. A field added
105
- * to {@link ThemeSelection} later gets no default here: `JSON.stringify` drops an
106
- * `undefined`, so a store written by a consumer that never sets the field stays
107
- * byte-identical to what it was before the field existed.
108
- */
109
- export const defaultThemeSelection: ThemeSelection = {
110
- appearance: 'system',
111
- lightThemeId: defaultLightThemeId,
112
- darkThemeId: defaultDarkThemeId,
113
- accent: 'theme',
114
- font: 'system',
115
- fonts: DEFAULT_APPEARANCE_EDITOR_FONT_SETTINGS,
116
- density: 'comfortable',
117
- }
118
-
119
- /**
120
- * A stored selection's density, defaulted.
121
- *
122
- * The stored value is read through here rather than trusted, because a preference
123
- * written before density existed has no `density` key and a hand-edited store may
124
- * hold anything.
125
- */
126
- export function resolvedDensity(selection: ThemeSelection): ThemeDensity {
127
- return selection.density === 'compact' ? 'compact' : 'comfortable'
128
- }
129
-
130
- export type ThemeContextValue = {
131
- /** The preference as stored. A {@link ThemeContextValue.preview} does not change it. */
132
- selection: () => ThemeSelection
133
- /** What the preference resolves to right now, after `system`. */
134
- resolvedAppearance: () => ThemeAppearance
135
- /** The variant in use. */
136
- variant: () => ThemeVariant
137
- /** Update part of the preference. */
138
- setSelection: (patch: Partial<ThemeSelection>) => void
139
- /**
140
- * The built-in catalogue, so a picker does not import it separately. A plain
141
- * array: {@link ThemeContextValue.allThemes} is the one that follows a host's
142
- * extra themes.
143
- */
144
- themes: readonly ThemeVariant[]
145
- /**
146
- * The accents the selected light/dark pair carries, offered beside the presets.
147
- * Follows the selection: a different theme pair may offer different slots.
148
- */
149
- themeAccents: () => readonly AccentPreset[]
150
- /**
151
- * The built-in catalogue followed by the provider's `themes` prop. Reactive when
152
- * that prop is a function that reads signals.
153
- */
154
- allThemes: () => readonly ThemeVariant[]
155
- /**
156
- * Paint the document from `draft` laid over the stored selection, without
157
- * persisting it or changing what {@link ThemeContextValue.selection} returns.
158
- * `undefined` ends the preview. A {@link ThemeContextValue.setSelection} ends it
159
- * too, because the committed selection is then what should be showing.
160
- */
161
- preview: (draft: Partial<ThemeSelection> | undefined) => void
162
- /** True while a {@link ThemeContextValue.preview} draft is on the document. */
163
- previewing: () => boolean
164
- /**
165
- * The roles `accent` paints over `variantId` (default: the variant in use), held
166
- * to the provider's `accentContrastFloor`, for an element-scoped override with
167
- * `paintAccentRoles` — a workspace that carries its own colour. `undefined` for
168
- * `null`, for `theme`, for an id that resolves to nothing, and for a variant the
169
- * registry does not hold: each means "keep the inherited accent".
170
- */
171
- accentRolesFor: (accent: string | null, variantId?: string) => AccentRoles | undefined
172
- /**
173
- * The surface the committed selection paints, after Reduce Transparency and the
174
- * host's translucency capability. `opaque` when the axis is off. Follows the
175
- * committed selection, not a preview, like {@link ThemeContextValue.variant}.
176
- */
177
- effectiveSurface: () => ThemeSurface
178
- /**
179
- * True while the operating system or the selection's `reduceTransparency` forces
180
- * opaque surfaces. Always false when the surface axis is off, because nothing is
181
- * then listening to the operating system.
182
- */
183
- reduceTransparencyActive: () => boolean
184
- /**
185
- * Step the three text sizes together, or put them back to the defaults: what the
186
- * `fontZoom` prop's chords do, for a host that binds its own keys or a menu. Goes
187
- * through {@link ThemeContextValue.setSelection}, so it persists and ends a preview.
188
- */
189
- zoomFonts: (intent: FontZoomIntent) => void
190
- }
191
-
192
- /** A selection with the `undefined` keys of a patch left out. */
193
- function definedOnly<T extends object>(value: T): Partial<T> {
194
- return Object.fromEntries(
195
- Object.entries(value).filter(([, entry]) => entry !== undefined)
196
- ) as Partial<T>
197
- }
198
-
199
- /** Keep the deprecated single-font view synchronized with the UI axis. */
200
- export function mergeThemeSelection(
201
- base: ThemeSelection,
202
- patch: Partial<ThemeSelection>
203
- ): ThemeSelection {
204
- const merged = { ...base, ...patch }
205
- let fonts: AppearanceEditorFontSettings
206
-
207
- if (patch.fonts !== undefined) {
208
- fonts = normalizeAppearanceEditorFontSettings(patch.fonts).settings
209
- } else if (patch.font !== undefined) {
210
- const currentFonts = normalizeAppearanceEditorFontSettings(base.fonts, base.font).settings
211
- fonts = normalizeAppearanceEditorFontSettings({
212
- ...currentFonts,
213
- ui: { ...currentFonts.ui, family: patch.font },
214
- }).settings
215
- } else {
216
- fonts = normalizeAppearanceEditorFontSettings(merged.fonts, merged.font).settings
217
- }
218
-
219
- return {
220
- ...merged,
221
- // One spelling for a custom colour, so equal choices compare and store equal.
222
- accent: normalizeCustomAccent(merged.accent) ?? merged.accent,
223
- fonts,
224
- font: fonts.ui.family,
225
- }
226
- }
227
-
228
- /**
229
- * The selection a provider starts from: the defaults, then whatever the store
230
- * returned, then `initial`.
231
- *
232
- * `initial` wins over the store on every mount. That precedence is part of the
233
- * contract Cortana relies on (its `initial` pins `appearance` and `font`), and it
234
- * is why the store's answer is only the middle layer. A store that returned
235
- * nothing, or threw, or answered with explicit `undefined`s, leaves the defaults
236
- * standing for those fields.
237
- */
238
- export function initialThemeSelection(
239
- stored: Partial<ThemeSelection> | undefined,
240
- initial: Partial<ThemeSelection> | undefined
241
- ): ThemeSelection {
242
- const base = mergeThemeSelection(defaultThemeSelection, definedOnly(stored ?? {}))
243
- return mergeThemeSelection(base, initial ?? {})
244
- }
245
-
246
- /** `system` resolved against what the operating system reports. */
247
- export function resolveAppearance(
248
- preference: ThemeSelection['appearance'],
249
- system: ThemeAppearance
250
- ): ThemeAppearance {
251
- return preference === 'system' ? system : preference
252
- }
253
-
254
- /** The theme id the selection names for a resolved appearance. */
255
- export function selectedThemeId(selection: ThemeSelection, appearance: ThemeAppearance): string {
256
- return appearance === 'dark' ? selection.darkThemeId : selection.lightThemeId
257
- }
258
-
259
- /**
260
- * What a selection paints: the resolved appearance and the variant for it.
261
- *
262
- * An id the registry does not hold resolves to the appearance's default rather than
263
- * being rewritten (see `resolveVariant`), so the selection a host stores is never
264
- * changed by being unresolvable.
265
- */
266
- export function resolveThemeState(
267
- selection: ThemeSelection,
268
- system: ThemeAppearance,
269
- registry: readonly ThemeVariant[]
270
- ): { appearance: ThemeAppearance; variant: ThemeVariant } {
271
- const appearance = resolveAppearance(selection.appearance, system)
272
- return {
273
- appearance,
274
- variant: resolveVariant(registry, selectedThemeId(selection, appearance), appearance),
275
- }
276
- }
277
-
278
- /**
279
- * The variant whose terminal palette is pinned, or `undefined` when the terminal
280
- * follows the interface theme.
281
- *
282
- * `theme`, an absent id and an id the registry does not hold all mean the interface
283
- * palette: a terminal must never resolve to a blank or half-painted palette because a
284
- * theme was deleted after it was pinned.
285
- */
286
- export function resolveTerminalVariant(
287
- terminalThemeId: string | undefined,
288
- registry: readonly ThemeVariant[]
289
- ): ThemeVariant | undefined {
290
- if (terminalThemeId === undefined || terminalThemeId === 'theme') return undefined
291
- return registry.find((candidate) => candidate.id === terminalThemeId)
292
- }
293
-
294
- /**
295
- * The font settings a zoom chord asks for: one pixel up or down on every axis, or
296
- * the defaults for `reset`.
297
- */
298
- export function zoomedFontSettings(
299
- selection: ThemeSelection,
300
- intent: FontZoomIntent
301
- ): AppearanceEditorFontSettings {
302
- if (intent === 'reset') return { ...DEFAULT_APPEARANCE_EDITOR_FONT_SETTINGS }
303
- return stepAppearanceFontSizes(selection.fonts, intent === 'in' ? 1 : -1)
304
- }
305
-
306
- /**
307
- * The registry a provider resolves against: the catalogue, then the host's extras.
308
- *
309
- * The catalogue wins an id collision, because it is listed first and `resolveVariant`
310
- * takes the first match: a host's variant can add a theme but cannot shadow one.
311
- */
312
- export function mergeRegistry(
313
- builtin: readonly ThemeVariant[],
314
- extra: readonly ThemeVariant[] | undefined
315
- ): readonly ThemeVariant[] {
316
- return extra && extra.length > 0 ? [...builtin, ...extra] : builtin
317
- }
6
+ export * from './theme-state-core'
318
7
 
319
8
  /**
320
- * The roles an accent paints over a variant, for {@link ThemeContextValue.accentRolesFor}.
9
+ * The roles an accent paints over a variant, for `ThemeContextValue.accentRolesFor`,
10
+ * resolved against the whole catalogue.
321
11
  *
322
12
  * Resolved against the committed selection's theme pair, so an `ansi-*` accent means
323
- * the same colour here as it does on `<html>`.
13
+ * the same colour here as it does on `<html>`. The lazy provider uses
14
+ * {@link accentRolesForStateWith} with the engine it has.
324
15
  */
325
16
  export function accentRolesForState(
326
17
  accent: string | null,
@@ -332,15 +23,5 @@ export function accentRolesForState(
332
23
  accentContrastFloor?: number
333
24
  }
334
25
  ): AccentRoles | undefined {
335
- if (accent === null) return undefined
336
- const variant =
337
- variantId === undefined
338
- ? state.variant
339
- : state.registry.find((candidate) => candidate.id === variantId)
340
- if (!variant) return undefined
341
- return accentRolesForSelection(accent, variant.appearance, state.selection, {
342
- background: variant.colors.background,
343
- floor: state.accentContrastFloor,
344
- variantId: variant.id,
345
- })
26
+ return accentRolesForStateWith(catalogueThemeEngine, accent, variantId, state)
346
27
  }
@@ -3,7 +3,7 @@ import type { ComponentProps } from 'solid-js'
3
3
  import { For, splitProps } from 'solid-js'
4
4
  import { cn } from '../../lib/utils'
5
5
  import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group/toggle-group'
6
- import { useTheme } from './theme-provider'
6
+ import { useTheme } from './theme-context'
7
7
 
8
8
  /**
9
9
  * ThemeToggle.
@@ -0,0 +1,43 @@
1
+ import { ACCENTS } from '@adea-ai/themes'
2
+
3
+ /*
4
+ * Its own module so the theme provider's start-up path (the stored-preference reader
5
+ * and the boot script both need the preset ids) does not import the token manifest in
6
+ * `tokens.ts`, which is 20 KB of documentation data a provider never reads.
7
+ * `tokens.ts` re-exports both names, so every existing import keeps working.
8
+ */
9
+
10
+ /**
11
+ * The accent axis.
12
+ *
13
+ * adea's accent presets, as a selection rather than a constant. `theme` is the
14
+ * default and means "the variant's own primary" — monochrome in adea's palette —
15
+ * and each named preset overrides the interactive roles through the
16
+ * `[data-accent]` blocks in `theme.css`.
17
+ *
18
+ * These are the same six presets `accentPresets` exports in adea's own
19
+ * `appearance.ts`, and the values match. A consumer's appearance picker and the
20
+ * workshop's theme toolbar read this one list, so the two cannot disagree about
21
+ * what a preset is called or what colour it is.
22
+ */
23
+ export type AccentPreset = {
24
+ /** The `data-accent` value. `theme` is the variant's own primary. */
25
+ id: string
26
+ label: string
27
+ /** What the preset is for, in the gallery and in a picker. */
28
+ description: string
29
+ /** The accent as it appears in the light theme. Absent for `theme`. */
30
+ light?: string
31
+ /** The accent as it appears in the dark theme. Absent for `theme`. */
32
+ dark?: string
33
+ }
34
+
35
+ export const accentPresets: readonly AccentPreset[] = Object.freeze([
36
+ {
37
+ id: 'theme',
38
+ label: 'Theme',
39
+ description:
40
+ "The variant's own primary. Neutral in adea's palette, so the interface stays monochrome.",
41
+ },
42
+ ...ACCENTS,
43
+ ])
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The full theme engine, backed by the whole catalogue.
3
+ *
4
+ * This is the module a provider loads when it needs more than the default pair — and,
5
+ * for `ThemeProvider`, always. It imports `lib/themes` (which imports the catalogue)
6
+ * and nothing imports it statically except the eager provider, so a bundler can put
7
+ * it, and the catalogue behind it, in a chunk of its own for the lazy provider.
8
+ */
9
+ import {
10
+ accentRolesForSelection,
11
+ accentVariables,
12
+ builtinThemes,
13
+ isCatalogueVariant,
14
+ themeAccentsFor,
15
+ themeCssVariables,
16
+ } from './themes'
17
+ import type { ThemeEngine } from './theme-engine'
18
+
19
+ export const catalogueThemeEngine: ThemeEngine = {
20
+ builtinThemes,
21
+ isCatalogueVariant,
22
+ themeCssVariables,
23
+ accentVariables,
24
+ accentRolesForSelection,
25
+ themeAccentsFor,
26
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * The mapping from a variant's roles to this system's custom properties, without the
3
+ * catalogue.
4
+ *
5
+ * `themeCssVariables` needs two things only the catalogue can measure for a theme:
6
+ * the strength of its `--primary-subtle` tint and its floor-projected editor ramp.
7
+ * Those are the `hooks`, so this module imports no catalogue data and a host that
8
+ * never loads the catalogue can still paint a variant it already holds. `lib/themes`
9
+ * binds the hooks to the catalogue and keeps exporting the same
10
+ * `themeCssVariables`; `lib/theme-engine` binds them to the precomputed default pair.
11
+ */
12
+ import { primaryHover } from '@adea-ai/themes'
13
+ import { oklchToHex, parseColor } from '@adea-ai/themes/oklch'
14
+ import type {
15
+ EditorRolesMode,
16
+ ThemeCssVariableOptions,
17
+ ThemeEditorPalette,
18
+ ThemeVariant,
19
+ } from './themes'
20
+
21
+ /** What a variant's custom properties need that depends on the catalogue. */
22
+ export type ThemeCssHooks = {
23
+ /** The `--primary-subtle` expression of a variant. */
24
+ primarySubtle(theme: Pick<ThemeVariant, 'id' | 'appearance'>): string
25
+ /** The editor ramp of a variant under a projection. */
26
+ editor(theme: ThemeVariant, mode: EditorRolesMode): ThemeEditorPalette
27
+ }
28
+
29
+ /**
30
+ * The roles a variant sets, as CSS custom properties.
31
+ *
32
+ * The one place that knows the mapping from a theme's roles to this system's variable
33
+ * names. A provider applies this; nothing else repeats it. The `--terminal-*` and
34
+ * `--editor-*` ramps are presentations of the catalogue's ANSI and syntax data rather
35
+ * than separate roles, which is why they are built here and not in the catalogue.
36
+ *
37
+ * `options.editor` picks the `--editor-*` projection ({@link EditorRolesMode}).
38
+ * Absent it, the result is what it has always been; the generated `theme-variants.css`
39
+ * and the design-system artifact are built without it.
40
+ */
41
+ export function themeCssVariablesWith(
42
+ hooks: ThemeCssHooks,
43
+ theme: ThemeVariant,
44
+ options: ThemeCssVariableOptions = {}
45
+ ): Record<string, string> {
46
+ const c = theme.colors
47
+ const t = theme.terminal
48
+ const e = hooks.editor(theme, options.editor ?? 'syntax')
49
+ const derived = {
50
+ '--background': c.background,
51
+ '--foreground': c.foreground,
52
+ '--card': c.card,
53
+ '--card-foreground': c.cardForeground,
54
+ '--popover': c.popover,
55
+ '--popover-foreground': c.popoverForeground,
56
+ '--primary': c.primary,
57
+ '--primary-foreground': c.primaryForeground,
58
+ /*
59
+ * The two tokens derived from the primary, and they are here rather than left to
60
+ * `theme.css` because they must follow `--primary` — which this function writes
61
+ * for *any* theme in the catalogue, not just the two defaults `theme.css` carries.
62
+ *
63
+ * `--primary-hover` is a resolved colour, not a `color-mix()`: a uniform hover
64
+ * step cannot be expressed as a mix (see `primaryHover`). Left out, a theme switch
65
+ * would keep the previous theme's hover on the new theme's primary.
66
+ *
67
+ * `--primary-subtle` is a mix over `--primary`, so it follows an accent on its
68
+ * own, but its *strength* is per theme: `primarySubtleCss(record)` lowers the
69
+ * appearance ceiling only as far as this theme needs for body text to clear 4.5:1
70
+ * on the tint, over every surface, for every accent it offers. It has to be
71
+ * written per theme for that reason — the appearance-wide value is not legible on
72
+ * every palette.
73
+ */
74
+ '--primary-hover': primaryHover(c.primary, theme.appearance),
75
+ '--primary-subtle': hooks.primarySubtle(theme),
76
+ '--secondary': c.secondary,
77
+ '--secondary-foreground': c.secondaryForeground,
78
+ '--muted': c.muted,
79
+ '--muted-foreground': c.mutedForeground,
80
+ '--accent': c.accent,
81
+ '--accent-foreground': c.accentForeground,
82
+ '--destructive': c.destructive,
83
+ '--destructive-foreground': c.destructiveForeground,
84
+ '--destructive-subtle': c.destructiveSubtle,
85
+ '--destructive-action': c.destructiveAction,
86
+ '--destructive-action-foreground': c.destructiveActionForeground,
87
+ '--success': c.success,
88
+ '--success-foreground': c.successForeground,
89
+ '--success-subtle': c.successSubtle,
90
+ '--warning': c.warning,
91
+ '--warning-foreground': c.warningForeground,
92
+ '--warning-subtle': c.warningSubtle,
93
+ '--info': c.info,
94
+ '--info-foreground': c.infoForeground,
95
+ '--info-subtle': c.infoSubtle,
96
+ '--border': c.border,
97
+ '--input': c.input,
98
+ '--ring': c.ring,
99
+ '--sidebar': c.sidebar,
100
+ '--sidebar-foreground': c.sidebarForeground,
101
+ '--sidebar-accent': c.sidebarAccent,
102
+ '--sidebar-border': c.sidebarBorder,
103
+ '--sidebar-primary': c.primary,
104
+ '--sidebar-primary-foreground': c.primaryForeground,
105
+ '--sidebar-ring': c.ring,
106
+ '--sidebar-muted-foreground': c.sidebarMutedForeground,
107
+ '--surface-sunken': c.muted,
108
+ '--surface-hover': c.accent,
109
+ '--surface-active': c.border,
110
+ '--chrome': c.background,
111
+ ...terminalVariables(t),
112
+ '--editor-keyword': e.keyword,
113
+ '--editor-string': e.string,
114
+ '--editor-number': e.number,
115
+ '--editor-comment': e.comment,
116
+ '--editor-function': e.function,
117
+ '--editor-variable': e.variable,
118
+ '--editor-type': e.type,
119
+ '--editor-tag': e.tag,
120
+ '--editor-attribute': e.attribute,
121
+ '--editor-operator': e.operator,
122
+ '--editor-heading': e.heading,
123
+ '--editor-link': e.link,
124
+ '--editor-diff-add': e.diffAdd,
125
+ '--editor-diff-delete': e.diffDelete,
126
+ '--editor-diff-hunk': e.diffHunk,
127
+ '--editor-search-match': e.searchMatch,
128
+ '--chart-1': theme.chart[0] ?? c.primary,
129
+ '--chart-2': theme.chart[1] ?? c.info,
130
+ '--chart-3': theme.chart[2] ?? c.success,
131
+ '--chart-4': theme.chart[3] ?? c.warning,
132
+ '--chart-5': theme.chart[4] ?? c.destructive,
133
+ '--chart-6': theme.chart[5] ?? c.mutedForeground,
134
+ }
135
+ return theme.tokens ? { ...derived, ...theme.tokens } : derived
136
+ }
137
+
138
+ function terminalVariables(t: ThemeVariant['terminal']): Record<string, string> {
139
+ return {
140
+ '--terminal-background': t.background,
141
+ '--terminal-foreground': t.foreground,
142
+ '--terminal-cursor': t.cursor,
143
+ '--terminal-selection': t.selection,
144
+ '--terminal-ansi-black': t.black,
145
+ '--terminal-ansi-red': t.red,
146
+ '--terminal-ansi-green': t.green,
147
+ '--terminal-ansi-yellow': t.yellow,
148
+ '--terminal-ansi-blue': t.blue,
149
+ '--terminal-ansi-magenta': t.magenta,
150
+ '--terminal-ansi-cyan': t.cyan,
151
+ '--terminal-ansi-white': t.white,
152
+ '--terminal-ansi-bright-black': t.brightBlack,
153
+ '--terminal-ansi-bright-red': t.brightRed,
154
+ '--terminal-ansi-bright-green': t.brightGreen,
155
+ '--terminal-ansi-bright-yellow': t.brightYellow,
156
+ '--terminal-ansi-bright-blue': t.brightBlue,
157
+ '--terminal-ansi-bright-magenta': t.brightMagenta,
158
+ '--terminal-ansi-bright-cyan': t.brightCyan,
159
+ '--terminal-ansi-bright-white': t.brightWhite,
160
+ }
161
+ }
162
+
163
+ /**
164
+ * The `--terminal-*` properties of a variant: the palette a terminal paints.
165
+ *
166
+ * What a pinned terminal theme writes over the interface variant's own. The names
167
+ * are the ones {@link themeCssVariablesWith} writes, and a custom variant's verbatim
168
+ * `tokens` still win, so they can never drift from what the interface variant would
169
+ * have written for the same palette. Built from the terminal palette alone: it needs
170
+ * neither the catalogue nor the rest of the variant's roles.
171
+ */
172
+ export function terminalCssVariables(theme: ThemeVariant): Record<string, string> {
173
+ const properties = terminalVariables(theme.terminal)
174
+ if (!theme.tokens) return properties
175
+ for (const [name, value] of Object.entries(theme.tokens)) {
176
+ if (name.startsWith('--terminal-')) properties[name] = value
177
+ }
178
+ return properties
179
+ }
180
+
181
+ /**
182
+ * Token values with every `oklch()` colour written as `#rrggbb`.
183
+ *
184
+ * For a host whose stylesheet, tests or native shell speak hex: the catalogue's
185
+ * roles are `oklch()` strings, and this is the one conversion, gamut-mapped by the
186
+ * catalogue's own `oklchToHex`. A value that is already hex, carries alpha
187
+ * (`rgba()`), or is not a plain colour (`color-mix()`, `var()`) is left byte for
188
+ * byte, because converting would either lose the alpha or be a guess. Suitable as
189
+ * `ThemeProvider`'s `transformTokens`.
190
+ */
191
+ export function tokensAsHex(tokens: Readonly<Record<string, string>>): Record<string, string> {
192
+ const converted: Record<string, string> = {}
193
+ for (const [name, value] of Object.entries(tokens)) {
194
+ const trimmed = value.trim()
195
+ // `parseOklch` discards alpha, so a translucent oklch() stays as written.
196
+ const opaque = trimmed.startsWith('oklch(') && !trimmed.includes('/')
197
+ const parsed = opaque ? parseColor(trimmed) : undefined
198
+ converted[name] = parsed ? oklchToHex(parsed) : value
199
+ }
200
+ return converted
201
+ }