@adea-ai/ui 0.105.0 → 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 (147) 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/kbd.js +1 -1
  57. package/dist/components/ui/kbd/kbd.js.map +1 -1
  58. package/dist/components/ui/menubar/menubar.js +1 -1
  59. package/dist/components/ui/menubar/menubar.js.map +1 -1
  60. package/dist/components/ui/typography/typography.d.ts +3 -5
  61. package/dist/components/ui/typography/typography.d.ts.map +1 -1
  62. package/dist/components/ui/typography/typography.js +6 -8
  63. package/dist/components/ui/typography/typography.js.map +1 -1
  64. package/dist/index.js +6 -3
  65. package/dist/lib/appearance-font-settings.d.ts +53 -0
  66. package/dist/lib/appearance-font-settings.d.ts.map +1 -0
  67. package/dist/lib/appearance-font-settings.js +160 -0
  68. package/dist/lib/appearance-font-settings.js.map +1 -0
  69. package/dist/lib/font-catalog.d.ts +72 -0
  70. package/dist/lib/font-catalog.d.ts.map +1 -0
  71. package/dist/lib/font-catalog.js +71 -0
  72. package/dist/lib/font-catalog.js.map +1 -0
  73. package/dist/lib/safe-script.d.ts +3 -0
  74. package/dist/lib/safe-script.d.ts.map +1 -0
  75. package/dist/lib/safe-script.js +9 -0
  76. package/dist/lib/safe-script.js.map +1 -0
  77. package/dist/lib/tokens.d.ts +2 -19
  78. package/dist/lib/tokens.d.ts.map +1 -1
  79. package/dist/lib/tokens.js +63 -39
  80. package/dist/lib/tokens.js.map +1 -1
  81. package/dist/r/appearance-editor.json +11 -0
  82. package/dist/r/lib.json +15 -0
  83. package/dist/r/registry.json +33 -0
  84. package/dist/r/src/components/composites/action-button/action-button.tsx +2 -1
  85. package/dist/r/src/components/composites/appearance-editor/accent-swatch-groups.tsx +147 -0
  86. package/dist/r/src/components/composites/appearance-editor/appearance-choices.tsx +17 -74
  87. package/dist/r/src/components/composites/appearance-editor/appearance-editor.tsx +19 -6
  88. package/dist/r/src/components/composites/appearance-editor/appearance-types.ts +9 -0
  89. package/dist/r/src/components/composites/appearance-editor/font-settings-group.tsx +350 -0
  90. package/dist/r/src/components/composites/appearance-editor/index.ts +19 -0
  91. package/dist/r/src/components/composites/update-dialog/update-dialog.tsx +2 -2
  92. package/dist/r/src/components/conversation/message-row.tsx +7 -3
  93. package/dist/r/src/components/conversation/paste-token-editor.tsx +1 -1
  94. package/dist/r/src/components/layout/side-rail/side-rail.tsx +3 -3
  95. package/dist/r/src/components/layout/top-bar/top-bar.tsx +1 -1
  96. package/dist/r/src/components/theme/appearance-panel.tsx +34 -83
  97. package/dist/r/src/components/theme/theme-provider.tsx +50 -20
  98. package/dist/r/src/components/ui/code-block/code-block.tsx +6 -6
  99. package/dist/r/src/components/ui/command/command.tsx +1 -1
  100. package/dist/r/src/components/ui/context-menu/context-menu.tsx +1 -1
  101. package/dist/r/src/components/ui/diff-block/diff-block.tsx +7 -7
  102. package/dist/r/src/components/ui/dropdown-menu/dropdown-menu.tsx +145 -7
  103. package/dist/r/src/components/ui/kbd/kbd.tsx +1 -1
  104. package/dist/r/src/components/ui/menubar/menubar.tsx +1 -1
  105. package/dist/r/src/components/ui/typography/typography.tsx +6 -8
  106. package/dist/r/src/lib/appearance-font-settings.ts +233 -0
  107. package/dist/r/src/lib/font-catalog.ts +80 -0
  108. package/dist/r/src/lib/safe-script.ts +7 -0
  109. package/dist/r/src/lib/tokens.ts +64 -59
  110. package/dist/r/src/styles/appearance-font-settings.css +110 -0
  111. package/dist/r/src/styles/fonts.css +4 -5
  112. package/dist/r/src/styles/globals.css +4 -1
  113. package/dist/r/src/styles/theme.css +14 -0
  114. package/dist/r/theme.json +5 -0
  115. package/dist/r/theming.json +2 -0
  116. package/package.json +2 -1
  117. package/registry.json +33 -0
  118. package/src/components/composites/action-button/action-button.tsx +2 -1
  119. package/src/components/composites/appearance-editor/accent-swatch-groups.tsx +147 -0
  120. package/src/components/composites/appearance-editor/appearance-choices.tsx +17 -74
  121. package/src/components/composites/appearance-editor/appearance-editor.tsx +19 -6
  122. package/src/components/composites/appearance-editor/appearance-types.ts +9 -0
  123. package/src/components/composites/appearance-editor/font-settings-group.tsx +350 -0
  124. package/src/components/composites/appearance-editor/index.ts +19 -0
  125. package/src/components/composites/update-dialog/update-dialog.tsx +2 -2
  126. package/src/components/conversation/message-row.tsx +7 -3
  127. package/src/components/conversation/paste-token-editor.tsx +1 -1
  128. package/src/components/layout/side-rail/side-rail.tsx +3 -3
  129. package/src/components/layout/top-bar/top-bar.tsx +1 -1
  130. package/src/components/theme/appearance-panel.tsx +34 -83
  131. package/src/components/theme/theme-provider.tsx +50 -20
  132. package/src/components/ui/code-block/code-block.tsx +6 -6
  133. package/src/components/ui/command/command.tsx +1 -1
  134. package/src/components/ui/context-menu/context-menu.tsx +1 -1
  135. package/src/components/ui/diff-block/diff-block.tsx +7 -7
  136. package/src/components/ui/dropdown-menu/dropdown-menu.tsx +145 -7
  137. package/src/components/ui/kbd/kbd.tsx +1 -1
  138. package/src/components/ui/menubar/menubar.tsx +1 -1
  139. package/src/components/ui/typography/typography.tsx +6 -8
  140. package/src/lib/appearance-font-settings.ts +233 -0
  141. package/src/lib/font-catalog.ts +80 -0
  142. package/src/lib/safe-script.ts +7 -0
  143. package/src/lib/tokens.ts +64 -59
  144. package/src/styles/appearance-font-settings.css +110 -0
  145. package/src/styles/fonts.css +4 -5
  146. package/src/styles/globals.css +4 -1
  147. package/src/styles/theme.css +14 -0
