@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,124 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * SchemaForm: the settings vocabulary's generic form engine. It renders one
4
+ * labeled control per property of a JSON Schema (the input-form projection
5
+ * drivers register with the field base — zod's `toJSONSchema`), on the touch
6
+ * baseline: enums and literal unions become TouchSelect rows (committing the
7
+ * literal's own type — a number const rides back as a number), numbers open
8
+ * the NumberPad, booleans ride a Switch. Property `title`s (zod
9
+ * `.meta({ title })`) label the fields; the host's zod schemas remain the
10
+ * only validator — this form shapes input, it does not judge it.
11
+ *
12
+ * The vocabulary is meant to grow (discovery pickers, repeatable groups);
13
+ * what a schema expresses beyond today's subset renders as a plain text
14
+ * field and still round-trips through the host's validation.
15
+ *
16
+ * @module @snap-rail/framework-shadcn/ui/schema-form
17
+ */
18
+ import {} from 'react';
19
+ import { Input } from "./input.js";
20
+ import { Label } from "./label.js";
21
+ import { NumberInput } from "./number-input.js";
22
+ import { Switch } from "./switch.js";
23
+ import { TouchSelect } from "./touch-select.js";
24
+ import { cn } from "./utils.js";
25
+ function isPropertySchema(raw) {
26
+ return typeof raw === 'object' && raw !== null;
27
+ }
28
+ /** Plan the renderable fields of an object schema (property order kept). */
29
+ export function planSchemaFields(schema, hidden = []) {
30
+ const properties = schema.properties;
31
+ const required = new Set(Array.isArray(schema.required) ? schema.required.filter((name) => typeof name === 'string') : []);
32
+ const fields = [];
33
+ if (!isPropertySchema(properties))
34
+ return fields;
35
+ for (const [name, rawProperty] of Object.entries(properties)) {
36
+ if (hidden.includes(name))
37
+ continue;
38
+ const property = rawProperty;
39
+ const label = typeof property.title === 'string' && property.title !== '' ? property.title : name;
40
+ const field = { name, label, required: required.has(name), kind: 'string' };
41
+ // zod emits a union of literals either way: plain enums as `enum`, and
42
+ // `z.union([z.literal(1), …])` as `anyOf: [{const: 1}, …]`.
43
+ const literals = Array.isArray(property.enum) ? property.enum
44
+ : Array.isArray(property.anyOf)
45
+ ? property.anyOf.map(entry => isPropertySchema(entry) && 'const' in entry
46
+ ? entry.const
47
+ : undefined)
48
+ : undefined;
49
+ const typed = literals?.filter((value) => typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean');
50
+ if (literals !== undefined && typed !== undefined && typed.length > 0 && typed.length === literals.length) {
51
+ const valueOf = {};
52
+ field.kind = 'enum';
53
+ field.options = typed.map(value => {
54
+ const key = String(value);
55
+ valueOf[key] = value;
56
+ return { value: key, label: key };
57
+ });
58
+ field.literalOf = valueOf;
59
+ }
60
+ else {
61
+ switch (property.type) {
62
+ case 'integer':
63
+ field.kind = 'integer';
64
+ break;
65
+ case 'number':
66
+ field.kind = 'number';
67
+ break;
68
+ case 'boolean':
69
+ field.kind = 'boolean';
70
+ break;
71
+ case 'string':
72
+ field.kind = 'string';
73
+ break;
74
+ default: field.kind = 'text';
75
+ }
76
+ }
77
+ if (typeof property.description === 'string')
78
+ field.placeholder = property.description;
79
+ fields.push(field);
80
+ }
81
+ return fields;
82
+ }
83
+ /** Initial values from an object schema's `default`s (create dialogs). */
84
+ export function schemaDefaults(schema) {
85
+ const value = {};
86
+ const properties = schema.properties;
87
+ if (!isPropertySchema(properties))
88
+ return value;
89
+ for (const [name, rawProperty] of Object.entries(properties)) {
90
+ const fallback = rawProperty.default;
91
+ if (typeof fallback === 'string' || typeof fallback === 'number' || typeof fallback === 'boolean') {
92
+ value[name] = fallback;
93
+ }
94
+ }
95
+ return value;
96
+ }
97
+ /** Strip keys whose value is `undefined` so unset optionals ride clean. */
98
+ export function compactSchemaValue(value) {
99
+ const out = {};
100
+ for (const [key, entry] of Object.entries(value)) {
101
+ if (entry !== undefined)
102
+ out[key] = entry;
103
+ }
104
+ return out;
105
+ }
106
+ /** One labeled control. */
107
+ function SchemaField(props) {
108
+ const { field, value, onChange, disabled = false } = props;
109
+ const setString = (next) => { onChange(next === '' ? undefined : next); };
110
+ return (_jsxs("div", { className: "grid gap-2", "data-schema-field": field.name, children: [_jsxs(Label, { htmlFor: `schema-${field.name}`, children: [field.label, field.required && _jsx("span", { className: "ml-1 text-destructive", children: "*" })] }), field.kind === 'enum' && field.options !== undefined && (_jsx(TouchSelect, { label: field.label, value: value === undefined ? '' : String(value), options: field.options, onValueChange: next => { onChange(field.literalOf?.[next] ?? next); }, disabled: disabled })), (field.kind === 'integer' || field.kind === 'number') && (_jsx(NumberInput, { label: field.label, value: value === undefined ? '' : String(value), onChange: text => {
111
+ if (text === '') {
112
+ onChange(undefined);
113
+ return;
114
+ }
115
+ const parsed = field.kind === 'integer' ? Number.parseInt(text, 10) : Number.parseFloat(text);
116
+ onChange(Number.isNaN(parsed) ? undefined : parsed);
117
+ }, allowDecimal: field.kind === 'number', placeholder: field.placeholder ?? '—', disabled: disabled })), field.kind === 'boolean' && (_jsx("div", { className: "flex h-12 items-center", children: _jsx(Switch, { id: `schema-${field.name}`, checked: value === true, onCheckedChange: onChange, disabled: disabled }) })), (field.kind === 'string' || field.kind === 'text') && (_jsx(Input, { id: `schema-${field.name}`, value: value === undefined ? '' : String(value), onChange: event => { setString(event.target.value); }, placeholder: field.placeholder, disabled: disabled }))] }));
118
+ }
119
+ /** A controlled, schema-driven form body (labels + controls, no buttons). */
120
+ export function SchemaForm(props) {
121
+ const { schema, value, onChange, hidden = [], disabled = false, className } = props;
122
+ const fields = planSchemaFields(schema, hidden);
123
+ return (_jsxs("div", { className: cn('grid gap-5', className), "data-schema-form": true, children: [fields.map(field => (_jsx(SchemaField, { field: field, value: value[field.name], disabled: disabled, onChange: next => { onChange({ ...value, [field.name]: next }); } }, field.name))), fields.length === 0 && (_jsx("div", { className: "rounded-md border border-dashed border-border px-4 py-6 text-center text-sm text-muted-foreground", children: "\u6B64\u9A71\u52A8\u65E0\u989D\u5916\u914D\u7F6E\u9879" }))] }));
124
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * ScrollArea: Radix scroll area with a themed slim scrollbar — panels keep
3
+ * native scrolling semantics without OS chrome.
4
+ *
5
+ * @module @snap-rail/framework-shadcn/ui/scroll-area
6
+ */
7
+ import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
8
+ import type { ComponentProps } from 'react';
9
+ /** A themed scrollable viewport. */
10
+ export declare function ScrollArea({ className, children, ...props }: ComponentProps<typeof ScrollAreaPrimitive.Root>): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ScrollArea: Radix scroll area with a themed slim scrollbar — panels keep
4
+ * native scrolling semantics without OS chrome.
5
+ *
6
+ * @module @snap-rail/framework-shadcn/ui/scroll-area
7
+ */
8
+ import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
9
+ import { cn } from "./utils.js";
10
+ /** A themed scrollable viewport. */
11
+ export function ScrollArea({ className, children, ...props }) {
12
+ return (_jsxs(ScrollAreaPrimitive.Root, { className: cn('overflow-hidden', className), ...props, children: [_jsx(ScrollAreaPrimitive.Viewport, { className: "h-full w-full", children: children }), _jsx(ScrollAreaPrimitive.Scrollbar, { orientation: "vertical", className: "flex w-1.5 touch-none select-none border-l border-transparent p-px", children: _jsx(ScrollAreaPrimitive.Thumb, { className: "relative flex-1 rounded-full bg-muted-foreground/40" }) }), _jsx(ScrollAreaPrimitive.Corner, {})] }));
13
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Switch: the Radix switch with snap-rail chrome — boolean control for
3
+ * toggles like the offline simulation.
4
+ *
5
+ * @module @snap-rail/framework-shadcn/ui/switch
6
+ */
7
+ import * as SwitchPrimitive from '@radix-ui/react-switch';
8
+ import type { ComponentProps } from 'react';
9
+ /** A themed on/off control. */
10
+ export declare function Switch({ className, ...props }: ComponentProps<typeof SwitchPrimitive.Root>): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Switch: the Radix switch with snap-rail chrome — boolean control for
4
+ * toggles like the offline simulation.
5
+ *
6
+ * @module @snap-rail/framework-shadcn/ui/switch
7
+ */
8
+ import * as SwitchPrimitive from '@radix-ui/react-switch';
9
+ import { cn } from "./utils.js";
10
+ /** A themed on/off control. */
11
+ export function Switch({ className, ...props }) {
12
+ return (_jsx(SwitchPrimitive.Root, { 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), ...props, 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" }) }));
13
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Table: the data-table family (table, header, body, row, head, cell) —
3
+ * themed native elements in the shadcn shape, no behaviour attached.
4
+ *
5
+ * @module @snap-rail/framework-shadcn/ui/table
6
+ */
7
+ import type { ComponentProps } from 'react';
8
+ /** The grid container with a hairline bottom rule; the wrapper pans under
9
+ * a held mouse drag (see {@link DragScroll}). */
10
+ export declare function Table({ className, ...props }: ComponentProps<'table'>): import("react").JSX.Element;
11
+ /** Header group. */
12
+ export declare function TableHeader({ className, ...props }: ComponentProps<'thead'>): import("react").JSX.Element;
13
+ /** Body group. */
14
+ export declare function TableBody({ className, ...props }: ComponentProps<'tbody'>): import("react").JSX.Element;
15
+ /** One row: hairline separators, hover affordance when interactive. */
16
+ export declare function TableRow({ className, ...props }: ComponentProps<'tr'>): import("react").JSX.Element;
17
+ /** One column head: muted, left-aligned unless overridden. */
18
+ export declare function TableHead({ className, ...props }: ComponentProps<'th'>): import("react").JSX.Element;
19
+ /** One cell. */
20
+ export declare function TableCell({ className, ...props }: ComponentProps<'td'>): import("react").JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { DragScroll } from "./drag-scroll.js";
3
+ import { cn } from "./utils.js";
4
+ /** The grid container with a hairline bottom rule; the wrapper pans under
5
+ * a held mouse drag (see {@link DragScroll}). */
6
+ export function Table({ className, ...props }) {
7
+ return (_jsx(DragScroll, { className: "w-full", children: _jsx("table", { className: cn('w-full caption-bottom text-sm', className), ...props }) }));
8
+ }
9
+ /** Header group. */
10
+ export function TableHeader({ className, ...props }) {
11
+ return _jsx("thead", { className: cn('[&_tr]:border-b', className), ...props });
12
+ }
13
+ /** Body group. */
14
+ export function TableBody({ className, ...props }) {
15
+ return _jsx("tbody", { className: cn('[&_tr:last-child]:border-0', className), ...props });
16
+ }
17
+ /** One row: hairline separators, hover affordance when interactive. */
18
+ export function TableRow({ className, ...props }) {
19
+ return (_jsx("tr", { className: cn('border-b border-border transition-colors hover:bg-accent/40 data-[state=selected]:bg-accent', className), ...props }));
20
+ }
21
+ /** One column head: muted, left-aligned unless overridden. */
22
+ export function TableHead({ className, ...props }) {
23
+ return _jsx("th", { className: cn('h-11 px-3 text-left align-middle text-sm font-medium text-muted-foreground', className), ...props });
24
+ }
25
+ /** One cell. */
26
+ export function TableCell({ className, ...props }) {
27
+ return _jsx("td", { className: cn('px-3 py-3 align-middle', className), ...props });
28
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Tabs: themed pager over Radix Tabs (list, trigger, content). Icons are
3
+ * inline strokes so the UI seam needs no icon dependency.
4
+ *
5
+ * @module @snap-rail/framework-shadcn/ui/tabs
6
+ */
7
+ import * as TabsPrimitive from '@radix-ui/react-tabs';
8
+ import type { ComponentProps } from 'react';
9
+ /** Controlled/uncontrolled root; re-exported so callers need one import. */
10
+ export declare const Tabs: import("react").ForwardRefExoticComponent<TabsPrimitive.TabsProps & import("react").RefAttributes<HTMLDivElement>>;
11
+ /** The tab strip; triggers rest on the muted surface, the active one lifts. */
12
+ export declare function TabsList({ className, ...props }: ComponentProps<typeof TabsPrimitive.List>): import("react").JSX.Element;
13
+ /** One tab button; the data attributes carry the active/hover styling. */
14
+ export declare function TabsTrigger({ className, ...props }: ComponentProps<typeof TabsPrimitive.Trigger>): import("react").JSX.Element;
15
+ /** The panel of one tab; mounted only while its trigger is active. */
16
+ export declare function TabsContent({ className, ...props }: ComponentProps<typeof TabsPrimitive.Content>): import("react").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Tabs: themed pager over Radix Tabs (list, trigger, content). Icons are
4
+ * inline strokes so the UI seam needs no icon dependency.
5
+ *
6
+ * @module @snap-rail/framework-shadcn/ui/tabs
7
+ */
8
+ import * as TabsPrimitive from '@radix-ui/react-tabs';
9
+ import { cn } from "./utils.js";
10
+ /** Controlled/uncontrolled root; re-exported so callers need one import. */
11
+ export const Tabs = TabsPrimitive.Root;
12
+ /** The tab strip; triggers rest on the muted surface, the active one lifts. */
13
+ export function TabsList({ className, ...props }) {
14
+ return (_jsx(TabsPrimitive.List, { className: cn('inline-flex h-12 items-center gap-1 rounded-lg bg-muted p-1 text-muted-foreground', className), ...props }));
15
+ }
16
+ /** One tab button; the data attributes carry the active/hover styling. */
17
+ export function TabsTrigger({ className, ...props }) {
18
+ return (_jsx(TabsPrimitive.Trigger, { 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), ...props }));
19
+ }
20
+ /** The panel of one tab; mounted only while its trigger is active. */
21
+ export function TabsContent({ className, ...props }) {
22
+ return (_jsx(TabsPrimitive.Content, { className: cn('mt-4 focus-visible:outline-none', className), ...props }));
23
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Textarea: themed multi-line text entry for prompts, notes, and free-form
3
+ * descriptions (ported from shadcn/ui's textarea primitive, flattened to
4
+ * this theme's tokens and touch baseline).
5
+ *
6
+ * @module @snap-rail/framework-shadcn/ui/textarea
7
+ */
8
+ import type { ComponentProps } from 'react';
9
+ /** A themed multi-line text area. */
10
+ export declare function Textarea({ className, ...props }: ComponentProps<'textarea'>): import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from "./utils.js";
3
+ /** A themed multi-line text area. */
4
+ export function Textarea({ className, ...props }) {
5
+ return (_jsx("textarea", { 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), ...props }));
6
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Theme settings: the `ui.theme` settings.json row (a mode choice — the
3
+ * cobalt accent is fixed per mode) and its DOM application. The renderer
4
+ * resident that owns the 主题 settings page reads and writes the row; the
5
+ * `settings/changed` frame hot-applies it on every surface without a
6
+ * restart.
7
+ *
8
+ * @module @snap-rail/framework-shadcn/ui/theme
9
+ */
10
+ import { z } from 'zod';
11
+ /** settings.json key carrying the theme row. */
12
+ export declare const THEME_SETTINGS_KEY = "ui.theme";
13
+ /** The theme row; invalid stored rows fall back to {@link DEFAULT_THEME}. */
14
+ export declare const themeSettingsSchema: z.ZodObject<{
15
+ mode: z.ZodEnum<{
16
+ system: "system";
17
+ light: "light";
18
+ dark: "dark";
19
+ }>;
20
+ }, z.core.$strip>;
21
+ /** Parsed theme row. */
22
+ export type ThemeSettings = z.infer<typeof themeSettingsSchema>;
23
+ /** Applied before any settings arrive; keeps the terminal dark as before. */
24
+ export declare const DEFAULT_THEME: ThemeSettings;
25
+ /** The mode a setting resolves to (`system` consults the OS preference). */
26
+ export declare function resolveThemeMode(mode: ThemeSettings['mode'], prefersLight: boolean): 'light' | 'dark';
27
+ /** Apply a theme row: sets `data-mode` on the document element (the token
28
+ * blocks in theme.css key off it). A missing matchMedia — as in tests —
29
+ * resolves `system` to dark. */
30
+ export declare function applyTheme(document: Document, theme: ThemeSettings): void;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Theme settings: the `ui.theme` settings.json row (a mode choice — the
3
+ * cobalt accent is fixed per mode) and its DOM application. The renderer
4
+ * resident that owns the 主题 settings page reads and writes the row; the
5
+ * `settings/changed` frame hot-applies it on every surface without a
6
+ * restart.
7
+ *
8
+ * @module @snap-rail/framework-shadcn/ui/theme
9
+ */
10
+ import { z } from 'zod';
11
+ /** settings.json key carrying the theme row. */
12
+ export const THEME_SETTINGS_KEY = 'ui.theme';
13
+ /** The theme row; invalid stored rows fall back to {@link DEFAULT_THEME}. */
14
+ export const themeSettingsSchema = z.object({
15
+ mode: z.enum(['system', 'light', 'dark']),
16
+ });
17
+ /** Applied before any settings arrive; keeps the terminal dark as before. */
18
+ export const DEFAULT_THEME = { mode: 'dark' };
19
+ /** The mode a setting resolves to (`system` consults the OS preference). */
20
+ export function resolveThemeMode(mode, prefersLight) {
21
+ if (mode === 'system')
22
+ return prefersLight ? 'light' : 'dark';
23
+ return mode;
24
+ }
25
+ /** Apply a theme row: sets `data-mode` on the document element (the token
26
+ * blocks in theme.css key off it). A missing matchMedia — as in tests —
27
+ * resolves `system` to dark. */
28
+ export function applyTheme(document, theme) {
29
+ const view = document.defaultView;
30
+ const prefersLight = view !== null && typeof view.matchMedia === 'function'
31
+ && view.matchMedia('(prefers-color-scheme: light)').matches;
32
+ document.documentElement.dataset.mode = resolveThemeMode(theme.mode, prefersLight);
33
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Tooltip: Radix tooltip wrapping its own provider so occupants mount it
3
+ * standalone (no app-level provider requirement).
4
+ *
5
+ * @module @snap-rail/framework-shadcn/ui/tooltip
6
+ */
7
+ import * as TooltipPrimitive from '@radix-ui/react-tooltip';
8
+ import type { ComponentProps, ReactNode } from 'react';
9
+ /** Props of {@link Tooltip}. */
10
+ export interface TooltipProps {
11
+ /** The tip text (or node). */
12
+ content: ReactNode;
13
+ /** The wrapped trigger; keeps its own markup. */
14
+ children: ReactNode;
15
+ /** Open delay in milliseconds; defaults near-instant for chrome controls. */
16
+ delay?: number;
17
+ }
18
+ /** Hover/focus tip over any trigger. */
19
+ export declare function Tooltip({ content, children, delay }: TooltipProps): import("react").JSX.Element;
20
+ /** Re-export the primitive root for direct composition. */
21
+ export declare const TooltipRoot: import("react").FC<TooltipPrimitive.TooltipProps>;
22
+ /** Props alias for content-side customization. */
23
+ export type TooltipContentProps = ComponentProps<typeof TooltipPrimitive.Content>;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Tooltip: Radix tooltip wrapping its own provider so occupants mount it
4
+ * standalone (no app-level provider requirement).
5
+ *
6
+ * @module @snap-rail/framework-shadcn/ui/tooltip
7
+ */
8
+ import * as TooltipPrimitive from '@radix-ui/react-tooltip';
9
+ import { cn } from "./utils.js";
10
+ /** Hover/focus tip over any trigger. */
11
+ export function Tooltip({ content, children, delay = 200 }) {
12
+ return (_jsx(TooltipPrimitive.Provider, { delayDuration: delay, children: _jsxs(TooltipPrimitive.Root, { children: [_jsx(TooltipPrimitive.Trigger, { asChild: true, children: children }), _jsx(TooltipPrimitive.Portal, { children: _jsx(TooltipPrimitive.Content, { sideOffset: 4, className: cn('z-50 rounded-md border border-border bg-popover px-2 py-1 text-xs text-popover-foreground'), children: content }) })] }) }));
13
+ }
14
+ /** Re-export the primitive root for direct composition. */
15
+ export const TooltipRoot = TooltipPrimitive.Root;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * TouchSelect: a touch-first single-choice picker. The trigger opens a
3
+ * modal listing one full-width button row per option — no popover aiming,
4
+ * every row a 56px target; tapping a row commits and closes. Replaces
5
+ * dropdown selects across the touch terminal.
6
+ *
7
+ * @module @snap-rail/framework-shadcn/ui/touch-select
8
+ */
9
+ import { type ReactNode } from 'react';
10
+ /** One selectable row: `label`/`hint` render inside the list; `triggerLabel`
11
+ * is what the closed trigger summarizes (defaults to `label`). */
12
+ export interface TouchSelectOption {
13
+ value: string;
14
+ label: ReactNode;
15
+ hint?: ReactNode;
16
+ triggerLabel?: ReactNode;
17
+ disabled?: boolean;
18
+ }
19
+ /** A modal-based picker: an outline trigger plus a Dialog list of rows. */
20
+ export declare function TouchSelect(props: {
21
+ /** Field name; doubles as the trigger's aria-label and the modal title. */
22
+ label: string;
23
+ value: string;
24
+ options: readonly TouchSelectOption[];
25
+ onValueChange: (value: string) => void;
26
+ placeholder?: string;
27
+ disabled?: boolean;
28
+ className?: string;
29
+ }): ReactNode;
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * TouchSelect: a touch-first single-choice picker. The trigger opens a
4
+ * modal listing one full-width button row per option — no popover aiming,
5
+ * every row a 56px target; tapping a row commits and closes. Replaces
6
+ * dropdown selects across the touch terminal.
7
+ *
8
+ * @module @snap-rail/framework-shadcn/ui/touch-select
9
+ */
10
+ import { useState } from 'react';
11
+ import { Button } from "./button.js";
12
+ import { Dialog, DialogContent, DialogTitle } from "./dialog.js";
13
+ import { DragScroll } from "./drag-scroll.js";
14
+ import { cn } from "./utils.js";
15
+ /** A chevron pointing up: the picker opens an upward modal, not a dropdown. */
16
+ function ChevronUp() {
17
+ return (_jsx("svg", { viewBox: "0 0 12 12", className: "h-4 w-4 shrink-0 opacity-60", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "m2.5 7.5 3.5-3.5 3.5 3.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }));
18
+ }
19
+ /** The committed row's check mark, mirroring the Checkbox indicator stroke. */
20
+ function CheckIcon() {
21
+ return (_jsx("svg", { viewBox: "0 0 12 12", className: "h-4 w-4 text-primary", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M2 6.5 4.8 9 10 3.5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
22
+ }
23
+ /** A modal-based picker: an outline trigger plus a Dialog list of rows. */
24
+ export function TouchSelect(props) {
25
+ const { label, value, options, onValueChange, placeholder = '请选择', disabled = false, className } = props;
26
+ const [open, setOpen] = useState(false);
27
+ const selected = options.find(option => option.value === value);
28
+ return (_jsxs(_Fragment, { children: [_jsxs(Button, { type: "button", variant: "outline", disabled: disabled, "aria-label": label, "data-touch-select-trigger": label, className: cn('h-12 w-full justify-between px-4 text-base font-normal', className), onClick: () => { setOpen(true); }, children: [selected === undefined
29
+ ? _jsx("span", { className: "truncate text-muted-foreground", children: placeholder })
30
+ : _jsx("span", { className: "min-w-0 truncate", children: selected.triggerLabel ?? selected.label }), _jsx(ChevronUp, {})] }), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { "aria-describedby": undefined, className: "max-w-md p-0", children: [_jsx("div", { className: "border-b border-border px-5 py-4", children: _jsx(DialogTitle, { className: "text-base font-medium", children: label }) }), _jsxs(DragScroll, { className: "max-h-[65vh] p-2", children: [options.length === 0 && (_jsx("div", { className: "px-4 py-6 text-center text-sm text-muted-foreground", children: "\u6682\u65E0\u9009\u9879" })), options.map(option => {
31
+ const isSelected = option.value === value;
32
+ return (_jsxs(Button, { type: "button", variant: "ghost", disabled: option.disabled, "data-option": option.value, "aria-pressed": isSelected, className: cn('h-14 w-full justify-between px-4 text-base font-normal', isSelected && 'bg-secondary channel-keyline'), onClick: () => {
33
+ onValueChange(option.value);
34
+ setOpen(false);
35
+ }, children: [_jsx("span", { className: "min-w-0 truncate", children: option.label }), _jsxs("span", { className: "flex shrink-0 items-center gap-2 text-sm text-muted-foreground", children: [option.hint, isSelected && _jsx(CheckIcon, {})] })] }, option.value));
36
+ })] })] }) })] }));
37
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Class-name combiner: clsx joins, tailwind-merge resolves conflicting
3
+ * Tailwind utilities with the last one winning — the shadcn convention.
4
+ *
5
+ * @module @snap-rail/framework-shadcn/ui/utils
6
+ */
7
+ import { type ClassValue } from 'clsx';
8
+ /**
9
+ * Merge conditional class names into one Tailwind-aware string.
10
+ * @param inputs - class values (strings, arrays, conditional objects).
11
+ * @returns the merged class string.
12
+ */
13
+ export declare function cn(...inputs: ClassValue[]): string;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Class-name combiner: clsx joins, tailwind-merge resolves conflicting
3
+ * Tailwind utilities with the last one winning — the shadcn convention.
4
+ *
5
+ * @module @snap-rail/framework-shadcn/ui/utils
6
+ */
7
+ import { clsx } from 'clsx';
8
+ import { twMerge } from 'tailwind-merge';
9
+ /**
10
+ * Merge conditional class names into one Tailwind-aware string.
11
+ * @param inputs - class values (strings, arrays, conditional objects).
12
+ * @returns the merged class string.
13
+ */
14
+ export function cn(...inputs) {
15
+ return twMerge(clsx(inputs));
16
+ }