@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.
- package/README.md +20 -2
- package/docs/framework-authoring.md +59 -0
- package/lib/fallback.js +307 -0
- package/lib/index.js +3036 -0
- package/lib/runtime.js +2827 -0
- package/lib/semantic.js +96 -0
- package/lib/session.js +48 -0
- package/lib/settings-pages.js +68 -0
- package/lib/slots.js +95 -0
- package/lib/types/Shell.d.ts +13 -0
- package/lib/types/Shell.js +17 -0
- package/lib/types/client.d.ts +41 -0
- package/lib/types/client.js +39 -0
- package/lib/types/fallback.d.ts +15 -0
- package/lib/types/fallback.js +52 -0
- package/lib/types/index.d.ts +17 -0
- package/lib/types/index.js +17 -0
- package/lib/types/runtime.d.ts +53 -0
- package/lib/types/runtime.js +75 -0
- package/lib/types/semantic.d.ts +55 -0
- package/lib/types/semantic.js +63 -0
- package/lib/types/session.d.ts +43 -0
- package/lib/types/session.js +76 -0
- package/lib/types/settings-dialog.d.ts +17 -0
- package/lib/types/settings-dialog.js +138 -0
- package/lib/types/settings-pages.d.ts +68 -0
- package/lib/types/settings-pages.js +85 -0
- package/lib/types/settings-plugins-page.d.ts +29 -0
- package/lib/types/settings-plugins-page.js +466 -0
- package/lib/types/settings-update-page.d.ts +17 -0
- package/lib/types/settings-update-page.js +124 -0
- package/lib/types/slots.d.ts +114 -0
- package/lib/types/slots.js +119 -0
- package/lib/types/ui/badge.d.ts +17 -0
- package/lib/types/ui/badge.js +29 -0
- package/lib/types/ui/button.d.ts +20 -0
- package/lib/types/ui/button.js +39 -0
- package/lib/types/ui/card.d.ts +14 -0
- package/lib/types/ui/card.js +18 -0
- package/lib/types/ui/checkbox.d.ts +10 -0
- package/lib/types/ui/checkbox.js +13 -0
- package/lib/types/ui/collapsible.d.ts +24 -0
- package/lib/types/ui/collapsible.js +31 -0
- package/lib/types/ui/dialog.d.ts +22 -0
- package/lib/types/ui/dialog.js +29 -0
- package/lib/types/ui/drag-scroll.d.ts +13 -0
- package/lib/types/ui/drag-scroll.js +85 -0
- package/lib/types/ui/hooks.d.ts +16 -0
- package/lib/types/ui/hooks.js +26 -0
- package/lib/types/ui/icons.d.ts +21 -0
- package/lib/types/ui/icons.js +18 -0
- package/lib/types/ui/index.d.ts +32 -0
- package/lib/types/ui/index.js +32 -0
- package/lib/types/ui/input.d.ts +8 -0
- package/lib/types/ui/input.js +6 -0
- package/lib/types/ui/label.d.ts +9 -0
- package/lib/types/ui/label.js +12 -0
- package/lib/types/ui/led.d.ts +13 -0
- package/lib/types/ui/led.js +11 -0
- package/lib/types/ui/number-input.d.ts +35 -0
- package/lib/types/ui/number-input.js +48 -0
- package/lib/types/ui/progress.d.ts +10 -0
- package/lib/types/ui/progress.js +14 -0
- package/lib/types/ui/radio-group.d.ts +12 -0
- package/lib/types/ui/radio-group.js +17 -0
- package/lib/types/ui/schema-form.d.ts +51 -0
- package/lib/types/ui/schema-form.js +124 -0
- package/lib/types/ui/scroll-area.d.ts +10 -0
- package/lib/types/ui/scroll-area.js +13 -0
- package/lib/types/ui/switch.d.ts +10 -0
- package/lib/types/ui/switch.js +13 -0
- package/lib/types/ui/table.d.ts +20 -0
- package/lib/types/ui/table.js +28 -0
- package/lib/types/ui/tabs.d.ts +16 -0
- package/lib/types/ui/tabs.js +23 -0
- package/lib/types/ui/textarea.d.ts +10 -0
- package/lib/types/ui/textarea.js +6 -0
- package/lib/types/ui/theme.d.ts +30 -0
- package/lib/types/ui/theme.js +33 -0
- package/lib/types/ui/tooltip.d.ts +23 -0
- package/lib/types/ui/tooltip.js +15 -0
- package/lib/types/ui/touch-select.d.ts +29 -0
- package/lib/types/ui/touch-select.js +37 -0
- package/lib/types/ui/utils.d.ts +13 -0
- package/lib/types/ui/utils.js +16 -0
- package/lib/types/variables.d.ts +168 -0
- package/lib/types/variables.js +282 -0
- package/lib/types/workflows.d.ts +98 -0
- package/lib/types/workflows.js +95 -0
- package/lib/ui/index.js +1101 -0
- package/lib/ui/theme.css +271 -0
- package/lib/variables.js +260 -0
- package/lib/workflows.js +85 -0
- package/package.json +138 -3
- package/src/ui/theme.css +271 -0
- package/templates/ui-extension/README.md +17 -0
- package/templates/ui-view/README.md +17 -0
package/lib/ui/index.js
ADDED
|
@@ -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 };
|