@@ -0,0 +1,350 @@
1
+ import { BookOpen, ChevronDown, Code2, PanelsTopLeft } from 'lucide-solid'
2
+ import { For, createMemo, createSignal, createUniqueId } from 'solid-js'
3
+ import { fontOptions } from '#lib/font-catalog'
4
+ import { Button } from '../../ui/button/button'
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuRadioGroup,
9
+ DropdownMenuRadioItem,
10
+ DropdownMenuSeparator,
11
+ DropdownMenuTrigger,
12
+ } from '../../ui/dropdown-menu/dropdown-menu'
13
+ import { InputControl } from '../../ui/input/input-control'
14
+ import { SettingsRow } from './appearance-rows'
15
+ import {
16
+ APPEARANCE_EDITOR_FONT_SIZE_MAX,
17
+ APPEARANCE_EDITOR_FONT_SIZE_MIN,
18
+ normalizeAppearanceEditorFontSettings,
19
+ normalizeAppearanceEditorFontSize,
20
+ type AppearanceEditorFontAxis,
21
+ type AppearanceEditorFontFamilyId,
22
+ type AppearanceEditorFontSettings,
23
+ } from '#lib/appearance-font-settings'
24
+
25
+ export type AppearanceFontSettingsGroupProps = {
26
+ settings: AppearanceEditorFontSettings
27
+ menuPortalMount?: HTMLElement
28
+ onChange: (settings: AppearanceEditorFontSettings) => void
29
+ disabled?: boolean
30
+ }
31
+
32
+ const FONT_ROWS = [
33
+ {
34
+ axis: 'ui',
35
+ label: 'UI',
36
+ description: 'Controls and interface labels.',
37
+ icon: PanelsTopLeft,
38
+ },
39
+ {
40
+ axis: 'content',
41
+ label: 'Content',
42
+ description: 'Reading and long-form text.',
43
+ icon: BookOpen,
44
+ },
45
+ {
46
+ axis: 'code',
47
+ label: 'Code',
48
+ description: 'Code, terminals, and file paths.',
49
+ icon: Code2,
50
+ },
51
+ ] as const satisfies readonly {
52
+ axis: AppearanceEditorFontAxis
53
+ label: string
54
+ description: string
55
+ icon: typeof PanelsTopLeft
56
+ }[]
57
+
58
+ const catalogFontOptions = fontOptions.filter((option) => option.id !== 'system')
59
+ const MENU_FOCUS_TARGET_SELECTOR =
60
+ 'a[href], area[href], button, input, select, textarea, iframe, [tabindex], [contenteditable], details > summary:first-of-type'
61
+
62
+ function isMenuTabStop(element: HTMLElement) {
63
+ return (
64
+ (element.tabIndex >= 0 ||
65
+ (element.matches('details > summary:first-of-type') && !element.hasAttribute('tabindex'))) &&
66
+ !element.matches(':disabled') &&
67
+ element.getAttribute('aria-disabled') !== 'true' &&
68
+ element.closest('[hidden], [inert], [aria-hidden="true"]') === null &&
69
+ element.getClientRects().length > 0
70
+ )
71
+ }
72
+
73
+ function FontFamilyMenu(props: {
74
+ label: string
75
+ family: AppearanceEditorFontFamilyId
76
+ menuPortalMount?: HTMLElement
77
+ disabled?: boolean
78
+ onSelect: (family: AppearanceEditorFontFamilyId) => void
79
+ }) {
80
+ const [menuOpen, setMenuOpen] = createSignal(false)
81
+ const [restoreTriggerFocus, setRestoreTriggerFocus] = createSignal(false)
82
+ const [triggerElement, setTriggerElement] = createSignal<HTMLButtonElement>()
83
+ let focusTargetAfterTab: HTMLElement | undefined
84
+
85
+ const moveFocusOnTab = (event: KeyboardEvent) => {
86
+ if (event.key !== 'Tab' || event.isComposing) return
87
+
88
+ const trigger = triggerElement()
89
+ const owner = props.menuPortalMount
90
+ const parentDialog = owner?.closest<HTMLElement>('[role="dialog"]')
91
+ const focusScope = parentDialog ?? owner?.ownerDocument.body
92
+ if (!trigger || !parentDialog || !focusScope) return
93
+
94
+ const focusStops = Array.from(focusScope.querySelectorAll<HTMLElement>('*')).filter(
95
+ (element) =>
96
+ isMenuTabStop(element) &&
97
+ element.closest('[role="menu"], [hidden], [inert], [aria-hidden="true"]') === null
98
+ )
99
+ const orderedFocusStops = [
100
+ ...focusStops
101
+ .filter((element) => element.tabIndex > 0)
102
+ .toSorted((a, b) => a.tabIndex - b.tabIndex),
103
+ ...focusStops.filter((element) => element.tabIndex <= 0),
104
+ ]
105
+ const triggerIndex = orderedFocusStops.indexOf(trigger)
106
+ if (triggerIndex < 0) return
107
+ const direction = event.shiftKey ? -1 : 1
108
+ const adjacent =
109
+ orderedFocusStops[triggerIndex + direction] ??
110
+ (direction > 0 ? orderedFocusStops[0] : orderedFocusStops.at(-1))
111
+
112
+ event.stopPropagation()
113
+ setRestoreTriggerFocus(false)
114
+ if (adjacent) {
115
+ event.preventDefault()
116
+ focusTargetAfterTab = adjacent
117
+ setMenuOpen(false)
118
+ adjacent.focus({ preventScroll: true })
119
+ } else {
120
+ focusTargetAfterTab = undefined
121
+ setMenuOpen(false)
122
+ }
123
+ }
124
+ const selectedLabel = () =>
125
+ fontOptions.find((option) => option.id === props.family)?.label ??
126
+ fontOptions.find((option) => option.id === 'system')?.label ??
127
+ 'System'
128
+
129
+ return (
130
+ <DropdownMenu
131
+ modal={false}
132
+ open={menuOpen()}
133
+ onOpenChange={(open) => {
134
+ setMenuOpen(open)
135
+ if (open) setRestoreTriggerFocus(false)
136
+ }}
137
+ >
138
+ <DropdownMenuTrigger
139
+ ref={setTriggerElement}
140
+ as={Button}
141
+ variant="outline"
142
+ size="sm"
143
+ aria-label={`${props.label} font family`}
144
+ disabled={props.disabled}
145
+ class="w-40 max-w-full justify-between"
146
+ >
147
+ <span class="truncate">{selectedLabel()}</span>
148
+ <ChevronDown aria-hidden="true" class="ms-2 size-4 shrink-0 text-muted-foreground" />
149
+ </DropdownMenuTrigger>
150
+ <DropdownMenuContent
151
+ hideArrow
152
+ portalMount={props.menuPortalMount}
153
+ class="min-w-48"
154
+ onFocusOutside={(event) => {
155
+ // WebKit may briefly focus the owning Popover root while the menu's
156
+ // focus scope starts. Keep the menu open for that transient target;
157
+ // genuine focus moves to another control still dismiss it.
158
+ if (
159
+ !event.defaultPrevented &&
160
+ props.menuPortalMount &&
161
+ event.detail.originalEvent.target === props.menuPortalMount
162
+ ) {
163
+ setRestoreTriggerFocus(true)
164
+ event.preventDefault()
165
+ } else if (
166
+ event.detail.originalEvent.target instanceof HTMLElement &&
167
+ props.menuPortalMount?.contains(event.detail.originalEvent.target)
168
+ ) {
169
+ setRestoreTriggerFocus(false)
170
+ }
171
+ }}
172
+ onInteractOutside={(event) => {
173
+ if (event.detail.originalEvent.type !== 'pointerdown') return
174
+ const owner = props.menuPortalMount
175
+ const target = event.detail.originalEvent.target
176
+ if (!(target instanceof Element) || !owner?.contains(target)) return
177
+
178
+ const focusTarget = target.closest<HTMLElement>(MENU_FOCUS_TARGET_SELECTOR)
179
+ setRestoreTriggerFocus(!focusTarget || !isMenuTabStop(focusTarget))
180
+ }}
181
+ onKeyDown={moveFocusOnTab}
182
+ onEscapeKeyDown={() => setRestoreTriggerFocus(true)}
183
+ onCloseAutoFocus={(event) => {
184
+ if (menuOpen()) {
185
+ event.preventDefault()
186
+ return
187
+ }
188
+ if (focusTargetAfterTab) {
189
+ const focusTarget = focusTargetAfterTab
190
+ focusTargetAfterTab = undefined
191
+ event.preventDefault()
192
+ queueMicrotask(() => {
193
+ if (!menuOpen() && focusTarget.isConnected) {
194
+ focusTarget.focus({ preventScroll: true })
195
+ }
196
+ })
197
+ return
198
+ }
199
+ if (!restoreTriggerFocus()) return
200
+
201
+ event.preventDefault()
202
+ setRestoreTriggerFocus(false)
203
+ const trigger = triggerElement()
204
+ if (
205
+ trigger?.isConnected &&
206
+ !trigger.disabled &&
207
+ trigger.closest('[hidden], [inert], [aria-hidden="true"]') === null &&
208
+ trigger.getClientRects().length > 0
209
+ ) {
210
+ trigger.focus({ preventScroll: true })
211
+ }
212
+ }}
213
+ >
214
+ <DropdownMenuRadioGroup
215
+ value={props.family}
216
+ aria-label={`${props.label} system font`}
217
+ onChange={(family) => {
218
+ if (family === 'system') props.onSelect('system')
219
+ }}
220
+ >
221
+ <DropdownMenuRadioItem value="system" closeOnSelect>
222
+ System
223
+ </DropdownMenuRadioItem>
224
+ </DropdownMenuRadioGroup>
225
+ <DropdownMenuSeparator />
226
+ <DropdownMenuRadioGroup
227
+ value={props.family}
228
+ aria-label={`${props.label} available fonts`}
229
+ onChange={(family) => {
230
+ const option = fontOptions.find((candidate) => candidate.id === family)
231
+ if (option && option.id !== 'system') props.onSelect(option.id)
232
+ }}
233
+ >
234
+ <For each={catalogFontOptions}>
235
+ {(option) => (
236
+ <DropdownMenuRadioItem value={option.id} closeOnSelect>
237
+ {option.label}
238
+ </DropdownMenuRadioItem>
239
+ )}
240
+ </For>
241
+ </DropdownMenuRadioGroup>
242
+ </DropdownMenuContent>
243
+ </DropdownMenu>
244
+ )
245
+ }
246
+
247
+ /** Controlled font controls shared by AppearanceEditor and host-owned settings. */
248
+ export function AppearanceFontSettingsGroup(props: AppearanceFontSettingsGroupProps) {
249
+ const [sizeDrafts, setSizeDrafts] = createSignal<
250
+ Partial<Record<AppearanceEditorFontAxis, string>>
251
+ >({})
252
+ const sizeHintPrefix = createUniqueId()
253
+ const settings = createMemo(() => normalizeAppearanceEditorFontSettings(props.settings).settings)
254
+
255
+ const updateAxis = (
256
+ axis: AppearanceEditorFontAxis,
257
+ patch: { family?: AppearanceEditorFontFamilyId; size?: number }
258
+ ) => {
259
+ const current = settings()
260
+ props.onChange({
261
+ ...current,
262
+ [axis]: {
263
+ ...current[axis],
264
+ ...patch,
265
+ },
266
+ })
267
+ }
268
+
269
+ const commitSize = (axis: AppearanceEditorFontAxis) => {
270
+ const input = sizeDrafts()[axis]
271
+ if (input === undefined) return
272
+ const size = normalizeAppearanceEditorFontSize(input, settings()[axis].size)
273
+ setSizeDrafts((current) => {
274
+ const next = { ...current }
275
+ delete next[axis]
276
+ return next
277
+ })
278
+ if (size !== settings()[axis].size) updateAxis(axis, { size })
279
+ }
280
+
281
+ return (
282
+ <div class="global-appearance-font-settings">
283
+ <For each={FONT_ROWS}>
284
+ {(row) => {
285
+ const Icon = row.icon
286
+ const value = () => settings()[row.axis]
287
+ const inputValue = () => sizeDrafts()[row.axis] ?? String(value().size)
288
+ const sizeHintId = `${sizeHintPrefix}-${row.axis}-font-size-help`
289
+ const sizeIsInvalid = () => {
290
+ const draft = sizeDrafts()[row.axis]
291
+ if (draft === undefined || draft.trim() === '') return false
292
+ const parsed = Number(draft)
293
+ return (
294
+ !Number.isFinite(parsed) ||
295
+ !Number.isInteger(parsed) ||
296
+ parsed < APPEARANCE_EDITOR_FONT_SIZE_MIN ||
297
+ parsed > APPEARANCE_EDITOR_FONT_SIZE_MAX
298
+ )
299
+ }
300
+ return (
301
+ <SettingsRow title={row.label} icon={<Icon />} description={row.description}>
302
+ <div class="flex min-w-0 max-w-full flex-wrap items-center gap-2">
303
+ <FontFamilyMenu
304
+ label={row.label}
305
+ family={value().family}
306
+ menuPortalMount={props.menuPortalMount}
307
+ disabled={props.disabled}
308
+ onSelect={(family) => updateAxis(row.axis, { family })}
309
+ />
310
+ <div class="flex shrink-0 items-center gap-1">
311
+ <InputControl
312
+ type="number"
313
+ min={APPEARANCE_EDITOR_FONT_SIZE_MIN}
314
+ max={APPEARANCE_EDITOR_FONT_SIZE_MAX}
315
+ step={1}
316
+ value={inputValue()}
317
+ disabled={props.disabled}
318
+ aria-label={`${row.label} font size in pixels`}
319
+ aria-describedby={sizeHintId}
320
+ aria-invalid={sizeIsInvalid()}
321
+ class="w-16"
322
+ onInput={(event) =>
323
+ setSizeDrafts((current) => ({
324
+ ...current,
325
+ [row.axis]: event.currentTarget.value,
326
+ }))
327
+ }
328
+ onBlur={() => commitSize(row.axis)}
329
+ onKeyDown={(event) => {
330
+ if (event.key !== 'Enter') return
331
+ event.preventDefault()
332
+ commitSize(row.axis)
333
+ }}
334
+ />
335
+ <span aria-hidden="true" class="text-xs text-muted-foreground">
336
+ px
337
+ </span>
338
+ </div>
339
+ <span id={sizeHintId} class="sr-only">
340
+ Enter a whole number from {APPEARANCE_EDITOR_FONT_SIZE_MIN} to{' '}
341
+ {APPEARANCE_EDITOR_FONT_SIZE_MAX} pixels.
342
+ </span>
343
+ </div>
344
+ </SettingsRow>
345
+ )
346
+ }}
347
+ </For>
348
+ </div>
349
+ )
350
+ }
@@ -1,4 +1,23 @@
1
1
  export { AppearanceEditor, AppearancePopover } from './appearance-editor'
