@silverpoint/core 0.1.1 → 0.3.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/ui.js ADDED
@@ -0,0 +1,715 @@
1
+ import { round2, roundPathData, arcPath, circlePath, diagnose, resolveSeed, DEFAULTS } from './chunk-I6BAZ7MJ.js';
2
+
3
+ // src/ui/catalog.ts
4
+ var BATCH = {
5
+ Button: "B1",
6
+ Input: "B1",
7
+ Checkbox: "B1",
8
+ Switch: "B1",
9
+ Card: "B1",
10
+ Divider: "B1",
11
+ RadioGroup: "B2",
12
+ Segmented: "B2",
13
+ Tabs: "B2",
14
+ Slider: "B2",
15
+ Rate: "B2"
16
+ };
17
+ var row = (name, group, states) => Object.freeze({
18
+ name,
19
+ batch: BATCH[name] ?? "B3",
20
+ slug: name.replace(/[A-Z]/g, (m, i) => (i ? "-" : "") + m.toLowerCase()),
21
+ component: `Sp${name}`,
22
+ group,
23
+ states: Object.freeze([...states])
24
+ });
25
+ var UI_COMPONENTS = /* @__PURE__ */ Object.freeze([
26
+ row("Button", "actions", ["default", "primary", "danger", "disabled"]),
27
+ row("Input", "data-entry", ["empty", "filled", "invalid", "disabled"]),
28
+ row("Checkbox", "data-entry", ["unchecked", "checked", "indeterminate", "disabled"]),
29
+ row("RadioGroup", "data-entry", ["selected", "disabled-item"]),
30
+ row("Switch", "data-entry", ["off", "on", "disabled"]),
31
+ row("Slider", "data-entry", ["marks", "disabled"]),
32
+ row("Rate", "data-entry", ["three", "read-only"]),
33
+ row("Segmented", "data-entry", ["first", "middle"]),
34
+ row("Tabs", "navigation", ["first", "disabled-tab"]),
35
+ row("Steps", "navigation", ["current", "error"]),
36
+ row("Card", "data-display", ["plain", "titled"]),
37
+ row("Tag", "data-display", ["tone-1", "closable"]),
38
+ row("Badge", "data-display", ["count", "dot", "overflow"]),
39
+ row("Divider", "data-display", ["plain", "text"]),
40
+ row("Progress", "feedback", ["line", "circle", "indeterminate"]),
41
+ row("Alert", "feedback", ["info", "success", "warning", "error"]),
42
+ row("Skeleton", "feedback", ["paragraph", "avatar"])
43
+ ]);
44
+
45
+ // src/ui/value.ts
46
+ var DEFAULT_RANGE = { min: 0, max: 100, step: 1 };
47
+ function warn(component, property, message) {
48
+ if (process.env.NODE_ENV !== "production") diagnose("SP017", component, { property, message });
49
+ }
50
+ var tidy = (x) => Number.parseFloat(x.toPrecision(12));
51
+ function uiRange(range, component = "UiValue") {
52
+ const { min, max, step } = range;
53
+ const stepOk = step === void 0 || Number.isFinite(step) && step > 0;
54
+ if (Number.isFinite(min) && Number.isFinite(max) && min < max && stepOk) return range;
55
+ warn(component, "min", `The range ${min}..${max} step ${step} is not valid; 0..100 step 1 is used.`);
56
+ return DEFAULT_RANGE;
57
+ }
58
+ function uiValue(value, range, component = "UiValue") {
59
+ const valid = uiRange(range, component);
60
+ const { min, max, step } = valid;
61
+ let next = Number.isFinite(value) ? Math.min(Math.max(value, min), max) : min;
62
+ if (step !== void 0) {
63
+ const steps = Math.min(Math.round((next - min) / step), Math.floor(tidy((max - min) / step)));
64
+ next = tidy(min + steps * step);
65
+ }
66
+ const corrected = valid !== range || next !== value;
67
+ if (next !== value) warn(component, "value", `${value} is drawn as ${next}.`);
68
+ return { value: next, fraction: round2((next - min) / (max - min)), corrected };
69
+ }
70
+ function uiRateCount(count, component = "SpRate") {
71
+ if (count === void 0) return 5;
72
+ const next = Number.isFinite(count) ? Math.min(Math.max(Math.round(count), 1), 10) : 5;
73
+ if (next !== count) warn(component, "count", `${count} is drawn as ${next}.`);
74
+ return next;
75
+ }
76
+
77
+ // src/ui/progress.ts
78
+ var UI_PROGRESS_VIEWBOX = 100;
79
+ function uiProgressArc(fraction, stroke) {
80
+ const centre = UI_PROGRESS_VIEWBOX / 2;
81
+ const radius = centre - stroke / 2;
82
+ const frame2 = { cx: centre, cy: centre, radius };
83
+ const f = Number.isFinite(fraction) ? Math.min(Math.max(fraction, 0), 1) : 0;
84
+ return {
85
+ track: { d: roundPathData(circlePath(centre, centre, radius)), role: "encoding", part: "rule" },
86
+ fill: { d: roundPathData(arcPath(frame2, radius, 0, f * 2 * Math.PI)), role: "encoding", part: "ink" }
87
+ };
88
+ }
89
+
90
+ // src/ui/items.ts
91
+ function uiItems(items, component) {
92
+ const seen = /* @__PURE__ */ new Set();
93
+ const kept = [];
94
+ for (const item of items) {
95
+ if (item.key !== "" && !seen.has(item.key)) {
96
+ seen.add(item.key);
97
+ kept.push(item);
98
+ } else if (process.env.NODE_ENV !== "production") {
99
+ const why = item.key === "" ? "An item has an empty key" : `The key "${item.key}" repeats`;
100
+ diagnose("SP019", component, { property: "items", message: `${why}; that item is skipped.` });
101
+ }
102
+ }
103
+ return kept.length === items.length ? items : kept;
104
+ }
105
+ function uiSelectedKey(items, value, fallback) {
106
+ if (items.some((item) => item.key === value && item.disabled !== true)) return value;
107
+ return fallback === "first" ? items.find((item) => item.disabled !== true)?.key ?? null : null;
108
+ }
109
+
110
+ // src/ui/steps.ts
111
+ function uiSteps(items, current, component = "SpSteps") {
112
+ const steps = uiItems(items, component);
113
+ if (steps.length === 0) return [];
114
+ const last = steps.length - 1;
115
+ const at = Number.isFinite(current) ? Math.min(Math.max(Math.round(current), 0), last) : 0;
116
+ if (at !== current && process.env.NODE_ENV !== "production") {
117
+ diagnose("SP017", component, { property: "current", message: `${current} is drawn as ${at}.` });
118
+ }
119
+ const statuses = steps.map((item, i) => item.status ?? (i < at ? "finish" : i === at ? "process" : "wait"));
120
+ return steps.map((item, i) => ({ key: item.key, status: statuses[i], connector: i < last ? statuses[i + 1] : null, current: i === at }));
121
+ }
122
+
123
+ // src/ui/keyboard.ts
124
+ function direction(key, orientation, dir) {
125
+ if (orientation === "vertical") return key === "ArrowDown" ? 1 : key === "ArrowUp" ? -1 : 0;
126
+ const forward = key === "ArrowRight" ? 1 : key === "ArrowLeft" ? -1 : 0;
127
+ return dir === "rtl" ? -forward : forward;
128
+ }
129
+ function uiRovingKey(state, key, orientation, dir) {
130
+ const { index, count, disabled } = state;
131
+ const enabled = (i2) => disabled[i2] !== true;
132
+ if (key === "Home" || key === "End") {
133
+ const step2 = key === "Home" ? 1 : -1;
134
+ for (let n = 0, i2 = key === "Home" ? 0 : count - 1; n < count; n++, i2 += step2) if (enabled(i2)) return i2;
135
+ return index;
136
+ }
137
+ const step = direction(key, orientation, dir);
138
+ if (step === 0) return index;
139
+ let i = index < 0 || index >= count ? step > 0 ? -1 : count : index;
140
+ for (let n = 0; n < count; n++) {
141
+ i = ((i + step) % count + count) % count;
142
+ if (enabled(i)) return i;
143
+ }
144
+ return index;
145
+ }
146
+ var ROVING = /* @__PURE__ */ new Set(["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"]);
147
+ var uiIsRovingKey = (key) => ROVING.has(key);
148
+
149
+ // src/ui/roving.ts
150
+ function uiRovingFocus(event, root2, selector, orientation, activate) {
151
+ const { key } = event;
152
+ if (!uiIsRovingKey(key)) return;
153
+ const items = Array.from(root2.querySelectorAll(selector));
154
+ const index = items.indexOf(root2.ownerDocument?.activeElement);
155
+ if (index < 0) return;
156
+ const axis = orientation !== "both" ? orientation : key === "ArrowUp" || key === "ArrowDown" ? "vertical" : "horizontal";
157
+ const dir = root2.closest("[dir]")?.getAttribute("dir") === "rtl" ? "rtl" : "ltr";
158
+ const next = uiRovingKey({ index, count: items.length, disabled: items.map((item) => item.disabled === true) }, key, axis, dir);
159
+ if (next === index) return;
160
+ event.preventDefault();
161
+ items[next].focus();
162
+ if (activate) items[next].click();
163
+ }
164
+
165
+ // src/ui/frame.ts
166
+ var UI_FRAME_KINDS = /* @__PURE__ */ Object.freeze({
167
+ control: Object.freeze({ size: 24, slice: 8, radius: 2 }),
168
+ pill: Object.freeze({ size: 36, slice: 12, radius: 12 }),
169
+ box: Object.freeze({ size: 16, slice: 8, radius: 2 }),
170
+ card: Object.freeze({ size: 48, slice: 16, radius: 2 }),
171
+ round: Object.freeze({ size: 24, slice: 12, radius: 12 })
172
+ });
173
+ var MAX_VARIANTS = 6;
174
+ function uiFrameVariant(seed, id, variants) {
175
+ const n = Number.isFinite(variants) ? Math.min(Math.max(Math.floor(variants), 1), MAX_VARIANTS) : 1;
176
+ if (seed === void 0 && !id) return 0;
177
+ return resolveSeed(seed, id ?? "") % n;
178
+ }
179
+ var INSET = 0.5;
180
+ function roundedSquare(size, radius) {
181
+ const lo = INSET;
182
+ const hi = size - INSET;
183
+ const r = Math.min(radius, (hi - lo) / 2);
184
+ const arc = (x, y) => `A${r},${r},0,0,1,${x},${y}`;
185
+ return `M${lo + r},${lo}H${hi - r}${arc(hi, lo + r)}V${hi - r}${arc(hi - r, hi)}H${lo + r}${arc(lo, hi - r)}V${lo + r}${arc(lo + r, lo)}Z`;
186
+ }
187
+ function uiFrameOutline(kind) {
188
+ const { size, radius } = UI_FRAME_KINDS[kind];
189
+ const box = { x: 0, y: 0, width: size, height: size };
190
+ return {
191
+ viewBox: box,
192
+ plot: box,
193
+ strokes: [{ d: roundPathData(roundedSquare(size, radius)), role: "ornament", part: "ink" }],
194
+ labels: [],
195
+ hitAreas: [],
196
+ defs: []
197
+ };
198
+ }
199
+
200
+ // src/ui/names.ts
201
+ function uiRequireName(component, text2, label) {
202
+ if (process.env.NODE_ENV === "production") return;
203
+ if (text2?.trim() || label?.trim()) return;
204
+ diagnose("SP018", component, { property: "label" });
205
+ }
206
+
207
+ // src/ui/tokens.ts
208
+ var UI_TOKEN_DEFAULTS = /* @__PURE__ */ Object.freeze({
209
+ frame: "css",
210
+ frameVariants: 1,
211
+ controlHeight: Object.freeze({ sm: 24, md: 32, lg: 40 }),
212
+ radius: 2,
213
+ focusWidth: 2,
214
+ tone: Object.freeze({ selected: 3, primary: 2, danger: 4, disabled: 1, alertError: 1 })
215
+ });
216
+ var MIN_HEIGHT = 24;
217
+ var MIN_FOCUS = 2;
218
+ var within = (value, lo, hi, fallback) => Number.isFinite(value) ? Math.min(Math.max(value, lo), hi) : fallback;
219
+ function resolveUiTokens(ground) {
220
+ const ui = ground.ui ?? UI_TOKEN_DEFAULTS;
221
+ const inked = ui.frame === "inked" && ground.tonalMechanism === "hatch";
222
+ const { sm, md, lg } = ui.controlHeight;
223
+ return {
224
+ frame: inked ? "inked" : "css",
225
+ frameVariants: inked ? Math.round(within(ui.frameVariants, 1, 6, 1)) : 1,
226
+ controlHeight: {
227
+ sm: within(sm, MIN_HEIGHT, Infinity, 24),
228
+ md: within(md, MIN_HEIGHT, Infinity, 32),
229
+ lg: within(lg, MIN_HEIGHT, Infinity, 40)
230
+ },
231
+ radius: within(ui.radius, 0, Infinity, 2),
232
+ focusWidth: within(ui.focusWidth, MIN_FOCUS, Infinity, 2),
233
+ tone: ui.tone
234
+ };
235
+ }
236
+
237
+ // src/ui/tone.ts
238
+ var RAD = Math.PI / 180;
239
+ var EPSILON = 1e-6;
240
+ var TILE_SPAN = 24;
241
+ function family(gap, angle) {
242
+ const sin = Math.sin(angle * RAD);
243
+ const cos = Math.cos(angle * RAD);
244
+ const lines = Math.max(2, Math.round(TILE_SPAN / gap));
245
+ const width = Math.abs(sin) < EPSILON ? lines * gap : lines * gap / Math.abs(sin);
246
+ const height = Math.abs(cos) < EPSILON ? lines * gap : lines * gap / Math.abs(cos);
247
+ const offset = (x, y) => -sin * x + cos * y;
248
+ const corners = [offset(0, 0), offset(width, 0), offset(0, height), offset(width, height)];
249
+ const first = Math.ceil(Math.min(...corners) / gap - 0.5);
250
+ const last = Math.floor(Math.max(...corners) / gap - 0.5);
251
+ const strokes = [];
252
+ for (let k = first; k <= last; k++) {
253
+ const c = (k + 0.5) * gap;
254
+ const at = (t) => ({ x: -sin * c + cos * t, y: cos * c + sin * t, t });
255
+ const hits = [
256
+ Math.abs(cos) > EPSILON ? [(0 + sin * c) / cos, (width + sin * c) / cos] : [],
257
+ Math.abs(sin) > EPSILON ? [(0 - cos * c) / sin, (height - cos * c) / sin] : []
258
+ ].flat().map(at).filter((p) => p.x >= -EPSILON && p.x <= width + EPSILON && p.y >= -EPSILON && p.y <= height + EPSILON).sort((a, b) => a.t - b.t);
259
+ const from = hits[0];
260
+ const to = hits.at(-1);
261
+ if (!from || !to || to.t - from.t < EPSILON * 1e3) continue;
262
+ const clamp = (v, hi) => round2(Math.min(Math.max(v, 0), hi));
263
+ const w = round2(width);
264
+ const h = round2(height);
265
+ strokes.push({
266
+ d: `M${clamp(from.x, w)},${clamp(from.y, h)}L${clamp(to.x, w)},${clamp(to.y, h)}`,
267
+ role: "ornament",
268
+ part: "ink"
269
+ });
270
+ }
271
+ return { width: round2(width), height: round2(height), strokes };
272
+ }
273
+ function uiToneTile(spec) {
274
+ if (spec.style === "weight") return [];
275
+ const layers = [family(spec.gap, spec.angle)];
276
+ if (spec.style === "cross-hatch") layers.push(family(spec.gap, spec.angle + 90));
277
+ return layers;
278
+ }
279
+
280
+ // src/ui/view-kit.ts
281
+ function clean(attrs) {
282
+ const out = {};
283
+ for (const [name, value] of Object.entries(attrs)) if (value !== void 0 && value !== false) out[name] = value;
284
+ return out;
285
+ }
286
+ var el = (tag, attrs, children = [], bind) => ({
287
+ tag,
288
+ attrs: clean(attrs),
289
+ children,
290
+ ...bind ? { bind } : {}
291
+ });
292
+ var text = (value) => ({ text: value });
293
+ var slot = (name) => ({ slot: name });
294
+ function root(slug, props, r, attrs = {}) {
295
+ return {
296
+ class: ["sp-ui", `sp-${slug}`, `sp-ground-${r.ground}`, props.className].filter(Boolean).join(" "),
297
+ "data-ground": r.ground,
298
+ "data-substrate": r.substrate,
299
+ "data-mode": r.mode,
300
+ "data-size": r.size,
301
+ "data-frame": String(r.frame),
302
+ ...attrs
303
+ };
304
+ }
305
+ var frame = (kind) => el("span", { part: "sp-frame", "data-kind": kind, "aria-hidden": "true" });
306
+ var tone = (value, kind) => el("span", { part: "sp-tone", "data-tone": value, "data-kind": kind, "aria-hidden": "true" });
307
+ var UI_GLYPHS = Object.freeze({
308
+ tick: "M3.5,8.5L6.5,11.5L12.5,4.5",
309
+ dash: "M4,8H12",
310
+ cross: "M4.5,4.5L11.5,11.5M11.5,4.5L4.5,11.5",
311
+ warning: "M8,2.5L14.5,13.5H1.5Z M8,6.5V9.5 M8,11.5V11.5",
312
+ /** A radio's selection: filled by ui.css. */
313
+ dot: "M11,8A3,3 0 1,1 5,8A3,3 0 1,1 11,8Z",
314
+ /** A Rate mark (C-2). */
315
+ lozenge: "M8,1.5L14.5,8L8,14.5L1.5,8Z",
316
+ info: "M14.5,8A6.5,6.5 0 1,1 1.5,8A6.5,6.5 0 1,1 14.5,8Z M8,7.5V11.5 M8,4.75V4.75"
317
+ });
318
+ var glyph = (name) => el("svg", { part: "sp-mark", "data-glyph": name, viewBox: "0 0 16 16", "aria-hidden": "true" }, [el("path", { d: UI_GLYPHS[name] })]);
319
+ var sr = (value) => el("span", { class: "sp-ui-sr" }, [text(value)]);
320
+ var baseOf = (props) => props.id ?? props.name;
321
+ function nativeInput(type, attrs) {
322
+ return el("input", { type, class: "sp-ui-native", ...attrs }, [], "native");
323
+ }
324
+
325
+ // src/ui/view.ts
326
+ var FRAME_SLOTS = 6;
327
+ function resolveUi(props, env) {
328
+ const { provider = {}, cell = {} } = env;
329
+ const ground = props.ground ?? cell.ground ?? provider.ground ?? DEFAULTS.ground;
330
+ return {
331
+ ground: typeof ground === "string" ? ground : ground.name,
332
+ substrate: props.substrate ?? cell.substrate ?? provider.substrate ?? DEFAULTS.substrate,
333
+ mode: env.forcedPrecision ? "precision" : props.mode ?? cell.mode ?? provider.mode ?? DEFAULTS.mode,
334
+ size: props.size ?? "md",
335
+ frame: uiFrameVariant(props.seed, props.id, FRAME_SLOTS)
336
+ };
337
+ }
338
+ function uiButtonView(props, r, content) {
339
+ uiRequireName("SpButton", content.text, props.label);
340
+ const variant = props.variant ?? "default";
341
+ const link = props.href !== void 0;
342
+ const toned = props.disabled ? "disabled" : variant === "default" ? void 0 : variant;
343
+ return el(
344
+ link ? "a" : "button",
345
+ root("button", props, r, {
346
+ id: props.id,
347
+ type: link ? void 0 : props.type ?? "button",
348
+ href: link && !props.disabled ? props.href : void 0,
349
+ disabled: !link && props.disabled === true,
350
+ "aria-disabled": link && props.disabled ? "true" : void 0,
351
+ "aria-label": props.label,
352
+ "data-variant": variant,
353
+ "data-block": props.block ? "true" : void 0
354
+ }),
355
+ [
356
+ frame("control"),
357
+ ...toned ? [tone(toned, "control")] : [],
358
+ ...variant === "danger" ? [glyph("cross")] : [],
359
+ el("span", { class: toned ? "sp-ui-label sp-ui-plate" : "sp-ui-label" }, [slot("content")])
360
+ ]
361
+ );
362
+ }
363
+ function uiInputView(props, r, state) {
364
+ const base = baseOf(props);
365
+ const message = props.message ? props.message : void 0;
366
+ const messageId = message && base ? `${base}--message` : void 0;
367
+ return el("span", root("input", props, r, { "data-invalid": props.invalid ? "true" : void 0 }), [
368
+ el("span", { class: "sp-ui-box" }, [
369
+ frame("control"),
370
+ ...state.prefix ? [el("span", { class: "sp-ui-affix", "data-slot": "prefix" }, [slot("prefix")])] : [],
371
+ el(
372
+ "input",
373
+ {
374
+ class: "sp-ui-control",
375
+ id: props.id,
376
+ type: props.type ?? "text",
377
+ name: props.name,
378
+ value: state.value,
379
+ placeholder: props.placeholder,
380
+ disabled: props.disabled === true,
381
+ readonly: props.readOnly === true,
382
+ "aria-label": props.label,
383
+ "aria-invalid": props.invalid ? "true" : void 0,
384
+ "aria-describedby": messageId
385
+ },
386
+ [],
387
+ "native"
388
+ ),
389
+ ...state.suffix ? [el("span", { class: "sp-ui-affix", "data-slot": "suffix" }, [slot("suffix")])] : [],
390
+ ...props.invalid ? [glyph("warning")] : []
391
+ ]),
392
+ ...message ? [el("span", { class: "sp-ui-message", id: messageId }, [text(message)])] : []
393
+ ]);
394
+ }
395
+ function nativeCheckbox(props, checked, role) {
396
+ return el(
397
+ "input",
398
+ { type: "checkbox", class: "sp-ui-native", role, id: props.id, name: props.name, checked, disabled: props.disabled === true },
399
+ [],
400
+ "native"
401
+ );
402
+ }
403
+ function uiCheckboxView(props, r, state) {
404
+ return el("label", root("checkbox", props, r, { "data-indeterminate": props.indeterminate ? "true" : void 0 }), [
405
+ nativeCheckbox(props, state.checked),
406
+ el("span", { class: "sp-ui-box", "aria-hidden": "true" }, [frame("box"), tone("selected", "box"), glyph("tick"), glyph("dash")]),
407
+ el("span", { class: "sp-ui-label" }, [text(props.label)])
408
+ ]);
409
+ }
410
+ function uiSwitchView(props, r, state) {
411
+ return el("label", root("switch", props, r), [
412
+ nativeCheckbox(props, state.checked, "switch"),
413
+ el("span", { class: "sp-ui-track", "aria-hidden": "true" }, [frame("pill"), tone("selected", "pill"), el("span", { part: "sp-knob" })]),
414
+ el("span", { class: "sp-ui-label" }, [text(props.label)])
415
+ ]);
416
+ }
417
+ function uiCardView(props, r, slots) {
418
+ const titled = props.title !== void 0;
419
+ const titleId = titled && props.id ? `${props.id}--title` : void 0;
420
+ const header = titled || slots.extra;
421
+ return el(
422
+ titled ? "article" : "section",
423
+ root("card", props, r, { id: props.id, "aria-labelledby": titleId, "aria-label": titled && !titleId ? props.title : void 0 }),
424
+ [
425
+ frame("card"),
426
+ ...header ? [
427
+ el("div", { class: "sp-ui-card-header" }, [
428
+ ...titled ? [el(`h${props.headingLevel ?? 3}`, { class: "sp-ui-card-title", id: titleId }, [text(props.title)])] : [],
429
+ ...slots.extra ? [el("div", { class: "sp-ui-card-extra" }, [slot("extra")])] : []
430
+ ]),
431
+ el("span", { part: "sp-rule", "aria-hidden": "true" })
432
+ ] : [],
433
+ el("div", { class: "sp-ui-card-body" }, [slot("content")]),
434
+ ...slots.footer ? [el("div", { class: "sp-ui-card-footer" }, [slot("footer")])] : []
435
+ ]
436
+ );
437
+ }
438
+ function uiDividerView(props, r) {
439
+ const vertical = props.orientation === "vertical";
440
+ const rule = () => el("span", { part: "sp-rule", "aria-hidden": "true" });
441
+ return el(
442
+ "div",
443
+ root("divider", props, r, {
444
+ id: props.id,
445
+ role: "separator",
446
+ "aria-orientation": vertical ? "vertical" : void 0,
447
+ "data-orientation": vertical ? "vertical" : "horizontal",
448
+ "data-align": props.text ? props.align ?? "center" : void 0
449
+ }),
450
+ props.text ? [rule(), el("span", { class: "sp-ui-divider-text" }, [text(props.text)]), rule()] : [rule()]
451
+ );
452
+ }
453
+
454
+ // src/ui/view-b2.ts
455
+ var heighten = () => el("span", { part: "sp-heighten", "aria-hidden": "true" });
456
+ var legend = (label, forSight) => el("legend", { class: forSight ? "sp-ui-legend" : "sp-ui-legend sp-ui-sr" }, [text(label)]);
457
+ var groupName = (props) => props.name ?? (props.id ? `${props.id}--value` : void 0);
458
+ function uiRadioGroupView(props, r, state) {
459
+ const items = uiItems(props.items, "SpRadioGroup");
460
+ const value = uiSelectedKey(items, state.value, "none");
461
+ return el("fieldset", root("radio-group", props, r, { id: props.id, "data-orientation": props.orientation ?? "vertical" }), [
462
+ legend(props.label, true),
463
+ ...items.map(
464
+ (item) => el("label", { class: "sp-ui-item", "data-key": item.key, "data-disabled": item.disabled ? "true" : void 0 }, [
465
+ nativeInput("radio", {
466
+ id: props.id ? `${props.id}--${item.key}` : void 0,
467
+ name: props.name,
468
+ value: item.key,
469
+ checked: item.key === value,
470
+ disabled: item.disabled === true
471
+ }),
472
+ el("span", { class: "sp-ui-box", "aria-hidden": "true" }, [frame("round"), tone("selected", "round"), glyph("dot")]),
473
+ el("span", { class: "sp-ui-label" }, [text(item.label)])
474
+ ])
475
+ )
476
+ ]);
477
+ }
478
+ function uiSegmentedView(props, r, state) {
479
+ const items = uiItems(props.items, "SpSegmented");
480
+ const value = uiSelectedKey(items, state.value, "first");
481
+ const name = groupName(props);
482
+ return el("fieldset", root("segmented", props, r, { id: props.id, "data-block": props.block ? "true" : void 0 }), [
483
+ legend(props.label, false),
484
+ el("span", { class: "sp-ui-segments" }, [
485
+ frame("control"),
486
+ ...items.map((item) => {
487
+ const selected = item.key === value;
488
+ return el("label", { class: "sp-ui-item", "data-key": item.key, "data-disabled": item.disabled ? "true" : void 0 }, [
489
+ nativeInput("radio", { name, value: item.key, checked: selected, disabled: item.disabled === true }),
490
+ el("span", { class: "sp-ui-segment" }, [
491
+ ...selected ? [tone("selected", "control"), heighten()] : [],
492
+ el("span", { class: selected ? "sp-ui-label sp-ui-plate" : "sp-ui-label" }, [text(item.label)])
493
+ ])
494
+ ]);
495
+ })
496
+ ])
497
+ ]);
498
+ }
499
+ function uiTabsView(props, r, state) {
500
+ const items = uiItems(props.items, "SpTabs");
501
+ const value = uiSelectedKey(items, state.value, "first");
502
+ const vertical = props.orientation === "vertical";
503
+ const id = props.id;
504
+ return el("div", root("tabs", props, r, { id, "data-orientation": vertical ? "vertical" : "horizontal" }), [
505
+ el("div", { class: "sp-ui-tablist", role: "tablist", "aria-label": props.label, "aria-orientation": vertical ? "vertical" : void 0 }, [
506
+ ...items.map((item) => {
507
+ const selected = item.key === value;
508
+ return el(
509
+ "button",
510
+ {
511
+ type: "button",
512
+ class: "sp-ui-tab",
513
+ role: "tab",
514
+ id: id ? `${id}--tab-${item.key}` : void 0,
515
+ "aria-selected": selected ? "true" : "false",
516
+ "aria-controls": id ? `${id}--panel-${item.key}` : void 0,
517
+ tabindex: selected ? "0" : "-1",
518
+ disabled: item.disabled === true,
519
+ "data-key": item.key
520
+ },
521
+ [...selected ? [heighten()] : [], el("span", { class: "sp-ui-label" }, [text(item.label)])],
522
+ "native"
523
+ );
524
+ }),
525
+ el("span", { part: "sp-rule", "aria-hidden": "true" })
526
+ ]),
527
+ el("div", { class: "sp-ui-tabpanels" }, [slot("content")])
528
+ ]);
529
+ }
530
+ function uiTabPanelView(props, tabs) {
531
+ const { id } = tabs;
532
+ return el(
533
+ "div",
534
+ {
535
+ class: "sp-ui-tabpanel",
536
+ role: "tabpanel",
537
+ id: id ? `${id}--panel-${props.value}` : void 0,
538
+ "aria-labelledby": id ? `${id}--tab-${props.value}` : void 0,
539
+ tabindex: "0",
540
+ hidden: props.value !== tabs.value
541
+ },
542
+ [slot("content")]
543
+ );
544
+ }
545
+ function uiSliderView(props, r, state) {
546
+ const valid = uiRange({ min: props.min ?? 0, max: props.max ?? 100, step: props.step ?? 1 }, "SpSlider");
547
+ const { value, fraction } = uiValue(state.value, valid, "SpSlider");
548
+ const span = valid.max - valid.min;
549
+ const marks = (props.marks ?? []).filter((m) => Number.isFinite(m) && m >= valid.min && m <= valid.max);
550
+ return el("label", root("slider", props, r, { style: `--sp-ui-fraction: ${fraction}`, "data-disabled": props.disabled ? "true" : void 0 }), [
551
+ el("span", { class: "sp-ui-label" }, [text(props.label)]),
552
+ el("span", { class: "sp-ui-rail" }, [
553
+ el("span", { part: "sp-track", "aria-hidden": "true" }),
554
+ el("span", { part: "sp-fill", "aria-hidden": "true" }, [tone("selected", "pill")]),
555
+ el("span", { part: "sp-thumb sp-heighten", "aria-hidden": "true" }),
556
+ el(
557
+ "input",
558
+ {
559
+ type: "range",
560
+ class: "sp-ui-range",
561
+ id: props.id,
562
+ name: props.name,
563
+ min: String(valid.min),
564
+ max: String(valid.max),
565
+ step: String(valid.step ?? 1),
566
+ value: String(value),
567
+ disabled: props.disabled === true
568
+ },
569
+ [],
570
+ "native"
571
+ )
572
+ ]),
573
+ ...marks.length ? [
574
+ el(
575
+ "span",
576
+ { class: "sp-ui-scale", "aria-hidden": "true" },
577
+ marks.map((m) => el("span", { class: "sp-ui-scale-mark", style: `--sp-ui-at: ${round2((m - valid.min) / span)}` }, [text(String(m))]))
578
+ )
579
+ ] : []
580
+ ]);
581
+ }
582
+ function uiRateView(props, r, state) {
583
+ const count = uiRateCount(props.count);
584
+ const { value } = uiValue(state.value, { min: 0, max: count, step: 1 }, "SpRate");
585
+ const name = groupName(props);
586
+ const readOnly = props.readOnly === true;
587
+ return el(
588
+ "fieldset",
589
+ root("rate", props, r, {
590
+ id: props.id,
591
+ role: readOnly ? "radiogroup" : void 0,
592
+ "aria-readonly": readOnly ? "true" : void 0,
593
+ "data-readonly": readOnly ? "true" : void 0
594
+ }),
595
+ [
596
+ legend(props.label, false),
597
+ ...Array.from({ length: count }, (_, i) => {
598
+ const n = i + 1;
599
+ const filled = n <= value;
600
+ return el("label", { class: "sp-ui-item", "data-filled": filled ? "true" : void 0 }, [
601
+ nativeInput("radio", { name, value: String(n), checked: n === value, disabled: props.disabled === true, "aria-label": String(n) }),
602
+ el("span", { class: "sp-ui-box", "aria-hidden": "true" }, [...filled ? [tone("selected", "round")] : [], glyph("lozenge")])
603
+ ]);
604
+ })
605
+ ]
606
+ );
607
+ }
608
+
609
+ // src/ui/view-b3.ts
610
+ var closeButton = (label) => el("button", { type: "button", class: "sp-ui-close", "aria-label": label }, [glyph("cross")], "close");
611
+ function uiStepsView(props, r) {
612
+ const steps = uiSteps(props.items, props.current);
613
+ const titles = new Map(props.items.map((item) => [item.key, item]));
614
+ return el(
615
+ "ol",
616
+ root("steps", props, r, { id: props.id, "aria-label": props.label, "data-orientation": props.orientation === "vertical" ? "vertical" : "horizontal" }),
617
+ steps.map((step, i) => {
618
+ const item = titles.get(step.key);
619
+ const mark = step.status === "finish" ? glyph("tick") : step.status === "error" ? glyph("cross") : text(String(i + 1));
620
+ return el("li", { class: "sp-ui-step", "data-status": step.status, "aria-current": step.current ? "step" : void 0 }, [
621
+ el("span", { class: "sp-ui-step-mark", part: step.current ? "sp-heighten" : void 0, "aria-hidden": "true" }, [mark]),
622
+ el("span", { class: "sp-ui-step-text" }, [
623
+ el("span", { class: "sp-ui-step-title" }, [text(item.title)]),
624
+ ...item.description ? [el("span", { class: "sp-ui-step-description" }, [text(item.description)])] : []
625
+ ]),
626
+ // Exact, dashed while it leads to a step still waiting (C-4, DD-026).
627
+ ...step.connector ? [el("span", { part: "sp-connector", "data-status": step.connector, "aria-hidden": "true" })] : []
628
+ ]);
629
+ })
630
+ );
631
+ }
632
+ function uiTagView(props, r, content) {
633
+ const level = String(props.tone ?? 1);
634
+ const label = props.closeLabel ?? (content.text ? `Remove ${content.text}` : "Remove");
635
+ return el("span", root("tag", props, r, { id: props.id, "data-tone": level }), [
636
+ frame("control"),
637
+ tone(level, "control"),
638
+ el("span", { class: "sp-ui-label sp-ui-plate" }, [slot("content")]),
639
+ ...props.closable ? [closeButton(label)] : []
640
+ ]);
641
+ }
642
+ function uiBadgeView(props, r, content) {
643
+ const max = props.max ?? 99;
644
+ const count = props.count !== void 0 && Number.isFinite(props.count) ? Math.max(Math.round(props.count), 0) : void 0;
645
+ const shown = count === void 0 ? void 0 : count > max ? `${max}+` : String(count);
646
+ const dot = props.dot === true;
647
+ uiRequireName("SpBadge", content.text ?? (dot ? void 0 : shown), props.label);
648
+ return el("span", root("badge", props, r, { id: props.id }), [
649
+ el("span", { class: "sp-ui-label" }, [slot("content")]),
650
+ ...dot ? [el("span", { class: "sp-ui-badge-dot", "aria-hidden": "true" })] : shown !== void 0 ? [el("span", { class: "sp-ui-badge-count" }, [frame("pill"), el("span", { class: "sp-ui-badge-text" }, [text(shown)])])] : [],
651
+ ...props.label ? [sr(props.label)] : []
652
+ ]);
653
+ }
654
+ var RING = 8;
655
+ function uiProgressView(props, r) {
656
+ const determinate = props.value !== void 0;
657
+ const { value, fraction } = determinate ? uiValue(props.value, { min: 0, max: 100 }, "SpProgress") : { value: 0, fraction: 0 };
658
+ const circle = props.shape === "circle";
659
+ const drawing = circle ? (() => {
660
+ const arc = uiProgressArc(determinate ? fraction : 0.25, RING);
661
+ return el("svg", { class: "sp-ui-ring", viewBox: `0 0 ${UI_PROGRESS_VIEWBOX} ${UI_PROGRESS_VIEWBOX}`, "aria-hidden": "true" }, [
662
+ el("path", { class: "sp-ui-ring-track", d: arc.track.d }),
663
+ el("path", { class: "sp-ui-ring-fill", d: arc.fill.d })
664
+ ]);
665
+ })() : el("span", { class: "sp-ui-rail" }, [
666
+ el("span", { part: "sp-track", "aria-hidden": "true" }),
667
+ el("span", { part: "sp-fill", "aria-hidden": "true" }, [tone(determinate ? "selected" : "1", "pill")])
668
+ ]);
669
+ return el(
670
+ "div",
671
+ root("progress", props, r, {
672
+ id: props.id,
673
+ role: "progressbar",
674
+ "aria-label": props.label,
675
+ "aria-valuemin": "0",
676
+ "aria-valuemax": "100",
677
+ "aria-valuenow": determinate ? String(value) : void 0,
678
+ "data-shape": circle ? "circle" : "line",
679
+ "data-indeterminate": determinate ? void 0 : "true",
680
+ style: determinate ? `--sp-ui-fraction: ${fraction}` : void 0
681
+ }),
682
+ [drawing, ...determinate && props.showValue !== false ? [el("span", { class: "sp-ui-value", "aria-hidden": "true" }, [text(`${value}%`)])] : []]
683
+ );
684
+ }
685
+ var ALERT_GLYPHS = { info: "info", success: "tick", warning: "warning", error: "cross" };
686
+ function uiAlertView(props, r, state) {
687
+ const kind = props.kind ?? "info";
688
+ const error = kind === "error";
689
+ return el("div", root("alert", props, r, { id: props.id, role: error || kind === "warning" ? "alert" : "status", "data-kind": kind }), [
690
+ frame("card"),
691
+ ...error ? [tone("alertError", "card")] : [],
692
+ glyph(ALERT_GLYPHS[kind]),
693
+ el("div", { class: error ? "sp-ui-alert-body sp-ui-plate" : "sp-ui-alert-body" }, [
694
+ ...props.title ? [el("div", { class: "sp-ui-alert-title" }, [text(props.title)])] : [],
695
+ el("div", { class: "sp-ui-alert-text" }, [slot("content")])
696
+ ]),
697
+ ...state.closable ? [closeButton(props.closeLabel ?? "Close")] : []
698
+ ]);
699
+ }
700
+ function uiSkeletonView(props, r) {
701
+ const lines = props.lines !== void 0 && Number.isFinite(props.lines) ? Math.min(Math.max(Math.round(props.lines), 1), 8) : 3;
702
+ return el("div", root("skeleton", props, r, { id: props.id, "aria-busy": "true" }), [
703
+ sr(props.label ?? "Loading"),
704
+ ...props.avatar ? [el("span", { class: "sp-ui-skeleton-avatar", "aria-hidden": "true" }, [tone("1", "round")])] : [],
705
+ el(
706
+ "span",
707
+ { class: "sp-ui-skeleton-lines", "aria-hidden": "true" },
708
+ Array.from({ length: lines }, () => el("span", { class: "sp-ui-skeleton-line" }, [tone("1", "control")]))
709
+ )
710
+ ]);
711
+ }
712
+
713
+ export { UI_COMPONENTS, UI_FRAME_KINDS, UI_GLYPHS, UI_PROGRESS_VIEWBOX, UI_TOKEN_DEFAULTS, resolveUi, resolveUiTokens, uiAlertView, uiBadgeView, uiButtonView, uiCardView, uiCheckboxView, uiDividerView, uiFrameOutline, uiFrameVariant, uiInputView, uiIsRovingKey, uiItems, uiProgressArc, uiProgressView, uiRadioGroupView, uiRange, uiRateCount, uiRateView, uiRequireName, uiRovingFocus, uiRovingKey, uiSegmentedView, uiSelectedKey, uiSkeletonView, uiSliderView, uiSteps, uiStepsView, uiSwitchView, uiTabPanelView, uiTabsView, uiTagView, uiToneTile, uiValue };
714
+ //# sourceMappingURL=ui.js.map
715
+ //# sourceMappingURL=ui.js.map