@adea-ai/ui 0.130.4 → 0.131.1

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 (53) hide show
  1. package/README.md +15 -0
  2. package/dist/components/composites/appearance-editor/accent-swatch-groups.d.ts +7 -0
  3. package/dist/components/composites/appearance-editor/accent-swatch-groups.d.ts.map +1 -1
  4. package/dist/components/composites/appearance-editor/accent-swatch-groups.js +39 -13
  5. package/dist/components/composites/appearance-editor/accent-swatch-groups.js.map +1 -1
  6. package/dist/components/composites/appearance-editor/appearance-choices.d.ts.map +1 -1
  7. package/dist/components/composites/appearance-editor/appearance-choices.js +222 -116
  8. package/dist/components/composites/appearance-editor/appearance-choices.js.map +1 -1
  9. package/dist/components/composites/appearance-editor/appearance-editor.d.ts.map +1 -1
  10. package/dist/components/composites/appearance-editor/appearance-editor.js +6 -1
  11. package/dist/components/composites/appearance-editor/appearance-editor.js.map +1 -1
  12. package/dist/components/composites/appearance-editor/appearance-types.d.ts +22 -3
  13. package/dist/components/composites/appearance-editor/appearance-types.d.ts.map +1 -1
  14. package/dist/components/ui/accordion/accordion.d.ts.map +1 -1
  15. package/dist/components/ui/accordion/accordion.js +1 -1
  16. package/dist/components/ui/accordion/accordion.js.map +1 -1
  17. package/dist/components/ui/radio-group/radio-group.d.ts +3 -1
  18. package/dist/components/ui/radio-group/radio-group.d.ts.map +1 -1
  19. package/dist/components/ui/radio-group/radio-group.js +3 -1
  20. package/dist/components/ui/radio-group/radio-group.js.map +1 -1
  21. package/dist/lib/accent-presets.d.ts +7 -0
  22. package/dist/lib/accent-presets.d.ts.map +1 -1
  23. package/dist/lib/accent-presets.js +2 -2
  24. package/dist/lib/accent-presets.js.map +1 -1
  25. package/dist/lib/theme-projection.d.ts +23 -0
  26. package/dist/lib/theme-projection.d.ts.map +1 -0
  27. package/dist/lib/theme-projection.js +48 -0
  28. package/dist/lib/theme-projection.js.map +1 -0
  29. package/dist/lib/theme-records.d.ts +2 -0
  30. package/dist/lib/theme-records.d.ts.map +1 -1
  31. package/dist/lib/theme-records.js.map +1 -1
  32. package/dist/r/lib.json +5 -0
  33. package/dist/r/registry.json +5 -0
  34. package/dist/r/src/components/composites/appearance-editor/accent-swatch-groups.tsx +44 -10
  35. package/dist/r/src/components/composites/appearance-editor/appearance-choices.tsx +158 -80
  36. package/dist/r/src/components/composites/appearance-editor/appearance-editor.tsx +7 -2
  37. package/dist/r/src/components/composites/appearance-editor/appearance-types.ts +22 -3
  38. package/dist/r/src/components/ui/accordion/accordion.tsx +5 -1
  39. package/dist/r/src/components/ui/radio-group/radio-group.tsx +10 -2
  40. package/dist/r/src/lib/accent-presets.ts +8 -0
  41. package/dist/r/src/lib/theme-projection.ts +52 -0
  42. package/dist/r/src/lib/theme-records.ts +3 -0
  43. package/package.json +1 -1
  44. package/registry.json +5 -0
  45. package/src/components/composites/appearance-editor/accent-swatch-groups.tsx +44 -10
  46. package/src/components/composites/appearance-editor/appearance-choices.tsx +158 -80
  47. package/src/components/composites/appearance-editor/appearance-editor.tsx +7 -2
  48. package/src/components/composites/appearance-editor/appearance-types.ts +22 -3
  49. package/src/components/ui/accordion/accordion.tsx +5 -1
  50. package/src/components/ui/radio-group/radio-group.tsx +10 -2
  51. package/src/lib/accent-presets.ts +8 -0
  52. package/src/lib/theme-projection.ts +52 -0
  53. package/src/lib/theme-records.ts +3 -0
