@adea-ai/ui 0.104.3 → 0.106.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.
Files changed (191) hide show
  1. package/README.md +44 -8
  2. package/dist/components/composites/action-button/action-button.d.ts.map +1 -1
  3. package/dist/components/composites/action-button/action-button.js +6 -8
  4. package/dist/components/composites/action-button/action-button.js.map +1 -1
  5. package/dist/components/composites/appearance-editor/accent-swatch-groups.d.ts +16 -0
  6. package/dist/components/composites/appearance-editor/accent-swatch-groups.d.ts.map +1 -0
  7. package/dist/components/composites/appearance-editor/accent-swatch-groups.js +151 -0
  8. package/dist/components/composites/appearance-editor/accent-swatch-groups.js.map +1 -0
  9. package/dist/components/composites/appearance-editor/appearance-choices.d.ts.map +1 -1
  10. package/dist/components/composites/appearance-editor/appearance-choices.js +24 -84
  11. package/dist/components/composites/appearance-editor/appearance-choices.js.map +1 -1
  12. package/dist/components/composites/appearance-editor/appearance-editor.d.ts +2 -2
  13. package/dist/components/composites/appearance-editor/appearance-editor.d.ts.map +1 -1
  14. package/dist/components/composites/appearance-editor/appearance-editor.js +25 -7
  15. package/dist/components/composites/appearance-editor/appearance-editor.js.map +1 -1
  16. package/dist/components/composites/appearance-editor/appearance-types.d.ts +9 -0
  17. package/dist/components/composites/appearance-editor/appearance-types.d.ts.map +1 -1
  18. package/dist/components/composites/appearance-editor/font-settings-group.d.ts +10 -0
  19. package/dist/components/composites/appearance-editor/font-settings-group.d.ts.map +1 -0
  20. package/dist/components/composites/appearance-editor/font-settings-group.js +310 -0
  21. package/dist/components/composites/appearance-editor/font-settings-group.js.map +1 -0
  22. package/dist/components/composites/appearance-editor/index.d.ts +2 -0
  23. package/dist/components/composites/appearance-editor/index.d.ts.map +1 -1
  24. package/dist/components/composites/appearance-editor/index.js +3 -1
  25. package/dist/components/composites/update-dialog/update-dialog.js +2 -2
  26. package/dist/components/composites/update-dialog/update-dialog.js.map +1 -1
  27. package/dist/components/conversation/message-row.d.ts.map +1 -1
  28. package/dist/components/conversation/message-row.js +3 -3
  29. package/dist/components/conversation/message-row.js.map +1 -1
  30. package/dist/components/conversation/paste-token-editor.js +1 -1
  31. package/dist/components/conversation/paste-token-editor.js.map +1 -1
  32. package/dist/components/layout/side-rail/side-rail.js +2 -2
  33. package/dist/components/layout/side-rail/side-rail.js.map +1 -1
  34. package/dist/components/layout/top-bar/top-bar.js +1 -1
  35. package/dist/components/layout/top-bar/top-bar.js.map +1 -1
  36. package/dist/components/theme/appearance-panel.d.ts +1 -1
  37. package/dist/components/theme/appearance-panel.d.ts.map +1 -1
  38. package/dist/components/theme/appearance-panel.js +45 -105
  39. package/dist/components/theme/appearance-panel.js.map +1 -1
  40. package/dist/components/theme/theme-provider.d.ts +5 -2
  41. package/dist/components/theme/theme-provider.d.ts.map +1 -1
  42. package/dist/components/theme/theme-provider.js +38 -19
  43. package/dist/components/theme/theme-provider.js.map +1 -1
  44. package/dist/components/ui/code-block/code-block.d.ts +3 -3
  45. package/dist/components/ui/code-block/code-block.js +6 -6
  46. package/dist/components/ui/code-block/code-block.js.map +1 -1
  47. package/dist/components/ui/command/command.js +1 -1
  48. package/dist/components/ui/command/command.js.map +1 -1
  49. package/dist/components/ui/context-menu/context-menu.js +1 -1
  50. package/dist/components/ui/context-menu/context-menu.js.map +1 -1
  51. package/dist/components/ui/diff-block/diff-block.js +5 -5
  52. package/dist/components/ui/diff-block/diff-block.js.map +1 -1
  53. package/dist/components/ui/dropdown-menu/dropdown-menu.d.ts.map +1 -1
  54. package/dist/components/ui/dropdown-menu/dropdown-menu.js +101 -8
  55. package/dist/components/ui/dropdown-menu/dropdown-menu.js.map +1 -1
  56. package/dist/components/ui/kbd/index.d.ts +1 -1
  57. package/dist/components/ui/kbd/index.d.ts.map +1 -1
  58. package/dist/components/ui/kbd/index.js +2 -2
  59. package/dist/components/ui/kbd/kbd.d.ts +40 -2
  60. package/dist/components/ui/kbd/kbd.d.ts.map +1 -1
  61. package/dist/components/ui/kbd/kbd.js +74 -7
  62. package/dist/components/ui/kbd/kbd.js.map +1 -1
  63. package/dist/components/ui/menubar/menubar.js +1 -1
  64. package/dist/components/ui/menubar/menubar.js.map +1 -1
  65. package/dist/components/ui/typography/typography.d.ts +3 -5
  66. package/dist/components/ui/typography/typography.d.ts.map +1 -1
  67. package/dist/components/ui/typography/typography.js +6 -8
  68. package/dist/components/ui/typography/typography.js.map +1 -1
  69. package/dist/index.js +7 -4
  70. package/dist/lib/appearance-font-settings.d.ts +53 -0
  71. package/dist/lib/appearance-font-settings.d.ts.map +1 -0
  72. package/dist/lib/appearance-font-settings.js +160 -0
  73. package/dist/lib/appearance-font-settings.js.map +1 -0
  74. package/dist/lib/font-catalog.d.ts +72 -0
  75. package/dist/lib/font-catalog.d.ts.map +1 -0
  76. package/dist/lib/font-catalog.js +71 -0
  77. package/dist/lib/font-catalog.js.map +1 -0
  78. package/dist/lib/safe-script.d.ts +3 -0
  79. package/dist/lib/safe-script.d.ts.map +1 -0
  80. package/dist/lib/safe-script.js +9 -0
  81. package/dist/lib/safe-script.js.map +1 -0
  82. package/dist/lib/tokens.d.ts +2 -19
  83. package/dist/lib/tokens.d.ts.map +1 -1
  84. package/dist/lib/tokens.js +63 -39
  85. package/dist/lib/tokens.js.map +1 -1
  86. package/dist/r/about-dialog.json +1 -1
  87. package/dist/r/accordion.json +1 -1
  88. package/dist/r/account-menu.json +1 -1
  89. package/dist/r/alert.json +1 -1
  90. package/dist/r/appearance-editor.json +13 -2
  91. package/dist/r/breadcrumb.json +1 -1
  92. package/dist/r/calendar.json +1 -1
  93. package/dist/r/carousel.json +1 -1
  94. package/dist/r/catalog-browser.json +1 -1
  95. package/dist/r/checkbox.json +1 -1
  96. package/dist/r/code-block.json +1 -1
  97. package/dist/r/combobox.json +1 -1
  98. package/dist/r/command.json +1 -1
  99. package/dist/r/context-menu.json +1 -1
  100. package/dist/r/conversation.json +1 -1
  101. package/dist/r/detail-panel.json +1 -1
  102. package/dist/r/dialog.json +1 -1
  103. package/dist/r/dropdown-menu.json +1 -1
  104. package/dist/r/empty.json +1 -1
  105. package/dist/r/floating-preview.json +1 -1
  106. package/dist/r/help-center.json +1 -1
  107. package/dist/r/lib.json +16 -1
  108. package/dist/r/menubar.json +1 -1
  109. package/dist/r/native-select.json +1 -1
  110. package/dist/r/navigation-menu.json +1 -1
  111. package/dist/r/pagination.json +1 -1
  112. package/dist/r/radio-group.json +1 -1
  113. package/dist/r/registry.json +75 -42
  114. package/dist/r/resizable.json +1 -1
  115. package/dist/r/scene-controls.json +1 -1
  116. package/dist/r/select.json +1 -1
  117. package/dist/r/sheet.json +1 -1
  118. package/dist/r/sidebar-nav.json +1 -1
  119. package/dist/r/spinner.json +1 -1
  120. package/dist/r/split-layout.json +1 -1
  121. package/dist/r/src/components/composites/action-button/action-button.tsx +2 -1
  122. package/dist/r/src/components/composites/appearance-editor/accent-swatch-groups.tsx +147 -0
  123. package/dist/r/src/components/composites/appearance-editor/appearance-choices.tsx +17 -74
  124. package/dist/r/src/components/composites/appearance-editor/appearance-editor.tsx +19 -6
  125. package/dist/r/src/components/composites/appearance-editor/appearance-types.ts +9 -0
  126. package/dist/r/src/components/composites/appearance-editor/font-settings-group.tsx +350 -0
  127. package/dist/r/src/components/composites/appearance-editor/index.ts +19 -0
  128. package/dist/r/src/components/composites/update-dialog/update-dialog.tsx +2 -2
  129. package/dist/r/src/components/conversation/message-row.tsx +7 -3
  130. package/dist/r/src/components/conversation/paste-token-editor.tsx +1 -1
  131. package/dist/r/src/components/layout/side-rail/side-rail.tsx +3 -3
  132. package/dist/r/src/components/layout/top-bar/top-bar.tsx +1 -1
  133. package/dist/r/src/components/theme/appearance-panel.tsx +34 -83
  134. package/dist/r/src/components/theme/theme-provider.tsx +50 -20
  135. package/dist/r/src/components/ui/code-block/code-block.tsx +6 -6
  136. package/dist/r/src/components/ui/command/command.tsx +1 -1
  137. package/dist/r/src/components/ui/context-menu/context-menu.tsx +1 -1
  138. package/dist/r/src/components/ui/diff-block/diff-block.tsx +7 -7
  139. package/dist/r/src/components/ui/dropdown-menu/dropdown-menu.tsx +145 -7
  140. package/dist/r/src/components/ui/kbd/index.ts +10 -1
  141. package/dist/r/src/components/ui/kbd/kbd.tsx +77 -14
  142. package/dist/r/src/components/ui/menubar/menubar.tsx +1 -1
  143. package/dist/r/src/components/ui/typography/typography.tsx +6 -8
  144. package/dist/r/src/lib/appearance-font-settings.ts +233 -0
  145. package/dist/r/src/lib/font-catalog.ts +80 -0
  146. package/dist/r/src/lib/safe-script.ts +7 -0
  147. package/dist/r/src/lib/tokens.ts +64 -59
  148. package/dist/r/src/styles/appearance-font-settings.css +110 -0
  149. package/dist/r/src/styles/fonts.css +4 -5
  150. package/dist/r/src/styles/globals.css +4 -1
  151. package/dist/r/src/styles/theme.css +14 -0
  152. package/dist/r/stat.json +1 -1
  153. package/dist/r/theme.json +5 -0
  154. package/dist/r/theming.json +4 -2
  155. package/dist/r/toast.json +1 -1
  156. package/dist/r/top-bar.json +1 -1
  157. package/dist/r/tree.json +1 -1
  158. package/dist/r/update-dialog.json +1 -1
  159. package/package.json +7 -6
  160. package/registry.json +75 -42
  161. package/src/components/composites/action-button/action-button.tsx +2 -1
  162. package/src/components/composites/appearance-editor/accent-swatch-groups.tsx +147 -0
  163. package/src/components/composites/appearance-editor/appearance-choices.tsx +17 -74
  164. package/src/components/composites/appearance-editor/appearance-editor.tsx +19 -6
  165. package/src/components/composites/appearance-editor/appearance-types.ts +9 -0
  166. package/src/components/composites/appearance-editor/font-settings-group.tsx +350 -0
  167. package/src/components/composites/appearance-editor/index.ts +19 -0
  168. package/src/components/composites/update-dialog/update-dialog.tsx +2 -2
  169. package/src/components/conversation/message-row.tsx +7 -3
  170. package/src/components/conversation/paste-token-editor.tsx +1 -1
  171. package/src/components/layout/side-rail/side-rail.tsx +3 -3
  172. package/src/components/layout/top-bar/top-bar.tsx +1 -1
  173. package/src/components/theme/appearance-panel.tsx +34 -83
  174. package/src/components/theme/theme-provider.tsx +50 -20
  175. package/src/components/ui/code-block/code-block.tsx +6 -6
  176. package/src/components/ui/command/command.tsx +1 -1
  177. package/src/components/ui/context-menu/context-menu.tsx +1 -1
  178. package/src/components/ui/diff-block/diff-block.tsx +7 -7
  179. package/src/components/ui/dropdown-menu/dropdown-menu.tsx +145 -7
  180. package/src/components/ui/kbd/index.ts +10 -1
  181. package/src/components/ui/kbd/kbd.tsx +77 -14
  182. package/src/components/ui/menubar/menubar.tsx +1 -1
  183. package/src/components/ui/typography/typography.tsx +6 -8
  184. package/src/lib/appearance-font-settings.ts +233 -0
  185. package/src/lib/font-catalog.ts +80 -0
  186. package/src/lib/safe-script.ts +7 -0
  187. package/src/lib/tokens.ts +64 -59
  188. package/src/styles/appearance-font-settings.css +110 -0
  189. package/src/styles/fonts.css +4 -5
  190. package/src/styles/globals.css +4 -1
  191. package/src/styles/theme.css +14 -0
