@adea-ai/ui 0.127.0 → 0.128.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/r/theme.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "theme",
3
3
  "type": "registry:style",
4
4
  "title": "Adea theme",
5
- "description": "The design tokens: the semantic palette in OKLCH, the type and control scales, shells geometry, shadows, motion and the z-index stack.",
5
+ "description": "The design tokens: the semantic palette in OKLCH, the type and control scales, shells geometry, shadows, motion and the z-index stack, plus the pre-paint stylesheet that carries every catalogue theme.",
6
6
  "dependencies": [
7
7
  "@fontsource-variable/geist@^5.3.0",
8
8
  "@fontsource-variable/geist-mono@^5.3.0",
@@ -22,6 +22,11 @@
22
22
  "type": "registry:style",
23
23
  "target": "styles/theme.css"
24
24
  },
25
+ {
26
+ "path": "src/styles/theme-variants.css",
27
+ "type": "registry:style",
28
+ "target": "styles/theme-variants.css"
29
+ },
25
30
  {
26
31
  "path": "src/styles/base.css",
27
32
  "type": "registry:style",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.127.0",
3
+ "version": "0.128.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",
@@ -68,6 +68,7 @@
68
68
  },
69
69
  "./globals.css": "./src/styles/globals.css",
70
70
  "./theme.css": "./src/styles/theme.css",
71
+ "./theme-variants.css": "./src/styles/theme-variants.css",
71
72
  "./base.css": "./src/styles/base.css",
72
73
  "./appearance-font-settings.css": "./src/styles/appearance-font-settings.css",
73
74
  "./fonts.css": "./src/styles/fonts.css",
package/registry.json CHANGED
@@ -2600,7 +2600,7 @@
2600
2600
  "name": "theme",
2601
2601
  "type": "registry:style",
2602
2602
  "title": "Adea theme",
2603
- "description": "The design tokens: the semantic palette in OKLCH, the type and control scales, shells geometry, shadows, motion and the z-index stack.",
2603
+ "description": "The design tokens: the semantic palette in OKLCH, the type and control scales, shells geometry, shadows, motion and the z-index stack, plus the pre-paint stylesheet that carries every catalogue theme.",
2604
2604
  "dependencies": [
2605
2605
  "@fontsource-variable/geist@^5.3.0",
2606
2606
  "@fontsource-variable/geist-mono@^5.3.0",
@@ -2620,6 +2620,11 @@
2620
2620
  "type": "registry:style",
2621
2621
  "target": "styles/theme.css"
2622
2622
  },