2
+ export {
3
+ AppearanceFontSettingsGroup,
4
+ type AppearanceFontSettingsGroupProps,
5
+ } from './font-settings-group'
6
+ export {
7
+ APPEARANCE_EDITOR_FONT_AXES,
8
+ APPEARANCE_EDITOR_FONT_SIZE_MAX,
9
+ APPEARANCE_EDITOR_FONT_SIZE_MIN,
10
+ DEFAULT_APPEARANCE_EDITOR_FONT_SETTINGS,
11
+ applyAppearanceFontSettings,
12
+ fontSettingsBootstrapScript,
13
+ normalizeAppearanceEditorFontSettings,
14
+ normalizeAppearanceEditorFontSize,
15
+ type AppearanceEditorFontAxis,
16
+ type AppearanceEditorFontFamilyId,
17
+ type AppearanceEditorFontSetting,
18
+ type AppearanceEditorFontSettings,
19
+ type NormalizedAppearanceEditorFontSettings,
20
+ } from '#lib/appearance-font-settings'
2
21
  export type {
3
22
  AppearanceDraft,
4
23
  AppearanceEditorProps,
@@ -857,7 +857,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
857
857
  stop, named by the heading above it, or a keyboard user cannot
858
858
  scroll it (`scrollable-region-focusable`). */}
