@geometra/ui 1.48.0 → 1.49.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.
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import { box, bodyText, text } from '@geometra/core';
2
+ import { theme, font, lineHeight } from './theme.js';
3
+ // Re-export theme API
4
+ export { theme, setTheme, peekTheme, mergeTheme, darkTheme, font, lineHeight } from './theme.js';
2
5
  let inputMeasureCtx = null;
3
6
  function getInputMeasureCtx() {
4
7
  if (inputMeasureCtx)
@@ -18,7 +21,7 @@ function getCaretOffsetFromLocalX(textValue, localX) {
18
21
  const approxCharWidth = 8;
19
22
  return Math.max(0, Math.min(textValue.length, Math.round(clampedX / approxCharWidth)));
20
23
  }
21
- ctx.font = '13px Inter';
24
+ ctx.font = font('', 'base');
22
25
  let running = 0;
23
26
  for (let i = 0; i < textValue.length; i++) {
24
27
  const ch = textValue[i];
@@ -30,16 +33,17 @@ function getCaretOffsetFromLocalX(textValue, localX) {
30
33
  return textValue.length;
31
34
  }
32
35
  export function button(label, onClick) {
36
+ const t = theme();
33
37
  return box({
34
38
  paddingLeft: 12,
35
39
  paddingRight: 12,
36
40
  paddingTop: 8,
37
41
  paddingBottom: 8,
38
- borderRadius: 8,
39
- backgroundColor: '#2563eb',
42
+ borderRadius: t.radii.md,
43
+ backgroundColor: t.colors.accent,
40
44
  cursor: 'pointer',
41
45
  onClick,
42
- }, [text({ text: label, font: '13px Inter', lineHeight: 18, color: '#ffffff' })]);
46
+ }, [text({ text: label, font: font('', 'base'), lineHeight: lineHeight('base'), color: t.colors.accentText })]);
43
47
  }
44
48
  /**
45
49
  * Inline text link that opens a URL on click.
@@ -48,7 +52,8 @@ export function button(label, onClick) {
48
52
  * href via `window.open`.
49
53
  */
50
54
  export function link(label, href, options = {}) {
51
- const { font = '12px Inter', color = '#38bdf8', lineHeight = 16, newTab = true, } = options;
55
+ const t = theme();
56
+ const { font: fontOverride = font('', 'small'), color = t.colors.link, lineHeight: lhOverride = lineHeight('small'), newTab = true, } = options;
52
57
  return box({
53
58
  cursor: 'pointer',
54
59
  onClick: () => {
@@ -57,14 +62,15 @@ export function link(label, href, options = {}) {
57
62
  }
58
63
  },
59
64
  semantic: { tag: 'a', role: 'link', ariaLabel: label },
60
- }, [bodyText({ text: label, font, lineHeight, color })]);
65
+ }, [bodyText({ text: label, font: fontOverride, lineHeight: lhOverride, color })]);
61
66
  }