@@ -1 +1 @@
1
- {"version":3,"file":"theme-records.js","names":[],"sources":["../../src/lib/theme-records.ts"],"sourcesContent":["/**\n * One catalogue record, as this system's {@link ThemeVariant}.\n *\n * `variantFromRecord` and the lazy form the catalogue is built with live here, and not\n * beside the 34-theme table in `lib/themes.ts`, because a bundler places a module\n * whole: a host that imports `variantFromRecord` from the same module as the table\n * ships the table with it. This module reads a record it is handed and imports no\n * catalogue, so a start-up path that only turns a stored record into a variant\n * (an imported theme, a reconstruction) stays small. `lib/themes.ts` re-exports\n * `variantFromRecord`, so `@adea-ai/ui/lib/themes` keeps working; the lazy start-up\n * gate (`check:theme-startup`) holds the split in place.\n *\n * It does import `@adea-ai/themes` for the projection helpers (`chartSeries`,\n * `tint`, `shadcnVariables`…), none of which reads the table of themes.\n */\nimport {\n chartSeries,\n shadcnDestructiveProjection,\n statusForeground,\n syntaxRoles,\n tint,\n type AdeaThemeRecord,\n} from '@adea-ai/themes'\nimport { shadcnVariables } from '@adea-ai/themes/adapters/shadcn'\nimport type {\n ThemeColors,\n ThemeEditorPalette,\n ThemeProvenance,\n ThemeTerminalPalette,\n ThemeVariant,\n} from './themes'\n\n/**\n * The shadcn-vocabulary roles of one catalogue entry.\n *\n * The expensive half of a variant: `shadcnVariables` projects the whole palette\n * (about 4 ms a theme), which is why {@link lazyVariantFromRecord} defers it.\n */\nfunction colorsOf(theme: AdeaThemeRecord): ThemeColors {\n const projected = shadcnVariables(theme)\n const token = (name: string): string => {\n const value = projected[`--${name}`]\n if (!value) throw new Error(`${theme.id} has no shared shadcn projection for --${name}`)\n return value\n }\n const destructiveAction = shadcnDestructiveProjection(theme)\n return {\n background: token('background'),\n foreground: token('foreground'),\n card: token('card'),\n cardForeground: token('card-foreground'),\n popover: token('popover'),\n popoverForeground: token('popover-foreground'),\n primary: token('primary'),\n primaryForeground: token('primary-foreground'),\n secondary: token('secondary'),\n secondaryForeground: token('secondary-foreground'),\n muted: token('muted'),\n mutedForeground: token('muted-foreground'),\n accent: token('accent'),\n accentForeground: token('accent-foreground'),\n // Preserve the UI's semantic error token. The Themes projection uses its\n // contrast-safe solid-action fill for --destructive; UI keeps that fill in\n // the separate destructiveAction pair below.\n destructive: theme.colors.error,\n destructiveForeground: statusForeground(theme, 'error'),\n destructiveSubtle: tint(theme.colors.error, theme.colors.background),\n destructiveAction: destructiveAction.fill,\n destructiveActionForeground: destructiveAction.foreground,\n success: token('success'),\n successForeground: statusForeground(theme, 'success'),\n successSubtle: tint(theme.colors.success, theme.colors.background),\n warning: token('warning'),\n warningForeground: statusForeground(theme, 'warning'),\n warningSubtle: tint(theme.colors.warning, theme.colors.background),\n info: token('info'),\n infoForeground: statusForeground(theme, 'info'),\n infoSubtle: tint(theme.colors.info, theme.colors.background),\n border: token('border'),\n input: token('input'),\n ring: token('ring'),\n sidebar: token('sidebar'),\n sidebarForeground: token('sidebar-foreground'),\n sidebarAccent: token('sidebar-accent'),\n sidebarBorder: token('sidebar-border'),\n sidebarMutedForeground: token('sidebar-muted-foreground'),\n }\n}\n\n// The embedded terminal uses the theme's own canvas rather than a palette of its\n// own. The ANSI colours came from the palette that produced the canvas, so they\n// were chosen against it; a terminal on a different background would be the one\n// surface in the application not lit by the active theme.\nfunction terminalOf(theme: AdeaThemeRecord): ThemeTerminalPalette {\n return {\n background: theme.colors.background,\n foreground: theme.colors.foreground,\n cursor: theme.cursor,\n selection: theme.selection,\n ...theme.ansi,\n }\n}\n\nfunction editorOf(theme: AdeaThemeRecord): ThemeEditorPalette {\n const syntax = syntaxRoles(theme)\n return {\n keyword: syntax.keyword,\n string: syntax.string,\n number: syntax.number,\n comment: syntax.comment,\n function: syntax.function,\n variable: syntax.variable,\n type: syntax.type,\n tag: syntax.tag,\n attribute: syntax.attribute,\n operator: syntax.operator,\n heading: syntax.heading,\n link: syntax.link,\n diffAdd: syntax.diffAdd,\n diffDelete: syntax.diffDelete,\n diffHunk: syntax.diffHunk,\n searchMatch: syntax.searchMatch,\n }\n}\n\nfunction provenanceOf(theme: AdeaThemeRecord): ThemeProvenance {\n return {\n source: theme.provenance.project,\n url: theme.provenance.url,\n license: theme.provenance.license,\n }\n}\n\n/**\n * The design system's view of one catalogue record, built in full and at once.\n *\n * For a host that holds a record the catalogue does not — an imported theme, a\n * reconstruction from stored data — and wants a variant it can hand to\n * `ThemeProvider`'s `themes`. It throws for a record the shadcn projection cannot\n * map, here and now, rather than on first paint. The catalogue itself goes through\n * {@link lazyVariantFromRecord} instead.\n */\nexport function variantFromRecord(theme: AdeaThemeRecord): ThemeVariant {\n return {\n id: theme.id,\n family: theme.family,\n familyLabel: theme.familyLabel,\n label: theme.label,\n appearance: theme.appearance,\n description: theme.description,\n colors: colorsOf(theme),\n terminal: terminalOf(theme),\n editor: editorOf(theme),\n chart: chartSeries(theme),\n provenance: provenanceOf(theme),\n }\n}\n\n/** Runs `build` on first call and returns that value from then on. A throw is not cached. */\nfunction once<T>(build: () => T): () => T {\n let built = false\n let value: T\n return () => {\n if (!built) {\n value = build()\n built = true\n }\n return value\n }\n}\n\nconst lazy = <T>(build: () => T): PropertyDescriptor => ({\n get: once(build),\n enumerable: true,\n})\n\n/**\n * {@link variantFromRecord} with the four derived palettes built on first read.\n *\n * Evaluating this module used to project all 34 catalogue themes up front, about\n * 155 ms before a single one was needed, on the hydration path of every consumer.\n * A page paints one variant, and a picker's labels and appearance are plain\n * fields, so only a variant whose `colors`, `terminal`, `editor` or `chart` is read\n * pays for them, once. The properties stay enumerable data-shaped accessors, so\n * spreading, `Object.entries` and JSON see the same object they always did.\n */\nexport function lazyVariantFromRecord(theme: AdeaThemeRecord): ThemeVariant {\n return Object.defineProperties(\n {\n id: theme.id,\n family: theme.family,\n familyLabel: theme.familyLabel,\n label: theme.label,\n appearance: theme.appearance,\n description: theme.description,\n provenance: provenanceOf(theme),\n },\n {\n colors: lazy(() => colorsOf(theme)),\n terminal: lazy(() => terminalOf(theme)),\n editor: lazy(() => editorOf(theme)),\n chart: lazy(() => chartSeries(theme)),\n }\n ) as ThemeVariant\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"theme-records.js","names":[],"sources":["../../src/lib/theme-records.ts"],"sourcesContent":["/**\n * One catalogue record, as this system's {@link ThemeVariant}.\n *\n * `variantFromRecord` and the lazy form the catalogue is built with live here, and not\n * beside the 34-theme table in `lib/themes.ts`, because a bundler places a module\n * whole: a host that imports `variantFromRecord` from the same module as the table\n * ships the table with it. This module reads a record it is handed and imports no\n * catalogue, so a start-up path that only turns a stored record into a variant\n * (an imported theme, a reconstruction) stays small. `lib/themes.ts` re-exports\n * `variantFromRecord`, so `@adea-ai/ui/lib/themes` keeps working; the lazy start-up\n * gate (`check:theme-startup`) holds the split in place.\n *\n * It does import `@adea-ai/themes` for the projection helpers (`chartSeries`,\n * `tint`, `shadcnVariables`…), none of which reads the table of themes.\n */\nimport {\n chartSeries,\n shadcnDestructiveProjection,\n statusForeground,\n syntaxRoles,\n tint,\n type AdeaThemeRecord,\n} from '@adea-ai/themes'\nimport { shadcnVariables } from '@adea-ai/themes/adapters/shadcn'\nimport type {\n ThemeColors,\n ThemeEditorPalette,\n ThemeProvenance,\n ThemeTerminalPalette,\n ThemeVariant,\n} from './themes'\n\n/** The catalogue's canonical theme shapes, for a host that holds records and no catalogue. */\nexport type { AdeaTheme, AdeaThemeRecord } from '@adea-ai/themes'\n\n/**\n * The shadcn-vocabulary roles of one catalogue entry.\n *\n * The expensive half of a variant: `shadcnVariables` projects the whole palette\n * (about 4 ms a theme), which is why {@link lazyVariantFromRecord} defers it.\n */\nfunction colorsOf(theme: AdeaThemeRecord): ThemeColors {\n const projected = shadcnVariables(theme)\n const token = (name: string): string => {\n const value = projected[`--${name}`]\n if (!value) throw new Error(`${theme.id} has no shared shadcn projection for --${name}`)\n return value\n }\n const destructiveAction = shadcnDestructiveProjection(theme)\n return {\n background: token('background'),\n foreground: token('foreground'),\n card: token('card'),\n cardForeground: token('card-foreground'),\n popover: token('popover'),\n popoverForeground: token('popover-foreground'),\n primary: token('primary'),\n primaryForeground: token('primary-foreground'),\n secondary: token('secondary'),\n secondaryForeground: token('secondary-foreground'),\n muted: token('muted'),\n mutedForeground: token('muted-foreground'),\n accent: token('accent'),\n accentForeground: token('accent-foreground'),\n // Preserve the UI's semantic error token. The Themes projection uses its\n // contrast-safe solid-action fill for --destructive; UI keeps that fill in\n // the separate destructiveAction pair below.\n destructive: theme.colors.error,\n destructiveForeground: statusForeground(theme, 'error'),\n destructiveSubtle: tint(theme.colors.error, theme.colors.background),\n destructiveAction: destructiveAction.fill,\n destructiveActionForeground: destructiveAction.foreground,\n success: token('success'),\n successForeground: statusForeground(theme, 'success'),\n successSubtle: tint(theme.colors.success, theme.colors.background),\n warning: token('warning'),\n warningForeground: statusForeground(theme, 'warning'),\n warningSubtle: tint(theme.colors.warning, theme.colors.background),\n info: token('info'),\n infoForeground: statusForeground(theme, 'info'),\n infoSubtle: tint(theme.colors.info, theme.colors.background),\n border: token('border'),\n input: token('input'),\n ring: token('ring'),\n sidebar: token('sidebar'),\n sidebarForeground: token('sidebar-foreground'),\n sidebarAccent: token('sidebar-accent'),\n sidebarBorder: token('sidebar-border'),\n sidebarMutedForeground: token('sidebar-muted-foreground'),\n }\n}\n\n// The embedded terminal uses the theme's own canvas rather than a palette of its\n// own. The ANSI colours came from the palette that produced the canvas, so they\n// were chosen against it; a terminal on a different background would be the one\n// surface in the application not lit by the active theme.\nfunction terminalOf(theme: AdeaThemeRecord): ThemeTerminalPalette {\n return {\n background: theme.colors.background,\n foreground: theme.colors.foreground,\n cursor: theme.cursor,\n selection: theme.selection,\n ...theme.ansi,\n }\n}\n\nfunction editorOf(theme: AdeaThemeRecord): ThemeEditorPalette {\n const syntax = syntaxRoles(theme)\n return {\n keyword: syntax.keyword,\n string: syntax.string,\n number: syntax.number,\n comment: syntax.comment,\n function: syntax.function,\n variable: syntax.variable,\n type: syntax.type,\n tag: syntax.tag,\n attribute: syntax.attribute,\n operator: syntax.operator,\n heading: syntax.heading,\n link: syntax.link,\n diffAdd: syntax.diffAdd,\n diffDelete: syntax.diffDelete,\n diffHunk: syntax.diffHunk,\n searchMatch: syntax.searchMatch,\n }\n}\n\nfunction provenanceOf(theme: AdeaThemeRecord): ThemeProvenance {\n return {\n source: theme.provenance.project,\n url: theme.provenance.url,\n license: theme.provenance.license,\n }\n}\n\n/**\n * The design system's view of one catalogue record, built in full and at once.\n *\n * For a host that holds a record the catalogue does not — an imported theme, a\n * reconstruction from stored data — and wants a variant it can hand to\n * `ThemeProvider`'s `themes`. It throws for a record the shadcn projection cannot\n * map, here and now, rather than on first paint. The catalogue itself goes through\n * {@link lazyVariantFromRecord} instead.\n */\nexport function variantFromRecord(theme: AdeaThemeRecord): ThemeVariant {\n return {\n id: theme.id,\n family: theme.family,\n familyLabel: theme.familyLabel,\n label: theme.label,\n appearance: theme.appearance,\n description: theme.description,\n colors: colorsOf(theme),\n terminal: terminalOf(theme),\n editor: editorOf(theme),\n chart: chartSeries(theme),\n provenance: provenanceOf(theme),\n }\n}\n\n/** Runs `build` on first call and returns that value from then on. A throw is not cached. */\nfunction once<T>(build: () => T): () => T {\n let built = false\n let value: T\n return () => {\n if (!built) {\n value = build()\n built = true\n }\n return value\n }\n}\n\nconst lazy = <T>(build: () => T): PropertyDescriptor => ({\n get: once(build),\n enumerable: true,\n})\n\n/**\n * {@link variantFromRecord} with the four derived palettes built on first read.\n *\n * Evaluating this module used to project all 34 catalogue themes up front, about\n * 155 ms before a single one was needed, on the hydration path of every consumer.\n * A page paints one variant, and a picker's labels and appearance are plain\n * fields, so only a variant whose `colors`, `terminal`, `editor` or `chart` is read\n * pays for them, once. The properties stay enumerable data-shaped accessors, so\n * spreading, `Object.entries` and JSON see the same object they always did.\n */\nexport function lazyVariantFromRecord(theme: AdeaThemeRecord): ThemeVariant {\n return Object.defineProperties(\n {\n id: theme.id,\n family: theme.family,\n familyLabel: theme.familyLabel,\n label: theme.label,\n appearance: theme.appearance,\n description: theme.description,\n provenance: provenanceOf(theme),\n },\n {\n colors: lazy(() => colorsOf(theme)),\n terminal: lazy(() => terminalOf(theme)),\n editor: lazy(() => editorOf(theme)),\n chart: lazy(() => chartSeries(theme)),\n }\n ) as ThemeVariant\n}\n"],"mappings":"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"}
package/dist/r/lib.json CHANGED
@@ -110,6 +110,11 @@
110
110
  "type": "registry:lib",
111
111
  "target": "lib/theme-engine.ts"
112
112
  },
113
+ {
114
+ "path": "src/lib/theme-projection.ts",
115
+ "type": "registry:lib",
116
+ "target": "lib/theme-projection.ts"
117
+ },
113
118
  {
114
119
  "path": "src/lib/theme-records.ts",
115
120
  "type": "registry:lib",
@@ -2794,6 +2794,11 @@
2794
2794
  "type": "registry:lib",
2795
2795
  "target": "lib/theme-engine.ts"
2796
2796
  },
2797
+ {
2798
+ "path": "src/lib/theme-projection.ts",
2799
+ "type": "registry:lib",
2800
+ "target": "lib/theme-projection.ts"
2801
+ },
2797
2802
  {
2798
2803
  "path": "src/lib/theme-records.ts",
2799
2804
  "type": "registry:lib",
@@ -23,6 +23,7 @@
23
23
  */
24
24
  import { RadioGroup as Radio } from '@kobalte/core/radio-group'
25
25
  import { For, Show } from 'solid-js'
26
+ import { cn } from '../../../lib/utils'
26
27
 
27
28
  type AccentOption = Readonly<{
28
29
  id: string
@@ -50,6 +51,13 @@ type AccentSwatchGroupsProps = {
50
51
  * colour reads as a duplicate choice.
51
52
  */
52
53
  showThemeDefault?: boolean
54
+ /**
55
+ * `grid` — the default — wraps at four to a line. `row` keeps every swatch on
56
+ * one line at a smaller size, the columns sharing the width equally, so a
57
+ * narrow panel does not wrap them; each colour is then the fill of an SVG
58
+ * circle, which is where a host that asserts on swatch colours reads it.
59
+ */
60
+ layout?: 'grid' | 'row'
53
61
  }
54
62
 
55
63
  /**
@@ -62,11 +70,20 @@ type AccentSwatchGroupsProps = {
62
70
  */
63
71
  export function AccentSwatchGroups(props: AccentSwatchGroupsProps) {
64
72
  const presets = () => props.accentOptions.filter((option) => option.id !== 'theme')
73
+ const row = () => props.layout === 'row'
65
74
 
66
75
  return (
67
- <div class="grid grid-cols-4 gap-2" data-accent-grid>
76
+ <div
77
+ class={row() ? 'grid auto-cols-fr grid-flow-col' : 'grid grid-cols-4 gap-2'}
78
+ data-accent-grid
79
+ >
68
80
  <Show when={props.showThemeDefault ?? true}>
69
- <AccentSwatch id="theme" label="Theme default" color={props.themeDefaultColor} />
81
+ <AccentSwatch
82
+ id="theme"
83
+ label="Theme default"
84
+ color={props.themeDefaultColor}
85
+ row={row()}
86
+ />
70
87
  </Show>
71
88
  <For each={presets()}>
72
89
  {(option) => (
@@ -74,6 +91,7 @@ export function AccentSwatchGroups(props: AccentSwatchGroupsProps) {
74
91
  id={option.id}
75
92
  label={option.label}
76
93
  color={(props.resolvedAppearance === 'light' ? option.light : option.dark) ?? ''}
94
+ row={row()}
77
95
  />
78
96
  )}
79
97
  </For>
@@ -81,20 +99,36 @@ export function AccentSwatchGroups(props: AccentSwatchGroupsProps) {
81
99
  )
82
100
  }
83
101
 
84
- function AccentSwatch(props: { id: string; label: string; color: string }) {
102
+ function AccentSwatch(props: { id: string; label: string; color: string; row: boolean }) {
85
103
  return (
86
104
  <Radio.Item
87
105
  value={props.id}
88
106
  data-accent-option={props.id}
89
- class="rounded-full focus-within:ring-3 focus-within:ring-ring/50"
107
+ class={cn('rounded-full focus-within:ring-3 focus-within:ring-ring/50', {
108
+ 'flex justify-center': props.row,
109
+ })}
90
110
  >
91
111
  <Radio.ItemInput />
92
- <Radio.ItemLabel class="block size-7 cursor-pointer rounded-full border p-0.5 data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
93
- <span
94
- class="block size-full rounded-full"
95
- aria-hidden="true"
96
- style={{ 'background-color': props.color }}
97
- />
112
+ <Radio.ItemLabel
113
+ class={cn(
114
+ 'block cursor-pointer rounded-full border p-0.5 data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary',
115
+ props.row ? 'size-6' : 'size-7'
116
+ )}
117
+ >
118
+ <Show
119
+ when={props.row}
120
+ fallback={
121
+ <span
122
+ class="block size-full rounded-full"
123
+ aria-hidden="true"
124
+ style={{ 'background-color': props.color }}
125
+ />
126
+ }
127
+ >
128
+ <svg class="size-full" viewBox="0 0 16 16" aria-hidden="true">
129
+ <circle cx="8" cy="8" r="8" fill={props.color} />
130
+ </svg>
131
+ </Show>
98
132
  <span class="sr-only">{props.label}</span>
99
133
  </Radio.ItemLabel>
100
134
  </Radio.Item>
@@ -24,8 +24,11 @@
24
24
  // Pinned mode-card, accent-swatch, and glass-chip composition. Kobalte replaces
25
25
  // the app port's manual roving focus; it does not replace the donor hierarchy.
26
26
  import { RadioGroup as Radio } from '@kobalte/core/radio-group'
27
+ import { Monitor, Moon, Sun } from 'lucide-solid'
27
28
  import { For, Show } from 'solid-js'
28
29
  import { isThemeAccentId } from '../../../lib/theme-accents'
30
+ import { Button } from '../../ui/button/button'
31
+ import { Input } from '../../ui/input/input'
29
32
  import { AccentSwatchGroups } from './accent-swatch-groups'
30
33
  import type { AppearanceEditorProps } from './appearance-types'
31
34
  import { ThemeMiniature, ThemeMiniatureSplit } from './theme-preview'
@@ -40,6 +43,8 @@ const SURFACES = [
40
43
  { value: 'frosted', label: 'Frosted' },
41
44
  { value: 'opaque', label: 'Opaque' },
42
45
  ] as const
46
+ const MODE_ICONS = { system: Monitor, light: Sun, dark: Moon } as const
47
+
43
48
  export function ModeChoices(props: AppearanceEditorProps) {
44
49
  return (
45
50
  <Radio
@@ -54,30 +59,40 @@ export function ModeChoices(props: AppearanceEditorProps) {
54
59
  class="grid grid-cols-1 gap-3 px-4 py-4 sm:grid-cols-3"
55
60
  >
56
61
  <For each={MODES}>
57
- {(option) => (
58
- <Radio.Item
59
- value={option.value}
60
- data-mode={option.value}
61
- class="min-w-0 rounded-lg focus-within:ring-3 focus-within:ring-ring/50"
62
- >
63
- <Radio.ItemInput />
64
- <Radio.ItemLabel class="flex cursor-pointer flex-col gap-2 rounded-lg border p-2 text-center text-xs data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50">
65
- <span class="block h-20 w-full">
66
- <Show
67
- when={option.value === 'system'}
68
- fallback={
69
- <ThemeMiniature
70
- theme={option.value === 'light' ? props.lightTheme : props.darkTheme}
71
- />
72
- }
73
- >
74
- <ThemeMiniatureSplit light={props.lightTheme} dark={props.darkTheme} />
62
+ {(option) => {
63
+ const Icon = MODE_ICONS[option.value]
64
+ return (
65
+ <Radio.Item
66
+ value={option.value}
67
+ data-mode={option.value}
68
+ class="min-w-0 rounded-lg focus-within:ring-3 focus-within:ring-ring/50"
69
+ >
70
+ <Radio.ItemInput />
71
+ <Radio.ItemLabel class="flex cursor-pointer flex-col gap-2 rounded-lg border p-2 text-center text-xs data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50">
72
+ <span class="block h-20 w-full">
73
+ <Show
74
+ when={option.value === 'system'}
75
+ fallback={
76
+ <ThemeMiniature
77
+ theme={option.value === 'light' ? props.lightTheme : props.darkTheme}
78
+ />
79
+ }
80
+ >
81
+ <ThemeMiniatureSplit light={props.lightTheme} dark={props.darkTheme} />
82
+ </Show>
83
+ </span>
84
+ {/* The glyph names the axis beside the miniature, so the cards read
85
+ like ThemeModeToggle without losing the previews. */}
86
+ <Show when={props.showModeIcons} fallback={<span>{option.label}</span>}>
87
+ <span class="flex items-center justify-center gap-1.5">
88
+ <Icon aria-hidden="true" class="size-3.5" />
89
+ {option.label}
90
+ </span>
75
91
  </Show>
76
- </span>
77
- <span>{option.label}</span>
78
- </Radio.ItemLabel>
79
- </Radio.Item>
80
- )}
92
+ </Radio.ItemLabel>
93
+ </Radio.Item>
94
+ )
95
+ }}
81
96
  </For>
82
97
  </Radio>
83
98
  )
@@ -106,75 +121,138 @@ export function AccentChoices(props: AppearanceEditorProps) {
106
121
  props.draft.mode === 'system' ? props.resolvedAppearance : props.draft.mode
107
122
  const accentSelection = () => (isCustomAccent(props) ? 'custom' : props.draft.accent)
108
123
  const showThemeDefault = () => props.showThemeDefaultAccent ?? true
124
+ const entryStyle = () => props.accentEntryStyle ?? 'swatches'
125
+ const themeDefaultColor = () =>
126
+ resolvedAppearance() === 'light'
127
+ ? props.lightTheme.colors.accent
128
+ : props.darkTheme.colors.accent
109
129
  // The named-chip style feeds the same resolved preset list to both entries:
110
130
  // "Theme default" leads only when the host has not folded it into a preset.
111
131
  const labelEntries = () => {
112
132
  const presets = props.accentOptions.filter((option) => option.id !== 'theme')
113
133
  return showThemeDefault() ? [{ id: 'theme', label: 'Theme default' }, ...presets] : presets
114
134
  }
135
+ // The `row` style's Custom entry is a button that opens the native colour
136
+ // selector, so the draft only changes once a colour is chosen there. The
137
+ // input stays in the tree for the selector to anchor to and for the value a
138
+ // saved hex restores.
139
+ let colorPicker: HTMLInputElement | undefined
140
+ const openCustomColor = () => {
141
+ if (!colorPicker || props.saving) return
142
+ if (typeof colorPicker.showPicker === 'function') colorPicker.showPicker()
143
+ else colorPicker.click()
144
+ }
115
145
  return (
116
- <Radio
117
- value={accentSelection()}
118
- disabled={props.saving}
119
- orientation="horizontal"
120
- aria-label="Accent"
121
- class="flex flex-col items-start gap-3"
122
- onChange={(accent) =>
123
- props.onChange({
124
- accent: accent === 'custom' ? (props.customAccentValue ?? '') : accent,
125
- })
126
- }
127
- >
128
- <Show
129
- when={(props.accentEntryStyle ?? 'swatches') === 'labels'}
130
- fallback={
131
- <>
132
- <AccentSwatchGroups
133
- accentOptions={props.accentOptions}
134
- resolvedAppearance={resolvedAppearance()}
135
- showThemeDefault={showThemeDefault()}
136
- themeDefaultColor={
137
- resolvedAppearance() === 'light'
138
- ? props.lightTheme.colors.accent
139
- : props.darkTheme.colors.accent
140
- }
141
- />
142
- <div class="flex flex-col gap-1">
143
- <Show when={props.showCustomAccentCaption ?? true}>
144
- <span class="text-xs font-medium text-muted-foreground">Custom</span>
145
- </Show>
146
- <Radio.Item value="custom">
146
+ <Show
147
+ when={entryStyle() === 'row'}
148
+ fallback={
149
+ <Radio
150
+ value={accentSelection()}
151
+ disabled={props.saving}
152
+ orientation="horizontal"
153
+ aria-label="Accent"
154
+ class="flex flex-col items-start gap-3"
155
+ onChange={(accent) =>
156
+ props.onChange({
157
+ accent: accent === 'custom' ? (props.customAccentValue ?? '') : accent,
158
+ })
159
+ }
160
+ >
161
+ <Show
162
+ when={entryStyle() === 'labels'}
163
+ fallback={
164
+ <>
165
+ <AccentSwatchGroups
166
+ accentOptions={props.accentOptions}
167
+ resolvedAppearance={resolvedAppearance()}
168
+ showThemeDefault={showThemeDefault()}
169
+ themeDefaultColor={themeDefaultColor()}
170
+ />
171
+ <div class="flex flex-col gap-1">
172
+ <Show when={props.showCustomAccentCaption ?? true}>
173
+ <span class="text-xs font-medium text-muted-foreground">Custom</span>
174
+ </Show>
175
+ <Radio.Item value="custom">
176
+ <Radio.ItemInput />
177
+ <Radio.ItemLabel class="flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs whitespace-nowrap data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
178
+ Custom
179
+ </Radio.ItemLabel>
180
+ </Radio.Item>
181
+ </div>
182
+ </>
183
+ }
184
+ >
185
+ {/* The named-chip style: one grid, every choice a labelled control,
186
+ including the custom colour — nothing to caption from above. */}
187
+ <div class="grid grid-cols-2 gap-2 sm:grid-cols-3" data-accent-grid>
188
+ <For each={labelEntries()}>
189
+ {(option) => (
190
+ <Radio.Item value={option.id} class="min-w-0" data-accent-option={option.id}>
191
+ <Radio.ItemInput />
192
+ <Radio.ItemLabel class="flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs whitespace-nowrap data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
193
+ {option.label}
194
+ </Radio.ItemLabel>
195
+ </Radio.Item>
196
+ )}
197
+ </For>
198
+ <Radio.Item value="custom" class="min-w-0">
147
199
  <Radio.ItemInput />
148
200
  <Radio.ItemLabel class="flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs whitespace-nowrap data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
149
201
  Custom
150
202
  </Radio.ItemLabel>
151
203
  </Radio.Item>
152
204
  </div>
153
- </>
154
- }
155
- >
156
- {/* The named-chip style: one grid, every choice a labelled control,
157
- including the custom colour — nothing to caption from above. */}
158
- <div class="grid grid-cols-2 gap-2 sm:grid-cols-3" data-accent-grid>
159
- <For each={labelEntries()}>
160
- {(option) => (
161
- <Radio.Item value={option.id} class="min-w-0" data-accent-option={option.id}>
162
- <Radio.ItemInput />
163
- <Radio.ItemLabel class="flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs whitespace-nowrap data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
164
- {option.label}
165
- </Radio.ItemLabel>
166
- </Radio.Item>
167
- )}
168
- </For>
169
- <Radio.Item value="custom" class="min-w-0">
170
- <Radio.ItemInput />
171
- <Radio.ItemLabel class="flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs whitespace-nowrap data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
172
- Custom
173
- </Radio.ItemLabel>
174
- </Radio.Item>
175
- </div>
176
- </Show>
177
- </Radio>
205
+ </Show>
206
+ </Radio>
207
+ }
208
+ >
209
+ <div class="flex w-full min-w-0 items-center gap-2" data-accent-choices>
210
+ <Radio
211
+ value={accentSelection()}
212
+ disabled={props.saving}
213
+ orientation="horizontal"
214
+ aria-label="Accent"
215
+ class="min-w-0 flex-1"
216
+ onChange={(accent) => props.onChange({ accent })}
217
+ >
218
+ <AccentSwatchGroups
219
+ accentOptions={props.accentOptions}
220
+ resolvedAppearance={resolvedAppearance()}
221
+ showThemeDefault={showThemeDefault()}
222
+ themeDefaultColor={themeDefaultColor()}
223
+ layout="row"
224
+ />
225
+ </Radio>
226
+ <Button
227
+ type="button"
228
+ variant="outline"
229
+ size="sm"
230
+ disabled={props.saving}
231
+ aria-pressed={isCustomAccent(props)}
232
+ onClick={openCustomColor}
233
+ >
234
+ Custom
235
+ </Button>
236
+ <Input
237
+ ref={(element) => {
238
+ colorPicker = element
239
+ }}
240
+ type="color"
241
+ // `w-px` beats the Input's own `w-full`: an absolutely positioned
242
+ // full-width input sized the sheet's scroll width.
243
+ class="sr-only w-px"
244
+ tabIndex={-1}
245
+ aria-label="Custom accent color"
246
+ value={
247
+ /^#[0-9a-f]{6}$/i.test(props.draft.accent)
248
+ ? props.draft.accent
249
+ : (props.customAccentValue ?? '#000000')
250
+ }
251
+ disabled={props.saving}
252
+ onChange={(event) => props.onChange({ accent: event.currentTarget.value })}
253
+ />
254
+ </div>
255
+ </Show>
178
256
  )
179
257
  }
180
258
 
@@ -199,7 +277,7 @@ export function GlassChoices(props: AppearanceEditorProps) {
199
277
  class="rounded-md focus-within:ring-3 focus-within:ring-ring/50"
200
278
  >
201
279
  <Radio.ItemInput />
202
- <Radio.ItemLabel class="flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50">
280
+ <Radio.ItemLabel class="flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs whitespace-nowrap data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50">
203
281
  {option.label}
204
282
  </Radio.ItemLabel>
205
283
  </Radio.Item>
@@ -278,12 +278,17 @@ export function AppearancePopover(props: AppearancePopoverProps) {
278
278
  <Sheet open={props.open} onOpenChange={(open) => (open ? props.onOpen() : props.onDismiss())}>
279
279
  {/* The trigger is an icon-only action, so it explains itself through the
280
280
  shared ActionButton's tooltip (keyboard focus included) rather than a
281
- bare aria-label. */}
281
+ bare aria-label. `toolbar` also holds the pressed rung while Kobalte
282
+ marks the trigger `data-expanded`, and repeats the glyph in the
283
+ tooltip like the other toolbar actions. */}
282
284
  <SheetTrigger
283
285
  as={ActionButton}
284
- variant="ghost"
286
+ variant={props.triggerVariant === 'toolbar' ? 'toolbar' : 'ghost'}
285
287
  size="icon-sm"
286
288
  tooltip="Open appearance settings"
289
+ tooltipIcon={
290
+ props.triggerVariant === 'toolbar' ? <Palette aria-hidden="true" /> : undefined
291
+ }
287
292
  aria-label="Appearance settings"
288
293
  >
289
294
  <Palette />
@@ -67,10 +67,16 @@ export type AppearanceEditorProps = {
67
67
  fontSettingsPlacement?: 'inline' | 'end'
68
68
  /**
69
69
  * How the accent row renders its choices. `swatches` — the default — paints
70
- * each choice as a colour circle. `labels` renders every choice as a named
71
- * chip, for hosts whose accent contract is textual rather than chromatic.
70
+ * each choice as a colour circle in a wrapping grid, with Custom as one more
71
+ * chip. `labels` renders every choice as a named chip, for hosts whose accent
72
+ * contract is textual rather than chromatic. `row` draws the circles in ONE
73
+ * equally spaced line, which holds at a narrow panel width where the grid
74
+ * would wrap, and turns Custom into a button that opens the native colour
75
+ * selector: choosing a colour there is the only thing that changes the draft,
76
+ * so dismissing the selector leaves the accent as it was. The caption flag
77
+ * has no effect on `row`, which has nothing to caption.
72
78
  */
73
- accentEntryStyle?: 'swatches' | 'labels'
79
+ accentEntryStyle?: 'swatches' | 'labels' | 'row'
74
80
  /**
75
81
  * Whether the accent row leads with a "Theme default" swatch. Default on.
76
82
  * Hosts whose default accent already is one of the catalogue presets pass
@@ -83,10 +89,23 @@ export type AppearanceEditorProps = {
83
89
  * caption reads as a second "Custom" label on the control.
84
90
  */
85
91
  showCustomAccentCaption?: boolean
92
+ /**
93
+ * Whether each mode card names its axis with a sun, moon or monitor glyph
94
+ * beside the label, as `ThemeModeToggle` does. Default off.
95
+ */
96
+ showModeIcons?: boolean
86
97
  }
87
98
 
88
99
  export type AppearancePopoverProps = AppearanceEditorProps & {
89
100
  open: boolean
90
101
  onOpen: () => void
91
102
  onDismiss: () => void
103
+ /**
104
+ * The trigger's treatment. `ghost` — the default — is a quiet icon action.
105
+ * `toolbar` is the variant for a control that opens a surface: it holds the
106
+ * pressed rung while the sheet is open, so the one control that owns the
107
+ * visible sheet never reads like its idle neighbours, and its tooltip repeats
108
+ * the palette glyph the other toolbar actions carry.
109
+ */
110
+ triggerVariant?: 'ghost' | 'toolbar'
92
111
  }
@@ -37,7 +37,11 @@ export function AccordionTrigger(props: ComponentProps<typeof KobalteAccordion.T
37
37
  <KobalteAccordion.Header class="flex">
38
38
  <KobalteAccordion.Trigger
39
39
  class={cn(
40
- 'flex flex-1 items-center justify-between gap-2 py-3 text-left text-sm font-medium',
40
+ // `group/accordion` is what the chevron's `group-data-[expanded]/accordion`
41
+ // reads. Kobalte puts `data-expanded` on this trigger, not on the item, so
42
+ // the trigger is the group; without the class the rotation selector has no
43
+ // ancestor to match and the chevron never turns.
44
+ 'group/accordion flex flex-1 items-center justify-between gap-2 py-3 text-left text-sm font-medium',
41
45
  'transition-colors ease-out outline-none',
42
46
  'hover:text-foreground',
43
47
  'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
@@ -63,7 +63,9 @@ export type RadioGroupItemProps = ComponentProps<typeof KobalteRadioGroup.Item>
63
63
  * `card` draws the option as a bordered, selectable card — for a choice
64
64
  * whose options need a sentence each (a source type, a sync budget), where a
65
65
  * bare radio row reads as a form field rather than a decision. The whole card
66
- * is the hit target, and the selected card takes the primary edge.
66
+ * is the hit target, and the selected card takes the primary edge. The radio,
67
+ * `media` and text centre on one axis; the default variant keeps the radio on
68
+ * the first line of its label.
67
69
  */
68
70
  variant?: 'default' | 'card'
69
71
  /** A leading icon or `EntityIcon` between the radio and the text, for `card`. */
@@ -100,8 +102,14 @@ export function RadioGroupItem(props: RadioGroupItemProps) {
100
102
  data-variant={card() ? 'card' : undefined}
101
103
  title={local.title}
102
104
  class={cn(
103
- 'group/radio flex items-start gap-2.5',
105
+ 'group/radio flex gap-2.5',
104
106
  {
107
+ 'items-start': !card(),
108
+ // A card centres its radio, media and text on one axis. Top alignment
109
+ // is right for a bare row, where the radio sits on the label's first
110
+ // line, but in a card the media is taller than that line, so a
111
+ // top-aligned radio floats above the icon and the label beside it.
112
+ 'items-center': card(),
105
113
  'relative rounded-lg border border-border bg-card px-3 py-2.5 transition-[color,box-shadow,border-color,background-color] ease-out hover:bg-surface-hover data-[checked]:border-primary data-[disabled]:cursor-not-allowed data-[disabled]:hover:bg-card':
106
114
  card(),
107
115
  'has-[input:focus-visible]:border-ring has-[input:focus-visible]:ring-3 has-[input:focus-visible]:ring-primary-subtle':
@@ -8,6 +8,14 @@ import { ACCENTS } from '@adea-ai/themes'
8
8
  * `accents`, so every existing import keeps working.
9
9
  */
10
10
 
11
+ /**
12
+ * A preset by id (`blue`), or `undefined` for `theme`, a theme-carried accent or an
13
+ * unknown id. The catalogue's own lookup, re-exported rather than copied so a host's
14
+ * stored-preference reader resolves an id exactly as the catalogue does without
15
+ * importing `lib/themes`.
16
+ */
17
+ export { getAccent } from '@adea-ai/themes'
18
+
11
19
  /**
12
20
  * The accent axis.
13
21
  *
@@ -0,0 +1,52 @@
1
+ /**
2
+ * What a host needs to turn a canonical theme record into the values it stores.
3
+ *
4
+ * Adea imports user themes: a file is validated, repaired toward the readable floor,
5
+ * and projected onto the hex values the host persists. That projection is the
6
+ * catalogue's own (`shadcnVariables`, the xterm adapter, the editor roles), so it is
7
+ * re-exported here and the host depends on this package alone for it — the pin
8
+ * between this package and `@adea-ai/themes` cannot drift from the host's.
9
+ *
10
+ * Its own module, with no catalogue data and no definition of its own beyond the two
11
+ * default records, so that:
12
+ *
13
+ * - a host's start-up path never reaches it (it is for an importer loaded on demand);
14
+ * - `lib/themes.ts`, which holds the 34-theme table, is not the place a host has to
15
+ * import from to get a record-shaped helper.
16
+ */
17
+ import adeaDark from '@adea-ai/themes/themes/adea-dark'
18
+ import adeaLight from '@adea-ai/themes/themes/adea-light'
19
+ import {
20
+ themeAccentPresets,
21
+ type AccentPreset,
22
+ type AdeaTheme,
23
+ type AdeaThemeRecord,
24
+ } from '@adea-ai/themes'
25
+
26
+ export { editorRolesHex, syntaxRolesHex } from '@adea-ai/themes'
27
+ export { oklchToHex, parseColor as parseOklchColor } from '@adea-ai/themes/oklch'
28
+ export { shadcnDestructiveProjection, shadcnVariables } from '@adea-ai/themes/adapters/shadcn'
29
+ export { toXtermTheme } from '@adea-ai/themes/adapters/xterm'
30
+
31
+ /**
32
+ * The default theme of an appearance as a canonical record: the roles an imported
33
+ * theme falls back to for anything its file does not carry.
34
+ *
35
+ * `defaultVariantFor` is the same theme as a runtime variant; a record keeps the
36
+ * catalogue's own roles (`surfaceActive`, `textSubtle`, the ANSI ramp) that a variant
37
+ * folds together.
38
+ */
39
+ export function defaultThemeRecord(appearance: 'light' | 'dark'): AdeaThemeRecord {
40
+ return appearance === 'dark' ? adeaDark : adeaLight
41
+ }
42
+
43
+ /**
44
+ * The accents a light/dark pair of records carries itself, beside the presets.
45
+ *
46
+ * For a host whose pair is not in the catalogue — an imported theme — where
47
+ * `themeAccentsFor` (which looks the pair up by id) cannot answer. A slot is offered
48
+ * only when both themes clear the catalogue's accent floors.
49
+ */
50
+ export function themeAccentsForRecords(light: AdeaTheme, dark: AdeaTheme): readonly AccentPreset[] {
51
+ return themeAccentPresets(light, dark)
52
+ }