@snap-rail/framework-shadcn 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +20 -2
  2. package/docs/framework-authoring.md +59 -0
  3. package/lib/fallback.js +307 -0
  4. package/lib/index.js +3036 -0
  5. package/lib/runtime.js +2827 -0
  6. package/lib/semantic.js +96 -0
  7. package/lib/session.js +48 -0
  8. package/lib/settings-pages.js +68 -0
  9. package/lib/slots.js +95 -0
  10. package/lib/types/Shell.d.ts +13 -0
  11. package/lib/types/Shell.js +17 -0
  12. package/lib/types/client.d.ts +41 -0
  13. package/lib/types/client.js +39 -0
  14. package/lib/types/fallback.d.ts +15 -0
  15. package/lib/types/fallback.js +52 -0
  16. package/lib/types/index.d.ts +17 -0
  17. package/lib/types/index.js +17 -0
  18. package/lib/types/runtime.d.ts +53 -0
  19. package/lib/types/runtime.js +75 -0
  20. package/lib/types/semantic.d.ts +55 -0
  21. package/lib/types/semantic.js +63 -0
  22. package/lib/types/session.d.ts +43 -0
  23. package/lib/types/session.js +76 -0
  24. package/lib/types/settings-dialog.d.ts +17 -0
  25. package/lib/types/settings-dialog.js +138 -0
  26. package/lib/types/settings-pages.d.ts +68 -0
  27. package/lib/types/settings-pages.js +85 -0
  28. package/lib/types/settings-plugins-page.d.ts +29 -0
  29. package/lib/types/settings-plugins-page.js +466 -0
  30. package/lib/types/settings-update-page.d.ts +17 -0
  31. package/lib/types/settings-update-page.js +124 -0
  32. package/lib/types/slots.d.ts +114 -0
  33. package/lib/types/slots.js +119 -0
  34. package/lib/types/ui/badge.d.ts +17 -0
  35. package/lib/types/ui/badge.js +29 -0
  36. package/lib/types/ui/button.d.ts +20 -0
  37. package/lib/types/ui/button.js +39 -0
  38. package/lib/types/ui/card.d.ts +14 -0
  39. package/lib/types/ui/card.js +18 -0
  40. package/lib/types/ui/checkbox.d.ts +10 -0
  41. package/lib/types/ui/checkbox.js +13 -0
  42. package/lib/types/ui/collapsible.d.ts +24 -0
  43. package/lib/types/ui/collapsible.js +31 -0
  44. package/lib/types/ui/dialog.d.ts +22 -0
  45. package/lib/types/ui/dialog.js +29 -0
  46. package/lib/types/ui/drag-scroll.d.ts +13 -0
  47. package/lib/types/ui/drag-scroll.js +85 -0
  48. package/lib/types/ui/hooks.d.ts +16 -0
  49. package/lib/types/ui/hooks.js +26 -0
  50. package/lib/types/ui/icons.d.ts +21 -0
  51. package/lib/types/ui/icons.js +18 -0
  52. package/lib/types/ui/index.d.ts +32 -0
  53. package/lib/types/ui/index.js +32 -0
  54. package/lib/types/ui/input.d.ts +8 -0
  55. package/lib/types/ui/input.js +6 -0
  56. package/lib/types/ui/label.d.ts +9 -0
  57. package/lib/types/ui/label.js +12 -0
  58. package/lib/types/ui/led.d.ts +13 -0
  59. package/lib/types/ui/led.js +11 -0
  60. package/lib/types/ui/number-input.d.ts +35 -0
  61. package/lib/types/ui/number-input.js +48 -0
  62. package/lib/types/ui/progress.d.ts +10 -0
  63. package/lib/types/ui/progress.js +14 -0
  64. package/lib/types/ui/radio-group.d.ts +12 -0
  65. package/lib/types/ui/radio-group.js +17 -0
  66. package/lib/types/ui/schema-form.d.ts +51 -0
  67. package/lib/types/ui/schema-form.js +124 -0
  68. package/lib/types/ui/scroll-area.d.ts +10 -0
  69. package/lib/types/ui/scroll-area.js +13 -0
  70. package/lib/types/ui/switch.d.ts +10 -0
  71. package/lib/types/ui/switch.js +13 -0
  72. package/lib/types/ui/table.d.ts +20 -0
  73. package/lib/types/ui/table.js +28 -0
  74. package/lib/types/ui/tabs.d.ts +16 -0
  75. package/lib/types/ui/tabs.js +23 -0
  76. package/lib/types/ui/textarea.d.ts +10 -0
  77. package/lib/types/ui/textarea.js +6 -0
  78. package/lib/types/ui/theme.d.ts +30 -0
  79. package/lib/types/ui/theme.js +33 -0
  80. package/lib/types/ui/tooltip.d.ts +23 -0
  81. package/lib/types/ui/tooltip.js +15 -0
  82. package/lib/types/ui/touch-select.d.ts +29 -0
  83. package/lib/types/ui/touch-select.js +37 -0
  84. package/lib/types/ui/utils.d.ts +13 -0
  85. package/lib/types/ui/utils.js +16 -0
  86. package/lib/types/variables.d.ts +168 -0
  87. package/lib/types/variables.js +282 -0
  88. package/lib/types/workflows.d.ts +98 -0
  89. package/lib/types/workflows.js +95 -0
  90. package/lib/ui/index.js +1101 -0
  91. package/lib/ui/theme.css +271 -0
  92. package/lib/variables.js +260 -0
  93. package/lib/workflows.js +85 -0
  94. package/package.json +138 -3
  95. package/src/ui/theme.css +271 -0
  96. package/templates/ui-extension/README.md +17 -0
  97. package/templates/ui-view/README.md +17 -0