859
859
  <div
860
- class="max-h-52 overflow-y-auto rounded-xl border border-border bg-background/45 p-4 font-mono text-xs leading-5 whitespace-pre-wrap text-muted-foreground"
860
+ class="max-h-52 overflow-y-auto rounded-xl border border-border bg-background/45 p-4 font-code text-code leading-5 whitespace-pre-wrap text-muted-foreground"
861
861
  role="region"
862
862
  aria-labelledby="update-release-notes"
863
863
  tabindex="0"
@@ -886,7 +886,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
886
886
  stop, named by the heading above it, or a keyboard user cannot
887
887
  scroll it (`scrollable-region-focusable`). */}
888
888
  <div
889
- class="max-h-72 overflow-y-auto rounded-xl border border-border bg-background/45 p-4 font-mono text-xs leading-5 whitespace-pre-wrap text-muted-foreground"
889
+ class="max-h-72 overflow-y-auto rounded-xl border border-border bg-background/45 p-4 font-code text-code leading-5 whitespace-pre-wrap text-muted-foreground"
890
890
  role="region"
891
891
  aria-labelledby="update-changelog"
892
892
  tabindex="0"
@@ -141,10 +141,14 @@ export function MessageRow(props: MessageRowProps) {
141
141
  that repeats every speaker's name in every bubble is one nobody reads. */}
142
142
  <span class="visually-hidden">{local.senderName}</span>
143
143
 
144
- <div data-slot="message-body" class="text-sm">
144
+ <div data-slot="message-body" class="font-content text-content">
145
145
  <Show
146
146
  when={!local.deleted}
147
- fallback={<p class="text-muted-foreground italic">Message deleted</p>}
147
+ fallback={
148
+ <p class="font-content text-content text-muted-foreground italic">
149
+ Message deleted
150
+ </p>
151
+ }
148
152
  >
149
153
  {local.children}
150
154
  </Show>
@@ -376,7 +380,7 @@ function Prose(props: { text: string }) {
376
380
  const parts = () => props.text.split(/(`[^`\n]+`)/g).filter(Boolean)
377
381
 
378
382
  return (
379
- <p class="whitespace-pre-wrap">
383
+ <p class="font-content text-content whitespace-pre-wrap">
380
384
  <For each={parts()}>
381
385
  {(part) => (
382
386
  <Show
@@ -668,7 +668,7 @@ export function PasteTokenEditor(props: PasteTokenEditorProps) {
668
668
  data-slot="paste-token-preview"
669
669
  class="bg-scrim text-scrim-foreground z-(--z-tooltip) w-fit max-w-64 rounded-md px-2 py-1 text-xs origin-(--kb-tooltip-content-transform-origin) text-balance data-expanded:animate-in data-expanded:fade-in-0 data-expanded:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-expanded:duration-150 data-closed:duration-100"
670
670
  >
671
- <pre class="m-0 max-h-48 max-w-64 overflow-y-auto whitespace-pre-wrap break-words font-mono">
671
+ <pre class="m-0 max-h-48 max-w-64 overflow-y-auto whitespace-pre-wrap break-words font-code text-code">
672
672
  {summary().text}
673
673
  </pre>
674
674
  <Show when={summary().hiddenLines > 0}>
@@ -301,7 +301,7 @@ export function SideRailItem<T extends ValidComponent = 'a'>(props: SideRailItem
301
301
  <Show when={local.shortcut && local.trailing === undefined}>
302
302
  <span
303
303
  aria-hidden="true"
304
- class="text-sidebar-muted-foreground shrink-0 font-mono text-2xs leading-none opacity-0 transition-opacity group-hover/rail-item:opacity-100 group-focus-visible/rail-item:opacity-100 group-data-[collapsed=true]/rail:hidden"
304
+ class="text-sidebar-muted-foreground shrink-0 font-code text-code leading-none opacity-0 transition-opacity group-hover/rail-item:opacity-100 group-focus-visible/rail-item:opacity-100 group-data-[collapsed=true]/rail:hidden"
305
305
  >
306
306
  {local.shortcut}
307
307
  </span>
@@ -337,7 +337,7 @@ export function SideRailItem<T extends ValidComponent = 'a'>(props: SideRailItem
337
337
  </span>
338
338
  {local.label}
339
339
  <Show when={local.shortcut}>
340
- <span class="text-muted-foreground shrink-0 font-mono text-2xs leading-none">
340
+ <span class="text-muted-foreground shrink-0 font-code text-code leading-none">
341
341
  {local.shortcut}
342
342
  </span>
343
343
  </Show>
@@ -399,7 +399,7 @@ export function SideRailButton(
399
399
  <Show when={local.shortcut}>
400
400
  <span
401
401
  aria-hidden="true"
402
- class="text-sidebar-muted-foreground shrink-0 font-mono text-2xs leading-none opacity-0 transition-opacity group-hover/rail-item:opacity-100 group-focus-visible/rail-item:opacity-100 group-data-[collapsed=true]/rail:hidden"
402
+ class="text-sidebar-muted-foreground shrink-0 font-code text-code leading-none opacity-0 transition-opacity group-hover/rail-item:opacity-100 group-focus-visible/rail-item:opacity-100 group-data-[collapsed=true]/rail:hidden"
403
403
  >
404
404
  {local.shortcut}
405
405
  </span>
@@ -166,7 +166,7 @@ export function TopBarSearch(props: TopBarSearchProps) {
166
166
  <Show when={local.shortcut}>
167
167
  <kbd
168
168
  aria-hidden="true"
169
- class="bg-muted ms-auto hidden h-5 shrink-0 items-center rounded-sm border border-border px-1.5 font-mono text-2xs sm:flex"
169
+ class="bg-muted ms-auto hidden h-5 shrink-0 items-center rounded-sm border border-border px-1.5 font-code text-code sm:flex"
170
170
  >
171
171
  {local.shortcut}
172
172
  </kbd>
@@ -1,9 +1,12 @@
1
1
  import type { ComponentProps } from 'solid-js'
2
- import { For, Show, splitProps } from 'solid-js'
3
- import { accentPresets, fontOptions } from '#lib/tokens'
2
+ import { Show, splitProps } from 'solid-js'
3
+ import { RadioGroup as Radio } from '@kobalte/core/radio-group'
4
+ import { accentPresets } from '#lib/tokens'
4
5
  import { themesForAppearance } from '#lib/themes'
6
+ import { DEFAULT_APPEARANCE_EDITOR_FONT_SETTINGS } from '#lib/appearance-font-settings'
5
7
  import { cn } from '#lib/utils'
6
- import { Button } from '../ui/button/button'
8
+ import { AccentSwatchGroups } from '../composites/appearance-editor/accent-swatch-groups'
9
+ import { AppearanceFontSettingsGroup } from '../composites/appearance-editor/font-settings-group'
7
10
  import { RadioGroup, RadioGroupItem } from '../ui/radio-group/radio-group'
8
11
  import { ThemePicker } from './theme-picker'
9
12
  import { useTheme } from './theme-provider'
@@ -26,7 +29,7 @@ import { Heading } from '../ui/typography'
26
29
  * setting that keeps changing.
27
30
  * 3. **Accent** — adea's presets, then the accents the selected theme pair
28
31
  * carries itself, layered on whichever theme is active.
29
- * 4. **Font** — the interface face.
32
+ * 4. **Fonts** — separate families and sizes for interface, content and code.
30
33
  *
31
34
  * The panel reads and writes the provider's preference and keeps no state of its
32
35
  * own, so it cannot disagree with what is applied.
@@ -39,6 +42,11 @@ const DARK_THEMES = themesForAppearance('dark')
39
42
  export function AppearancePanel(props: AppearancePanelProps) {
40
43
  const [local, rest] = splitProps(props, ['class'])
41
44
  const { selection, setSelection, resolvedAppearance, themeAccents } = useTheme()
45
+ const selectedAccent = () =>
46
+ accentPresets.some((option) => option.id === selection().accent) ||
47
+ themeAccents().some((option) => option.id === selection().accent)
48
+ ? selection().accent
49
+ : 'theme'
42
50
 
43
51
  return (
44
52
  <div data-slot="appearance-panel" class={cn('flex flex-col gap-8', local.class)} {...rest}>
@@ -101,99 +109,42 @@ export function AppearancePanel(props: AppearancePanelProps) {
101
109
  unreadable.
102
110
  </p>
103
111
  </header>
104
- <div role="radiogroup" aria-label="Accent" class="flex flex-wrap gap-2">
105
- <For each={accentPresets}>
106
- {(preset) => (
107
- <AccentOption
108
- id={preset.id}
109
- label={preset.label}
110
- checked={selection().accent === preset.id}
111
- swatch={
112
- preset.id === 'theme'
113
- ? 'var(--primary)'
114
- : `var(--accent-${preset.id}, ${preset.dark})`
115
- }
116
- onSelect={() => setSelection({ accent: preset.id })}
117
- />
118
- )}
119
- </For>
120
- {/* The theme's own accents follow the presets. Their values are the
121
- theme pair's, so the swatch shows the side that is active now. */}
122
- <For each={themeAccents()}>
123
- {(accent) => (
124
- <AccentOption
125
- id={accent.id}
126
- label={`Theme ${accent.label.toLowerCase()}`}
127
- checked={selection().accent === accent.id}
128
- swatch={resolvedAppearance() === 'dark' ? accent.dark : accent.light}
129
- onSelect={() => setSelection({ accent: accent.id })}
130
- />
131
- )}
132
- </For>
133
- </div>
112
+ <Radio
113
+ value={selectedAccent()}
114
+ onChange={(accent) => setSelection({ accent })}
115
+ orientation="horizontal"
116
+ aria-label="Accent"
117
+ class="flex flex-col items-start gap-3"
118
+ >
119
+ <AccentSwatchGroups
120
+ accentOptions={accentPresets}
121
+ themeAccentOptions={themeAccents()}
122
+ resolvedAppearance={resolvedAppearance()}
123
+ themeDefaultColor="var(--primary)"
124
+ />
125
+ </Radio>
134
126
  </section>
135
127
 
136
- {/* --- 4. Font -------------------------------------------------------- */}
128
+ {/* --- 4. Fonts ------------------------------------------------------- */}
137
129
  <section class="flex flex-col gap-3">
138
130
  <header class="flex flex-col gap-0.5">
139
131
  <Heading as="h2" size="card">
140
- Typeface
132
+ Fonts
141
133
  </Heading>
142
134
  <p class="max-w-prose text-sm text-muted-foreground">
143
- The interface face. Every option is self-hosted, so nothing is fetched at runtime and
144
- the choice survives going offline.
135
+ Choose separate families and sizes for the interface, reading, and code. Font families
136
+ come from the shared catalogue and are self-hosted.
145
137
  </p>
146
138
  </header>
147
- <RadioGroup
148
- value={selection().font}
149
- onChange={(value) => setSelection({ font: value as string })}
150
- aria-label="Typeface"
151
- >
152
- <For each={fontOptions}>
153
- {(option) => (
154
- <RadioGroupItem
155
- value={option.id}
156
- label={option.label}
157
- description={option.description}
158
- />
159
- )}
160
- </For>
161
- </RadioGroup>
139
+ <AppearanceFontSettingsGroup
140
+ settings={selection().fonts ?? DEFAULT_APPEARANCE_EDITOR_FONT_SETTINGS}
141
+ onChange={(fonts) => setSelection({ fonts })}
142
+ />
162
143
  </section>
163
144
  </div>
164
145
  )
165
146
  }
166
147
 
167
- function AccentOption(props: {
168
- id: string
169
- label: string
170
- checked: boolean
171
- swatch: string
172
- onSelect: () => void
173
- }) {
174
- return (
175
- <Button
176
- as="button"
177
- type="button"
178
- variant="outline"
179
- size="sm"
180
- role="radio"
181
- data-accent-option={props.id}
182
- aria-checked={props.checked}
183
- aria-label={props.label}
184
- class={cn({ 'border-ring ring-3 ring-primary-subtle': props.checked })}
185
- onClick={() => props.onSelect()}
186
- >
187
- <span
188
- aria-hidden="true"
189
- class="size-3 rounded-full border border-border"
190
- style={{ 'background-color': props.swatch }}
191
- />
192
- {props.label}
193
- </Button>
194
- )
195
- }
196
-
197
148
  function ThemeSection(props: {
198
149
  title: string
199
150
  description: string