@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,205 @@
1
+ /**
2
+ * One catalogue record, as this system's {@link ThemeVariant}.
3
+ *
4
+ * `variantFromRecord` and the lazy form the catalogue is built with live here, and not
5
+ * beside the 34-theme table in `lib/themes.ts`, because a bundler places a module
6
+ * whole: a host that imports `variantFromRecord` from the same module as the table
7
+ * ships the table with it. This module reads a record it is handed and imports no
8
+ * catalogue, so a start-up path that only turns a stored record into a variant
9
+ * (an imported theme, a reconstruction) stays small. `lib/themes.ts` re-exports
10
+ * `variantFromRecord`, so `@adea-ai/ui/lib/themes` keeps working; the lazy start-up
11
+ * gate (`check:theme-startup`) holds the split in place.
12
+ *
13
+ * It does import `@adea-ai/themes` for the projection helpers (`chartSeries`,
14
+ * `tint`, `shadcnVariables`…), none of which reads the table of themes.
15
+ */
16
+ import {
17
+ chartSeries,
18
+ shadcnDestructiveProjection,
19
+ statusForeground,
20
+ syntaxRoles,
21
+ tint,
22
+ type AdeaThemeRecord,
23
+ } from '@adea-ai/themes'
24
+ import { shadcnVariables } from '@adea-ai/themes/adapters/shadcn'
25
+ import type {
26
+ ThemeColors,
27
+ ThemeEditorPalette,
28
+ ThemeProvenance,
29
+ ThemeTerminalPalette,
30
+ ThemeVariant,
31
+ } from './themes'
32
+
33
+ /**
34
+ * The shadcn-vocabulary roles of one catalogue entry.
35
+ *
36
+ * The expensive half of a variant: `shadcnVariables` projects the whole palette
37
+ * (about 4 ms a theme), which is why {@link lazyVariantFromRecord} defers it.
38
+ */
39
+ function colorsOf(theme: AdeaThemeRecord): ThemeColors {
40
+ const projected = shadcnVariables(theme)
41
+ const token = (name: string): string => {
42
+ const value = projected[`--${name}`]
43
+ if (!value) throw new Error(`${theme.id} has no shared shadcn projection for --${name}`)
44
+ return value
45
+ }
46
+ const destructiveAction = shadcnDestructiveProjection(theme)
47
+ return {
48
+ background: token('background'),
49
+ foreground: token('foreground'),
50
+ card: token('card'),
51
+ cardForeground: token('card-foreground'),
52
+ popover: token('popover'),
53
+ popoverForeground: token('popover-foreground'),
54
+ primary: token('primary'),
55
+ primaryForeground: token('primary-foreground'),
56
+ secondary: token('secondary'),
57
+ secondaryForeground: token('secondary-foreground'),
58
+ muted: token('muted'),
59
+ mutedForeground: token('muted-foreground'),
60
+ accent: token('accent'),
61
+ accentForeground: token('accent-foreground'),
62
+ // Preserve the UI's semantic error token. The Themes projection uses its
63
+ // contrast-safe solid-action fill for --destructive; UI keeps that fill in
64
+ // the separate destructiveAction pair below.
65
+ destructive: theme.colors.error,
66
+ destructiveForeground: statusForeground(theme, 'error'),
67
+ destructiveSubtle: tint(theme.colors.error, theme.colors.background),
68
+ destructiveAction: destructiveAction.fill,
69
+ destructiveActionForeground: destructiveAction.foreground,
70
+ success: token('success'),
71
+ successForeground: statusForeground(theme, 'success'),
72
+ successSubtle: tint(theme.colors.success, theme.colors.background),
73
+ warning: token('warning'),
74
+ warningForeground: statusForeground(theme, 'warning'),
75
+ warningSubtle: tint(theme.colors.warning, theme.colors.background),
76
+ info: token('info'),
77
+ infoForeground: statusForeground(theme, 'info'),
78
+ infoSubtle: tint(theme.colors.info, theme.colors.background),
79
+ border: token('border'),
80
+ input: token('input'),
81
+ ring: token('ring'),
82
+ sidebar: token('sidebar'),
83
+ sidebarForeground: token('sidebar-foreground'),
84
+ sidebarAccent: token('sidebar-accent'),
85
+ sidebarBorder: token('sidebar-border'),
86
+ sidebarMutedForeground: token('sidebar-muted-foreground'),
87
+ }
88
+ }
89
+
90
+ // The embedded terminal uses the theme's own canvas rather than a palette of its
91
+ // own. The ANSI colours came from the palette that produced the canvas, so they
92
+ // were chosen against it; a terminal on a different background would be the one
93
+ // surface in the application not lit by the active theme.
94
+ function terminalOf(theme: AdeaThemeRecord): ThemeTerminalPalette {
95
+ return {
96
+ background: theme.colors.background,
97
+ foreground: theme.colors.foreground,
98
+ cursor: theme.cursor,
99
+ selection: theme.selection,
100
+ ...theme.ansi,
101
+ }
102
+ }
103
+
104
+ function editorOf(theme: AdeaThemeRecord): ThemeEditorPalette {
105
+ const syntax = syntaxRoles(theme)
106
+ return {
107
+ keyword: syntax.keyword,
108
+ string: syntax.string,
109
+ number: syntax.number,
110
+ comment: syntax.comment,
111
+ function: syntax.function,
112
+ variable: syntax.variable,
113
+ type: syntax.type,
114
+ tag: syntax.tag,
115
+ attribute: syntax.attribute,
116
+ operator: syntax.operator,
117
+ heading: syntax.heading,
118
+ link: syntax.link,
119
+ diffAdd: syntax.diffAdd,
120
+ diffDelete: syntax.diffDelete,
121
+ diffHunk: syntax.diffHunk,
122
+ searchMatch: syntax.searchMatch,
123
+ }
124
+ }
125
+
126
+ function provenanceOf(theme: AdeaThemeRecord): ThemeProvenance {
127
+ return {
128
+ source: theme.provenance.project,
129
+ url: theme.provenance.url,
130
+ license: theme.provenance.license,
131
+ }
132
+ }
133
+
134
+ /**
135
+ * The design system's view of one catalogue record, built in full and at once.
136
+ *
137
+ * For a host that holds a record the catalogue does not — an imported theme, a
138
+ * reconstruction from stored data — and wants a variant it can hand to
139
+ * `ThemeProvider`'s `themes`. It throws for a record the shadcn projection cannot
140
+ * map, here and now, rather than on first paint. The catalogue itself goes through
141
+ * {@link lazyVariantFromRecord} instead.
142
+ */
143
+ export function variantFromRecord(theme: AdeaThemeRecord): ThemeVariant {
144
+ return {
145
+ id: theme.id,
146
+ family: theme.family,
147
+ familyLabel: theme.familyLabel,
148
+ label: theme.label,
149
+ appearance: theme.appearance,
150
+ description: theme.description,
151
+ colors: colorsOf(theme),
152
+ terminal: terminalOf(theme),
153
+ editor: editorOf(theme),
154
+ chart: chartSeries(theme),
155
+ provenance: provenanceOf(theme),
156
+ }
157
+ }
158
+
159
+ /** Runs `build` on first call and returns that value from then on. A throw is not cached. */
160
+ function once<T>(build: () => T): () => T {
161
+ let built = false
162
+ let value: T
163
+ return () => {
164
+ if (!built) {
165
+ value = build()
166
+ built = true
167
+ }
168
+ return value
169
+ }
170
+ }
171
+
172
+ const lazy = <T>(build: () => T): PropertyDescriptor => ({
173
+ get: once(build),
174
+ enumerable: true,
175
+ })
176
+
177
+ /**
178
+ * {@link variantFromRecord} with the four derived palettes built on first read.
179
+ *
180
+ * Evaluating this module used to project all 34 catalogue themes up front, about
181
+ * 155 ms before a single one was needed, on the hydration path of every consumer.
182
+ * A page paints one variant, and a picker's labels and appearance are plain
183
+ * fields, so only a variant whose `colors`, `terminal`, `editor` or `chart` is read
184
+ * pays for them, once. The properties stay enumerable data-shaped accessors, so
185
+ * spreading, `Object.entries` and JSON see the same object they always did.
186
+ */
187
+ export function lazyVariantFromRecord(theme: AdeaThemeRecord): ThemeVariant {
188
+ return Object.defineProperties(
189
+ {
190
+ id: theme.id,
191
+ family: theme.family,
192
+ familyLabel: theme.familyLabel,
193
+ label: theme.label,
194
+ appearance: theme.appearance,
195
+ description: theme.description,
196
+ provenance: provenanceOf(theme),
197
+ },
198
+ {
199
+ colors: lazy(() => colorsOf(theme)),
200
+ terminal: lazy(() => terminalOf(theme)),
201
+ editor: lazy(() => editorOf(theme)),
202
+ chart: lazy(() => chartSeries(theme)),
203
+ }
204
+ ) as ThemeVariant
205
+ }
package/src/lib/themes.ts CHANGED
@@ -34,19 +34,12 @@
34
34
  */
