@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,233 @@
1
+ import { fontOptions, type FontOption } from '#lib/font-catalog'
2
+ import { safeScriptStringLiteral } from '#lib/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
+ }
package/src/lib/tokens.ts CHANGED
@@ -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,
@@ -0,0 +1,110 @@
1
+ /* Optional typography roles and Appearance font controls.
2
+ *
3
+ * Import after `theme.css` and `base.css` when the host uses the shared font
4
+ * preference projection. This file defines selectors and utilities only; it
5
+ * never imports font assets. `fonts.css` remains a separate opt-in for bundled
6
+ * typefaces.
7
+ */
8
+
9
+ /* The marker scopes role defaults and wins over theme.css root tokens even
10
+ when a bundler hoists this optional sheet before shared globals. Family
11
+ values come from the validated shared runtime/bootstrap projection. */
12
+ :root[data-font-settings] {
13
+ --font-ui: var(--font-family-system);
14
+ --font-content: var(--font-family-system);
15
+ --font-code: var(--font-family-system-mono);
16
+ --font-sans: var(--font-ui);
17
+ --font-mono: var(--font-code);
18
+ --font-ui-scale: 1;
19
+ --font-content-scale: 1;
20
+ --font-code-scale: 1;
21
+
22
+ --text-2xs: calc(0.6875rem * var(--font-ui-scale));
23
+ --text-xs: calc(0.75rem * var(--font-ui-scale));
24
+ --text-sm: calc(0.875rem * var(--font-ui-scale));
25
+ --text-base: calc(1rem * var(--font-ui-scale));
26
+ --text-lg: calc(1.125rem * var(--font-ui-scale));
27
+ --text-xl: calc(1.25rem * var(--font-ui-scale));
28
+ --text-2xl: calc(1.5rem * var(--font-ui-scale));
29
+ --text-3xl: calc(1.875rem * var(--font-ui-scale));
30
+ --text-content: calc(0.875rem * var(--font-content-scale));
31
+ --text-content--line-height: var(--text-sm--line-height);
32
+ --text-code: calc(0.75rem * var(--font-code-scale));
33
+ --text-code--line-height: var(--text-xs--line-height);
34
+ }
35
+
36
+ @layer base {
37
+ code,
38
+ kbd,
39
+ pre,
40
+ samp {
41
+ font-family: var(--font-code);
42
+ font-size: var(--text-code);
43
+ }
44
+
45
+ /* Appearance's own controls grow with the UI preference while ordinary
46
+ controls retain their established density. */
47
+ .global-appearance-font-settings button,
48
+ .global-appearance-editor-actions button {
49
+ min-height: max(var(--control-height-sm), calc(1lh + 2px));
50
+ }
51
+
52
+ /* Text controls keep their density rung but gain room for their projected
53
+ line box. Icon-only controls use size-control-* and keep fixed targets. */
54
+ [data-font-settings] .h-control-2xs {
55
+ --font-control-height: var(--control-height-2xs);
56
+ }
57
+ [data-font-settings] .h-control-xs {
58
+ --font-control-height: var(--control-height-xs);
59
+ }
60
+ [data-font-settings] .h-control-sm {
61
+ --font-control-height: var(--control-height-sm);
62
+ }
63
+ [data-font-settings] .h-control-md {
64
+ --font-control-height: var(--control-height-md);
65
+ }
66
+ [data-font-settings] .h-control-lg {
67
+ --font-control-height: var(--control-height-lg);
68
+ }
69
+ [data-font-settings] .h-control-xl {
70
+ --font-control-height: var(--control-height-xl);
71
+ }
72
+ [data-font-settings] .h-control-2xl {
73
+ --font-control-height: var(--control-height-2xl);
74
+ }
75
+ [data-font-settings]
76
+ :is(
77
+ .h-control-2xs,
78
+ .h-control-xs,
79
+ .h-control-sm,
80
+ .h-control-md,
81
+ .h-control-lg,
82
+ .h-control-xl,
83
+ .h-control-2xl
84
+ ) {
85
+ min-height: max(var(--font-control-height), calc(1lh + 2px));
86
+ }
87
+
88
+ [data-font-settings]
89
+ input:is(
90
+ .h-control-2xs,
91
+ .h-control-xs,
92
+ .h-control-sm,
93
+ .h-control-md,
94
+ .h-control-lg,
95
+ .h-control-xl,
96
+ .h-control-2xl
97
+ ) {
98
+ min-height: max(var(--font-control-height), calc(1lh + var(--spacing) + var(--spacing) + 2px));
99
+ }
100
+
101
+ .global-appearance-font-settings input[type='number'] {
102
+ min-height: max(var(--control-height-md), calc(1lh + var(--spacing) + var(--spacing) + 3px));
103
+ }
104
+ }
105
+
106
+ /* Keep the numeric font-size value visible beside the native number stepper
107
+ as the UI font grows; unlayered so this minimum wins over min-w-0. */
108
+ .global-appearance-font-settings input[type='number'] {
109
+ min-width: calc(3ch + var(--control-padding-md) + var(--control-padding-md) + 1.25rem + 2px);
110
+ }
@@ -4,15 +4,14 @@
4
4
  * a desktop app must not fetch a font at runtime, or the first paint after a cold
