@geometra/ui 1.48.0 → 1.52.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.d.ts +8 -60
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +222 -206
- package/dist/index.js.map +1 -1
- package/dist/theme.d.ts +159 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +127 -0
- package/dist/theme.js.map +1 -0
- package/package.json +2 -2
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 = '
|
|
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:
|
|
39
|
-
backgroundColor:
|
|
42
|
+
borderRadius: t.radii.md,
|
|
43
|
+
backgroundColor: t.colors.accent,
|
|
40
44
|
cursor: 'pointer',
|
|
41
45
|
onClick,
|
|
42
|
-
}, [text({ text: label, font: '
|
|
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
|
|
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 ?
|
|
67
|
-
const placeholderColor = disabled ?
|
|
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:
|
|
91
|
+
children.push(box({ width: 1.5, minHeight: 14, backgroundColor: t.colors.focus }, []));
|
|
84
92
|
}
|
|
85
|
-
children.push(text({ text: placeholder, font:
|
|
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:
|
|
100
|
+
children.push(text({ text: beforeSel, font: f, lineHeight: lh, color: valueColor }));
|
|
93
101
|
}
|
|
94
|
-
children.push(box({ backgroundColor:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
142
|
-
paddingRight:
|
|
149
|
+
paddingLeft: t.spacing.md,
|
|
150
|
+
paddingRight: t.spacing.md,
|
|
143
151
|
paddingTop: 8,
|
|
144
152
|
paddingBottom: 8,
|
|
145
|
-
borderColor: disabled ?
|
|
153
|
+
borderColor: disabled ? t.colors.borderMuted : focused ? t.colors.focus : t.colors.border,
|
|
146
154
|
borderWidth: 1,
|
|
147
|
-
borderRadius:
|
|
155
|
+
borderRadius: t.radii.md,
|
|
148
156
|
cursor: disabled ? 'not-allowed' : 'text',
|
|
149
157
|
pointerEvents: disabled ? 'none' : undefined,
|
|
150
|
-
backgroundColor: disabled ?
|
|
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
|
-
|
|
165
|
-
|
|
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:
|
|
172
|
-
padding:
|
|
173
|
-
borderRadius:
|
|
174
|
-
borderColor:
|
|
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:
|
|
186
|
+
backgroundColor: t.colors.bg,
|
|
177
187
|
semantic: { role: 'dialog', ariaLabel: title },
|
|
178
188
|
}, [
|
|
179
|
-
bodyText({ text: title, font: 'bold
|
|
180
|
-
bodyText({ text: body, font: '
|
|
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 ?
|
|
188
|
-
const bg = disabled ?
|
|
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:
|
|
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:
|
|
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:
|
|
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: '
|
|
222
|
-
lineHeight:
|
|
223
|
-
color: disabled ?
|
|
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 ?
|
|
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:
|
|
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:
|
|
264
|
+
borderRadius: t.radii.md,
|
|
253
265
|
borderColor,
|
|
254
266
|
borderWidth: 1,
|
|
255
|
-
backgroundColor:
|
|
267
|
+
backgroundColor: t.colors.bgSubtle,
|
|
256
268
|
alignItems: 'center',
|
|
257
269
|
justifyContent: 'center',
|
|
258
270
|
}, checked
|
|
259
|
-
? [box({ width: 8, height: 8, borderRadius:
|
|
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: '
|
|
265
|
-
lineHeight:
|
|
266
|
-
color: disabled ?
|
|
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:
|
|
289
|
+
gap: t.spacing.md,
|
|
277
290
|
semantic: { role: 'tablist' },
|
|
278
291
|
}, [
|
|
279
292
|
box({
|
|
280
293
|
flexDirection: 'row',
|
|
281
|
-
gap:
|
|
294
|
+
gap: t.spacing.sm,
|
|
282
295
|
flexWrap: 'wrap',
|
|
283
296
|
}, items.map((item, idx) => box({
|
|
284
|
-
paddingLeft:
|
|
285
|
-
paddingRight:
|
|
286
|
-
paddingTop:
|
|
287
|
-
paddingBottom:
|
|
288
|
-
borderRadius:
|
|
289
|
-
borderColor: idx === activeIndex ?
|
|
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 ?
|
|
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: '
|
|
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:
|
|
310
|
+
borderColor: t.colors.border,
|
|
298
311
|
borderWidth: 1,
|
|
299
|
-
borderRadius:
|
|
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 =
|
|
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
|
|
323
|
-
lineHeight:
|
|
324
|
-
color: s.
|
|
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: '
|
|
330
|
-
lineHeight:
|
|
331
|
-
color: s.
|
|
337
|
+
font: font('', 'base'),
|
|
338
|
+
lineHeight: lineHeight('base'),
|
|
339
|
+
color: s.text,
|
|
332
340
|
}));
|
|
333
341
|
const row = [
|
|
334
|
-
box({ flexDirection: 'column', gap:
|
|
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:
|
|
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: '
|
|
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:
|
|
361
|
+
borderRadius: t.radii.lg,
|
|
354
362
|
borderWidth: 1,
|
|
355
363
|
borderColor: s.border,
|
|
356
|
-
backgroundColor: s.
|
|
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
|
|
374
|
+
const t = theme();
|
|
367
375
|
return box({
|
|
368
376
|
flexDirection: 'column',
|
|
369
377
|
gap: 2,
|
|
370
|
-
padding:
|
|
371
|
-
borderRadius:
|
|
378
|
+
padding: t.spacing.sm,
|
|
379
|
+
borderRadius: t.radii.lg,
|
|
372
380
|
borderWidth: 1,
|
|
373
|
-
borderColor:
|
|
374
|
-
backgroundColor:
|
|
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:
|
|
383
|
-
paddingRight:
|
|
390
|
+
paddingLeft: t.spacing.md,
|
|
391
|
+
paddingRight: t.spacing.md,
|
|
384
392
|
paddingTop: 8,
|
|
385
393
|
paddingBottom: 8,
|
|
386
|
-
borderRadius:
|
|
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: '
|
|
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: '
|
|
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:
|
|
407
|
-
borderRadius:
|
|
415
|
+
padding: t.spacing.sm,
|
|
416
|
+
borderRadius: t.radii.lg,
|
|
408
417
|
borderWidth: 1,
|
|
409
|
-
borderColor:
|
|
410
|
-
backgroundColor:
|
|
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 ?
|
|
422
|
+
const color = item.disabled ? t.colors.borderMuted : item.danger ? t.colors.danger : t.colors.text;
|
|
414
423
|
return box({
|
|
415
|
-
paddingLeft:
|
|
416
|
-
paddingRight:
|
|
424
|
+
paddingLeft: t.spacing.md,
|
|
425
|
+
paddingRight: t.spacing.md,
|
|
417
426
|
paddingTop: 8,
|
|
418
427
|
paddingBottom: 8,
|
|
419
|
-
borderRadius:
|
|
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: '
|
|
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:
|
|
444
|
-
paddingRight:
|
|
453
|
+
paddingLeft: t.spacing.md,
|
|
454
|
+
paddingRight: t.spacing.md,
|
|
445
455
|
paddingTop: 8,
|
|
446
456
|
paddingBottom: 8,
|
|
447
|
-
borderRadius:
|
|
457
|
+
borderRadius: t.radii.md,
|
|
448
458
|
borderWidth: 1,
|
|
449
|
-
borderColor:
|
|
450
|
-
backgroundColor:
|
|
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: '
|
|
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:
|
|
461
|
-
lineHeight:
|
|
462
|
-
color:
|
|
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:
|
|
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:
|
|
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
|
|
488
|
-
const divider = box({ height: 1, backgroundColor:
|
|
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:
|
|
493
|
-
paddingBottom:
|
|
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: '
|
|
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:
|
|
525
|
+
gap: t.spacing.sm,
|
|
514
526
|
minWidth: 0,
|
|
515
|
-
paddingLeft: 8 + depth *
|
|
516
|
-
paddingTop:
|
|
517
|
-
paddingBottom:
|
|
518
|
-
borderRadius:
|
|
519
|
-
backgroundColor: selected ?
|
|
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:
|
|
547
|
+
font: `10px ${t.typography.fontFamily}`,
|
|
536
548
|
lineHeight: 14,
|
|
537
|
-
color:
|
|
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: '
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
585
|
-
borderColor:
|
|
586
|
-
backgroundColor:
|
|
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
|
|
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:
|
|
605
|
-
}, [bodyText({ text: label, font:
|
|
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 ??
|
|
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 ??
|
|
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
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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 =
|
|
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
|
|
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: '
|
|
653
|
+
content.push(bodyText({ text: message, font: font('', 'base'), lineHeight: lineHeight('base'), color: s.text }));
|
|
647
654
|
const body = [
|
|
648
|
-
text({ text:
|
|
649
|
-
box({ flexDirection: 'column', gap:
|
|
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:
|
|
661
|
+
borderRadius: t.spacing.sm, cursor: 'pointer',
|
|
655
662
|
semantic: { role: 'button', ariaLabel: 'Dismiss' },
|
|
656
663
|
onClick: options.onDismiss,
|
|
657
|
-
}, [text({ text: '✕', font: '
|
|
664
|
+
}, [text({ text: '✕', font: font('', 'small'), lineHeight: 14, color: s.text })]));
|
|
658
665
|
}
|
|
659
666
|
return box({
|
|
660
|
-
flexDirection: 'row', alignItems: 'flex-start', gap:
|
|
661
|
-
padding: 12, borderRadius:
|
|
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: '
|
|
680
|
+
bodyText({ text: options.label, font: font('', 'small'), lineHeight: lineHeight('small'), color: t.colors.textMuted }),
|
|
673
681
|
]),
|
|
674
|
-
text({ text: `${Math.round(clamped)}%`, font: '
|
|
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:
|
|
678
|
-
box({ flexGrow: Math.max(clamped, 0.001), minWidth: 0, height: 6, borderRadius: 3, backgroundColor:
|
|
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:
|
|
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 ??
|
|
691
|
-
backgroundColor:
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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:
|
|
731
|
+
borderRadius: t.spacing.sm,
|
|
721
732
|
borderWidth: active ? 1 : 0,
|
|
722
|
-
borderColor: active ?
|
|
723
|
-
backgroundColor: active ?
|
|
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: '
|
|
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:
|
|
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 ?
|
|
751
|
-
const thumbBg = isDisabled ?
|
|
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:
|
|
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:
|
|
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: '
|
|
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:
|
|
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 *
|
|
779
|
-
const valueColor = disabled ?
|
|
780
|
-
const placeholderColor = disabled ?
|
|
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:
|
|
785
|
-
borderColor: disabled ?
|
|
786
|
-
borderWidth: 1, borderRadius:
|
|
787
|
-
backgroundColor: disabled ?
|
|
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: '
|
|
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:
|
|
825
|
+
height: 6, borderRadius: 3, backgroundColor: t.colors.border,
|
|
812
826
|
flexDirection: 'row', overflow: 'hidden',
|
|
813
|
-
}, [box({ width: pct, height: 6, backgroundColor: isDisabled ?
|
|
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: '
|
|
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: '
|
|
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:
|
|
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:
|
|
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:
|
|
847
|
-
backgroundColor:
|
|
848
|
-
borderTop: i > 0 ? 1 : 0, borderColor:
|
|
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
|
|
868
|
+
bodyText({ text: item.title, font: font('bold', 'base'), lineHeight: lineHeight('base'), color: t.colors.text }),
|
|
854
869
|
]),
|
|
855
|
-
text({ text: expanded ? '▲' : '▼', font:
|
|
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:
|
|
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
|
|
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:
|
|
899
|
+
borderRadius: t.spacing.sm, cursor: 'pointer',
|
|
884
900
|
semantic: { role: 'button', ariaLabel: 'Close' },
|
|
885
901
|
onClick: options.onClose,
|
|
886
|
-
}, [text({ text: '✕', font: '
|
|
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:
|
|
894
|
-
borderColor:
|
|
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 } : {}),
|