@@ -0,0 +1,1101 @@
1
+ import { clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import { Slot } from "@radix-ui/react-slot";
6
+ import { cva } from "class-variance-authority";
7
+ import * as SwitchPrimitive from "@radix-ui/react-switch";
8
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
9
+ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
10
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
11
+ import * as LabelPrimitive from "@radix-ui/react-label";
12
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
13
+ import * as ProgressPrimitive from "@radix-ui/react-progress";
14
+ import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
15
+ import * as TabsPrimitive from "@radix-ui/react-tabs";
16
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
17
+ import { z } from "zod";
18
+ //#region lib/types/ui/utils.js
19
+ /**
20
+ * Class-name combiner: clsx joins, tailwind-merge resolves conflicting
21
+ * Tailwind utilities with the last one winning — the shadcn convention.
22
+ *
23
+ * @module @snap-rail/framework-shadcn/ui/utils
24
+ */
25
+ /**
26
+ * Merge conditional class names into one Tailwind-aware string.
27
+ * @param inputs - class values (strings, arrays, conditional objects).
28
+ * @returns the merged class string.
29
+ */
30
+ function cn(...inputs) {
31
+ return twMerge(clsx(inputs));
32
+ }
33
+ //#endregion
34
+ //#region lib/types/ui/hooks.js
35
+ /**
36
+ * Shared React recipes over the client runtime's event fabric.
37
+ *
38
+ * @module @snap-rail/framework-shadcn/ui/hooks
39
+ */
40
+ /**
41
+ * Re-render the component whenever any of the named ctx events fires —
42
+ * the standard "surface follows the seam" tick. The cordis import is
43
+ * type-only: this module stays a pure React utility at runtime.
44
+ *
45
+ * @param ctx - the plugin context whose events drive the refresh.
46
+ * @param events - event names to listen on (identity is not part of the
47
+ * effect key; a freshly built array literal is fine).
48
+ */
49
+ function useRefresh(ctx, events) {
50
+ const [, setTick] = useState(0);
51
+ const key = events.join("\n");
52
+ useEffect(() => {
53
+ const bump = () => setTick((value) => value + 1);
54
+ const detachers = (key === "" ? [] : key.split("\n")).map((event) => ctx.on(event, bump));
55
+ return () => {
56
+ for (const detach of detachers) detach();
57
+ };
58
+ }, [ctx, key]);
59
+ }
60
+ //#endregion
61
+ //#region lib/types/ui/button.js
62
+ /**
63
+ * Button: cva variants over a native button. Ghost/outline cover chrome
64
+ * surfaces (titlebar, panel toggles); `icon` size squares the padding for
65
+ * glyph-only controls.
66
+ *
67
+ * @module @snap-rail/framework-shadcn/ui/button
68
+ */
69
+ const buttonVariants = cva("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-base font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", {
70
+ variants: {
71
+ variant: {
72
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
73
+ destructive: "bg-destructive/90 text-destructive-foreground hover:bg-destructive",
74
+ outline: "border border-border bg-transparent hover:bg-accent hover:text-accent-foreground",
75
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
76
+ ghost: "hover:bg-accent hover:text-accent-foreground",
77
+ link: "text-primary underline-offset-4 hover:underline"
78
+ },
79
+ size: {
80
+ xl: "h-16 px-8 text-lg",
81
+ lg: "h-14 px-6 text-lg",
82
+ default: "h-12 px-5 py-1.5",
83
+ sm: "h-10 px-4 text-sm",
84
+ icon: "h-12 w-12"
85
+ }
86
+ },
87
+ defaultVariants: {
88
+ variant: "default",
89
+ size: "default"
90
+ }
91
+ });
92
+ /** A themed button; `asChild` renders the child element instead. */
93
+ function Button({ className, variant, size, asChild = false, ...props }) {
94
+ return jsx(asChild ? Slot : "button", {
95
+ className: cn(buttonVariants({
96
+ variant,
97
+ size
98
+ }), className),
99
+ ...props
100
+ });
101
+ }
102
+ //#endregion
103
+ //#region lib/types/ui/card.js
104
+ /** The container surface. */
105
+ function Card({ className, ...props }) {
106
+ return jsx("div", {
107
+ className: cn("rounded-lg border border-border bg-card text-card-foreground", className),
108
+ ...props
109
+ });
110
+ }
111
+ /** Top padding/heading zone of a card. */
112
+ function CardHeader({ className, ...props }) {
113
+ return jsx("div", {
114
+ className: cn("flex flex-col gap-1 p-5", className),
115
+ ...props
116
+ });
117
+ }
118
+ /** Card heading line. */
119
+ function CardTitle({ className, ...props }) {
120
+ return jsx("h3", {
121
+ className: cn("text-sm font-medium text-muted-foreground", className),
122
+ ...props
123
+ });
124
+ }
125
+ /** Body zone of a card. */
126
+ function CardContent({ className, ...props }) {
127
+ return jsx("div", {
128
+ className: cn("p-5 pt-0", className),
129
+ ...props
130
+ });
131
+ }
132
+ //#endregion
133
+ //#region lib/types/ui/badge.js
134
+ /**
135
+ * Badge: small status/label chip. `success` carries the online/live state,
136
+ * `warning` the attention state (e.g. partial failure, float type),
137
+ * `destructive` the offline/abnormal one.
138
+ *
139
+ * @module @snap-rail/framework-shadcn/ui/badge
140
+ */
141
+ const badgeVariants = cva("inline-flex items-center gap-1 rounded-md border px-2.5 py-1 text-sm font-medium whitespace-nowrap", {
142
+ variants: { variant: {
143
+ default: "border-transparent bg-primary text-primary-foreground",
144
+ secondary: "border-transparent bg-secondary text-secondary-foreground",
145
+ success: "border-transparent bg-success/15 text-success",
146
+ warning: "border-transparent bg-warning/15 text-warning",
147
+ destructive: "border-transparent bg-destructive/15 text-destructive",
148
+ outline: "text-muted-foreground"
149
+ } },
150
+ defaultVariants: { variant: "default" }
151
+ });
152
+ /** A themed status chip. */
153
+ function Badge({ className, variant, ...props }) {
154
+ return jsx("span", {
155
+ className: cn(badgeVariants({ variant }), className),
156
+ ...props
157
+ });
158
+ }
159
+ //#endregion
160
+ //#region lib/types/ui/switch.js
161
+ /**
162
+ * Switch: the Radix switch with snap-rail chrome — boolean control for
163
+ * toggles like the offline simulation.
164
+ *
165
+ * @module @snap-rail/framework-shadcn/ui/switch
166
+ */
167
+ /** A themed on/off control. */
168
+ function Switch({ className, ...props }) {
169
+ return jsx(SwitchPrimitive.Root, {
170
+ className: cn("peer inline-flex h-6 w-12 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-secondary", className),
171
+ ...props,
172
+ children: jsx(SwitchPrimitive.Thumb, { className: "pointer-events-none block h-5 w-5 rounded-full bg-foreground shadow transition-transform data-[state=checked]:translate-x-6 data-[state=unchecked]:translate-x-1" })
173
+ });
174
+ }
175
+ //#endregion
176
+ //#region lib/types/ui/tooltip.js
177
+ /**
178
+ * Tooltip: Radix tooltip wrapping its own provider so occupants mount it
179
+ * standalone (no app-level provider requirement).
180
+ *
181
+ * @module @snap-rail/framework-shadcn/ui/tooltip
182
+ */
183
+ /** Hover/focus tip over any trigger. */
184
+ function Tooltip({ content, children, delay = 200 }) {
185
+ return jsx(TooltipPrimitive.Provider, {
186
+ delayDuration: delay,
187
+ children: jsxs(TooltipPrimitive.Root, { children: [jsx(TooltipPrimitive.Trigger, {
188
+ asChild: true,
189
+ children
190
+ }), jsx(TooltipPrimitive.Portal, { children: jsx(TooltipPrimitive.Content, {
191
+ sideOffset: 4,
192
+ className: cn("z-50 rounded-md border border-border bg-popover px-2 py-1 text-xs text-popover-foreground"),
193
+ children: content
194
+ }) })] })
195
+ });
196
+ }
197
+ /** Re-export the primitive root for direct composition. */
198
+ const TooltipRoot = TooltipPrimitive.Root;
199
+ //#endregion
200
+ //#region lib/types/ui/scroll-area.js
201
+ /**
202
+ * ScrollArea: Radix scroll area with a themed slim scrollbar — panels keep
203
+ * native scrolling semantics without OS chrome.
204
+ *
205
+ * @module @snap-rail/framework-shadcn/ui/scroll-area
206
+ */
207
+ /** A themed scrollable viewport. */
208
+ function ScrollArea({ className, children, ...props }) {
209
+ return jsxs(ScrollAreaPrimitive.Root, {
210
+ className: cn("overflow-hidden", className),
211
+ ...props,
212
+ children: [
213
+ jsx(ScrollAreaPrimitive.Viewport, {
214
+ className: "h-full w-full",
215
+ children
216
+ }),
217
+ jsx(ScrollAreaPrimitive.Scrollbar, {
218
+ orientation: "vertical",
219
+ className: "flex w-1.5 touch-none select-none border-l border-transparent p-px",
220
+ children: jsx(ScrollAreaPrimitive.Thumb, { className: "relative flex-1 rounded-full bg-muted-foreground/40" })
221
+ }),
222
+ jsx(ScrollAreaPrimitive.Corner, {})
223
+ ]
224
+ });
225
+ }
226
+ //#endregion
227
+ //#region lib/types/ui/dialog.js
228
+ /**
229
+ * Dialog: modal overlay + panel over Radix Dialog. Chrome surfaces use it
230
+ * for confirmations (window close, technician id entry); content pages use
231
+ * it for small forms. No built-in close affordance — callers render their
232
+ * own buttons in the footer.
233
+ *
234
+ * @module @snap-rail/framework-shadcn/ui/dialog
235
+ */
236
+ /** Controlled/uncontrolled root; re-exported so callers need one import. */
237
+ const Dialog = DialogPrimitive.Root;
238
+ /** The overlay + centered panel; mounts through a portal into body. */
239
+ function DialogContent({ className, children, ...props }) {
240
+ return jsxs(DialogPrimitive.Portal, { children: [jsx(DialogPrimitive.Overlay, { className: "overlay-in fixed inset-0 z-50 bg-background/80 backdrop-blur-[1px]" }), jsx(DialogPrimitive.Content, {
241
+ className: cn("dialog-in fixed left-1/2 top-1/2 z-50 w-[calc(100%-2.5rem)] max-w-md -translate-x-1/2 -translate-y-1/2", "rounded-lg border border-border bg-card text-card-foreground shadow-xl", "p-5 focus:outline-none", className),
242
+ ...props,
243
+ children
244
+ })] });
245
+ }
246
+ /** Title bar block inside a dialog. */
247
+ function DialogHeader({ className, ...props }) {
248
+ return jsx("div", {
249
+ className: cn("mb-4 space-y-1", className),
250
+ ...props
251
+ });
252
+ }
253
+ /** The dialog's accessible title. */
254
+ const DialogTitle = DialogPrimitive.Title;
255
+ /** Optional supporting line under the title. */
256
+ const DialogDescription = DialogPrimitive.Description;
257
+ /** Action row (buttons) at the bottom; defaults to right-aligned. */
258
+ function DialogFooter({ className, ...props }) {
259
+ return jsx("div", {
260
+ className: cn("mt-5 flex justify-end gap-3", className),
261
+ ...props
262
+ });
263
+ }
264
+ //#endregion
265
+ //#region lib/types/ui/input.js
266
+ /** A themed text input. */
267
+ function Input({ className, type = "text", ...props }) {
268
+ return jsx("input", {
269
+ type,
270
+ className: cn("flex h-12 w-full rounded-md border border-input bg-transparent px-4 py-2 text-base", "placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "disabled:cursor-not-allowed disabled:opacity-50", className),
271
+ ...props
272
+ });
273
+ }
274
+ //#endregion
275
+ //#region lib/types/ui/textarea.js
276
+ /** A themed multi-line text area. */
277
+ function Textarea({ className, ...props }) {
278
+ return jsx("textarea", {
279
+ className: cn("flex min-h-24 w-full rounded-md border border-input bg-transparent px-4 py-3 text-base", "placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "disabled:cursor-not-allowed disabled:opacity-50", className),
280
+ ...props
281
+ });
282
+ }
283
+ //#endregion
284
+ //#region lib/types/ui/label.js
285
+ /**
286
+ * Label: form field caption over Radix Label (click focuses the control).
287
+ *
288
+ * @module @snap-rail/framework-shadcn/ui/label
289
+ */
290
+ /** A themed field label. */
291
+ function Label({ className, ...props }) {
292
+ return jsx(LabelPrimitive.Root, {
293
+ className: cn("text-sm font-medium text-muted-foreground select-none", className),
294
+ ...props
295
+ });
296
+ }
297
+ //#endregion
298
+ //#region lib/types/ui/checkbox.js
299
+ /**
300
+ * Checkbox: themed check entry over Radix Checkbox; the indicator is an
301
+ * inline stroke so the UI seam needs no icon dependency.
302
+ *
303
+ * @module @snap-rail/framework-shadcn/ui/checkbox
304
+ */
305
+ /** A themed checkbox. */
306
+ function Checkbox({ className, ...props }) {
307
+ return jsx(CheckboxPrimitive.Root, {
308
+ className: cn("h-6 w-6 shrink-0 rounded-sm border border-input bg-transparent", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "data-[state=checked]:border-primary data-[state=checked]:bg-primary", "disabled:cursor-not-allowed disabled:opacity-50", className),
309
+ ...props,
310
+ children: jsx(CheckboxPrimitive.Indicator, {
311
+ className: "flex items-center justify-center text-primary-foreground",
312
+ children: jsx("svg", {
313
+ viewBox: "0 0 12 12",
314
+ className: "h-4 w-4",
315
+ fill: "none",
316
+ "aria-hidden": "true",
317
+ children: jsx("path", {
318
+ d: "M2 6.5 4.8 9 10 3.5",
319
+ stroke: "currentColor",
320
+ strokeWidth: "2",
321
+ strokeLinecap: "round",
322
+ strokeLinejoin: "round"
323
+ })
324
+ })
325
+ })
326
+ });
327
+ }
328
+ //#endregion
329
+ //#region lib/types/ui/progress.js
330
+ /**
331
+ * Progress: the Radix progress bar with snap-rail chrome — determinate
332
+ * download/scan progress for long-running host work.
333
+ *
334
+ * @module @snap-rail/framework-shadcn/ui/progress
335
+ */
336
+ /** A themed linear progress bar (`value` 0–100, `aria-valuenow` included). */
337
+ function Progress({ className, value, ...props }) {
338
+ const percent = typeof value === "number" ? Math.min(100, Math.max(0, value)) : 0;
339
+ return jsx(ProgressPrimitive.Root, {
340
+ className: cn("relative h-2 w-full overflow-hidden rounded-full bg-secondary", className),
341
+ ...props,
342
+ children: jsx(ProgressPrimitive.Indicator, {
343
+ className: "h-full w-full rounded-full bg-primary transition-transform",
344
+ style: { transform: `translateX(-${100 - percent}%)` }
345
+ })
346
+ });
347
+ }
348
+ //#endregion
349
+ //#region lib/types/ui/radio-group.js
350
+ /**
351
+ * RadioGroup: exclusive choice over Radix RadioGroup (是/否 switches, model
352
+ * picks); the indicator is a plain dot so the UI seam needs no icon dep.
353
+ *
354
+ * @module @snap-rail/framework-shadcn/ui/radio-group
355
+ */
356
+ /** The exclusive group root. */
357
+ function RadioGroup({ className, ...props }) {
358
+ return jsx(RadioGroupPrimitive.Root, {
359
+ className: cn("grid gap-3", className),
360
+ ...props
361
+ });
362
+ }
363
+ /** One themed choice. */
364
+ function RadioGroupItem({ className, ...props }) {
365
+ return jsx(RadioGroupPrimitive.Item, {
366
+ className: cn("aspect-square h-6 w-6 shrink-0 rounded-full border border-input bg-transparent", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "data-[state=checked]:border-primary disabled:cursor-not-allowed disabled:opacity-50", className),
367
+ ...props,
368
+ children: jsx(RadioGroupPrimitive.Indicator, {
369
+ className: "flex items-center justify-center",
370
+ children: jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-primary" })
371
+ })
372
+ });
373
+ }
374
+ //#endregion
375
+ //#region lib/types/ui/drag-scroll.js
376
+ /**
377
+ * DragScroll: an overflow-auto container that also pans when held and
378
+ * dragged with the mouse (both axes). Touch and pen keep Chromium's native
379
+ * pan, so only mouse pointers are intercepted. A 6px threshold separates a
380
+ * pan from a press; the click that follows a pan is swallowed so dragged
381
+ * rows never fire. DragScrolls nest (a table inside a scrolled page); only
382
+ * the innermost container under the pressed target takes the gesture.
383
+ *
384
+ * @module @snap-rail/framework-shadcn/ui/drag-scroll
385
+ */
386
+ /** Movement (px) before a held press turns into a pan. */
387
+ const PAN_THRESHOLD = 6;
388
+ /** An auto-scrolling div that pans under a held mouse drag. */
389
+ function DragScroll({ className, children, ...props }) {
390
+ const container = useRef(null);
391
+ const pan = useRef(null);
392
+ const swallowClick = useRef(false);
393
+ const onPointerDown = (event) => {
394
+ if (event.pointerType !== "mouse" || event.button !== 0) return;
395
+ if (container.current === null) return;
396
+ const innermost = event.target instanceof Element ? event.target.closest("[data-drag-scroll]") : null;
397
+ if (innermost !== null && innermost !== container.current) return;
398
+ pan.current = {
399
+ id: event.pointerId,
400
+ x: event.clientX,
401
+ y: event.clientY,
402
+ moved: false
403
+ };
404
+ };
405
+ const onPointerMove = (event) => {
406
+ const state = pan.current;
407
+ if (state === null || state.id !== event.pointerId) return;
408
+ if (event.buttons === 0) {
409
+ pan.current = null;
410
+ return;
411
+ }
412
+ const el = container.current;
413
+ if (el === null) return;
414
+ const dx = state.x - event.clientX;
415
+ const dy = state.y - event.clientY;
416
+ if (!state.moved) {
417
+ if (Math.hypot(dx, dy) < PAN_THRESHOLD) return;
418
+ state.moved = true;
419
+ el.setPointerCapture(event.pointerId);
420
+ el.dataset.panning = "true";
421
+ }
422
+ el.scrollLeft += dx;
423
+ el.scrollTop += dy;
424
+ state.x = event.clientX;
425
+ state.y = event.clientY;
426
+ };
427
+ const endPan = (event) => {
428
+ const state = pan.current;
429
+ if (state === null || state.id !== event.pointerId) return;
430
+ pan.current = null;
431
+ const el = container.current;
432
+ if (el === null) return;
433
+ if (state.moved) {
434
+ if (el.hasPointerCapture(event.pointerId)) el.releasePointerCapture(event.pointerId);
435
+ delete el.dataset.panning;
436
+ swallowClick.current = true;
437
+ }
438
+ };
439
+ return jsx("div", {
440
+ ref: container,
441
+ "data-drag-scroll": "",
442
+ className: cn("overflow-auto", className),
443
+ onPointerDown,
444
+ onPointerMove,
445
+ onPointerUp: endPan,
446
+ onPointerCancel: endPan,
447
+ onClickCapture: (event) => {
448
+ if (!swallowClick.current) return;
449
+ swallowClick.current = false;
450
+ event.preventDefault();
451
+ event.stopPropagation();
452
+ },
453
+ ...props,
454
+ children
455
+ });
456
+ }
457
+ //#endregion
458
+ //#region lib/types/ui/touch-select.js
459
+ /**
460
+ * TouchSelect: a touch-first single-choice picker. The trigger opens a
461
+ * modal listing one full-width button row per option — no popover aiming,
462
+ * every row a 56px target; tapping a row commits and closes. Replaces
463
+ * dropdown selects across the touch terminal.
464
+ *
465
+ * @module @snap-rail/framework-shadcn/ui/touch-select
466
+ */
467
+ /** A chevron pointing up: the picker opens an upward modal, not a dropdown. */
468
+ function ChevronUp() {
469
+ return jsx("svg", {
470
+ viewBox: "0 0 12 12",
471
+ className: "h-4 w-4 shrink-0 opacity-60",
472
+ fill: "none",
473
+ "aria-hidden": "true",
474
+ children: jsx("path", {
475
+ d: "m2.5 7.5 3.5-3.5 3.5 3.5",
476
+ stroke: "currentColor",
477
+ strokeWidth: "1.5",
478
+ strokeLinecap: "round",
479
+ strokeLinejoin: "round"
480
+ })
481
+ });
482
+ }
483
+ /** The committed row's check mark, mirroring the Checkbox indicator stroke. */
484
+ function CheckIcon() {
485
+ return jsx("svg", {
486
+ viewBox: "0 0 12 12",
487
+ className: "h-4 w-4 text-primary",
488
+ fill: "none",
489
+ "aria-hidden": "true",
490
+ children: jsx("path", {
491
+ d: "M2 6.5 4.8 9 10 3.5",
492
+ stroke: "currentColor",
493
+ strokeWidth: "2",
494
+ strokeLinecap: "round",
495
+ strokeLinejoin: "round"
496
+ })
497
+ });
498
+ }
499
+ /** A modal-based picker: an outline trigger plus a Dialog list of rows. */
500
+ function TouchSelect(props) {
501
+ const { label, value, options, onValueChange, placeholder = "请选择", disabled = false, className } = props;
502
+ const [open, setOpen] = useState(false);
503
+ const selected = options.find((option) => option.value === value);
504
+ return jsxs(Fragment, { children: [jsxs(Button, {
505
+ type: "button",
506
+ variant: "outline",
507
+ disabled,
508
+ "aria-label": label,
509
+ "data-touch-select-trigger": label,
510
+ className: cn("h-12 w-full justify-between px-4 text-base font-normal", className),
511
+ onClick: () => {
512
+ setOpen(true);
513
+ },
514
+ children: [selected === void 0 ? jsx("span", {
515
+ className: "truncate text-muted-foreground",
516
+ children: placeholder
517
+ }) : jsx("span", {
518
+ className: "min-w-0 truncate",
519
+ children: selected.triggerLabel ?? selected.label
520
+ }), jsx(ChevronUp, {})]
521
+ }), jsx(Dialog, {
522
+ open,
523
+ onOpenChange: setOpen,
524
+ children: jsxs(DialogContent, {
525
+ "aria-describedby": void 0,
526
+ className: "max-w-md p-0",
527
+ children: [jsx("div", {
528
+ className: "border-b border-border px-5 py-4",
529
+ children: jsx(DialogTitle, {
530
+ className: "text-base font-medium",
531
+ children: label
532
+ })
533
+ }), jsxs(DragScroll, {
534
+ className: "max-h-[65vh] p-2",
535
+ children: [options.length === 0 && jsx("div", {
536
+ className: "px-4 py-6 text-center text-sm text-muted-foreground",
537
+ children: "暂无选项"
538
+ }), options.map((option) => {
539
+ const isSelected = option.value === value;
540
+ return jsxs(Button, {
541
+ type: "button",
542
+ variant: "ghost",
543
+ disabled: option.disabled,
544
+ "data-option": option.value,
545
+ "aria-pressed": isSelected,
546
+ className: cn("h-14 w-full justify-between px-4 text-base font-normal", isSelected && "bg-secondary channel-keyline"),
547
+ onClick: () => {
548
+ onValueChange(option.value);
549
+ setOpen(false);
550
+ },
551
+ children: [jsx("span", {
552
+ className: "min-w-0 truncate",
553
+ children: option.label
554
+ }), jsxs("span", {
555
+ className: "flex shrink-0 items-center gap-2 text-sm text-muted-foreground",
556
+ children: [option.hint, isSelected && jsx(CheckIcon, {})]
557
+ })]
558
+ }, option.value);
559
+ })]
560
+ })]
561
+ })
562
+ })] });
563
+ }
564
+ //#endregion
565
+ //#region lib/types/ui/number-input.js
566
+ /**
567
+ * NumberInput / NumberPad: numeric entry for the touch terminal. The field
568
+ * is a button styled as an input (no system soft keyboard ever pops); a tap
569
+ * opens the NumberPad modal — a 3×4 keypad plus 清除/确定. NumberPad also
570
+ * mounts inline where the keypad should stay visible (the login card).
571
+ *
572
+ * @module @snap-rail/framework-shadcn/ui/number-input
573
+ */
574
+ /** The keypad's backspace glyph; inline so the seam carries no icon dep. */
575
+ function BackspaceIcon() {
576
+ return jsxs("svg", {
577
+ viewBox: "0 0 24 24",
578
+ className: "h-6 w-6",
579
+ fill: "none",
580
+ "aria-hidden": "true",
581
+ children: [jsx("path", {
582
+ d: "M9 5h11a1.5 1.5 0 0 1 1.5 1.5v11A1.5 1.5 0 0 1 20 19H9l-6.5-7L9 5Z",
583
+ stroke: "currentColor",
584
+ strokeWidth: "1.6",
585
+ strokeLinejoin: "round"
586
+ }), jsx("path", {
587
+ d: "m12.5 9.5 5 5m0-5-5 5",
588
+ stroke: "currentColor",
589
+ strokeWidth: "1.6",
590
+ strokeLinecap: "round"
591
+ })]
592
+ });
593
+ }
594
+ /** An inline 3×4 keypad (digits, optional decimal point, backspace) with a
595
+ * clear/confirm action row; every key is a full-size Button. */
596
+ function NumberPad(props) {
597
+ const { value, onChange, onConfirm, allowDecimal = false, confirmLabel = "确定", className } = props;
598
+ const press = (key) => {
599
+ if (key === "." && (value.includes(".") || value === "")) return;
600
+ onChange(value + key);
601
+ };
602
+ const backspace = () => {
603
+ onChange(value.slice(0, -1));
604
+ };
605
+ const clear = () => {
606
+ onChange("");
607
+ };
608
+ return jsxs("div", {
609
+ className: cn("grid grid-cols-3 gap-2", className),
610
+ children: [
611
+ [
612
+ "1",
613
+ "2",
614
+ "3",
615
+ "4",
616
+ "5",
617
+ "6",
618
+ "7",
619
+ "8",
620
+ "9"
621
+ ].map((digit) => jsx(Button, {
622
+ type: "button",
623
+ variant: "secondary",
624
+ "data-key": digit,
625
+ className: "h-14 font-mono text-xl",
626
+ onClick: () => {
627
+ press(digit);
628
+ },
629
+ children: digit
630
+ }, digit)),
631
+ allowDecimal ? jsx(Button, {
632
+ type: "button",
633
+ variant: "secondary",
634
+ "data-key": "dot",
635
+ className: "h-14 font-mono text-xl",
636
+ onClick: () => {
637
+ press(".");
638
+ },
639
+ children: "·"
640
+ }) : jsx("div", { "aria-hidden": "true" }),
641
+ jsx(Button, {
642
+ type: "button",
643
+ variant: "secondary",
644
+ "data-key": "0",
645
+ className: "h-14 font-mono text-xl",
646
+ onClick: () => {
647
+ press("0");
648
+ },
649
+ children: "0"
650
+ }),
651
+ jsx(Button, {
652
+ type: "button",
653
+ variant: "secondary",
654
+ "aria-label": "退格",
655
+ "data-key": "back",
656
+ className: "h-14",
657
+ onClick: backspace,
658
+ children: jsx(BackspaceIcon, {})
659
+ }),
660
+ jsx(Button, {
661
+ type: "button",
662
+ variant: "outline",
663
+ "data-key": "clear",
664
+ className: "h-14 text-base",
665
+ onClick: clear,
666
+ children: "清除"
667
+ }),
668
+ jsx(Button, {
669
+ type: "button",
670
+ "data-key": "confirm",
671
+ className: "col-span-2 h-14 text-base",
672
+ onClick: () => {
673
+ onConfirm?.();
674
+ },
675
+ children: confirmLabel
676
+ })
677
+ ]
678
+ });
679
+ }
680
+ /** A numeric field: an input-styled button that opens the NumberPad modal;
681
+ * `onChange` commits on 确定 only — a cancelled edit changes nothing. */
682
+ function NumberInput(props) {
683
+ const { label, value, onChange, placeholder = "", allowDecimal = false, disabled = false, className } = props;
684
+ const [open, setOpen] = useState(false);
685
+ const [draft, setDraft] = useState(value);
686
+ useEffect(() => {
687
+ if (open) setDraft(value);
688
+ }, [open, value]);
689
+ const confirm = () => {
690
+ onChange(draft);
691
+ setOpen(false);
692
+ };
693
+ return jsxs(Fragment, { children: [jsx("button", {
694
+ type: "button",
695
+ disabled,
696
+ "aria-label": label,
697
+ "data-number-input": label,
698
+ className: cn("flex h-12 w-full items-center rounded-md border border-input bg-transparent px-4 text-left", "font-mono text-base tabular-nums", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "disabled:cursor-not-allowed disabled:opacity-50", className),
699
+ onClick: () => {
700
+ setOpen(true);
701
+ },
702
+ children: value === "" ? jsx("span", {
703
+ className: "text-muted-foreground",
704
+ children: placeholder
705
+ }) : value
706
+ }), jsx(Dialog, {
707
+ open,
708
+ onOpenChange: setOpen,
709
+ children: jsxs(DialogContent, {
710
+ "aria-describedby": void 0,
711
+ className: "max-w-sm p-0",
712
+ children: [jsx("div", {
713
+ className: "border-b border-border px-5 py-4",
714
+ children: jsx(DialogTitle, {
715
+ className: "text-base font-medium",
716
+ children: label
717
+ })
718
+ }), jsxs("div", {
719
+ className: "px-5 pb-5 pt-4",
720
+ children: [jsx("div", {
721
+ className: "mb-4 min-h-14 rounded-md border border-border bg-muted px-4 py-3 text-right font-mono text-2xl tabular-nums",
722
+ "data-number-display": true,
723
+ children: draft === "" ? jsx("span", {
724
+ className: "text-muted-foreground",
725
+ children: "—"
726
+ }) : draft
727
+ }), jsx(NumberPad, {
728
+ value: draft,
729
+ onChange: setDraft,
730
+ onConfirm: confirm,
731
+ allowDecimal
732
+ })]
733
+ })]
734
+ })
735
+ })] });
736
+ }
737
+ //#endregion
738
+ //#region lib/types/ui/schema-form.js
739
+ function isPropertySchema(raw) {
740
+ return typeof raw === "object" && raw !== null;
741
+ }
742
+ /** Plan the renderable fields of an object schema (property order kept). */
743
+ function planSchemaFields(schema, hidden = []) {
744
+ const properties = schema.properties;
745
+ const required = new Set(Array.isArray(schema.required) ? schema.required.filter((name) => typeof name === "string") : []);
746
+ const fields = [];
747
+ if (!isPropertySchema(properties)) return fields;
748
+ for (const [name, rawProperty] of Object.entries(properties)) {
749
+ if (hidden.includes(name)) continue;
750
+ const property = rawProperty;
751
+ const field = {
752
+ name,
753
+ label: typeof property.title === "string" && property.title !== "" ? property.title : name,
754
+ required: required.has(name),
755
+ kind: "string"
756
+ };
757
+ const literals = Array.isArray(property.enum) ? property.enum : Array.isArray(property.anyOf) ? property.anyOf.map((entry) => isPropertySchema(entry) && "const" in entry ? entry.const : void 0) : void 0;
758
+ const typed = literals?.filter((value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean");
759
+ if (literals !== void 0 && typed !== void 0 && typed.length > 0 && typed.length === literals.length) {
760
+ const valueOf = {};
761
+ field.kind = "enum";
762
+ field.options = typed.map((value) => {
763
+ const key = String(value);
764
+ valueOf[key] = value;
765
+ return {
766
+ value: key,
767
+ label: key
768
+ };
769
+ });
770
+ field.literalOf = valueOf;
771
+ } else switch (property.type) {
772
+ case "integer":
773
+ field.kind = "integer";
774
+ break;
775
+ case "number":
776
+ field.kind = "number";
777
+ break;
778
+ case "boolean":
779
+ field.kind = "boolean";
780
+ break;
781
+ case "string":
782
+ field.kind = "string";
783
+ break;
784
+ default: field.kind = "text";
785
+ }
786
+ if (typeof property.description === "string") field.placeholder = property.description;
787
+ fields.push(field);
788
+ }
789
+ return fields;
790
+ }
791
+ /** Initial values from an object schema's `default`s (create dialogs). */
792
+ function schemaDefaults(schema) {
793
+ const value = {};
794
+ const properties = schema.properties;
795
+ if (!isPropertySchema(properties)) return value;
796
+ for (const [name, rawProperty] of Object.entries(properties)) {
797
+ const fallback = rawProperty.default;
798
+ if (typeof fallback === "string" || typeof fallback === "number" || typeof fallback === "boolean") value[name] = fallback;
799
+ }
800
+ return value;
801
+ }
802
+ /** Strip keys whose value is `undefined` so unset optionals ride clean. */
803
+ function compactSchemaValue(value) {
804
+ const out = {};
805
+ for (const [key, entry] of Object.entries(value)) if (entry !== void 0) out[key] = entry;
806
+ return out;
807
+ }
808
+ /** One labeled control. */
809
+ function SchemaField(props) {
810
+ const { field, value, onChange, disabled = false } = props;
811
+ const setString = (next) => {
812
+ onChange(next === "" ? void 0 : next);
813
+ };
814
+ return jsxs("div", {
815
+ className: "grid gap-2",
816
+ "data-schema-field": field.name,
817
+ children: [
818
+ jsxs(Label, {
819
+ htmlFor: `schema-${field.name}`,
820
+ children: [field.label, field.required && jsx("span", {
821
+ className: "ml-1 text-destructive",
822
+ children: "*"
823
+ })]
824
+ }),
825
+ field.kind === "enum" && field.options !== void 0 && jsx(TouchSelect, {
826
+ label: field.label,
827
+ value: value === void 0 ? "" : String(value),
828
+ options: field.options,
829
+ onValueChange: (next) => {
830
+ onChange(field.literalOf?.[next] ?? next);
831
+ },
832
+ disabled
833
+ }),
834
+ (field.kind === "integer" || field.kind === "number") && jsx(NumberInput, {
835
+ label: field.label,
836
+ value: value === void 0 ? "" : String(value),
837
+ onChange: (text) => {
838
+ if (text === "") {
839
+ onChange(void 0);
840
+ return;
841
+ }
842
+ const parsed = field.kind === "integer" ? Number.parseInt(text, 10) : Number.parseFloat(text);
843
+ onChange(Number.isNaN(parsed) ? void 0 : parsed);
844
+ },
845
+ allowDecimal: field.kind === "number",
846
+ placeholder: field.placeholder ?? "—",
847
+ disabled
848
+ }),
849
+ field.kind === "boolean" && jsx("div", {
850
+ className: "flex h-12 items-center",
851
+ children: jsx(Switch, {
852
+ id: `schema-${field.name}`,
853
+ checked: value === true,
854
+ onCheckedChange: onChange,
855
+ disabled
856
+ })
857
+ }),
858
+ (field.kind === "string" || field.kind === "text") && jsx(Input, {
859
+ id: `schema-${field.name}`,
860
+ value: value === void 0 ? "" : String(value),
861
+ onChange: (event) => {
862
+ setString(event.target.value);
863
+ },
864
+ placeholder: field.placeholder,
865
+ disabled
866
+ })
867
+ ]
868
+ });
869
+ }
870
+ /** A controlled, schema-driven form body (labels + controls, no buttons). */
871
+ function SchemaForm(props) {
872
+ const { schema, value, onChange, hidden = [], disabled = false, className } = props;
873
+ const fields = planSchemaFields(schema, hidden);
874
+ return jsxs("div", {
875
+ className: cn("grid gap-5", className),
876
+ "data-schema-form": true,
877
+ children: [fields.map((field) => jsx(SchemaField, {
878
+ field,
879
+ value: value[field.name],
880
+ disabled,
881
+ onChange: (next) => {
882
+ onChange({
883
+ ...value,
884
+ [field.name]: next
885
+ });
886
+ }
887
+ }, field.name)), fields.length === 0 && jsx("div", {
888
+ className: "rounded-md border border-dashed border-border px-4 py-6 text-center text-sm text-muted-foreground",
889
+ children: "此驱动无额外配置项"
890
+ })]
891
+ });
892
+ }
893
+ //#endregion
894
+ //#region lib/types/ui/table.js
895
+ /** The grid container with a hairline bottom rule; the wrapper pans under
896
+ * a held mouse drag (see {@link DragScroll}). */
897
+ function Table({ className, ...props }) {
898
+ return jsx(DragScroll, {
899
+ className: "w-full",
900
+ children: jsx("table", {
901
+ className: cn("w-full caption-bottom text-sm", className),
902
+ ...props
903
+ })
904
+ });
905
+ }
906
+ /** Header group. */
907
+ function TableHeader({ className, ...props }) {
908
+ return jsx("thead", {
909
+ className: cn("[&_tr]:border-b", className),
910
+ ...props
911
+ });
912
+ }
913
+ /** Body group. */
914
+ function TableBody({ className, ...props }) {
915
+ return jsx("tbody", {
916
+ className: cn("[&_tr:last-child]:border-0", className),
917
+ ...props
918
+ });
919
+ }
920
+ /** One row: hairline separators, hover affordance when interactive. */
921
+ function TableRow({ className, ...props }) {
922
+ return jsx("tr", {
923
+ className: cn("border-b border-border transition-colors hover:bg-accent/40 data-[state=selected]:bg-accent", className),
924
+ ...props
925
+ });
926
+ }
927
+ /** One column head: muted, left-aligned unless overridden. */
928
+ function TableHead({ className, ...props }) {
929
+ return jsx("th", {
930
+ className: cn("h-11 px-3 text-left align-middle text-sm font-medium text-muted-foreground", className),
931
+ ...props
932
+ });
933
+ }
934
+ /** One cell. */
935
+ function TableCell({ className, ...props }) {
936
+ return jsx("td", {
937
+ className: cn("px-3 py-3 align-middle", className),
938
+ ...props
939
+ });
940
+ }
941
+ //#endregion
942
+ //#region lib/types/ui/tabs.js
943
+ /**
944
+ * Tabs: themed pager over Radix Tabs (list, trigger, content). Icons are
945
+ * inline strokes so the UI seam needs no icon dependency.
946
+ *
947
+ * @module @snap-rail/framework-shadcn/ui/tabs
948
+ */
949
+ /** Controlled/uncontrolled root; re-exported so callers need one import. */
950
+ const Tabs = TabsPrimitive.Root;
951
+ /** The tab strip; triggers rest on the muted surface, the active one lifts. */
952
+ function TabsList({ className, ...props }) {
953
+ return jsx(TabsPrimitive.List, {
954
+ className: cn("inline-flex h-12 items-center gap-1 rounded-lg bg-muted p-1 text-muted-foreground", className),
955
+ ...props
956
+ });
957
+ }
958
+ /** One tab button; the data attributes carry the active/hover styling. */
959
+ function TabsTrigger({ className, ...props }) {
960
+ return jsx(TabsPrimitive.Trigger, {
961
+ className: cn("inline-flex items-center gap-1.5 whitespace-nowrap rounded-md px-5 py-1.5 text-base font-medium", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "disabled:pointer-events-none disabled:opacity-50", "data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm", className),
962
+ ...props
963
+ });
964
+ }
965
+ /** The panel of one tab; mounted only while its trigger is active. */
966
+ function TabsContent({ className, ...props }) {
967
+ return jsx(TabsPrimitive.Content, {
968
+ className: cn("mt-4 focus-visible:outline-none", className),
969
+ ...props
970
+ });
971
+ }
972
+ //#endregion
973
+ //#region lib/types/ui/collapsible.js
974
+ /**
975
+ * Collapsible: the shadcn collapsible over Radix Collapsible — an
976
+ * independently controlled show/hide section. The root carries the card
977
+ * surface. A section may carry several triggers (title zone, chevron zone);
978
+ * each renders the shared {@link CollapsibleChevron} where it wants the
979
+ * rotating arrow. Icons are inline strokes so the UI seam needs no icon
980
+ * dependency.
981
+ *
982
+ * @module @snap-rail/framework-shadcn/ui/collapsible
983
+ */
984
+ /** Controlled/uncontrolled root; renders the bordered card surface. */
985
+ function Collapsible({ className, ...props }) {
986
+ return jsx(CollapsiblePrimitive.Root, {
987
+ className: cn("rounded-md border border-border bg-card", className),
988
+ ...props
989
+ });
990
+ }
991
+ /** The header button. The `group` class lets a nested chevron sense the
992
+ * open state (see {@link CollapsibleChevron}). */
993
+ function CollapsibleTrigger({ className, ...props }) {
994
+ return jsx(CollapsiblePrimitive.CollapsibleTrigger, {
995
+ className: cn("group flex w-full items-center gap-2 text-sm font-medium", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", className),
996
+ ...props
997
+ });
998
+ }
999
+ /** The rotating disclosure arrow; renders inside a trigger and turns with
1000
+ * that trigger's open state. */
1001
+ function CollapsibleChevron({ className }) {
1002
+ return jsx("svg", {
1003
+ viewBox: "0 0 12 12",
1004
+ className: cn("h-4 w-4 shrink-0 opacity-60 transition-transform duration-200 group-data-[state=open]:rotate-180", className),
1005
+ fill: "none",
1006
+ "aria-hidden": "true",
1007
+ children: jsx("path", {
1008
+ d: "m2.5 4.5 3.5 3.5 3.5-3.5",
1009
+ stroke: "currentColor",
1010
+ strokeWidth: "1.5",
1011
+ strokeLinecap: "round",
1012
+ strokeLinejoin: "round"
1013
+ })
1014
+ });
1015
+ }
1016
+ /** The body; mounted only while open. */
1017
+ function CollapsibleContent({ className, ...props }) {
1018
+ return jsx(CollapsiblePrimitive.CollapsibleContent, {
1019
+ className: cn("overflow-hidden", className),
1020
+ ...props
1021
+ });
1022
+ }
1023
+ //#endregion
1024
+ //#region lib/types/ui/icons.js
1025
+ /** The shared stroke skeleton every icon renders. */
1026
+ function icon(path, className) {
1027
+ return jsx("svg", {
1028
+ viewBox: "0 0 24 24",
1029
+ className: cn("h-4 w-4 shrink-0", className),
1030
+ fill: "none",
1031
+ stroke: "currentColor",
1032
+ strokeWidth: 2,
1033
+ strokeLinecap: "round",
1034
+ strokeLinejoin: "round",
1035
+ "aria-hidden": "true",
1036
+ children: path
1037
+ });
1038
+ }
1039
+ /** A plus glyph — the "add / install something" affordance. */
1040
+ function IconPlus({ className }) {
1041
+ return icon(jsxs(Fragment, { children: [jsx("path", { d: "M5 12h14" }), jsx("path", { d: "M12 5v14" })] }), className);
1042
+ }
1043
+ /** Chevrons pointing together (down-up) — the "collapse all" affordance. */
1044
+ function IconChevronsDownUp({ className }) {
1045
+ return icon(jsxs(Fragment, { children: [jsx("path", { d: "m7 20 5-5 5 5" }), jsx("path", { d: "m7 4 5 5 5-5" })] }), className);
1046
+ }
1047
+ /** Chevrons pointing apart (up-down) — the "expand all" affordance. */
1048
+ function IconChevronsUpDown({ className }) {
1049
+ return icon(jsxs(Fragment, { children: [jsx("path", { d: "m7 15 5 5 5-5" }), jsx("path", { d: "m7 9 5-5 5 5" })] }), className);
1050
+ }
1051
+ //#endregion
1052
+ //#region lib/types/ui/led.js
1053
+ const TONE_CLASS = {
1054
+ green: "bg-success",
1055
+ yellow: "bg-warning",
1056
+ red: "bg-destructive"
1057
+ };
1058
+ /** One pulsing dot; pass `aria-label` when it is the only status surface. */
1059
+ function Led({ tone, className, ...props }) {
1060
+ return jsx("span", {
1061
+ "data-tone": tone,
1062
+ className: cn("inline-block h-3 w-3 shrink-0 animate-pulse rounded-full", TONE_CLASS[tone], className),
1063
+ ...props
1064
+ });
1065
+ }
1066
+ //#endregion
1067
+ //#region lib/types/ui/theme.js
1068
+ /**
1069
+ * Theme settings: the `ui.theme` settings.json row (a mode choice — the
1070
+ * cobalt accent is fixed per mode) and its DOM application. The renderer
1071
+ * resident that owns the 主题 settings page reads and writes the row; the
1072
+ * `settings/changed` frame hot-applies it on every surface without a
1073
+ * restart.
1074
+ *
1075
+ * @module @snap-rail/framework-shadcn/ui/theme
1076
+ */
1077
+ /** settings.json key carrying the theme row. */
1078
+ const THEME_SETTINGS_KEY = "ui.theme";
1079
+ /** The theme row; invalid stored rows fall back to {@link DEFAULT_THEME}. */
1080
+ const themeSettingsSchema = z.object({ mode: z.enum([
1081
+ "system",
1082
+ "light",
1083
+ "dark"
1084
+ ]) });
1085
+ /** Applied before any settings arrive; keeps the terminal dark as before. */
1086
+ const DEFAULT_THEME = { mode: "dark" };
1087
+ /** The mode a setting resolves to (`system` consults the OS preference). */
1088
+ function resolveThemeMode(mode, prefersLight) {
1089
+ if (mode === "system") return prefersLight ? "light" : "dark";
1090
+ return mode;
1091
+ }
1092
+ /** Apply a theme row: sets `data-mode` on the document element (the token
1093
+ * blocks in theme.css key off it). A missing matchMedia — as in tests —
1094
+ * resolves `system` to dark. */
1095
+ function applyTheme(document, theme) {
1096
+ const view = document.defaultView;
1097
+ const prefersLight = view !== null && typeof view.matchMedia === "function" && view.matchMedia("(prefers-color-scheme: light)").matches;
1098
+ document.documentElement.dataset.mode = resolveThemeMode(theme.mode, prefersLight);
1099
+ }
1100
+ //#endregion
1101
+ export { Badge, Button, Card, CardContent, CardHeader, CardTitle, Checkbox, Collapsible, CollapsibleChevron, CollapsibleContent, CollapsibleTrigger, DEFAULT_THEME, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DragScroll, IconChevronsDownUp, IconChevronsUpDown, IconPlus, Input, Label, Led, NumberInput, NumberPad, Progress, RadioGroup, RadioGroupItem, SchemaForm, ScrollArea, Switch, THEME_SETTINGS_KEY, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, Tooltip, TooltipRoot, TouchSelect, applyTheme, cn, compactSchemaValue, planSchemaFields, resolveThemeMode, schemaDefaults, themeSettingsSchema, useRefresh };