62
67
  export function input(value, placeholder = '', options = {}) {
68
+ const t = theme();
63
69
  const disabled = options.disabled === true;
64
70
  const readOnly = !disabled && options.readOnly === true;
65
71
  const focused = !disabled && options.focused === true;
66
- const valueColor = disabled ? '#64748b' : '#e2e8f0';
67
- const placeholderColor = disabled ? '#475569' : '#64748b';
72
+ const valueColor = disabled ? t.colors.textDisabled : t.colors.text;
73
+ const placeholderColor = disabled ? t.colors.borderMuted : t.colors.textDisabled;
68
74
  const maxOffset = value.length;
69
75
  const requestedCaret = options.caretOffset;
70
76
  const caretBase = requestedCaret === undefined || !Number.isFinite(requestedCaret) ? maxOffset : requestedCaret;
@@ -77,23 +83,25 @@ export function input(value, placeholder = '', options = {}) {
77
83
  const selStart = Math.max(0, Math.min(Math.min(s0, s1), maxOffset));
78
84
  const selEnd = Math.max(0, Math.min(Math.max(s0, s1), maxOffset));
79
85
  const hasSelection = focused && selStart !== selEnd;
86
+ const f = font('', 'base');
87
+ const lh = lineHeight('base');
80
88
  const children = [];
81
89
  if (showPlaceholder) {
82
90
  if (focused) {
83
- children.push(box({ width: 1.5, minHeight: 14, backgroundColor: '#38bdf8' }, []));
91
+ children.push(box({ width: 1.5, minHeight: 14, backgroundColor: t.colors.focus }, []));
84
92
  }
85
- children.push(text({ text: placeholder, font: '13px Inter', lineHeight: 18, color: placeholderColor }));
93
+ children.push(text({ text: placeholder, font: f, lineHeight: lh, color: placeholderColor }));
86
94
  }
87
95
  else if (hasSelection) {
88
96
  const beforeSel = value.slice(0, selStart).replace(/ /g, '\u00A0');
89
97
  const selectedText = value.slice(selStart, selEnd).replace(/ /g, '\u00A0');
90
98
  const afterSel = value.slice(selEnd).replace(/ /g, '\u00A0');
91
99
  if (beforeSel.length > 0) {
92
- children.push(text({ text: beforeSel, font: '13px Inter', lineHeight: 18, color: valueColor }));
100
+ children.push(text({ text: beforeSel, font: f, lineHeight: lh, color: valueColor }));
93
101
  }
94
- children.push(box({ backgroundColor: 'rgba(56, 189, 248, 0.3)', borderRadius: 2 }, [text({ text: selectedText, font: '13px Inter', lineHeight: 18, color: valueColor })]));
102
+ children.push(box({ backgroundColor: t.colors.selectionBg, borderRadius: 2 }, [text({ text: selectedText, font: f, lineHeight: lh, color: valueColor })]));
95
103
  if (afterSel.length > 0) {
96
- children.push(text({ text: afterSel, font: '13px Inter', lineHeight: 18, color: valueColor }));
104
+ children.push(text({ text: afterSel, font: f, lineHeight: lh, color: valueColor }));
97
105
  }
98
106
  }
99
107
  else {
@@ -102,13 +110,13 @@ export function input(value, placeholder = '', options = {}) {
102
110
  const displayLeft = leftText.replace(/ /g, '\u00A0');
103
111
  const displayRight = rightText.replace(/ /g, '\u00A0');
104
112
  if (displayLeft.length > 0) {
105
- children.push(text({ text: displayLeft, font: '13px Inter', lineHeight: 18, color: valueColor }));
113
+ children.push(text({ text: displayLeft, font: f, lineHeight: lh, color: valueColor }));
106
114
  }
107
115
  if (focused) {
108
- children.push(box({ width: 1.5, minHeight: 14, backgroundColor: '#38bdf8' }, []));
116
+ children.push(box({ width: 1.5, minHeight: 14, backgroundColor: t.colors.focus }, []));
109
117
  }
110
118
  if (displayRight.length > 0) {
111
- children.push(text({ text: displayRight, font: '13px Inter', lineHeight: 18, color: valueColor }));
119
+ children.push(text({ text: displayRight, font: f, lineHeight: lh, color: valueColor }));
112
120
  }
113
121
  }
114
122
  const handleClick = (e) => {
@@ -138,16 +146,16 @@ export function input(value, placeholder = '', options = {}) {
138
146
  flexDirection: 'row',
139
147
  alignItems: 'center',
140
148
  gap: 0,
141
- paddingLeft: 10,
142
- paddingRight: 10,
149
+ paddingLeft: t.spacing.md,
150
+ paddingRight: t.spacing.md,
143
151
  paddingTop: 8,
144
152
  paddingBottom: 8,
145
- borderColor: disabled ? '#475569' : focused ? '#38bdf8' : '#334155',
153
+ borderColor: disabled ? t.colors.borderMuted : focused ? t.colors.focus : t.colors.border,
146
154
  borderWidth: 1,
147
- borderRadius: 8,
155
+ borderRadius: t.radii.md,
148
156
  cursor: disabled ? 'not-allowed' : 'text',
149
157
  pointerEvents: disabled ? 'none' : undefined,
150
- backgroundColor: disabled ? '#0f172a' : focused ? '#111827' : undefined,
158
+ backgroundColor: disabled ? t.colors.bg : focused ? t.colors.bgSubtle : undefined,
151
159
  semantic: disabled
152
160
  ? { tag: 'input', ariaDisabled: true }
153
161
  : readOnly
@@ -161,31 +169,34 @@ export function input(value, placeholder = '', options = {}) {
161
169
  }, children);
162
170
  }
163
171
  export function list(items) {
164
- return box({ flexDirection: 'column', gap: 4, semantic: { tag: 'ul' } }, items.map((item) => box({ paddingLeft: 8, paddingTop: 4, paddingBottom: 4, semantic: { tag: 'li' } }, [
165
- bodyText({ text: item, font: '13px Inter', lineHeight: 18, color: '#e2e8f0' }),
172
+ const t = theme();
173
+ return box({ flexDirection: 'column', gap: t.spacing.xs, semantic: { tag: 'ul' } }, items.map((item) => box({ paddingLeft: 8, paddingTop: t.spacing.xs, paddingBottom: t.spacing.xs, semantic: { tag: 'li' } }, [
174
+ bodyText({ text: item, font: font('', 'base'), lineHeight: lineHeight('base'), color: t.colors.text }),
166
175
  ])));
167
176
  }
168
177
  export function dialog(title, body, actions = []) {
178
+ const t = theme();
169
179
  return box({
170
180
  flexDirection: 'column',
171
- gap: 10,
172
- padding: 14,
173
- borderRadius: 10,
174
- borderColor: '#334155',
181
+ gap: t.spacing.md,
182
+ padding: t.spacing.lg,
183
+ borderRadius: t.radii.lg,
184
+ borderColor: t.colors.border,
175
185
  borderWidth: 1,
176
- backgroundColor: '#0f172a',
186
+ backgroundColor: t.colors.bg,
177
187
  semantic: { role: 'dialog', ariaLabel: title },
178
188
  }, [
179
- bodyText({ text: title, font: 'bold 16px Inter', lineHeight: 20, color: '#f8fafc' }),
180
- bodyText({ text: body, font: '13px Inter', lineHeight: 18, color: '#cbd5e1' }),
189
+ bodyText({ text: title, font: font('bold', 'heading'), lineHeight: lineHeight('heading'), color: t.colors.textHeading }),
190
+ bodyText({ text: body, font: font('', 'base'), lineHeight: lineHeight('base'), color: t.colors.textSubtle }),
181
191
  box({ flexDirection: 'row', gap: 8 }, actions),
182
192
  ]);
183
193
  }
184
194
  export function checkbox(label, options = {}) {
195
+ const t = theme();
185
196
  const checked = options.checked === true;
186
197
  const disabled = options.disabled === true;
187
- const borderColor = disabled ? '#475569' : checked ? '#22c55e' : '#64748b';
188
- const bg = disabled ? '#0f172a' : checked ? '#14532d' : '#111827';
198
+ const borderColor = disabled ? t.colors.borderMuted : checked ? t.colors.success : t.colors.textDisabled;
199
+ const bg = disabled ? t.colors.bg : checked ? t.colors.successBg : t.colors.bgSubtle;
189
200
  const toggle = () => {
190
201
  if (disabled)
191
202
  return;
@@ -193,7 +204,7 @@ export function checkbox(label, options = {}) {
193
204
  };
194
205
  return box({
195
206
  flexDirection: 'row',
196
- gap: 10,
207
+ gap: t.spacing.md,
197
208
  alignItems: 'center',
198
209
  cursor: disabled ? 'not-allowed' : 'pointer',
199
210
  semantic: { role: 'checkbox', ariaLabel: label, ariaSelected: checked, ariaDisabled: disabled },
@@ -206,29 +217,30 @@ export function checkbox(label, options = {}) {
206
217
  box({
207
218
  width: 16,
208
219
  height: 16,
209
- borderRadius: 4,
220
+ borderRadius: t.radii.sm,
210
221
  borderColor,
211
222
  borderWidth: 1,
212
223
  backgroundColor: bg,
213
224
  alignItems: 'center',
214
225
  justifyContent: 'center',
215
226
  }, checked
216
- ? [text({ text: '✓', font: 'bold 11px Inter', lineHeight: 12, color: disabled ? '#94a3b8' : '#86efac' })]
227
+ ? [text({ text: '✓', font: `bold 11px ${t.typography.fontFamily}`, lineHeight: 12, color: disabled ? t.colors.textMuted : t.colors.successTextLight })]
217
228
  : []),
218
229
  box({ flexGrow: 1, minWidth: 0 }, [
219
230
  bodyText({
220
231
  text: label,
221
- font: '13px Inter',
222
- lineHeight: 18,
223
- color: disabled ? '#64748b' : '#e2e8f0',
232
+ font: font('', 'base'),
233
+ lineHeight: lineHeight('base'),
234
+ color: disabled ? t.colors.textDisabled : t.colors.text,
224
235
  }),
225
236
  ]),
226
237
  ]);
227
238
  }
228
239
  export function radio(label, options = {}) {
240
+ const t = theme();
229
241
  const checked = options.checked === true;
230
242
  const disabled = options.disabled === true;
231
- const borderColor = disabled ? '#475569' : checked ? '#38bdf8' : '#64748b';
243
+ const borderColor = disabled ? t.colors.borderMuted : checked ? t.colors.focus : t.colors.textDisabled;
232
244
  const select = () => {
233
245
  if (disabled || checked)
234
246
  return;
@@ -236,7 +248,7 @@ export function radio(label, options = {}) {
236
248
  };
237
249
  return box({
238
250
  flexDirection: 'row',
239
- gap: 10,
251
+ gap: t.spacing.md,
240
252
  alignItems: 'center',
241
253
  cursor: disabled ? 'not-allowed' : 'pointer',
242
254
  semantic: { role: 'radio', ariaLabel: label, ariaSelected: checked, ariaDisabled: disabled },
@@ -249,89 +261,85 @@ export function radio(label, options = {}) {
249
261
  box({
250
262
  width: 16,
251
263
  height: 16,
252
- borderRadius: 8,
264
+ borderRadius: t.radii.md,
253
265
  borderColor,
254
266
  borderWidth: 1,
255
- backgroundColor: '#111827',
267
+ backgroundColor: t.colors.bgSubtle,
256
268
  alignItems: 'center',
257
269
  justifyContent: 'center',
258
270
  }, checked
259
- ? [box({ width: 8, height: 8, borderRadius: 4, backgroundColor: disabled ? '#64748b' : '#38bdf8' }, [])]
271
+ ? [box({ width: 8, height: 8, borderRadius: t.radii.sm, backgroundColor: disabled ? t.colors.textDisabled : t.colors.focus }, [])]
260
272
  : []),
261
273
  box({ flexGrow: 1, minWidth: 0 }, [
262
274
  bodyText({
263
275
  text: label,
264
- font: '13px Inter',
265
- lineHeight: 18,
266
- color: disabled ? '#64748b' : '#e2e8f0',
276
+ font: font('', 'base'),
277
+ lineHeight: lineHeight('base'),
278
+ color: disabled ? t.colors.textDisabled : t.colors.text,
267
279
  }),
268
280
  ]),
269
281
  ]);
270
282
  }
271
283
  export function tabs(items, options = {}) {
284
+ const t = theme();
272
285
  const activeIndex = Math.max(0, Math.min(options.activeIndex ?? 0, Math.max(0, items.length - 1)));
273
286
  const active = items[activeIndex];
274
287
  return box({
275
288
  flexDirection: 'column',
276
- gap: 10,
289
+ gap: t.spacing.md,
277
290
  semantic: { role: 'tablist' },
278
291
  }, [
279
292
  box({
280
293
  flexDirection: 'row',
281
- gap: 6,
294
+ gap: t.spacing.sm,
282
295
  flexWrap: 'wrap',
283
296
  }, items.map((item, idx) => box({
284
- paddingLeft: 10,
285
- paddingRight: 10,
286
- paddingTop: 6,
287
- paddingBottom: 6,
288
- borderRadius: 8,
289
- borderColor: idx === activeIndex ? '#38bdf8' : '#334155',
297
+ paddingLeft: t.spacing.md,
298
+ paddingRight: t.spacing.md,
299
+ paddingTop: t.spacing.sm,
300
+ paddingBottom: t.spacing.sm,
301
+ borderRadius: t.radii.md,
302
+ borderColor: idx === activeIndex ? t.colors.focus : t.colors.border,
290
303
  borderWidth: 1,
291
- backgroundColor: idx === activeIndex ? '#082f49' : '#111827',
304
+ backgroundColor: idx === activeIndex ? t.colors.accentSoft : t.colors.bgSubtle,
292
305
  cursor: 'pointer',
293
306
  semantic: { role: 'tab', ariaLabel: item.label, ariaSelected: idx === activeIndex },
294
307
  onClick: () => options.onTabChange?.(idx),
295
- }, [bodyText({ text: item.label, font: '13px Inter', lineHeight: 18, color: idx === activeIndex ? '#bae6fd' : '#cbd5e1' })]))),
308
+ }, [bodyText({ text: item.label, font: font('', 'base'), lineHeight: lineHeight('base'), color: idx === activeIndex ? t.colors.accentSoftText : t.colors.textSubtle })]))),
296
309
  box({
297
- borderColor: '#334155',
310
+ borderColor: t.colors.border,
298
311
  borderWidth: 1,
299
- borderRadius: 10,
312
+ borderRadius: t.radii.lg,
300
313
  padding: 12,
301
314
  semantic: { role: 'tabpanel' },
302
315
  }, active ? [active.content] : []),
303
316
  ]);
304
317
  }
305
- const toastVariantStyle = {
306
- info: { border: '#334155', background: '#0f172a', color: '#e2e8f0' },
307
- success: { border: '#166534', background: '#052e16', color: '#bbf7d0' },
308
- warning: { border: '#a16207', background: '#422006', color: '#fef08a' },
309
- error: { border: '#991b1b', background: '#450a0a', color: '#fecaca' },
310
- };
311
318
  /**
312
319
  * Inline toast / status region (app controls visibility by swapping the tree).
313
320
  * Uses `role="status"` for assistive tech.
314
321
  */
315
322
  export function toast(message, options = {}) {
323
+ const t = theme();
316
324
  const variant = options.variant ?? 'info';
317
- const s = toastVariantStyle[variant];
325
+ const s = t.colors.variants[variant];
318
326
  const children = [];
319
327
  if (options.title) {
320
328
  children.push(bodyText({
321
329
  text: options.title,
322
- font: 'bold 13px Inter',
323
- lineHeight: 18,
324
- color: s.color,
330
+ font: font('bold', 'base'),
331
+ lineHeight: lineHeight('base'),
332
+ color: s.text,
325
333
  }));
326
334
  }
327
335
  children.push(bodyText({
328
336
  text: message,
329
- font: '13px Inter',
330
- lineHeight: 18,
331
- color: s.color,
337
+ font: font('', 'base'),
338
+ lineHeight: lineHeight('base'),
339
+ color: s.text,
332
340
  }));
333
341
  const row = [
334
- box({ flexDirection: 'column', gap: 4, flexGrow: 1 }, children),
342
+ box({ flexDirection: 'column', gap: t.spacing.xs, flexGrow: 1 }, children),
335
343
  ];
336
344
  if (options.onDismiss) {
337
345
  row.push(box({
@@ -339,21 +347,21 @@ export function toast(message, options = {}) {
339
347
  paddingRight: 8,
340
348
  paddingTop: 4,
341
349
  paddingBottom: 4,
342
- borderRadius: 6,
350
+ borderRadius: t.spacing.sm,
343
351
  cursor: 'pointer',
344
352
  semantic: { role: 'button', ariaLabel: 'Dismiss' },
345
353
  onClick: options.onDismiss,
346
- }, [text({ text: '✕', font: '12px Inter', lineHeight: 14, color: s.color })]));
354
+ }, [text({ text: '✕', font: font('', 'small'), lineHeight: 14, color: s.text })]));
347
355
  }
348
356
  return box({
349
357
  flexDirection: 'row',
350
358
  alignItems: 'flex-start',
351
359
  gap: 8,
352
360
  padding: 12,
353
- borderRadius: 10,
361
+ borderRadius: t.radii.lg,
354
362
  borderWidth: 1,
355
363
  borderColor: s.border,
356
- backgroundColor: s.background,
364
+ backgroundColor: s.bg,
357
365
  maxWidth: 360,
358
366
  semantic: { role: 'status', ariaLabel: options.title ? `${options.title}: ${message}` : message },
359
367
  }, row);
@@ -363,15 +371,15 @@ export function toast(message, options = {}) {
363
371
  * (filtering, keyboard) — this renders rows with optional shortcuts.
364
372
  */
365
373
  export function commandPalette(commands, options = {}) {
366
- const muted = '#64748b';
374
+ const t = theme();
367
375
  return box({
368
376
  flexDirection: 'column',
369
377
  gap: 2,
370
- padding: 6,
371
- borderRadius: 10,
378
+ padding: t.spacing.sm,
379
+ borderRadius: t.radii.lg,
372
380
  borderWidth: 1,
373
- borderColor: '#334155',
374
- backgroundColor: '#020617',
381
+ borderColor: t.colors.border,
382
+ backgroundColor: t.colors.bgAlt,
375
383
  semantic: { role: 'listbox', ariaLabel: 'Commands' },
376
384
  }, commands.map((cmd) => box({
377
385
  flexDirection: 'row',
@@ -379,20 +387,20 @@ export function commandPalette(commands, options = {}) {
379
387
  alignItems: 'center',
380
388
  gap: 12,
381
389
  minWidth: 0,
382
- paddingLeft: 10,
383
- paddingRight: 10,
390
+ paddingLeft: t.spacing.md,
391
+ paddingRight: t.spacing.md,
384
392
  paddingTop: 8,
385
393
  paddingBottom: 8,
386
- borderRadius: 8,
394
+ borderRadius: t.radii.md,
387
395
  cursor: 'pointer',
388
396
  semantic: { role: 'option', ariaLabel: cmd.label },
389
397
  onClick: () => options.onSelect?.(cmd.id),
390
398
  }, [
391
399
  box({ flexGrow: 1, minWidth: 0 }, [
392
- bodyText({ text: cmd.label, font: '13px Inter', lineHeight: 18, color: '#e2e8f0' }),
400
+ bodyText({ text: cmd.label, font: font('', 'base'), lineHeight: lineHeight('base'), color: t.colors.text }),
393
401
  ]),
394
402
  cmd.shortcut
395
- ? text({ text: cmd.shortcut, font: '12px Inter', lineHeight: 16, color: muted })
403
+ ? text({ text: cmd.shortcut, font: font('', 'small'), lineHeight: lineHeight('small'), color: t.colors.textDisabled })
396
404
  : box({ width: 1, height: 1 }, []),
397
405
  ])));
398
406
  }
@@ -400,23 +408,24 @@ export function commandPalette(commands, options = {}) {
400
408
  * Vertical menu (`role="menu"`). App owns open/close and positioning.
401
409
  */
402
410
  export function menu(items, options = {}) {
411
+ const t = theme();
403
412
  return box({
404
413
  flexDirection: 'column',
405
414
  gap: 2,
406
- padding: 6,
407
- borderRadius: 10,
415
+ padding: t.spacing.sm,
416
+ borderRadius: t.radii.lg,
408
417
  borderWidth: 1,
409
- borderColor: '#334155',
410
- backgroundColor: '#020617',
418
+ borderColor: t.colors.border,
419
+ backgroundColor: t.colors.bgAlt,
411
420
  semantic: { role: 'menu', ariaLabel: options.ariaLabel ?? 'Menu' },
412
421
  }, items.map((item) => {
413
- const color = item.disabled ? '#475569' : item.danger ? '#fca5a5' : '#e2e8f0';
422
+ const color = item.disabled ? t.colors.borderMuted : item.danger ? t.colors.danger : t.colors.text;
414
423
  return box({
415
- paddingLeft: 10,
416
- paddingRight: 10,
424
+ paddingLeft: t.spacing.md,
425
+ paddingRight: t.spacing.md,
417
426
  paddingTop: 8,
418
427
  paddingBottom: 8,
419
- borderRadius: 8,
428
+ borderRadius: t.radii.md,
420
429
  cursor: item.disabled ? 'not-allowed' : 'pointer',
421
430
  semantic: {
422
431
  role: 'menuitem',
@@ -424,7 +433,7 @@ export function menu(items, options = {}) {
424
433
  ariaDisabled: item.disabled === true,
425
434
  },
426
435
  onClick: item.disabled ? undefined : () => options.onSelect?.(item.id),
427
- }, [bodyText({ text: item.label, font: '13px Inter', lineHeight: 18, color })]);
436
+ }, [bodyText({ text: item.label, font: font('', 'base'), lineHeight: lineHeight('base'), color })]);
428
437
  }));
429
438
  }
430
439
  /**
@@ -432,6 +441,7 @@ export function menu(items, options = {}) {
432
441
  * after `onChange` if the panel should close.
433
442
  */
434
443
  export function selectControl(opts) {
444
+ const t = theme();
435
445
  const selected = opts.options.find(o => o.value === opts.value);
436
446
  const label = selected?.label ?? opts.placeholder ?? 'Select…';
437
447
  const trigger = box({
@@ -440,30 +450,30 @@ export function selectControl(opts) {
440
450
  alignItems: 'center',
441
451
  gap: 8,
442
452
  minWidth: 0,
443
- paddingLeft: 10,
444
- paddingRight: 10,
453
+ paddingLeft: t.spacing.md,
454
+ paddingRight: t.spacing.md,
445
455
  paddingTop: 8,
446
456
  paddingBottom: 8,
447
- borderRadius: 8,
457
+ borderRadius: t.radii.md,
448
458
  borderWidth: 1,
449
- borderColor: '#334155',
450
- backgroundColor: '#0f172a',
459
+ borderColor: t.colors.border,
460
+ backgroundColor: t.colors.bg,
451
461
  cursor: 'pointer',
452
462
  semantic: { role: 'button', ariaLabel: 'Select', ariaExpanded: opts.open },
453
463
  onClick: () => opts.onToggle?.(),
454
464
  }, [
455
465
  box({ flexGrow: 1, minWidth: 0 }, [
456
- bodyText({ text: label, font: '13px Inter', lineHeight: 18, color: '#e2e8f0' }),
466
+ bodyText({ text: label, font: font('', 'base'), lineHeight: lineHeight('base'), color: t.colors.text }),
457
467
  ]),
458
468
  text({
459
469
  text: opts.open ? '▲' : '▼',
460
- font: '10px Inter',
461
- lineHeight: 18,
462
- color: '#94a3b8',
470
+ font: `10px ${t.typography.fontFamily}`,
471
+ lineHeight: lineHeight('base'),
472
+ color: t.colors.textMuted,
463
473
  }),
464
474
  ]);
465
475
  if (!opts.open) {
466
- return box({ flexDirection: 'column', gap: 4 }, [trigger]);
476
+ return box({ flexDirection: 'column', gap: t.spacing.xs }, [trigger]);
467
477
  }
468
478
  const panel = menu(opts.options.map(o => ({
469
479
  id: o.value,
@@ -473,29 +483,30 @@ export function selectControl(opts) {
473
483
  ariaLabel: 'Options',
474
484
  onSelect: (id) => opts.onChange(id),
475
485
  });
476
- return box({ flexDirection: 'column', gap: 4 }, [trigger, panel]);
486
+ return box({ flexDirection: 'column', gap: t.spacing.xs }, [trigger, panel]);
477
487
  }
478
488
  /**
479
489
  * Simple columnar table (header + uniform rows). Keys in each row match `columns[].key`.
480
490
  */
481
491
  export function dataTable(columns, rows, options = {}) {
492
+ const t = theme();
482
493
  const headerRow = box({
483
494
  flexDirection: 'row',
484
495
  gap: 8,
485
496
  paddingBottom: 8,
486
497
  semantic: { role: 'row' },
487
- }, columns.map(col => box({ flexGrow: 1, minWidth: 0, semantic: { role: 'columnheader', ariaLabel: col.header } }, [bodyText({ text: col.header, font: 'bold 12px Inter', lineHeight: 16, color: '#94a3b8' })])));
488
- const divider = box({ height: 1, backgroundColor: '#334155' }, []);
498
+ }, columns.map(col => box({ flexGrow: 1, minWidth: 0, semantic: { role: 'columnheader', ariaLabel: col.header } }, [bodyText({ text: col.header, font: font('bold', 'small'), lineHeight: lineHeight('small'), color: t.colors.textMuted })])));
499
+ const divider = box({ height: 1, backgroundColor: t.colors.border }, []);
489
500
  const bodyRows = rows.map((row, ri) => box({
490
501
  flexDirection: 'row',
491
502
  gap: 8,
492
- paddingTop: 6,
493
- paddingBottom: 6,
503
+ paddingTop: t.spacing.sm,
504
+ paddingBottom: t.spacing.sm,
494
505
  cursor: options.onRowClick ? 'pointer' : 'default',
495
506
  semantic: { role: 'row' },
496
507
  onClick: options.onRowClick ? () => options.onRowClick?.(ri) : undefined,
497
508
  }, columns.map(col => box({ flexGrow: 1, minWidth: 0, semantic: { role: 'cell' } }, [
498
- bodyText({ text: row[col.key] ?? '', font: '13px Inter', lineHeight: 18, color: '#e2e8f0' }),
509
+ bodyText({ text: row[col.key] ?? '', font: font('', 'base'), lineHeight: lineHeight('base'), color: t.colors.text }),
499
510
  ]))));
500
511
  return box({
501
512
  flexDirection: 'column',
@@ -504,19 +515,20 @@ export function dataTable(columns, rows, options = {}) {
504
515
  }, [headerRow, divider, ...bodyRows]);
505
516
  }
506
517
  function treeNodeElement(node, depth, options) {
518
+ const t = theme();
507
519
  const hasChildren = !!(node.children && node.children.length > 0);
508
520
  const expanded = options.expandedIds.has(node.id);
509
521
  const selected = options.selectedId === node.id;
510
522
  const row = box({
511
523
  flexDirection: 'row',
512
524
  alignItems: 'center',
513
- gap: 6,
525
+ gap: t.spacing.sm,
514
526
  minWidth: 0,
515
- paddingLeft: 8 + depth * 14,
516
- paddingTop: 4,
517
- paddingBottom: 4,
518
- borderRadius: 6,
519
- backgroundColor: selected ? '#1e3a5f' : undefined,
527
+ paddingLeft: 8 + depth * t.spacing.lg,
528
+ paddingTop: t.spacing.xs,
529
+ paddingBottom: t.spacing.xs,
530
+ borderRadius: t.spacing.sm,
531
+ backgroundColor: selected ? t.colors.selected : undefined,
520
532
  cursor: 'pointer',
521
533
  semantic: {
522
534
  role: 'treeitem',
@@ -532,13 +544,13 @@ function treeNodeElement(node, depth, options) {
532
544
  hasChildren
533
545
  ? text({
534
546
  text: expanded ? '▼' : '▶',
535
- font: '10px Inter',
547
+ font: `10px ${t.typography.fontFamily}`,
536
548
  lineHeight: 14,
537
- color: '#94a3b8',
549
+ color: t.colors.textMuted,
538
550
  })
539
551
  : box({ width: 14 }, []),
540
552
  box({ flexGrow: 1, minWidth: 0 }, [
541
- bodyText({ text: node.label, font: '13px Inter', lineHeight: 18, color: '#e2e8f0' }),
553
+ bodyText({ text: node.label, font: font('', 'base'), lineHeight: lineHeight('base'), color: t.colors.text }),
542
554
  ]),
543
555
  ]);
544
556
  if (!hasChildren || !expanded)
@@ -568,52 +580,48 @@ export function comboboxField(value, placeholder, suggestions, options) {
568
580
  return box({ flexDirection: 'column', gap: 8 }, [input(value, placeholder, options.input), palette]);
569
581
  }
570
582
  export function card(options = {}) {
583
+ const t = theme();
584
+ const border = options.borderColor ?? t.colors.border;
585
+ const bg = options.backgroundColor ?? t.colors.bg;
571
586
  const sections = [];
572
587
  if (options.header) {
573
- sections.push(box({ paddingLeft: 16, paddingRight: 16, paddingTop: 14, paddingBottom: 14, borderBottom: 1, borderColor: options.borderColor ?? '#334155', minWidth: 0 }, [options.header]));
588
+ sections.push(box({ paddingLeft: t.spacing.xl, paddingRight: t.spacing.xl, paddingTop: t.spacing.lg, paddingBottom: t.spacing.lg, borderBottom: 1, borderColor: border, minWidth: 0 }, [options.header]));
574
589
  }
575
590
  if (options.children && options.children.length > 0) {
576
- sections.push(box({ flexDirection: 'column', padding: 16, gap: options.gap ?? 14, minWidth: 0 }, options.children));
591
+ sections.push(box({ flexDirection: 'column', padding: t.spacing.xl, gap: options.gap ?? t.spacing.lg, minWidth: 0 }, options.children));
577
592
  }
578
593
  if (options.footer) {
579
- sections.push(box({ paddingLeft: 16, paddingRight: 16, paddingTop: 14, paddingBottom: 14, borderTop: 1, borderColor: options.borderColor ?? '#334155', minWidth: 0 }, [options.footer]));
594
+ sections.push(box({ paddingLeft: t.spacing.xl, paddingRight: t.spacing.xl, paddingTop: t.spacing.lg, paddingBottom: t.spacing.lg, borderTop: 1, borderColor: border, minWidth: 0 }, [options.footer]));
580
595
  }
581
596
  return box({
582
597
  flexDirection: 'column',
583
598
  minWidth: 0,
584
- borderRadius: 10, borderWidth: 1,
585
- borderColor: options.borderColor ?? '#334155',
586
- backgroundColor: options.backgroundColor ?? '#0f172a',
599
+ borderRadius: t.radii.lg, borderWidth: 1,
600
+ borderColor: border,
601
+ backgroundColor: bg,
587
602
  overflow: 'hidden',
588
603
  }, sections);
589
604
  }
590
- // ---------------------------------------------------------------------------
591
- // Badge
592
- // ---------------------------------------------------------------------------
593
- const badgeVariantStyle = {
594
- default: { bg: '#334155', color: '#e2e8f0' },
595
- success: { bg: '#14532d', color: '#bbf7d0' },
596
- warning: { bg: '#422006', color: '#fef08a' },
597
- error: { bg: '#450a0a', color: '#fecaca' },
598
- info: { bg: '#082f49', color: '#bae6fd' },
599
- };
600
605
  export function badge(label, options = {}) {
601
- const s = badgeVariantStyle[options.variant ?? 'default'];
606
+ const t = theme();
607
+ const s = t.colors.badgeVariants[options.variant ?? 'default'];
602
608
  return box({
603
609
  paddingLeft: 8, paddingRight: 8, paddingTop: 2, paddingBottom: 2,
604
- borderRadius: 9999, backgroundColor: s.bg,
605
- }, [bodyText({ text: label, font: 'bold 11px Inter', lineHeight: 14, color: s.color })]);
610
+ borderRadius: t.radii.full, backgroundColor: s.bg,
611
+ }, [bodyText({ text: label, font: `bold 11px ${t.typography.fontFamily}`, lineHeight: 14, color: s.text })]);
606
612
  }
607
613
  export function separator(options = {}) {
614
+ const t = theme();
608
615
  const vertical = options.direction === 'vertical';
609
- const color = options.color ?? '#334155';
616
+ const color = options.color ?? t.colors.border;
610
617
  return box(vertical
611
618
  ? { width: 1, alignSelf: 'stretch', backgroundColor: color, semantic: { role: 'separator' } }
612
619
  : { height: 1, alignSelf: 'stretch', backgroundColor: color, semantic: { role: 'separator' } }, []);
613
620
  }
614
621
  export function avatar(name, options = {}) {
622
+ const t = theme();
615
623
  const size = options.size ?? 32;
616
- const bg = options.backgroundColor ?? '#2563eb';
624
+ const bg = options.backgroundColor ?? t.colors.accent;
617
625
  const initials = name
618
626
  .split(/\s+/)
619
627
  .filter(Boolean)
@@ -625,86 +633,88 @@ export function avatar(name, options = {}) {
625
633
  width: size, height: size, borderRadius: size / 2,
626
634
  backgroundColor: bg, alignItems: 'center', justifyContent: 'center',
627
635
  semantic: { ariaLabel: name },
628
- }, [text({ text: initials, font: `bold ${fontSize}px Inter`, lineHeight: Math.round(fontSize * 1.2), color: '#ffffff' })]);
629
- }
630
- // ---------------------------------------------------------------------------
631
- // Alert
632
- // ---------------------------------------------------------------------------
633
- const alertVariantStyle = {
634
- info: { border: '#334155', bg: '#0f172a', color: '#e2e8f0', icon: 'ℹ' },
635
- success: { border: '#166534', bg: '#052e16', color: '#bbf7d0', icon: '✓' },
636
- warning: { border: '#a16207', bg: '#422006', color: '#fef08a', icon: '⚠' },
637
- error: { border: '#991b1b', bg: '#450a0a', color: '#fecaca', icon: '✕' },
636
+ }, [text({ text: initials, font: `bold ${fontSize}px ${t.typography.fontFamily}`, lineHeight: Math.round(fontSize * 1.2), color: t.colors.accentText })]);
637
+ }
638
+ const alertIcons = {
639
+ info: 'ℹ',
640
+ success: '✓',
641
+ warning: '⚠',
642
+ error: '',
638
643
  };
639
644
  export function alert(message, options = {}) {
645
+ const t = theme();
640
646
  const variant = options.variant ?? 'info';
641
- const s = alertVariantStyle[variant];
647
+ const s = t.colors.variants[variant];
648
+ const icon = alertIcons[variant] ?? 'ℹ';
642
649
  const content = [];
643
650
  if (options.title) {
644
- content.push(bodyText({ text: options.title, font: 'bold 13px Inter', lineHeight: 18, color: s.color }));
651
+ content.push(bodyText({ text: options.title, font: font('bold', 'base'), lineHeight: lineHeight('base'), color: s.text }));
645
652
  }
646
- content.push(bodyText({ text: message, font: '13px Inter', lineHeight: 18, color: s.color }));
653
+ content.push(bodyText({ text: message, font: font('', 'base'), lineHeight: lineHeight('base'), color: s.text }));
647
654
  const body = [
648
- text({ text: s.icon, font: '13px Inter', lineHeight: 18, color: s.color }),
649
- box({ flexDirection: 'column', gap: 4, flexGrow: 1, flexShrink: 1, minWidth: 0 }, content),
655
+ text({ text: icon, font: font('', 'base'), lineHeight: lineHeight('base'), color: s.text }),
656
+ box({ flexDirection: 'column', gap: t.spacing.xs, flexGrow: 1, flexShrink: 1, minWidth: 0 }, content),
650
657
  ];
651
658
  if (options.onDismiss) {
652
659
  body.push(box({
653
660
  paddingLeft: 8, paddingRight: 8, paddingTop: 4, paddingBottom: 4,
654
- borderRadius: 6, cursor: 'pointer',
661
+ borderRadius: t.spacing.sm, cursor: 'pointer',
655
662
  semantic: { role: 'button', ariaLabel: 'Dismiss' },
656
663
  onClick: options.onDismiss,
657
- }, [text({ text: '✕', font: '12px Inter', lineHeight: 14, color: s.color })]));
664
+ }, [text({ text: '✕', font: font('', 'small'), lineHeight: 14, color: s.text })]));
658
665
  }
659
666
  return box({
660
- flexDirection: 'row', alignItems: 'flex-start', gap: 10,
661
- padding: 12, borderRadius: 10, borderWidth: 1, minWidth: 0,
667
+ flexDirection: 'row', alignItems: 'flex-start', gap: t.spacing.md,
668
+ padding: 12, borderRadius: t.radii.lg, borderWidth: 1, minWidth: 0,
662
669
  borderColor: s.border, backgroundColor: s.bg,
663
670
  semantic: { role: 'alert', ariaLabel: options.title ? `${options.title}: ${message}` : message },
664
671
  }, body);
665
672
  }
666
673
  export function progress(value, options = {}) {
674
+ const t = theme();
667
675
  const clamped = Math.max(0, Math.min(100, value));
668
676
  const children = [];
669
677
  if (options.label) {
670
678
  children.push(box({ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 8, minWidth: 0 }, [
671
679
  box({ flexGrow: 1, minWidth: 0 }, [
672
- bodyText({ text: options.label, font: '12px Inter', lineHeight: 16, color: '#94a3b8' }),
680
+ bodyText({ text: options.label, font: font('', 'small'), lineHeight: lineHeight('small'), color: t.colors.textMuted }),
673
681
  ]),
674
- text({ text: `${Math.round(clamped)}%`, font: '12px Inter', lineHeight: 16, color: '#94a3b8' }),
682
+ text({ text: `${Math.round(clamped)}%`, font: font('', 'small'), lineHeight: lineHeight('small'), color: t.colors.textMuted }),
675
683
  ]));
676
684
  }
677
- children.push(box({ flexDirection: 'row', height: 6, borderRadius: 3, backgroundColor: '#334155', overflow: 'hidden' }, [
678
- box({ flexGrow: Math.max(clamped, 0.001), minWidth: 0, height: 6, borderRadius: 3, backgroundColor: '#2563eb' }),
685
+ children.push(box({ flexDirection: 'row', height: 6, borderRadius: 3, backgroundColor: t.colors.border, overflow: 'hidden' }, [
686
+ box({ flexGrow: Math.max(clamped, 0.001), minWidth: 0, height: 6, borderRadius: 3, backgroundColor: t.colors.accent }),
679
687
  box({ flexGrow: Math.max(100 - clamped, 0.001), minWidth: 0, height: 6 }),
680
688
  ]));
681
689
  return box({
682
- flexDirection: 'column', gap: 6,
690
+ flexDirection: 'column', gap: t.spacing.sm,
683
691
  semantic: { role: 'progressbar', ariaLabel: options.label ?? 'Progress' },
684
692
  }, children);
685
693
  }
686
694
  export function skeleton(options = {}) {
695
+ const t = theme();
687
696
  return box({
688
697
  width: options.width ?? 100,
689
698
  height: options.height ?? 16,
690
- borderRadius: options.borderRadius ?? 4,
691
- backgroundColor: '#1e293b',
699
+ borderRadius: options.borderRadius ?? t.radii.sm,
700
+ backgroundColor: t.colors.skeleton,
692
701
  }, []);
693
702
  }
694
703
  export function breadcrumb(items, options = {}) {
704
+ const t = theme();
695
705
  const sep = options.separator ?? '/';
696
706
  const children = [];
697
707
  for (let i = 0; i < items.length; i++) {
698
708
  const item = items[i];
699
709
  const isLast = i === items.length - 1;
700
710
  if (item.onClick && !isLast) {
701
- children.push(box({ cursor: 'pointer', onClick: item.onClick }, [bodyText({ text: item.label, font: '13px Inter', lineHeight: 18, color: '#38bdf8' })]));
711
+ children.push(box({ cursor: 'pointer', onClick: item.onClick }, [bodyText({ text: item.label, font: font('', 'base'), lineHeight: lineHeight('base'), color: t.colors.link })]));
702
712
  }
703
713
  else {
704
- children.push(bodyText({ text: item.label, font: '13px Inter', lineHeight: 18, color: isLast ? '#e2e8f0' : '#94a3b8' }));
714
+ children.push(bodyText({ text: item.label, font: font('', 'base'), lineHeight: lineHeight('base'), color: isLast ? t.colors.text : t.colors.textMuted }));
705
715
  }
706
716
  if (!isLast) {
707
- children.push(text({ text: ` ${sep} `, font: '13px Inter', lineHeight: 18, color: '#475569' }));
717
+ children.push(text({ text: ` ${sep} `, font: font('', 'base'), lineHeight: lineHeight('base'), color: t.colors.borderMuted }));
708
718
  }
709
719
  }
710
720
  return box({
@@ -713,18 +723,19 @@ export function breadcrumb(items, options = {}) {
713
723
  }, children);
714
724
  }
715
725
  export function pagination(options) {
726
+ const t = theme();
716
727
  const { page, totalPages, onPageChange } = options;
717
728
  const current = Math.max(1, Math.min(page, totalPages));
718
729
  const pageBtn = (label, target, active, disabled) => box({
719
730
  paddingLeft: 8, paddingRight: 8, paddingTop: 4, paddingBottom: 4,
720
- borderRadius: 6,
731
+ borderRadius: t.spacing.sm,
721
732
  borderWidth: active ? 1 : 0,
722
- borderColor: active ? '#38bdf8' : undefined,
723
- backgroundColor: active ? '#082f49' : undefined,
733
+ borderColor: active ? t.colors.focus : undefined,
734
+ backgroundColor: active ? t.colors.accentSoft : undefined,
724
735
  cursor: disabled ? 'not-allowed' : 'pointer',
725
736
  onClick: disabled ? undefined : () => onPageChange(target),
726
737
  semantic: { role: 'button', ariaLabel: label, ariaDisabled: disabled },
727
- }, [text({ text: label, font: '13px Inter', lineHeight: 18, color: disabled ? '#475569' : active ? '#bae6fd' : '#e2e8f0' })]);
738
+ }, [text({ text: label, font: font('', 'base'), lineHeight: lineHeight('base'), color: disabled ? t.colors.borderMuted : active ? t.colors.accentSoftText : t.colors.text })]);
728
739
  const children = [
729
740
  pageBtn('‹', current - 1, false, current <= 1),
730
741
  ];
@@ -735,11 +746,12 @@ export function pagination(options) {
735
746
  }
736
747
  children.push(pageBtn('›', current + 1, false, current >= totalPages));
737
748
  return box({
738
- flexDirection: 'row', gap: 4, alignItems: 'center',
749
+ flexDirection: 'row', gap: t.spacing.xs, alignItems: 'center',
739
750
  semantic: { tag: 'nav', ariaLabel: 'Pagination' },
740
751
  }, children);
741
752
  }
742
753
  export function switchControl(options) {
754
+ const t = theme();
743
755
  const { checked, disabled } = options;
744
756
  const isDisabled = disabled === true;
745
757
  const toggle = () => {
@@ -747,23 +759,23 @@ export function switchControl(options) {
747
759
  return;
748
760
  options.onChange(!checked);
749
761
  };
750
- const trackBg = isDisabled ? '#1e293b' : checked ? '#166534' : '#334155';
751
- const thumbBg = isDisabled ? '#475569' : checked ? '#22c55e' : '#94a3b8';
762
+ const trackBg = isDisabled ? t.colors.switchTrackDisabled : checked ? t.colors.switchTrackOn : t.colors.border;
763
+ const thumbBg = isDisabled ? t.colors.switchThumbDisabled : checked ? t.colors.switchThumbOn : t.colors.switchThumbOff;
752
764
  const track = box({
753
- width: 36, height: 20, borderRadius: 10,
765
+ width: 36, height: 20, borderRadius: t.radii.lg,
754
766
  backgroundColor: trackBg,
755
767
  flexDirection: 'row', alignItems: 'center',
756
768
  paddingLeft: checked ? 18 : 2,
757
769
  paddingRight: checked ? 2 : 18,
758
- }, [box({ width: 16, height: 16, borderRadius: 8, backgroundColor: thumbBg }, [])]);
770
+ }, [box({ width: 16, height: 16, borderRadius: t.radii.md, backgroundColor: thumbBg }, [])]);
759
771
  const children = [track];
760
772
  if (options.label) {
761
773
  children.push(box({ flexGrow: 1, minWidth: 0 }, [
762
- bodyText({ text: options.label, font: '13px Inter', lineHeight: 18, color: isDisabled ? '#64748b' : '#e2e8f0' }),
774
+ bodyText({ text: options.label, font: font('', 'base'), lineHeight: lineHeight('base'), color: isDisabled ? t.colors.textDisabled : t.colors.text }),
763
775
  ]));
764
776
  }
765
777
  return box({
766
- flexDirection: 'row', gap: 10, alignItems: 'center', minWidth: 0,
778
+ flexDirection: 'row', gap: t.spacing.md, alignItems: 'center', minWidth: 0,
767
779
  cursor: isDisabled ? 'not-allowed' : 'pointer',
768
780
  semantic: { role: 'switch', ariaLabel: options.label ?? 'Toggle', ariaSelected: checked, ariaDisabled: isDisabled },
769
781
  onClick: toggle,
@@ -772,19 +784,20 @@ export function switchControl(options) {
772
784
  }, children);
773
785
  }
774
786
  export function textarea(value, placeholder = '', options = {}) {
787
+ const t = theme();
775
788
  const disabled = options.disabled === true;
776
789
  const focused = !disabled && options.focused === true;
777
790
  const rows = options.rows ?? 4;
778
- const minH = rows * 18 + 16;
779
- const valueColor = disabled ? '#64748b' : '#e2e8f0';
780
- const placeholderColor = disabled ? '#475569' : '#64748b';
791
+ const minH = rows * lineHeight('base') + t.spacing.xl;
792
+ const valueColor = disabled ? t.colors.textDisabled : t.colors.text;
793
+ const placeholderColor = disabled ? t.colors.borderMuted : t.colors.textDisabled;
781
794
  const showPlaceholder = value.length === 0;
782
795
  return box({
783
796
  flexDirection: 'column',
784
- paddingLeft: 10, paddingRight: 10, paddingTop: 8, paddingBottom: 8,
785
- borderColor: disabled ? '#475569' : focused ? '#38bdf8' : '#334155',
786
- borderWidth: 1, borderRadius: 8,
787
- backgroundColor: disabled ? '#0f172a' : focused ? '#111827' : undefined,
797
+ paddingLeft: t.spacing.md, paddingRight: t.spacing.md, paddingTop: 8, paddingBottom: 8,
798
+ borderColor: disabled ? t.colors.borderMuted : focused ? t.colors.focus : t.colors.border,
799
+ borderWidth: 1, borderRadius: t.radii.md,
800
+ backgroundColor: disabled ? t.colors.bg : focused ? t.colors.bgSubtle : undefined,
788
801
  cursor: disabled ? 'not-allowed' : 'text',
789
802
  pointerEvents: disabled ? 'none' : undefined,
790
803
  minHeight: minH,
@@ -794,13 +807,14 @@ export function textarea(value, placeholder = '', options = {}) {
794
807
  }, [
795
808
  text({
796
809
  text: showPlaceholder ? placeholder : value,
797
- font: '13px Inter', lineHeight: 18,
810
+ font: font('', 'base'), lineHeight: lineHeight('base'),
798
811
  color: showPlaceholder ? placeholderColor : valueColor,
799
812
  whiteSpace: 'pre-wrap',
800
813
  }),
801
814
  ]);
802
815
  }
803
816
  export function slider(options) {
817
+ const t = theme();
804
818
  const { value, onChange, disabled } = options;
805
819
  const isDisabled = disabled === true;
806
820
  const min = options.min ?? 0;
@@ -808,16 +822,16 @@ export function slider(options) {
808
822
  const range = max - min || 1;
809
823
  const pct = Math.max(0, Math.min(100, ((value - min) / range) * 100));
810
824
  const track = box({
811
- height: 6, borderRadius: 3, backgroundColor: '#334155',
825
+ height: 6, borderRadius: 3, backgroundColor: t.colors.border,
812
826
  flexDirection: 'row', overflow: 'hidden',
813
- }, [box({ width: pct, height: 6, backgroundColor: isDisabled ? '#475569' : '#2563eb' }, [])]);
827
+ }, [box({ width: pct, height: 6, backgroundColor: isDisabled ? t.colors.borderMuted : t.colors.accent }, [])]);
814
828
  const topRow = [];
815
829
  if (options.label) {
816
830
  topRow.push(box({ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 8, minWidth: 0 }, [
817
831
  box({ flexGrow: 1, minWidth: 0 }, [
818
- bodyText({ text: options.label, font: '12px Inter', lineHeight: 16, color: '#94a3b8' }),
832
+ bodyText({ text: options.label, font: font('', 'small'), lineHeight: lineHeight('small'), color: t.colors.textMuted }),
819
833
  ]),
820
- text({ text: String(Math.round(value)), font: '12px Inter', lineHeight: 16, color: '#94a3b8' }),
834
+ text({ text: String(Math.round(value)), font: font('', 'small'), lineHeight: lineHeight('small'), color: t.colors.textMuted }),
821
835
  ]));
822
836
  }
823
837
  const handleClick = isDisabled
@@ -828,43 +842,45 @@ export function slider(options) {
828
842
  onChange(Math.round(min + fraction * range));
829
843
  };
830
844
  return box({
831
- flexDirection: 'column', gap: 6,
845
+ flexDirection: 'column', gap: t.spacing.sm,
832
846
  cursor: isDisabled ? 'not-allowed' : 'pointer',
833
847
  semantic: { role: 'slider', ariaLabel: options.label ?? 'Slider', ariaDisabled: isDisabled },
834
848
  onClick: handleClick,
835
849
  }, [...topRow, track]);
836
850
  }
837
851
  export function accordion(items, options = {}) {
852
+ const t = theme();
838
853
  const expandedIds = options.expandedIds ?? new Set();
839
854
  return box({
840
855
  flexDirection: 'column',
841
- borderWidth: 1, borderColor: '#334155', borderRadius: 10, overflow: 'hidden',
856
+ borderWidth: 1, borderColor: t.colors.border, borderRadius: t.radii.lg, overflow: 'hidden',
842
857
  }, items.map((item, i) => {
843
858
  const expanded = expandedIds.has(item.id);
844
859
  const header = box({
845
860
  flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 8, minWidth: 0,
846
- paddingLeft: 14, paddingRight: 14, paddingTop: 10, paddingBottom: 10,
847
- backgroundColor: '#0f172a', cursor: 'pointer',
848
- borderTop: i > 0 ? 1 : 0, borderColor: '#334155',
861
+ paddingLeft: t.spacing.lg, paddingRight: t.spacing.lg, paddingTop: t.spacing.md, paddingBottom: t.spacing.md,
862
+ backgroundColor: t.colors.bg, cursor: 'pointer',
863
+ borderTop: i > 0 ? 1 : 0, borderColor: t.colors.border,
849
864
  semantic: { role: 'button', ariaLabel: item.title, ariaExpanded: expanded },
850
865
  onClick: () => options.onToggle?.(item.id),
851
866
  }, [
852
867
  box({ flexGrow: 1, minWidth: 0 }, [
853
- bodyText({ text: item.title, font: 'bold 13px Inter', lineHeight: 18, color: '#e2e8f0' }),
868
+ bodyText({ text: item.title, font: font('bold', 'base'), lineHeight: lineHeight('base'), color: t.colors.text }),
854
869
  ]),
855
- text({ text: expanded ? '▲' : '▼', font: '10px Inter', lineHeight: 14, color: '#94a3b8' }),
870
+ text({ text: expanded ? '▲' : '▼', font: `10px ${t.typography.fontFamily}`, lineHeight: 14, color: t.colors.textMuted }),
856
871
  ]);
857
872
  if (!expanded)
858
873
  return header;
859
874
  return box({ flexDirection: 'column' }, [
860
875
  header,
861
- box({ padding: 14, backgroundColor: '#020617', borderTop: 1, borderColor: '#334155' }, [item.content]),
876
+ box({ padding: t.spacing.lg, backgroundColor: t.colors.bgAlt, borderTop: 1, borderColor: t.colors.border }, [item.content]),
862
877
  ]);
863
878
  }));
864
879
  }
865
880
  export function sheet(options) {
866
881
  if (!options.open)
867
882
  return box({ display: 'none' }, []);
883
+ const t = theme();
868
884
  const side = options.side ?? 'right';
869
885
  const isVertical = side === 'left' || side === 'right';
870
886
  const panelWidth = isVertical ? (options.width ?? 320) : undefined;
@@ -874,24 +890,24 @@ export function sheet(options) {
874
890
  const headerChildren = [];
875
891
  if (options.title) {
876
892
  headerChildren.push(box({ flexGrow: 1, minWidth: 0 }, [
877
- bodyText({ text: options.title, font: 'bold 16px Inter', lineHeight: 20, color: '#f8fafc' }),
893
+ bodyText({ text: options.title, font: font('bold', 'heading'), lineHeight: lineHeight('heading'), color: t.colors.textHeading }),
878
894
  ]));
879
895
  }
880
896
  if (options.onClose) {
881
897
  headerChildren.push(box({
882
898
  paddingLeft: 8, paddingRight: 8, paddingTop: 4, paddingBottom: 4,
883
- borderRadius: 6, cursor: 'pointer',
899
+ borderRadius: t.spacing.sm, cursor: 'pointer',
884
900
  semantic: { role: 'button', ariaLabel: 'Close' },
885
901
  onClick: options.onClose,
886
- }, [text({ text: '✕', font: '12px Inter', lineHeight: 14, color: '#94a3b8' })]));
902
+ }, [text({ text: '✕', font: font('', 'small'), lineHeight: 14, color: t.colors.textMuted })]));
887
903
  }
888
904
  header.push(box({ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 8, minWidth: 0, paddingBottom: 12 }, headerChildren));
889
905
  }
890
906
  const panel = box({
891
907
  flexDirection: 'column',
892
908
  width: panelWidth, height: panelHeight,
893
- padding: 16, backgroundColor: '#0f172a',
894
- borderColor: '#334155',
909
+ padding: t.spacing.xl, backgroundColor: t.colors.bg,
910
+ borderColor: t.colors.border,
895
911
  ...(side === 'left' ? { borderRight: 1 } : {}),
896
912
  ...(side === 'right' ? { borderLeft: 1 } : {}),
897
913
  ...(side === 'top' ? { borderBottom: 1 } : {}),