5
5
  * start shows a fallback face and then reflows.
6
6
  *
7
- * Space Grotesk the interface default, and the face the visual language was
8
- * drawn against
7
+ * Space Grotesk a self-hosted proportional family
9
8
  * JetBrains Mono code, terminals, ids, paths
10
9
  * Geist an alternative interface face; cortana already ships it
11
10
  * Geist Mono its monospace counterpart
12
11
  *
13
- * The family is a *selection*, not a constant: `theme.css` exposes a `data-font`
14
- * axis, so a user can pick a face the way they pick an accent, and the system
15
- * default (`system`) is always available for someone who wants their OS's own.
12
+ * These families feed the shared UI, content and code axes. Their stable ids are
13
+ * projected by `@adea-ai/ui/lib/appearance-font-settings`; System is the default
14
+ * and needs no font asset.
16
15
  *
17
16
  * Consumers import this after the theme:
18
17
  *
@@ -5,10 +5,13 @@
5
5
  * @shadcn/lint discover, so it must stay the file that imports Tailwind.
6
6
  *
7
7
  * @import '@adea-ai/ui/globals.css';
8
+ * @import '@adea-ai/ui/appearance-font-settings.css'; <- optional role preferences
8
9
  * @import '@adea-ai/ui/fonts.css'; <- optional, self-hosted typefaces
9
10
  *
10
11
  * A consumer that already imports Tailwind itself should import `theme.css`
11
- * and `base.css` directly instead, and must not import this file.
12
+ * and `base.css` directly instead, and must not import this file. The font role
13
+ * stylesheet stays opt-in so applications that use only the base theme keep
14
+ * the existing CSS footprint.
12
15
  *
13
16
  * Tokens live in `theme.css` only. This file must never duplicate them.
14
17
  */
@@ -525,6 +525,14 @@
525
525
  --font-family-geist-mono: 'Geist Mono Variable', 'Geist Mono', monospace;
526
526
  --font-family-system: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
527
527
  --font-family-system-mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
528
+ /* Lightweight role defaults keep conversation and code components useful for
529
+ consumers that have not opted into independent Appearance font settings. */
530
+ --font-content: var(--font-sans);
531
+ --font-code: var(--font-mono);
532
+ --text-content: var(--text-sm);
533
+ --text-content--line-height: var(--text-sm--line-height);
534
+ --text-code: 0.75rem;
535
+ --text-code--line-height: 1.5;
528
536
 
529
537
  /* Width compensation for the mono UI fonts. `normal` on the proportional face,
530
538
  tightened by the `[data-font]` blocks above when the UI font is monospace. */
@@ -776,6 +784,8 @@
776
784
 
777
785
  --font-sans: var(--font-sans);
778
786
  --font-mono: var(--font-mono);
787
+ --font-content: var(--font-content);
788
+ --font-code: var(--font-code);
779
789
  --font-family-space-grotesk: var(--font-family-space-grotesk);
780
790
  --font-family-jetbrains-mono: var(--font-family-jetbrains-mono);
781
791
  --font-family-geist: var(--font-family-geist);
@@ -799,6 +809,10 @@
799
809
  --text-2xl--line-height: var(--text-2xl--line-height);
800
810
  --text-3xl: var(--text-3xl);
801
811
  --text-3xl--line-height: var(--text-3xl--line-height);
812
+ --text-content: var(--text-content);
813
+ --text-content--line-height: var(--text-content--line-height);
814
+ --text-code: var(--text-code);
815
+ --text-code--line-height: var(--text-code--line-height);
802
816
 
803
817
  --radius-sm: var(--radius-sm);
804
818
  --radius-md: var(--radius-md);