@adea-ai/ui 0.128.3 → 0.129.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,6 +33,11 @@
33
33
  "type": "registry:ui",
34
34
  "target": "components/theming/lazy.ts"
35
35
  },
36
+ {
37
+ "path": "src/components/theme/script.ts",
38
+ "type": "registry:ui",
39
+ "target": "components/theming/script.ts"
40
+ },
36
41
  {
37
42
  "path": "src/components/theme/theme-catalogue-source.ts",
38
43
  "type": "registry:ui",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.128.3",
3
+ "version": "0.129.0",
4
4
  "description": "The Adea design system: Kobalte-backed Solid components, semantic OKLCH tokens, and the app-shell layout primitives shared by Adea and Cortana.",
5
5
  "keywords": [
6
6
  "adea",
@@ -74,6 +74,12 @@
74
74
  "./fonts.css": "./src/styles/fonts.css",
75
75
  "./registry.json": "./registry.json",
76
76
  "./package.json": "./package.json",
77
+ "./components/theme/script": {
78
+ "types": "./dist/components/theme/script.d.ts",
79
+ "solid": "./src/components/theme/script.ts",
80
+ "development": "./src/components/theme/script.ts",
81
+ "import": "./dist/components/theme/script.js"
82
+ },
77
83
  "./components/theme/lazy": {
78
84
  "types": "./dist/components/theme/lazy.d.ts",
79
85
  "solid": "./src/components/theme/lazy.ts",
package/registry.json CHANGED
@@ -2532,6 +2532,11 @@
2532
2532
  "type": "registry:ui",
2533
2533
  "target": "components/theming/lazy.ts"
2534
2534
  },
2535
+ {
2536
+ "path": "src/components/theme/script.ts",
2537
+ "type": "registry:ui",
2538
+ "target": "components/theming/script.ts"
2539
+ },
2535
2540
  {
2536
2541
  "path": "src/components/theme/theme-catalogue-source.ts",
2537
2542
  "type": "registry:ui",
@@ -2789,6 +2794,11 @@
2789
2794
  "type": "registry:lib",
2790
2795
  "target": "lib/theme-engine.ts"
2791
2796
  },
2797
+ {
2798
+ "path": "src/lib/theme-records.ts",
2799
+ "type": "registry:lib",
2800
+ "target": "lib/theme-records.ts"
2801
+ },
2792
2802
  {
2793
2803
  "path": "src/lib/theme-surface.ts",
2794
2804
  "type": "registry:lib",
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The no-flash boot script, importable where there is no UI runtime.
3
+ *
4
+ * `themeScript` writes the document attributes before first paint. A host that
5
+ * renders its HTML at build time (a Vite `index.html` served by a desktop shell
6
+ * whose CSP allows no inline script) needs the string in Node, and the provider
7
+ * entries cannot be imported there: they load Solid's client runtime. This entry
8
+ * holds only the script and the types it takes, so it loads anywhere.
9
+ */
10
+ export { inlineScriptLiteral, themeScript, type ThemeScriptOptions } from './theme-script'
11
+ export type { ThemeStorageSchema } from './theme-persistence'
@@ -4,7 +4,8 @@ import { ACCENTS } from '@adea-ai/themes'
4
4
  * Its own module so the theme provider's start-up path (the stored-preference reader
5
5
  * and the boot script both need the preset ids) does not import the token manifest in
6
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.
7
+ * `tokens.ts` re-exports `accentPresets` and `AccentPreset`, and `themes.ts` re-exports
8
+ * `accents`, so every existing import keeps working.
8
9
  */
9
10
 
10
11
  /**
@@ -41,3 +42,13 @@ export const accentPresets: readonly AccentPreset[] = Object.freeze([
41
42
  },
42
43
  ...ACCENTS,
43
44
  ])
45
+
46
+ /**
47
+ * The six brand accent presets, in the order a picker should show them: the list
48
+ * `accentPresets` carries after its own `theme` entry.
49
+ *
50
+ * Here and not beside the theme table in `lib/themes.ts` (which re-exports it, so
51
+ * `@adea-ai/ui/lib/themes` still has it) because a bundler places a module whole, and
52
+ * a host that wants only the list should not ship the 34-theme catalogue with it.
53
+ */
54
+ export const accents = ACCENTS
@@ -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,18 +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
42
  primarySubtleCss,
48
- syntaxRoles,
49
43
  themes as catalogue,
50
44
  themesByAppearance,
51
45
  themeAccentPresets,
@@ -55,15 +49,17 @@ import {
55
49
  type AdeaThemeRecord,
56
50
  type ContrastFinding,
57
51
  } from '@adea-ai/themes'
58
- import { shadcnVariables } from '@adea-ai/themes/adapters/shadcn'
59
52
  import { accentOverrideVariables, resolveAccentRoles, type AccentRoles } from './accent-roles'
60
53
  import { isThemeAccentId } from './theme-accents'
61
54
  import { themeCssVariablesWith } from './theme-css'
62
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'
65
60
  export { terminalCssVariables, tokensAsHex } from './theme-css'
66
61
  export { defaultDarkThemeId, defaultLightThemeId, resolveVariant } from './theme-defaults'
62
+ export { variantFromRecord } from './theme-records'
67
63
 
68
64
  export type ThemeAppearance = 'light' | 'dark'
69
65
 
@@ -194,180 +190,6 @@ export type ThemeVariant = {
194
190
  tokens?: Readonly<Record<string, string>>
195
191
  }
196
192
 
197
- /**
198
- * The shadcn-vocabulary roles of one catalogue entry.
199
- *
200
- * The expensive half of a variant: `shadcnVariables` projects the whole palette
201
- * (about 4 ms a theme), which is why {@link lazyVariant} defers it.
202
- */
203
- function colorsOf(theme: AdeaThemeRecord): ThemeColors {
204
- const projected = shadcnVariables(theme)
205
- const token = (name: string): string => {
206
- const value = projected[`--${name}`]
207
- if (!value) throw new Error(`${theme.id} has no shared shadcn projection for --${name}`)
208
- return value
209
- }
210
- const destructiveAction = shadcnDestructiveProjection(theme)
211
- return {
212
- background: token('background'),
213
- foreground: token('foreground'),
214
- card: token('card'),
215
- cardForeground: token('card-foreground'),
216
- popover: token('popover'),
217
- popoverForeground: token('popover-foreground'),
218
- primary: token('primary'),
219
- primaryForeground: token('primary-foreground'),
220
- secondary: token('secondary'),
221
- secondaryForeground: token('secondary-foreground'),
222
- muted: token('muted'),
223
- mutedForeground: token('muted-foreground'),
224
- accent: token('accent'),
225
- accentForeground: token('accent-foreground'),
226
- // Preserve the UI's semantic error token. The Themes projection uses its
227
- // contrast-safe solid-action fill for --destructive; UI keeps that fill in
228
- // the separate destructiveAction pair below.
229
- destructive: theme.colors.error,
230
- destructiveForeground: statusForeground(theme, 'error'),
231
- destructiveSubtle: tint(theme.colors.error, theme.colors.background),
232
- destructiveAction: destructiveAction.fill,
233
- destructiveActionForeground: destructiveAction.foreground,
234
- success: token('success'),
235
- successForeground: statusForeground(theme, 'success'),
236
- successSubtle: tint(theme.colors.success, theme.colors.background),
237
- warning: token('warning'),
238
- warningForeground: statusForeground(theme, 'warning'),
239
- warningSubtle: tint(theme.colors.warning, theme.colors.background),
240
- info: token('info'),
241
- infoForeground: statusForeground(theme, 'info'),
242
- infoSubtle: tint(theme.colors.info, theme.colors.background),
243
- border: token('border'),
244
- input: token('input'),
245
- ring: token('ring'),
246
- sidebar: token('sidebar'),
247
- sidebarForeground: token('sidebar-foreground'),
248
- sidebarAccent: token('sidebar-accent'),
249
- sidebarBorder: token('sidebar-border'),
250
- sidebarMutedForeground: token('sidebar-muted-foreground'),
251
- }
252
- }
253
-
254
- // The embedded terminal uses the theme's own canvas rather than a palette of its
255
- // own. The ANSI colours came from the palette that produced the canvas, so they
256
- // were chosen against it; a terminal on a different background would be the one
257
- // surface in the application not lit by the active theme.
258
- function terminalOf(theme: AdeaThemeRecord): ThemeTerminalPalette {
259
- return {
260
- background: theme.colors.background,
261
- foreground: theme.colors.foreground,
262
- cursor: theme.cursor,
263
- selection: theme.selection,
264
- ...theme.ansi,
265
- }
266
- }
267
-
268
- function editorOf(theme: AdeaThemeRecord): ThemeEditorPalette {
269
- const syntax = syntaxRoles(theme)
270
- return {
271
- keyword: syntax.keyword,
272
- string: syntax.string,
273
- number: syntax.number,
274
- comment: syntax.comment,
275
- function: syntax.function,
276
- variable: syntax.variable,
277
- type: syntax.type,
278
- tag: syntax.tag,
279
- attribute: syntax.attribute,
280
- operator: syntax.operator,
281
- heading: syntax.heading,
282
- link: syntax.link,
283
- diffAdd: syntax.diffAdd,
284
- diffDelete: syntax.diffDelete,
285
- diffHunk: syntax.diffHunk,
286
- searchMatch: syntax.searchMatch,
287
- }
288
- }
289
-
290
- function provenanceOf(theme: AdeaThemeRecord): ThemeProvenance {
291
- return {
292
- source: theme.provenance.project,
293
- url: theme.provenance.url,
294
- license: theme.provenance.license,
295
- }
296
- }
297
-
298
- /**
299
- * The design system's view of one catalogue record, built in full and at once.
300
- *
301
- * For a host that holds a record the catalogue does not — an imported theme, a
302
- * reconstruction from stored data — and wants a variant it can hand to
303
- * `ThemeProvider`'s `themes`. It throws for a record the shadcn projection cannot
304
- * map, here and now, rather than on first paint. The catalogue itself goes through
305
- * {@link lazyVariant} instead.
306
- */
307
- export function variantFromRecord(theme: AdeaThemeRecord): ThemeVariant {
308
- return {
309
- id: theme.id,
310
- family: theme.family,
311
- familyLabel: theme.familyLabel,
312
- label: theme.label,
313
- appearance: theme.appearance,
314
- description: theme.description,
315
- colors: colorsOf(theme),
316
- terminal: terminalOf(theme),
317
- editor: editorOf(theme),
318
- chart: chartSeries(theme),
319
- provenance: provenanceOf(theme),
320
- }
321
- }
322
-
323
- /** Runs `build` on first call and returns that value from then on. A throw is not cached. */
324
- function once<T>(build: () => T): () => T {
325
- let built = false
326
- let value: T
327
- return () => {
328
- if (!built) {
329
- value = build()
330
- built = true
331
- }
332
- return value
333
- }
334
- }
335
-
336
- const lazy = <T>(build: () => T): PropertyDescriptor => ({
337
- get: once(build),
338
- enumerable: true,
339
- })
340
-
341
- /**
342
- * {@link variantFromRecord} with the four derived palettes built on first read.
343
- *
344
- * Evaluating this module used to project all 34 catalogue themes up front, about
345
- * 155 ms before a single one was needed, on the hydration path of every consumer.
346
- * A page paints one variant, and a picker's labels and appearance are plain
347
- * fields, so only a variant whose `colors`, `terminal`, `editor` or `chart` is read
348
- * pays for them, once. The properties stay enumerable data-shaped accessors, so
349
- * spreading, `Object.entries` and JSON see the same object they always did.
350
- */
351
- function lazyVariant(theme: AdeaThemeRecord): ThemeVariant {
352
- return Object.defineProperties(
353
- {
354
- id: theme.id,
355
- family: theme.family,
356
- familyLabel: theme.familyLabel,
357
- label: theme.label,
358
- appearance: theme.appearance,
359
- description: theme.description,
360
- provenance: provenanceOf(theme),
361
- },
362
- {
363
- colors: lazy(() => colorsOf(theme)),
364
- terminal: lazy(() => terminalOf(theme)),
365
- editor: lazy(() => editorOf(theme)),
366
- chart: lazy(() => chartSeries(theme)),
367
- }
368
- ) as ThemeVariant
369
- }
370
-
371
193
  /**
372
194
  * A variant's `--primary-subtle`: the tint over `var(--primary)` at the strength its
373
195
  * catalogue record measures (`primarySubtleAlpha`). A variant that is not in the
@@ -386,7 +208,7 @@ export function primarySubtleFor(theme: Pick<ThemeVariant, 'id' | 'appearance'>)
386
208
  * (`tokensAsHex`, `isThemeAccentId`, `resolveVariant`…) and never reads it.
387
209
  */
388
210
  export const builtinThemes: readonly ThemeVariant[] = /* @__PURE__ */ Object.freeze(
389
- /* @__PURE__ */ catalogue.map(lazyVariant)
211
+ /* @__PURE__ */ catalogue.map(lazyVariantFromRecord)
390
212
  )
391
213
 
392
214
  export function themeById(id: string): ThemeVariant | undefined {
@@ -415,9 +237,6 @@ export function themeFamilies(): { family: string; label: string; themes: ThemeV
415
237
  return [...families.values()]
416
238
  }
417
239
 
418
- /** The accent presets, in the order a picker should show them. */
419
- export const accents = ACCENTS
420
-
421
240
  /**
422
241
  * The canonical catalogue records, in catalogue order.
423
242
  *