35
35
 
36
36
  import {
37
- ACCENTS,
38
37
  accentForeground,
39
- chartSeries,
40
38
  editorRolesHex,
41
39
  getAccent,
42
40
  getTheme,
43
41
  hasTheme,
44
- shadcnDestructiveProjection,
45
- statusForeground,
46
- tint,
47
- primaryHover,
48
42
  primarySubtleCss,
49
- syntaxRoles,
50
43
  themes as catalogue,
51
44
  themesByAppearance,
52
45
  themeAccentPresets,
@@ -56,12 +49,17 @@ import {
56
49
  type AdeaThemeRecord,
57
50
  type ContrastFinding,
58
51
  } from '@adea-ai/themes'
59
- import { shadcnVariables } from '@adea-ai/themes/adapters/shadcn'
60
- import { oklchToHex, parseColor } from '@adea-ai/themes/oklch'
61
52
  import { accentOverrideVariables, resolveAccentRoles, type AccentRoles } from './accent-roles'
62
53
  import { isThemeAccentId } from './theme-accents'
54
+ import { themeCssVariablesWith } from './theme-css'
55
+ import { defaultDarkThemeId, defaultLightThemeId } from './theme-defaults'
56
+ import { lazyVariantFromRecord } from './theme-records'
63
57
 
58
+ export { accents } from './accent-presets'
64
59
  export { isThemeAccentId } from './theme-accents'
60
+ export { terminalCssVariables, tokensAsHex } from './theme-css'
61
+ export { defaultDarkThemeId, defaultLightThemeId, resolveVariant } from './theme-defaults'
62
+ export { variantFromRecord } from './theme-records'
65
63
 
66
64
  export type ThemeAppearance = 'light' | 'dark'
67
65
 
@@ -192,180 +190,6 @@ export type ThemeVariant = {
192
190
  tokens?: Readonly<Record<string, string>>
193
191
  }
194
192
 
195
- /**
196
- * The shadcn-vocabulary roles of one catalogue entry.
197
- *
198
- * The expensive half of a variant: `shadcnVariables` projects the whole palette
199
- * (about 4 ms a theme), which is why {@link lazyVariant} defers it.
200
- */
201
- function colorsOf(theme: AdeaThemeRecord): ThemeColors {
202
- const projected = shadcnVariables(theme)
203
- const token = (name: string): string => {
204
- const value = projected[`--${name}`]
205
- if (!value) throw new Error(`${theme.id} has no shared shadcn projection for --${name}`)
206
- return value
207
- }
208
- const destructiveAction = shadcnDestructiveProjection(theme)
209
- return {
210
- background: token('background'),
211
- foreground: token('foreground'),
212
- card: token('card'),
213
- cardForeground: token('card-foreground'),
214
- popover: token('popover'),
215
- popoverForeground: token('popover-foreground'),
216
- primary: token('primary'),
217
- primaryForeground: token('primary-foreground'),
218
- secondary: token('secondary'),
219
- secondaryForeground: token('secondary-foreground'),
220
- muted: token('muted'),
221
- mutedForeground: token('muted-foreground'),
222
- accent: token('accent'),
223
- accentForeground: token('accent-foreground'),
224
- // Preserve the UI's semantic error token. The Themes projection uses its
225
- // contrast-safe solid-action fill for --destructive; UI keeps that fill in
226
- // the separate destructiveAction pair below.
227
- destructive: theme.colors.error,
228
- destructiveForeground: statusForeground(theme, 'error'),
229
- destructiveSubtle: tint(theme.colors.error, theme.colors.background),
230
- destructiveAction: destructiveAction.fill,
231
- destructiveActionForeground: destructiveAction.foreground,
232
- success: token('success'),
233
- successForeground: statusForeground(theme, 'success'),
234
- successSubtle: tint(theme.colors.success, theme.colors.background),
235
- warning: token('warning'),
236
- warningForeground: statusForeground(theme, 'warning'),
237
- warningSubtle: tint(theme.colors.warning, theme.colors.background),
238
- info: token('info'),
239
- infoForeground: statusForeground(theme, 'info'),
240
- infoSubtle: tint(theme.colors.info, theme.colors.background),
241
- border: token('border'),
242
- input: token('input'),
243
- ring: token('ring'),
244
- sidebar: token('sidebar'),
245
- sidebarForeground: token('sidebar-foreground'),
246
- sidebarAccent: token('sidebar-accent'),
247
- sidebarBorder: token('sidebar-border'),
248
- sidebarMutedForeground: token('sidebar-muted-foreground'),
249
- }
250
- }
251
-
252
- // The embedded terminal uses the theme's own canvas rather than a palette of its
253
- // own. The ANSI colours came from the palette that produced the canvas, so they
254
- // were chosen against it; a terminal on a different background would be the one
255
- // surface in the application not lit by the active theme.
256
- function terminalOf(theme: AdeaThemeRecord): ThemeTerminalPalette {
257
- return {
258
- background: theme.colors.background,
259
- foreground: theme.colors.foreground,
260
- cursor: theme.cursor,
261
- selection: theme.selection,
262
- ...theme.ansi,
263
- }
264
- }
265
-
266
- function editorOf(theme: AdeaThemeRecord): ThemeEditorPalette {
267
- const syntax = syntaxRoles(theme)
268
- return {
269
- keyword: syntax.keyword,
270
- string: syntax.string,
271
- number: syntax.number,
272
- comment: syntax.comment,
273
- function: syntax.function,
274
- variable: syntax.variable,
275
- type: syntax.type,
276
- tag: syntax.tag,
277
- attribute: syntax.attribute,
278
- operator: syntax.operator,
279
- heading: syntax.heading,
280
- link: syntax.link,
281
- diffAdd: syntax.diffAdd,
282
- diffDelete: syntax.diffDelete,
283
- diffHunk: syntax.diffHunk,
284
- searchMatch: syntax.searchMatch,
285
- }
286
- }
287
-
288
- function provenanceOf(theme: AdeaThemeRecord): ThemeProvenance {
289
- return {
290
- source: theme.provenance.project,
291
- url: theme.provenance.url,
292
- license: theme.provenance.license,
293
- }
294
- }
295
-
296
- /**
297
- * The design system's view of one catalogue record, built in full and at once.
298
- *
299
- * For a host that holds a record the catalogue does not — an imported theme, a
300
- * reconstruction from stored data — and wants a variant it can hand to
301
- * `ThemeProvider`'s `themes`. It throws for a record the shadcn projection cannot
302
- * map, here and now, rather than on first paint. The catalogue itself goes through
303
- * {@link lazyVariant} instead.
304
- */
305
- export function variantFromRecord(theme: AdeaThemeRecord): ThemeVariant {
306
- return {
307
- id: theme.id,
308
- family: theme.family,
309
- familyLabel: theme.familyLabel,
310
- label: theme.label,
311
- appearance: theme.appearance,
312
- description: theme.description,
313
- colors: colorsOf(theme),
314
- terminal: terminalOf(theme),
315
- editor: editorOf(theme),
316
- chart: chartSeries(theme),
317
- provenance: provenanceOf(theme),
318
- }
319
- }
320
-
321
- /** Runs `build` on first call and returns that value from then on. A throw is not cached. */
322
- function once<T>(build: () => T): () => T {
323
- let built = false
324
- let value: T
325
- return () => {
326
- if (!built) {
327
- value = build()
328
- built = true
329
- }
330
- return value
331
- }
332
- }
333
-
334
- const lazy = <T>(build: () => T): PropertyDescriptor => ({
335
- get: once(build),
336
- enumerable: true,
337
- })
338
-
339
- /**
340
- * {@link variantFromRecord} with the four derived palettes built on first read.
341
- *
342
- * Evaluating this module used to project all 34 catalogue themes up front, about
343
- * 155 ms before a single one was needed, on the hydration path of every consumer.
344
- * A page paints one variant, and a picker's labels and appearance are plain
345
- * fields, so only a variant whose `colors`, `terminal`, `editor` or `chart` is read
346
- * pays for them, once. The properties stay enumerable data-shaped accessors, so
347
- * spreading, `Object.entries` and JSON see the same object they always did.
348
- */
349
- function lazyVariant(theme: AdeaThemeRecord): ThemeVariant {
350
- return Object.defineProperties(
351
- {
352
- id: theme.id,
353
- family: theme.family,
354
- familyLabel: theme.familyLabel,
355
- label: theme.label,
356
- appearance: theme.appearance,
357
- description: theme.description,
358
- provenance: provenanceOf(theme),
359
- },
360
- {
361
- colors: lazy(() => colorsOf(theme)),
362
- terminal: lazy(() => terminalOf(theme)),
363
- editor: lazy(() => editorOf(theme)),
364
- chart: lazy(() => chartSeries(theme)),
365
- }
366
- ) as ThemeVariant
367
- }
368
-
369
193
  /**
370
194
  * A variant's `--primary-subtle`: the tint over `var(--primary)` at the strength its
371
195
  * catalogue record measures (`primarySubtleAlpha`). A variant that is not in the
@@ -376,46 +200,22 @@ export function primarySubtleFor(theme: Pick<ThemeVariant, 'id' | 'appearance'>)
376
200
  return record ? primarySubtleCss(record) : primarySubtleCss(theme.appearance)
377
201
  }
378
202
 
379
- /** The catalogue, in catalogue order. */
380
- export const builtinThemes: readonly ThemeVariant[] = Object.freeze(catalogue.map(lazyVariant))
203
+ /**
204
+ * The catalogue, in catalogue order.
205
+ *
206
+ * The `@__PURE__` marks let a bundler drop the whole table, and with it the catalogue,
207
+ * from a consumer that imports only this module's catalogue-free helpers
208
+ * (`tokensAsHex`, `isThemeAccentId`, `resolveVariant`…) and never reads it.
209
+ */
210
+ export const builtinThemes: readonly ThemeVariant[] = /* @__PURE__ */ Object.freeze(
211
+ /* @__PURE__ */ catalogue.map(lazyVariantFromRecord)
212
+ )
381
213
 
382
214
  export function themeById(id: string): ThemeVariant | undefined {
383
215
  if (!hasTheme(id)) return undefined
384
216
  return builtinThemes.find((theme) => theme.id === id)
385
217
  }
386
218
 
387
- /**
388
- * The variant a stored id means, or the default of the same appearance.
389
- *
390
- * An id the registry does not hold — an imported theme that was deleted, a
391
- * catalogue entry that was renamed — must still paint *something of the appearance
392
- * being shown*. Falling back to whichever variant happens to be first in the
393
- * registry painted a dark palette under a light `color-scheme` for a light id, so
394
- * the fallback is the appearance's own default, then any variant of that
395
- * appearance, then the registry's first entry only when it holds nothing else.
396
- *
397
- * `registry` is the builtin catalogue plus whatever extras the host supplies; the
398
- * stored id is never rewritten, only resolved, so a host that keeps an unknown id
399
- * keeps resolving it if the theme comes back.
400
- *
401
- * The fallback order is translated from Zeron's `ThemeRegistry::resolve` (MIT,
402
- * Copyright (c) 2026 Wing); see NOTICE.
403
- */
404
- export function resolveVariant(
405
- registry: readonly ThemeVariant[],
406
- id: string,
407
- appearance: ThemeAppearance
408
- ): ThemeVariant {
409
- const found = registry.find((variant) => variant.id === id)
410
- if (found) return found
411
- const defaultId = appearance === 'dark' ? defaultDarkThemeId : defaultLightThemeId
412
- return (
413
- registry.find((variant) => variant.id === defaultId) ??
414
- registry.find((variant) => variant.appearance === appearance) ??
415
- registry[0]!
416
- )
417
- }
418
-
419
219
  /** The variants of one appearance, which is what a picker for that mode shows. */
420
220
  export function themesForAppearance(appearance: ThemeAppearance): readonly ThemeVariant[] {
421
221
  const ids = new Set(themesByAppearance(appearance).map((theme) => theme.id))
@@ -437,9 +237,6 @@ export function themeFamilies(): { family: string; label: string; themes: ThemeV
437
237
  return [...families.values()]
438
238
  }
439
239
 
440
- /** The accent presets, in the order a picker should show them. */
441
- export const accents = ACCENTS
442
-
443
240
  /**
444
241
  * The canonical catalogue records, in catalogue order.
445
242
  *
@@ -708,166 +505,27 @@ export function editorPaletteFor(
708
505
  /**
709
506
  * The roles a variant sets, as CSS custom properties.
710
507
  *
711
- * The one place that knows the mapping from a theme's roles to this system's variable
712
- * names. A provider applies this; nothing else repeats it. The `--terminal-*` and
713
- * `--editor-*` ramps are presentations of the catalogue's ANSI and syntax data rather
714
- * than separate roles, which is why they are built here and not in the catalogue.
715
- *
716
- * `options.editor` picks the `--editor-*` projection ({@link EditorRolesMode}).
717
- * Absent it, the result is what it has always been; the generated `theme-variants.css`
718
- * and the design-system artifact are built without it.
508
+ * The mapping itself is `themeCssVariablesWith` in `theme-css.ts`; this binds it to
509
+ * the catalogue, which measures a theme's `--primary-subtle` strength
510
+ * ({@link primarySubtleFor}) and projects its floor editor ramp
511
+ * ({@link editorPaletteFor}). Absent `options`, the result is what it has always
512
+ * been; the generated `theme-variants.css` and the design-system artifact are built
513
+ * without it.
719
514
  */
720
515
  export function themeCssVariables(
721
516
  theme: ThemeVariant,
722
517
  options: ThemeCssVariableOptions = {}
723
518
  ): Record<string, string> {
724
- const c = theme.colors
725
- const t = theme.terminal
726
- const e = editorPaletteFor(theme, options.editor)
727
- const derived = {
728
- '--background': c.background,
729
- '--foreground': c.foreground,
730
- '--card': c.card,
731
- '--card-foreground': c.cardForeground,
732
- '--popover': c.popover,
733
- '--popover-foreground': c.popoverForeground,
734
- '--primary': c.primary,
735
- '--primary-foreground': c.primaryForeground,
736
- /*
737
- * The two tokens derived from the primary, and they are here rather than left to
738
- * `theme.css` because they must follow `--primary` — which this function writes
739
- * for *any* theme in the catalogue, not just the two defaults `theme.css` carries.
740
- *
741
- * `--primary-hover` is a resolved colour, not a `color-mix()`: a uniform hover
742
- * step cannot be expressed as a mix (see `primaryHover`). Left out, a theme switch
743
- * would keep the previous theme's hover on the new theme's primary.
744
- *
745
- * `--primary-subtle` is a mix over `--primary`, so it follows an accent on its
746
- * own, but its *strength* is per theme: `primarySubtleCss(record)` lowers the
747
- * appearance ceiling only as far as this theme needs for body text to clear 4.5:1
748
- * on the tint, over every surface, for every accent it offers. It has to be
749
- * written per theme for that reason — the appearance-wide value is not legible on
750
- * every palette.
751
- */
752
- '--primary-hover': primaryHover(c.primary, theme.appearance),
753
- '--primary-subtle': primarySubtleFor(theme),
754
- '--secondary': c.secondary,
755
- '--secondary-foreground': c.secondaryForeground,
756
- '--muted': c.muted,
757
- '--muted-foreground': c.mutedForeground,
758
- '--accent': c.accent,
759
- '--accent-foreground': c.accentForeground,
760
- '--destructive': c.destructive,
761
- '--destructive-foreground': c.destructiveForeground,
762
- '--destructive-subtle': c.destructiveSubtle,
763
- '--destructive-action': c.destructiveAction,
764
- '--destructive-action-foreground': c.destructiveActionForeground,
765
- '--success': c.success,
766
- '--success-foreground': c.successForeground,
767
- '--success-subtle': c.successSubtle,
768
- '--warning': c.warning,
769
- '--warning-foreground': c.warningForeground,
770
- '--warning-subtle': c.warningSubtle,
771
- '--info': c.info,
772
- '--info-foreground': c.infoForeground,
773
- '--info-subtle': c.infoSubtle,
774
- '--border': c.border,
775
- '--input': c.input,
776
- '--ring': c.ring,
777
- '--sidebar': c.sidebar,
778
- '--sidebar-foreground': c.sidebarForeground,
779
- '--sidebar-accent': c.sidebarAccent,
780
- '--sidebar-border': c.sidebarBorder,
781
- '--sidebar-primary': c.primary,
782
- '--sidebar-primary-foreground': c.primaryForeground,
783
- '--sidebar-ring': c.ring,
784
- '--sidebar-muted-foreground': c.sidebarMutedForeground,
785
- '--surface-sunken': c.muted,
786
- '--surface-hover': c.accent,
787
- '--surface-active': c.border,
788
- '--chrome': c.background,
789
- '--terminal-background': t.background,
790
- '--terminal-foreground': t.foreground,
791
- '--terminal-cursor': t.cursor,
792
- '--terminal-selection': t.selection,
793
- '--terminal-ansi-black': t.black,
794
- '--terminal-ansi-red': t.red,
795
- '--terminal-ansi-green': t.green,
796
- '--terminal-ansi-yellow': t.yellow,
797
- '--terminal-ansi-blue': t.blue,
798
- '--terminal-ansi-magenta': t.magenta,
799
- '--terminal-ansi-cyan': t.cyan,
800
- '--terminal-ansi-white': t.white,
801
- '--terminal-ansi-bright-black': t.brightBlack,
802
- '--terminal-ansi-bright-red': t.brightRed,
803
- '--terminal-ansi-bright-green': t.brightGreen,
804
- '--terminal-ansi-bright-yellow': t.brightYellow,
805
- '--terminal-ansi-bright-blue': t.brightBlue,
806
- '--terminal-ansi-bright-magenta': t.brightMagenta,
807
- '--terminal-ansi-bright-cyan': t.brightCyan,
808
- '--terminal-ansi-bright-white': t.brightWhite,
809
- '--editor-keyword': e.keyword,
810
- '--editor-string': e.string,
811
- '--editor-number': e.number,
812
- '--editor-comment': e.comment,
813
- '--editor-function': e.function,
814
- '--editor-variable': e.variable,
815
- '--editor-type': e.type,
816
- '--editor-tag': e.tag,
817
- '--editor-attribute': e.attribute,
818
- '--editor-operator': e.operator,
819
- '--editor-heading': e.heading,
820
- '--editor-link': e.link,
821
- '--editor-diff-add': e.diffAdd,
822
- '--editor-diff-delete': e.diffDelete,
823
- '--editor-diff-hunk': e.diffHunk,
824
- '--editor-search-match': e.searchMatch,
825
- '--chart-1': theme.chart[0] ?? c.primary,
826
- '--chart-2': theme.chart[1] ?? c.info,
827
- '--chart-3': theme.chart[2] ?? c.success,
828
- '--chart-4': theme.chart[3] ?? c.warning,
829
- '--chart-5': theme.chart[4] ?? c.destructive,
830
- '--chart-6': theme.chart[5] ?? c.mutedForeground,
831
- }
832
- return theme.tokens ? { ...derived, ...theme.tokens } : derived
833
- }
834
-
835
- /**
836
- * The `--terminal-*` properties of a variant: the palette a terminal paints.
837
- *
838
- * What a pinned terminal theme writes over the interface variant's own. Taken from
839
- * {@link themeCssVariables} rather than rebuilt, so the names, and a custom
840
- * variant's verbatim `tokens`, can never drift from what the interface variant
841
- * would have written for the same palette.
842
- */
843
- export function terminalCssVariables(theme: ThemeVariant): Record<string, string> {
844
- return Object.fromEntries(
845
- Object.entries(themeCssVariables(theme)).filter(([name]) => name.startsWith('--terminal-'))
519
+ return themeCssVariablesWith(
520
+ {
521
+ primarySubtle: primarySubtleFor,
522
+ editor: (variant, mode) => editorPaletteFor(variant, mode),
523
+ },
524
+ theme,
525
+ options
846
526
  )
847
527
  }
848
528
 
849
- /**
850
- * Token values with every `oklch()` colour written as `#rrggbb`.
851
- *
852
- * For a host whose stylesheet, tests or native shell speak hex: the catalogue's
853
- * roles are `oklch()` strings, and this is the one conversion, gamut-mapped by the
854
- * catalogue's own `oklchToHex`. A value that is already hex, carries alpha
855
- * (`rgba()`), or is not a plain colour (`color-mix()`, `var()`) is left byte for
856
- * byte, because converting would either lose the alpha or be a guess. Suitable as
857
- * `ThemeProvider`'s `transformTokens`.
858
- */
859
- export function tokensAsHex(tokens: Readonly<Record<string, string>>): Record<string, string> {
860
- const converted: Record<string, string> = {}
861
- for (const [name, value] of Object.entries(tokens)) {
862
- const trimmed = value.trim()
863
- // `parseOklch` discards alpha, so a translucent oklch() stays as written.
864
- const opaque = trimmed.startsWith('oklch(') && !trimmed.includes('/')
865
- const parsed = opaque ? parseColor(trimmed) : undefined
866
- converted[name] = parsed ? oklchToHex(parsed) : value
867
- }
868
- return converted
869
- }
870
-
871
529
  /* ---------------------------------------------------------------------------
872
530
  * Validation
873
531
  *
@@ -914,8 +572,5 @@ export function validateThemeRegistry(
914
572
  return [...orphaned, ...validateCatalogue(records).map(toFinding)]
915
573
  }
916
574
 
917
- export const defaultLightThemeId = 'adea-light'
918
- export const defaultDarkThemeId = 'adea-dark'
919
-
920
575
  /** Re-exported so a consumer measuring a colour does not need a second import. */
921
576
  export { contrastRatio } from '@adea-ai/themes'