@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
@@ -32,10 +32,12 @@
32
32
  * custom accent/reduced-transparency are accepted Adea divergences (#425).
33
33
  */
34
34
  import { EyeOff, Palette, PanelsTopLeft, SlidersHorizontal } from 'lucide-solid'
35
- import { Show, createUniqueId } from 'solid-js'
35
+ import { Show, createSignal, createUniqueId } from 'solid-js'
36
36
  import { cn } from '../../../lib/utils'
37
37
  import { Button } from '../../ui/button/button'
38
38
  import { Input } from '../../ui/input/input'
39
+ import { AppearanceFontSettingsGroup } from './font-settings-group'
40
+ import { DEFAULT_APPEARANCE_EDITOR_FONT_SETTINGS } from '../../../lib/appearance-font-settings'
39
41
 
40
42
  import { Switch } from '../../ui/switch/switch'
41
43
  import {
@@ -54,8 +56,8 @@ import { SettingsRow, TerminalRow, ThemeRow } from './appearance-rows'
54
56
  * No ThemeProvider is required, and no catalogue, renderer engine or storage is
55
57
  * imported. Hosts normalize unknown IDs and custom colors, report recovery,
56
58
  * snapshot on open, preview on changes, commit on Save, and restore on dismissal.
57
- * Both theme rows remain mounted when mode changes. Typeface/density axes are
58
- * deliberately absent from Adea #425's accepted surface.
59
+ * Both theme rows remain mounted when mode changes. Density remains outside the
60
+ * accepted appearance surface; typeface settings are shared and host-persisted.
59
61
  */
60
62
  export function AppearanceEditor(props: AppearanceEditorProps) {
61
63
  const errorId = createUniqueId()
@@ -140,6 +142,12 @@ export function AppearanceEditor(props: AppearanceEditorProps) {
140
142
  </Show>
141
143
  </div>
142
144
  </Show>
145
+ <AppearanceFontSettingsGroup
146
+ settings={props.draft.fonts ?? DEFAULT_APPEARANCE_EDITOR_FONT_SETTINGS}
147
+ menuPortalMount={props.menuPortalMount}
148
+ disabled={props.saving}
149
+ onChange={(fonts) => props.onChange({ fonts })}
150
+ />
143
151
  <SettingsRow title="Glass" icon={<PanelsTopLeft />} description={glassDescription()}>
144
152
  <GlassChoices {...props} />
145
153
  </SettingsRow>
@@ -188,7 +196,7 @@ export function AppearanceEditor(props: AppearanceEditorProps) {
188
196
  {/* Sticky so Save/Cancel stay reachable in a tall scroll container: the
189
197
  editor scrolls with its host panel, and actions parked at the bottom
190
198
  of a long form were unreachable without scrolling past every row. */}
191
- <div class="sticky bottom-0 z-10 mt-2 flex flex-wrap items-center gap-2 border-t bg-background px-4 py-3">
199
+ <div class="global-appearance-editor-actions sticky bottom-0 z-10 mt-2 flex flex-wrap items-center gap-2 border-t bg-background px-4 py-3">
192
200
  <Button
193
201
  type="button"
194
202
  size="sm"
@@ -228,6 +236,8 @@ export function AppearanceEditor(props: AppearanceEditorProps) {
228
236
  * Its measured available height also bounds scrolling, keeping footer actions
229
237
  * reachable when a narrow popup begins below its trigger. */
230
238
  export function AppearancePopover(props: AppearancePopoverProps) {
239
+ const [menuPortalMount, setMenuPortalMount] = createSignal<HTMLElement>()
240
+
231
241
  return (
232
242
  <Popover
233
243
  open={props.open}
@@ -243,12 +253,15 @@ export function AppearancePopover(props: AppearancePopoverProps) {
243
253
  >
244
254
  <Palette />
245
255
  </PopoverTrigger>
246
- <PopoverContent class="w-lg max-w-[calc(100vw-2rem)] max-h-(--kb-popper-content-available-height) overflow-y-auto">
256
+ <PopoverContent
257
+ ref={setMenuPortalMount}
258
+ class="w-lg max-w-[calc(100vw-2rem)] max-h-(--kb-popper-content-available-height) overflow-y-auto"
259
+ >
247
260
  <PopoverTitle>Appearance</PopoverTitle>
248
261
  <PopoverDescription>
249
262
  Changes preview immediately. Save keeps them; Cancel restores the previous appearance.
250
263
  </PopoverDescription>
251
- <AppearanceEditor {...props} />
264
+ <AppearanceEditor {...props} menuPortalMount={menuPortalMount()} />
252
265
  </PopoverContent>
253
266
  </Popover>
254
267
  )
@@ -1,4 +1,5 @@
1
1
  import type { AccentPreset, AdeaTheme, AdeaThemeRecord } from '@adea-ai/themes'
2
+ import type { AppearanceEditorFontSettings } from '../../../lib/appearance-font-settings'
2
3
 
3
4
  export type AppearanceDraft = Readonly<{
4
5
  mode: 'system' | 'light' | 'dark'
@@ -13,6 +14,12 @@ export type AppearanceDraft = Readonly<{
13
14
  terminalThemeId?: string
14
15
  /** Theme default, a canonical preset id, or the host's custom accent draft. */
15
16
  accent: string
17
+ /**
18
+ * The UI, content and code typeface axes. Optional for compatibility with
19
+ * hosts upgrading from the earlier appearance contract; omitted means the
20
+ * shared System defaults.
21
+ */
22
+ fonts?: AppearanceEditorFontSettings
16
23
  surface: 'theme' | 'frosted' | 'opaque'
17
24
  reduceTransparency: boolean
18
25
  }>
@@ -45,6 +52,8 @@ export type AppearanceEditorProps = {
45
52
  surfaceCapability: { frosted: boolean; reason?: string; themeDefaultDescription?: string }
46
53
  /** Undefined means theme import is unavailable, never a working-looking action. */
47
54
  onManageThemes?: () => void
55
+ /** Mount nested font menus inside the containing overlay when one exists. */
56
+ menuPortalMount?: HTMLElement
48
57
  class?: string
49
58
  }
50
59
 
@@ -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>