@@ -1 +1,10 @@
1
- export { Kbd, KbdGroup } from './kbd'
1
+ export {
2
+ Kbd,
3
+ KbdChord,
4
+ KbdGroup,
5
+ kbdChordKeys,
6
+ kbdGroupVariants,
7
+ kbdVariants,
8
+ type KbdChordProps,
9
+ type KbdSize,
10
+ } from './kbd'
@@ -1,5 +1,6 @@
1
1
  import type { ComponentProps } from 'solid-js'
2
- import { splitProps } from 'solid-js'
2
+ import { For, splitProps } from 'solid-js'
3
+ import { cva, type VariantProps } from '../../../lib/variants'
3
4
  import { cn } from '../../../lib/utils'
4
5
 
5
6
  /**
@@ -9,32 +10,94 @@ import { cn } from '../../../lib/utils'
9
10
  * `<kbd>` so the semantics survive copy-paste, and the mono face is what makes
10
11
  * a shortcut visually distinct from the sentence around it.
11
12
  *
13
+ * `size="compact"` exists for the dense rows a default cap would outweigh — a
14
+ * rail search row, a sidebar shortcut hint. It drops a height rung, and it
15
+ * lightens the fill and weight with it because at that size a solid muted cap
16
+ * reads heavier than the row text it sits in. The default rendering is every
17
+ * cap that is not inside such a row, and it is untouched by the rung.
18
+ *
12
19
  * `KbdGroup` exists because a shortcut is a sequence: typing `⌘` then `K`
13
20
  * needs a gap between them that is a layout concern, not punctuation.
14
21
  */