2623
+ {
2624
+ "path": "src/styles/theme-variants.css",
2625
+ "type": "registry:style",
2626
+ "target": "styles/theme-variants.css"
2627
+ },
2623
2628
  {
2624
2629
  "path": "src/styles/base.css",
2625
2630
  "type": "registry:style",
@@ -17,6 +17,7 @@ export {
17
17
  themeScript,
18
18
  useOptionalTheme,
19
19
  useTheme,
20
+ type EditorRolesMode,
20
21
  type ThemeContextValue,
21
22
  type ThemeDensity,
22
23
  type ThemePersistence,
@@ -4,9 +4,10 @@ import {
4
4
  } from '#lib/appearance-font-settings'
5
5
  import {
6
6
  accentVariables,
7
+ isCatalogueVariant,
7
8
  terminalCssVariables,
8
- themeById,
9
9
  themeCssVariables,
10
+ type EditorRolesMode,
10
11
  type ThemeAppearance,
11
12
  type ThemeVariant,
12
13
  } from '#lib/themes'
@@ -67,11 +68,34 @@ export type ThemeDocumentOptions = {
67
68
  * imported one wrote, so nothing inline outlives it to beat the stylesheet.
68
69
  */
69
70
  palette?: 'inline' | 'stylesheet'
71
+ /**
72
+ * The `--editor-*` projection. `syntax` (the default) is the catalogue's quiet
73
+ * syntax roles, which is what `theme-variants.css` carries; `floor` holds every
74
+ * editor role to 4.5:1 on the canvas, as hex.
75
+ *
76
+ * With `palette: 'stylesheet'`, `floor` is the one palette write a catalogue
77
+ * variant still gets: the stylesheet is generated for the default projection, so
78
+ * the sixteen `--editor-*` properties are written inline over it. The rest of the
79
+ * palette stays in the stylesheet.
80
+ */
81
+ editorRoles?: EditorRolesMode
70
82
  }
71
83
 
72
- /** True for the catalogue's own variant, not a host's copy that merely shares its id. */
73
- function isCatalogueVariant(variant: ThemeVariant): boolean {
74
- return themeById(variant.id) === variant
84
+ /**
85
+ * What a catalogue variant still writes inline when its palette is the stylesheet's:
86
+ * nothing, unless the editor projection is not the one the stylesheet was generated
87
+ * for.
88
+ */
89
+ function stylesheetOverrides(
90
+ variant: ThemeVariant,
91
+ editorRoles: EditorRolesMode | undefined
92
+ ): Record<string, string> {
93
+ if (editorRoles !== 'floor') return {}
94
+ return Object.fromEntries(
95
+ Object.entries(themeCssVariables(variant, { editor: 'floor' })).filter(([name]) =>
96
+ name.startsWith('--editor-')
97
+ )
98
+ )
75
99
  }
76
100
 
77
101
  /**
@@ -91,7 +115,9 @@ export function themeDocumentTokens(
91
115
  options: ThemeDocumentOptions = {}
92
116
  ): { tokens: Record<string, string>; accented: boolean } {
93
117
  const fromStylesheet = options.palette === 'stylesheet' && isCatalogueVariant(state.variant)
94
- const properties = fromStylesheet ? {} : { ...themeCssVariables(state.variant) }
118
+ const properties = fromStylesheet
119
+ ? stylesheetOverrides(state.variant, options.editorRoles)
120
+ : { ...themeCssVariables(state.variant, { editor: options.editorRoles }) }
95
121
  const accent = accentVariables(state.selection.accent, state.appearance, state.selection, {
96
122
  background: state.variant.colors.background,
97
123
  floor: options.accentContrastFloor,
@@ -5,6 +5,7 @@ import { REDUCED_TRANSPARENCY_QUERY, resolveSurfaceState } from '#lib/theme-surf
5
5
  import {
6
6
  builtinThemes,
7
7
  themeAccentsFor,
8
+ type EditorRolesMode,
8
9
  type ThemeAppearance,
9
10
  type ThemeVariant,
10
11
  } from '#lib/themes'
@@ -38,6 +39,7 @@ export {
38
39
  type VersionedPersistenceOptions,
39
40
  } from './theme-persistence'
40
41
  export type { ThemeSurface } from '#lib/theme-surface'
42
+ export type { EditorRolesMode } from '#lib/themes'
41
43
  export { inlineScriptLiteral, themeScript, type ThemeScriptOptions } from './theme-script'
42
44
  export {
43
45
  defaultThemeSelection,
@@ -159,6 +161,19 @@ export type ThemeProviderProps = {
159
161
  * properties the host variant wrote.
160
162
  */
161
163
  palette?: 'inline' | 'stylesheet'
164
+ /**
165
+ * The projection behind the `--editor-*` ramp. `syntax` (the default) is the
166
+ * catalogue's quiet syntax roles, a comment among them deliberately below
167
+ * body-text contrast. `floor` holds all sixteen roles to 4.5:1 on the canvas, as
168
+ * hex, for a host that renders code as content (a diff, a file view) rather than
169
+ * as decoration.
170
+ *
171
+ * Applies to catalogue variants; a host's own `themes` keep their `editor` and
172
+ * their `tokens`. With `palette="stylesheet"` it is the one palette property a
173
+ * catalogue variant still writes inline, because `theme-variants.css` is generated
174
+ * for `syntax`. Absent, nothing changes.
175
+ */
176
+ editorRoles?: EditorRolesMode
162
177
  /**
163
178
  * Bind the platform zoom chords (Cmd or Ctrl with `=`, `+`, `-`, `0`) to the three
164
179
  * text sizes instead of the browser's page zoom: stepping them together is zoom
@@ -189,6 +204,7 @@ export function ThemeProvider(props: ThemeProviderProps) {
189
204
  'accentContrastFloor',
190
205
  'surface',
191
206
  'palette',
207
+ 'editorRoles',
192
208
  'fontZoom',
193
209
  ])
194
210
  const storageKey = () => local.storageKey ?? 'adea-appearance'
@@ -288,6 +304,7 @@ export function ThemeProvider(props: ThemeProviderProps) {
288
304
  transformTokens: local.transformTokens,
289
305
  accentContrastFloor: local.accentContrastFloor,
290
306
  palette: local.palette,
307
+ editorRoles: local.editorRoles,
291
308
  }
292
309
  const apply = () => {
293
310
  written = applyThemeToRoot(document.documentElement, state, options, written)
package/src/index.ts CHANGED
@@ -141,6 +141,7 @@ export {
141
141
  contrastRatio,
142
142
  defaultDarkThemeId,
143
143
  defaultLightThemeId,
144
+ editorPaletteFor,
144
145
  isThemeAccentId,
145
146
  resolveAccentPreset,
146
147
  resolveVariant,
@@ -157,8 +158,10 @@ export {
157
158
  validateThemeRegistry,
158
159
  variantFromRecord,
159
160
  withAccent,
161
+ type EditorRolesMode,
160
162
  type ThemeAppearance,
161
163
  type ThemeColors,
164
+ type ThemeCssVariableOptions,
162
165
  type ThemeEditorPalette,
163
166
  type ThemeFinding,
164
167
  type ThemeProvenance,
package/src/lib/themes.ts CHANGED
@@ -37,6 +37,7 @@ import {
37
37
  ACCENTS,
38
38
  accentForeground,
39
39
  chartSeries,
40
+ editorRolesHex,
40
41
  getAccent,
41
42
  getTheme,
42
43
  hasTheme,
@@ -613,6 +614,97 @@ export function accentRolesForSelection(
613
614
  })
614
615
  }
615
616
 
617
+ /**
618
+ * Which projection fills the `--editor-*` ramp.
619
+ *
620
+ * - `syntax` (the default) is the catalogue's quiet syntax roles, as `ThemeVariant.editor`
621
+ * holds them: a comment may sit below body-text contrast, because in a code view
622
+ * it is meant to recede.
623
+ * - `floor` holds every editor role to 4.5:1 on the canvas (`editorRolesHex`), for a
624
+ * host that renders code as content rather than as decoration. It is hex, where
625
+ * `syntax` is `oklch()`.
626
+ */
627
+ export type EditorRolesMode = 'syntax' | 'floor'
628
+
629
+ /** Options for {@link themeCssVariables}. */
630
+ export type ThemeCssVariableOptions = {
631
+ /** The `--editor-*` projection. Default `syntax`: absent, the output is unchanged. */
632
+ editor?: EditorRolesMode
633
+ }
634
+
635
+ /**
636
+ * True for the catalogue's own variant, not a host's copy that merely shares its id.
637
+ *
638
+ * Identity rather than id, because a host's imported theme may reuse a catalogue id
639
+ * with different colours, and projecting the catalogue's record for it would paint
640
+ * the wrong palette.
641
+ */
642
+ export function isCatalogueVariant(variant: ThemeVariant): boolean {
643
+ return themeById(variant.id) === variant
644
+ }
645
+
646
+ /**
647
+ * The editor palette held to 4.5:1, or `fallback` for a record that cannot reach it.
648
+ *
649
+ * Some palettes (an imported one, most often) cannot be repaired to the floor from
650
+ * their own ramp within the catalogue's lightness budget, and `editorRolesHex` says
651
+ * so by throwing. A palette that paints nothing is worse than one that paints its
652
+ * quiet roles, so the throw is the cue to fall back rather than to fail. `project` is
653
+ * a parameter so that path can be exercised: no catalogue theme takes it.
654
+ */
655
+ export function floorEditorPalette(
656
+ record: AdeaTheme,
657
+ fallback: ThemeEditorPalette,
658
+ project: (theme: AdeaTheme) => Record<keyof ThemeEditorPalette, string> = editorRolesHex
659
+ ): ThemeEditorPalette {
660
+ try {
661
+ const roles = project(record)
662
+ return {
663
+ keyword: roles.keyword,
664
+ string: roles.string,
665
+ number: roles.number,
666
+ comment: roles.comment,
667
+ function: roles.function,
668
+ variable: roles.variable,
669
+ type: roles.type,
670
+ tag: roles.tag,
671
+ attribute: roles.attribute,
672
+ operator: roles.operator,
673
+ heading: roles.heading,
674
+ link: roles.link,
675
+ diffAdd: roles.diffAdd,
676
+ diffDelete: roles.diffDelete,
677
+ diffHunk: roles.diffHunk,
678
+ searchMatch: roles.searchMatch,
679
+ }
680
+ } catch {
681
+ return fallback
682
+ }
683
+ }
684
+
685
+ const floorEditorCache = new Map<string, ThemeEditorPalette>()
686
+
687
+ /**
688
+ * The editor palette a variant paints under a projection.
689
+ *
690
+ * `floor` applies to the catalogue's own variants, whose record the catalogue holds.
691
+ * A variant it does not hold (a host's imported theme) has no record to project and
692
+ * keeps its own `editor`; such a theme normally carries its roles in `tokens`, which
693
+ * win over either projection anyway. Computed once per theme: the repair is a search.
694
+ */
695
+ export function editorPaletteFor(
696
+ theme: ThemeVariant,
697
+ mode: EditorRolesMode = 'syntax'
698
+ ): ThemeEditorPalette {
699
+ if (mode !== 'floor' || !isCatalogueVariant(theme)) return theme.editor
700
+ const cached = floorEditorCache.get(theme.id)
701
+ if (cached) return cached
702
+ const record = getTheme(theme.id)
703
+ const palette = record ? floorEditorPalette(record, theme.editor) : theme.editor
704
+ floorEditorCache.set(theme.id, palette)
705
+ return palette
706
+ }
707
+
616
708
  /**
617
709
  * The roles a variant sets, as CSS custom properties.
618
710
  *
@@ -620,11 +712,18 @@ export function accentRolesForSelection(
620
712
  * names. A provider applies this; nothing else repeats it. The `--terminal-*` and
621
713
  * `--editor-*` ramps are presentations of the catalogue's ANSI and syntax data rather
622
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.
623
719
  */
624
- export function themeCssVariables(theme: ThemeVariant): Record<string, string> {
720
+ export function themeCssVariables(
721
+ theme: ThemeVariant,
722
+ options: ThemeCssVariableOptions = {}
723
+ ): Record<string, string> {
625
724
  const c = theme.colors
626
725
  const t = theme.terminal
627
- const e = theme.editor
726
+ const e = editorPaletteFor(theme, options.editor)
628
727
  const derived = {
629
728
  '--background': c.background,
630
729
  '--foreground': c.foreground,