15
- export function Kbd(props: ComponentProps<'kbd'>) {
16
- const [local, rest] = splitProps(props, ['class'])
22
+ export const kbdVariants = cva(
23
+ 'bg-muted text-muted-foreground pointer-events-none inline-flex h-5 min-w-5 items-center justify-center gap-1 rounded-sm border border-border px-1 font-code text-code font-medium select-none',
24
+ {
25
+ variants: {
26
+ size: {
27
+ default: '',
28
+ compact: 'h-4 min-w-4 bg-muted/40 px-0.5 font-normal leading-none',
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ size: 'default',
33
+ },
34
+ }
35
+ )
36
+
37
+ export type KbdSize = NonNullable<VariantProps<typeof kbdVariants>['size']>
38
+
39
+ export function Kbd(props: ComponentProps<'kbd'> & { size?: KbdSize }) {
40
+ const [local, rest] = splitProps(props, ['size', 'class'])
17
41
 
18
42
  return (
19
- <kbd
20
- data-slot="kbd"
21
- class={cn(
22
- 'bg-muted text-muted-foreground pointer-events-none inline-flex h-5 min-w-5 items-center justify-center gap-1 rounded-sm border border-border px-1 font-mono text-2xs font-medium select-none',
23
- local.class
24
- )}
25
- {...rest}
26
- />
43
+ <kbd data-slot="kbd" class={cn(kbdVariants({ size: local.size }), local.class)} {...rest} />
27
44
  )
28
45
  }
29
46
 
30
- export function KbdGroup(props: ComponentProps<'span'>) {
31
- const [local, rest] = splitProps(props, ['class'])
47
+ export const kbdGroupVariants = cva('inline-flex items-center gap-1', {
48
+ variants: {
49
+ size: {
50
+ default: '',
51
+ /** The gap is what separates caps, so it tightens with them. */
52
+ compact: 'gap-0.5',
53
+ },
54
+ },
55
+ defaultVariants: {
56
+ size: 'default',
57
+ },
58
+ })
59
+
60
+ export function KbdGroup(props: ComponentProps<'span'> & { size?: KbdSize }) {
61
+ const [local, rest] = splitProps(props, ['size', 'class'])
32
62
 
33
63
  return (
34
64
  <span
35
65
  data-slot="kbd-group"
36
- class={cn('inline-flex items-center gap-1', local.class)}
66
+ class={cn(kbdGroupVariants({ size: local.size }), local.class)}
37
67
  {...rest}
38
68
  />
39
69
  )
40
70
  }
71
+
72
+ /**
73
+ * Split a written chord into one entry per key cap. Every character is a key:
74
+ * `⌘K` renders as `⌘` + `K`, and a longer chord like `⇧⌘P` stays three caps
75
+ * wide, which matches how the system draws chords in menus.
76
+ */
77
+ export function kbdChordKeys(keys: string): string[] {
78
+ return [...keys]
79
+ }
80
+
81
+ export type KbdChordProps = Omit<ComponentProps<'span'>, 'children'> & {
82
+ /** The chord as it is drawn, e.g. `"⌘K"` or `"⇧⌘P"`. One cap per character. */
83
+ keys: string
84
+ size?: KbdSize
85
+ }
86
+
87
+ /**
88
+ * KbdChord.
89
+ *
90
+ * A written chord drawn as caps, one per character. Decoration only: the
91
+ * composition renders `aria-hidden`, so the accessible name and the parseable
92
+ * `aria-keyshortcuts` stay on the control the chord belongs to. That is why
93
+ * there is no children — the chord says what was drawn, nothing more.
94
+ */
95
+ export function KbdChord(props: KbdChordProps) {
96
+ const [local, rest] = splitProps(props, ['keys', 'size', 'class'])
97
+
98
+ return (
99
+ <KbdGroup {...rest} size={local.size} class={local.class} aria-hidden="true">
100
+ <For each={kbdChordKeys(local.keys)}>{(key) => <Kbd size={local.size}>{key}</Kbd>}</For>
101
+ </KbdGroup>
102
+ )
103
+ }
@@ -123,7 +123,7 @@ export function MenubarItem(
123
123
  {local.shortcut ? (
124
124
  <span
125
125
  aria-hidden="true"
126
- class="text-muted-foreground ms-auto font-mono text-2xs tracking-widest"
126
+ class="text-muted-foreground ms-auto font-code text-code tracking-widest"
127
127
  >
128
128
  {local.shortcut}
129
129
  </span>
@@ -119,11 +119,9 @@ export function Heading<T extends ValidComponent = 'h2'>(props: HeadingProps<T>)
119
119
  /**
120
120
  * Text.
121
121
  *
122
- * Everything that is not a heading. `text-sm` is the interface default, not 16px,
123
- * and it carries three weights by role: 400 for body copy, 500 for a label or
124
- * control text, 600 for emphasis. Below it, `caption` (12px) is metadata and
125
- * `micro` (11px, 500) is the floor — keyboard keys and the status bar. `code` is
126
- * the mono face at 12px, for anything a user compares character by character.
122
+ * Everything that is not a heading. Body and strong copy use the content font
123
+ * axis; labels, captions and microcopy inherit the UI face. Code uses its own
124
+ * family and size axis for anything a user compares character by character.
127
125
  *
128
126
  * `tone` is a separate axis from `variant` because "secondary" is a colour
129
127
  * decision, not a size: a muted caption and a muted body line are both common.
@@ -136,17 +134,17 @@ export const textVariants = cva('', {
136
134
  variants: {
137
135
  variant: {
138
136
  /** 14px / 400. Body copy and descriptions. */
139
- body: 'text-sm',
137
+ body: 'font-content text-content',
140
138
  /** 14px / 500. Labels, control text, list-row titles. */
141
139
  label: 'text-sm font-medium',
142
140
  /** 14px / 600. Emphasis inside body copy. */
143
- strong: 'text-sm font-semibold',
141
+ strong: 'font-content text-content font-semibold',
144
142
  /** 12px / 400. Metadata, secondary rows, units. */
145
143
  caption: 'text-xs',
146
144
  /** 11px / 500. The floor: keyboard keys, the status bar. */
147
145
  micro: 'text-2xs font-medium',
148
146
  /** 12px mono. Ids, paths, hashes, inline code. */
149
- code: 'font-mono text-xs',
147
+ code: 'font-code text-code',
150
148
  },
151
149
  tone: {
152
150
  inherit: '',
@@ -0,0 +1,233 @@
1
+ import { fontOptions, type FontOption } from './font-catalog'
2
+ import { safeScriptStringLiteral } from './safe-script'
3
+
4
+ export const APPEARANCE_EDITOR_FONT_AXES = ['ui', 'content', 'code'] as const
5
+
6
+ export type AppearanceEditorFontAxis = (typeof APPEARANCE_EDITOR_FONT_AXES)[number]
7
+ export type AppearanceEditorFontFamilyId = FontOption['id']
8
+
9
+ export type AppearanceEditorFontSetting = Readonly<{
10
+ /** A stable id from the shared font catalogue, never a CSS family string. */
11
+ family: AppearanceEditorFontFamilyId
12
+ /** A pixel size, normalized to the supported range. */
13
+ size: number
14
+ }>
15
+
16
+ /** The host-owned choices edited by the shared appearance composition. */
17
+ export type AppearanceEditorFontSettings = Readonly<
18
+ Record<AppearanceEditorFontAxis, AppearanceEditorFontSetting>
19
+ >
20
+
21
+ export const APPEARANCE_EDITOR_FONT_SIZE_MIN = 10
22
+ export const APPEARANCE_EDITOR_FONT_SIZE_MAX = 32
23
+
24
+ /** New preferences use the platform face in each text role. */
25
+ export const DEFAULT_APPEARANCE_EDITOR_FONT_SETTINGS: AppearanceEditorFontSettings = Object.freeze({
26
+ ui: Object.freeze({ family: 'system', size: 14 }),
27
+ content: Object.freeze({ family: 'system', size: 14 }),
28
+ code: Object.freeze({ family: 'system', size: 12 }),
29
+ })
30
+
31
+ export type NormalizedAppearanceEditorFontSettings = Readonly<{
32
+ settings: AppearanceEditorFontSettings
33
+ /** Axes whose persisted family or size was unavailable or outside its limits. */
34
+ recoveredAxes: readonly AppearanceEditorFontAxis[]
35
+ }>
36
+
37
+ const fontFamilyIds: ReadonlySet<string> = new Set(fontOptions.map((option) => option.id))
38
+ const fontOptionsById = Object.freeze(
39
+ Object.fromEntries(fontOptions.map((option) => [option.id, option])) as Record<
40
+ AppearanceEditorFontFamilyId,
41
+ FontOption
42
+ >
43
+ )
44
+ const fontProjectionById = Object.freeze(
45
+ Object.fromEntries(
46
+ fontOptions.map((option) => [
47
+ option.id,
48
+ {
49
+ familyVariables: option.familyVariables,
50
+ uiTracking: option.uiTracking,
51
+ uiWordSpacing: option.uiWordSpacing,
52
+ },
53
+ ])
54
+ ) as Record<
55
+ AppearanceEditorFontFamilyId,
56
+ Pick<FontOption, 'familyVariables' | 'uiTracking' | 'uiWordSpacing'>
57
+ >
58
+ )
59
+ const fontAxisProjection = Object.freeze({
60
+ ui: Object.freeze({
61
+ familyAttribute: 'data-ui-font',
62
+ familyProperty: '--font-ui',
63
+ sizeProperty: '--font-ui-size',
64
+ scaleProperty: '--font-ui-scale',
65
+ defaultSize: DEFAULT_APPEARANCE_EDITOR_FONT_SETTINGS.ui.size,
66
+ }),
67
+ content: Object.freeze({
68
+ familyAttribute: 'data-content-font',
69
+ familyProperty: '--font-content',
70
+ sizeProperty: '--font-content-size',
71
+ scaleProperty: '--font-content-scale',
72
+ defaultSize: DEFAULT_APPEARANCE_EDITOR_FONT_SETTINGS.content.size,
73
+ }),
74
+ code: Object.freeze({
75
+ familyAttribute: 'data-code-font',
76
+ familyProperty: '--font-code',
77
+ sizeProperty: '--font-code-size',
78
+ scaleProperty: '--font-code-scale',
79
+ defaultSize: DEFAULT_APPEARANCE_EDITOR_FONT_SETTINGS.code.size,
80
+ }),
81
+ })
82
+
83
+ function isRecord(value: unknown): value is Record<string, unknown> {
84
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
85
+ }
86
+
87
+ function hasOwn(value: Record<string, unknown>, key: string): boolean {
88
+ return Object.prototype.hasOwnProperty.call(value, key)
89
+ }
90
+
91
+ function normalizeFamily(value: unknown): AppearanceEditorFontFamilyId | undefined {
92
+ return typeof value === 'string' && fontFamilyIds.has(value)
93
+ ? (value as AppearanceEditorFontFamilyId)
94
+ : undefined
95
+ }
96
+
97
+ function clampFontSize(value: number): number {
98
+ return Math.min(
99
+ APPEARANCE_EDITOR_FONT_SIZE_MAX,
100
+ Math.max(APPEARANCE_EDITOR_FONT_SIZE_MIN, Math.round(value))
101
+ )
102
+ }
103
+
104
+ /** Normalize a single number-input value; invalid edits return the current size. */
105
+ export function normalizeAppearanceEditorFontSize(value: unknown, fallback: number): number {
106
+ const parsed =
107
+ typeof value === 'number'
108
+ ? value
109
+ : typeof value === 'string' && value.trim() !== ''
110
+ ? Number(value)
111
+ : Number.NaN
112
+ return Number.isFinite(parsed) ? clampFontSize(parsed) : clampFontSize(fallback)
113
+ }
114
+
115
+ /**
116
+ * Recover persisted font choices against the shared font catalogue.
117
+ *
118
+ * `legacyUiFamily` lets a host migrate its previous single font field into the
119
+ * UI axis. Content and code remain on the new System defaults during that
120
+ * migration. Missing settings are ordinary defaults; an unknown saved id or an
121
+ * invalid size is reported through `recoveredAxes` for the host's notice.
122
+ */
123
+ export function normalizeAppearanceEditorFontSettings(
124
+ value: unknown,
125
+ legacyUiFamily?: unknown
126
+ ): NormalizedAppearanceEditorFontSettings {
127
+ const source = isRecord(value) ? value : undefined
128
+ const malformedRoot = value !== undefined && value !== null && source === undefined
129
+ const recovered = new Set<AppearanceEditorFontAxis>()
130
+ const settings = {} as Record<AppearanceEditorFontAxis, AppearanceEditorFontSetting>
131
+
132
+ for (const axis of APPEARANCE_EDITOR_FONT_AXES) {
133
+ const axisPresent = source ? hasOwn(source, axis) : false
134
+ const axisValue = source?.[axis]
135
+ const axisSource = isRecord(axisValue) ? axisValue : undefined
136
+ const fallback = DEFAULT_APPEARANCE_EDITOR_FONT_SETTINGS[axis]
137
+
138
+ if (malformedRoot || (axisPresent && axisSource === undefined)) recovered.add(axis)
139
+
140
+ const hasFamily = axisSource ? hasOwn(axisSource, 'family') : false
141
+ const familyInput = hasFamily
142
+ ? axisSource?.['family']
143
+ : axis === 'ui' && legacyUiFamily !== undefined
144
+ ? legacyUiFamily
145
+ : undefined
146
+ const family = normalizeFamily(familyInput) ?? fallback.family
147
+ if (
148
+ (hasFamily || (axis === 'ui' && legacyUiFamily !== undefined)) &&
149
+ !normalizeFamily(familyInput)
150
+ ) {
151
+ recovered.add(axis)
152
+ }
153
+
154
+ const hasSize = axisSource ? hasOwn(axisSource, 'size') : false
155
+ const sizeInput = axisSource?.['size']
156
+ const size = hasSize
157
+ ? normalizeAppearanceEditorFontSize(sizeInput, fallback.size)
158
+ : fallback.size
159
+ if (
160
+ hasSize &&
161
+ (typeof sizeInput !== 'number' || !Number.isFinite(sizeInput) || sizeInput !== size)
162
+ ) {
163
+ recovered.add(axis)
164
+ }
165
+
166
+ settings[axis] = Object.freeze({ family, size })
167
+ }
168
+
169
+ return Object.freeze({
170
+ settings: Object.freeze(settings),
171
+ recoveredAxes: Object.freeze(APPEARANCE_EDITOR_FONT_AXES.filter((axis) => recovered.has(axis))),
172
+ })
173
+ }
174
+
175
+ /**
176
+ * Project normalized font preferences onto a document root.
177
+ *
178
+ * This is shared by `ThemeProvider` and hosts that own their own appearance
179
+ * persistence, so UI/content/code use the same catalogue ids, theme tokens,
180
+ * sizes, and projected CSS variables. The marker activates the optional role
181
+ * stylesheet and survives CSS import reordering.
182
+ */
183
+ export function applyAppearanceFontSettings(
184
+ root: HTMLElement,
185
+ value: unknown
186
+ ): NormalizedAppearanceEditorFontSettings {
187
+ const normalized = normalizeAppearanceEditorFontSettings(value)
188
+ const { settings } = normalized
189
+
190
+ root.removeAttribute('data-font')
191
+ root.setAttribute('data-font-settings', '')
192
+ for (const axis of APPEARANCE_EDITOR_FONT_AXES) {
193
+ const projection = fontAxisProjection[axis]
194
+ const setting = settings[axis]
195
+ const option = fontOptionsById[setting.family]
196
+ if (setting.family === 'system') root.removeAttribute(projection.familyAttribute)
197
+ else root.setAttribute(projection.familyAttribute, setting.family)
198
+ root.style.setProperty(projection.familyProperty, `var(${option.familyVariables[axis]})`)
199
+ root.style.setProperty(projection.sizeProperty, String(setting.size) + 'px')
200
+ root.style.setProperty(projection.scaleProperty, String(setting.size / projection.defaultSize))
201
+ if (axis === 'ui') {
202
+ root.style.setProperty('--ui-tracking', option.uiTracking)
203
+ root.style.setProperty('--ui-word-spacing', option.uiWordSpacing)
204
+ }
205
+ }
206
+
207
+ return normalized
208
+ }
209
+
210
+ /**
211
+ * Generate a safe inline prepaint script for a host-owned appearance record.
212
+ *
213
+ * The stored object is read from `localStorage[storageKey].fonts`. A legacy
214
+ * top-level `font` family migrates into UI only; content/code use the shared
215
+ * defaults. The script applies the same catalogue whitelist, limits, attributes,
216
+ * and CSS properties as `applyAppearanceFontSettings` before styles paint. When
217
+ * `persistedVersion` is supplied, only a record with that exact top-level version
218
+ * is used; absent or mismatched records receive the System defaults.
219
+ */
220
+ export function fontSettingsBootstrapScript(storageKey: string, persistedVersion?: number): string {
221
+ if (persistedVersion !== undefined && !Number.isSafeInteger(persistedVersion)) {
222
+ throw new RangeError('persistedVersion must be a safe integer')
223
+ }
224
+ const axisConfig = JSON.stringify(fontAxisProjection)
225
+ const fontProjection = JSON.stringify(fontProjectionById)
226
+ const fontIds = `[${fontOptions.map((option) => safeScriptStringLiteral(option.id)).join(',')}]`
227
+ const persistedVersionGuard =
228
+ persistedVersion === undefined
229
+ ? ''
230
+ : `&&parsed.version===Number(${safeScriptStringLiteral(String(persistedVersion))})`
231
+
232
+ return `(function(){var r=document.documentElement;var axes=JSON.parse(${safeScriptStringLiteral(axisConfig)});var projection=JSON.parse(${safeScriptStringLiteral(fontProjection)});var ids=${fontIds};var has=function(v,k){return Object.prototype.hasOwnProperty.call(v,k)};var record=function(v){return typeof v==='object'&&v!==null&&!Array.isArray(v)};var p={};try{var raw=localStorage.getItem(${safeScriptStringLiteral(storageKey)});var parsed=raw?JSON.parse(raw):{};if(record(parsed)${persistedVersionGuard})p=parsed;}catch(e){}var saved=record(p.fonts)?p.fonts:{};var valid=function(v,d){return ids.indexOf(v)!==-1?v:d};var size=function(v,d){var n=typeof v==='number'?v:typeof v==='string'&&v.trim()!==''?Number(v):NaN;return Number.isFinite(n)?Math.min(${APPEARANCE_EDITOR_FONT_SIZE_MAX},Math.max(${APPEARANCE_EDITOR_FONT_SIZE_MIN},Math.round(n))):d};r.setAttribute('data-font-settings','');for(var axis in axes){if(!has(axes,axis))continue;var spec=axes[axis];var candidate=saved[axis];var value=record(candidate)?candidate:{};var legacyUi=axis==='ui'&&!has(value,'family');var family=legacyUi?valid(p.font,'system'):valid(value.family,'system');var font=projection[family];if(family==='system')r.removeAttribute(spec.familyAttribute);else r.setAttribute(spec.familyAttribute,family);r.style.setProperty(spec.familyProperty,'var('+font.familyVariables[axis]+')');var px=size(value.size,spec.defaultSize);r.style.setProperty(spec.sizeProperty,px+'px');r.style.setProperty(spec.scaleProperty,String(px/spec.defaultSize));if(axis==='ui'){r.style.setProperty('--ui-tracking',font.uiTracking);r.style.setProperty('--ui-word-spacing',font.uiWordSpacing);}}r.removeAttribute('data-font');})();`
233
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Stable font-family choices shared by the appearance editor and its host APIs.
3
+ *
4
+ * This stays independent of the theme-token manifest so consumers that need
5
+ * only font settings do not retain the palette catalogue.
6
+ */
7
+ // Explicit catalogue choices resolve to the same named face in every role.
8
+ // System Code is the exception: it resolves to the platform monospace stack.
9
+ const FONT_OPTIONS = [
10
+ {
11
+ id: 'space-grotesk',
12
+ label: 'Space Grotesk',
13
+ description: 'The self-hosted Space Grotesk family.',
14
+ familyVariables: {
15
+ ui: '--font-family-space-grotesk',
16
+ content: '--font-family-space-grotesk',
17
+ code: '--font-family-space-grotesk',
18
+ },
19
+ uiTracking: 'normal',
20
+ uiWordSpacing: 'normal',
21
+ stack: 'both',
22
+ },
23
+ {
24
+ id: 'system',
25
+ label: 'System',
26
+ description: "The platform's own interface family and system monospace counterpart.",
27
+ familyVariables: {
28
+ ui: '--font-family-system',
29
+ content: '--font-family-system',
30
+ code: '--font-family-system-mono',
31
+ },
32
+ uiTracking: 'normal',
33
+ uiWordSpacing: 'normal',
34
+ stack: 'both',
35
+ },
36
+ {
37
+ id: 'geist',
38
+ label: 'Geist',
39
+ description: 'The face cortana already ships. Neutral and wide.',
40
+ familyVariables: {
41
+ ui: '--font-family-geist',
42
+ content: '--font-family-geist',
43
+ code: '--font-family-geist',
44
+ },
45
+ uiTracking: 'normal',
46
+ uiWordSpacing: 'normal',
47
+ stack: 'both',
48
+ },
49
+ {
50
+ id: 'geist-mono',
51
+ label: 'Geist Mono',
52
+ description: 'Monospace throughout, for someone who wants a uniform texture.',
53
+ familyVariables: {
54
+ ui: '--font-family-geist-mono',
55
+ content: '--font-family-geist-mono',
56
+ code: '--font-family-geist-mono',
57
+ },
58
+ uiTracking: '-0.03em',
59
+ uiWordSpacing: '-1.5px',
60
+ stack: 'both',
61
+ },
62
+ {
63
+ id: 'jetbrains-mono',
64
+ label: 'JetBrains Mono',
65
+ description: 'Monospace throughout, with coding ligatures off.',
66
+ familyVariables: {
67
+ ui: '--font-family-jetbrains-mono',
68
+ content: '--font-family-jetbrains-mono',
69
+ code: '--font-family-jetbrains-mono',
70
+ },
71
+ uiTracking: '-0.04em',
72
+ uiWordSpacing: '-2.5px',
73
+ stack: 'both',
74
+ },
75
+ ] as const
76
+
77
+ /** Stable catalogue record type; saved values never contain CSS family strings. */
78
+ export type FontOption = (typeof FONT_OPTIONS)[number]
79
+
80
+ export const fontOptions: readonly FontOption[] = Object.freeze(FONT_OPTIONS)
@@ -0,0 +1,7 @@
1
+ /** Escape a string literal for an inline script's JavaScript source. */
2
+ export function safeScriptStringLiteral(value: string): string {
3
+ return JSON.stringify(value).replace(
4
+ /[<>\u2028\u2029]/g,
5
+ (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`
6
+ )
7
+ }
@@ -18,6 +18,8 @@
18
18
  */
19
19
 
20
20
  import { ACCENTS } from '@adea-ai/themes'
21
+ export { fontOptions } from './font-catalog'
22
+ export type { FontOption } from './font-catalog'
21
23
 
22
24
  export type TokenKind =
23
25
  | 'color'
@@ -388,20 +390,35 @@ export const radiusTokens: TokenDefinition[] = [
388
390
  ]
389
391
 
390
392
  export const typographyTokens: TokenDefinition[] = [
393
+ {
394
+ name: 'font-ui',
395
+ kind: 'font-family',
396
+ description: 'The selected family for interface controls, labels and headings.',
397
+ },
398
+ {
399
+ name: 'font-content',
400
+ kind: 'font-family',
401
+ description: 'The selected family for prose, transcripts and long-form reading.',
402
+ },
403
+ {
404
+ name: 'font-code',
405
+ kind: 'font-family',
406
+ description: 'The selected family for code, terminals, keyboard keys, ids and paths.',
407
+ },
391
408
  {
392
409
  name: 'font-sans',
393
410
  kind: 'font-family',
394
- description: 'The selected interface face. Space Grotesk unless the font axis says otherwise.',
411
+ description: 'Compatibility alias for the selected UI family.',
395
412
  },
396
413
  {
397
414
  name: 'font-mono',
398
415
  kind: 'font-family',
399
- description: 'The selected monospace face. Code, terminals, ids, paths.',
416
+ description: 'Compatibility alias for the selected code family.',
400
417
  },
401
418
  {
402
419
  name: 'font-family-space-grotesk',
403
420
  kind: 'font-family',
404
- description: 'The interface default, and the face the visual language was drawn against.',
421
+ description: 'The self-hosted Space Grotesk family.',
405
422
  },
406
423
  {
407
424
  name: 'font-family-jetbrains-mono',
@@ -428,6 +445,36 @@ export const typographyTokens: TokenDefinition[] = [
428
445
  kind: 'font-family',
429
446
  description: "The platform's own monospace face.",
430
447
  },
448
+ {
449
+ name: 'font-ui-size',
450
+ kind: 'dimension',
451
+ description: 'Runtime-projected interface size in pixels; scales the shared UI text ladder.',
452
+ },
453
+ {
454
+ name: 'font-content-size',
455
+ kind: 'dimension',
456
+ description: 'Runtime-projected prose and transcript size in pixels.',
457
+ },
458
+ {
459
+ name: 'font-code-size',
460
+ kind: 'dimension',
461
+ description: 'Runtime-projected code, terminal and keyboard-key size in pixels.',
462
+ },
463
+ {
464
+ name: 'font-ui-scale',
465
+ kind: 'number',
466
+ description: 'The UI size ratio applied to the interface text ladder.',
467
+ },
468
+ {
469
+ name: 'font-content-scale',
470
+ kind: 'number',
471
+ description: 'The content size ratio applied to rem-relative prose text.',
472
+ },
473
+ {
474
+ name: 'font-code-scale',
475
+ kind: 'number',
476
+ description: 'The code size ratio applied to rem-relative code text.',
477
+ },
431
478
  {
432
479
  name: 'ui-tracking',
433
480
  kind: 'text',
@@ -443,13 +490,13 @@ export const typographyTokens: TokenDefinition[] = [
443
490
  {
444
491
  name: 'text-2xs',
445
492
  kind: 'text',
446
- description: '11px — the smallest label the system permits: kbd keys, status bar.',
493
+ description: 'The smallest UI-scaled label: status bar and compact metadata.',
447
494
  },
448
- { name: 'text-xs', kind: 'text', description: '12px — metadata, badges, secondary rows.' },
495
+ { name: 'text-xs', kind: 'text', description: 'UI-scaled metadata, badges and secondary rows.' },
449
496
  {
450
497
  name: 'text-sm',
451
498
  kind: 'text',
452
- description: '14px — the interface default. Body copy, labels, controls.',
499
+ description: 'The UI-scaled interface default for labels and controls.',
453
500
  },
454
501
  { name: 'text-base', kind: 'text', description: '16px — card titles and dialog titles.' },
455
502
  { name: 'text-lg', kind: 'text', description: '18px — a section heading inside a page.' },
@@ -458,7 +505,17 @@ export const typographyTokens: TokenDefinition[] = [
458
505
  {
459
506
  name: 'text-3xl',
460
507
  kind: 'text',
461
- description: '30px — the largest size in the system. Reserved for an empty-state headline.',
508
+ description: 'The largest UI-scaled rung. Reserved for an empty-state headline.',
509
+ },
510
+ {
511
+ name: 'text-content',
512
+ kind: 'text',
513
+ description: 'The selected content size with the prose line-height.',
514
+ },
515
+ {
516
+ name: 'text-code',
517
+ kind: 'text',
518
+ description: 'The selected code size with the code line-height.',
462
519
  },
463
520
  ]
464
521
 
@@ -661,58 +718,6 @@ export const accentPresets: readonly AccentPreset[] = Object.freeze([
661
718
  ...ACCENTS,
662
719
  ])
663
720
 
664
- /**
665
- * The font axis.
666
- *
667
- * The family is a selection, like the accent: `data-font` sits on the same element
668
- * and each option swaps the interface face. `space-grotesk` is the default because
669
- * it is the face the visual language was drawn against, and `system` is a
670
- * first-class option rather than a fallback — an application that wants the
671
- * platform's own face should be able to say so without losing the rest of the
672
- * system.
673
- */
674
- export type FontOption = {
675
- /** The `data-font` value. `space-grotesk` is the default and needs no attribute. */
676
- id: string
677
- label: string
678
- description: string
679
- /** Which of the two stacks this option sets. */
680
- stack: 'sans' | 'mono' | 'both'
681
- }
682
-
683
- export const fontOptions: readonly FontOption[] = Object.freeze([
684
- {
685
- id: 'space-grotesk',
686
- label: 'Space Grotesk',
687
- description: 'The default, and the face the visual language was drawn against.',
688
- stack: 'both',
689
- },
690
- {
691
- id: 'system',
692
- label: 'System',
693
- description: "The platform's own face. What a terminal or editor usually wants.",
694
- stack: 'both',
695
- },
696
- {
697
- id: 'geist',
698
- label: 'Geist',
699
- description: 'The face cortana already ships. Neutral and wide.',
700
- stack: 'both',
701
- },
702
- {
703
- id: 'geist-mono',
704
- label: 'Geist Mono',
705
- description: 'Monospace throughout, for someone who wants a uniform texture.',
706
- stack: 'both',
707
- },
708
- {
709
- id: 'jetbrains-mono',
710
- label: 'JetBrains Mono',
711
- description: 'Monospace throughout, with coding ligatures off.',
712
- stack: 'both',
713
- },
714
- ])
715
-
716
721
  /** Every token, in gallery order. */
717
722
  export const designTokens = {
718
723
  color: colorTokens,