@zodal/dials-ui-shadcn 0.1.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 ADDED
@@ -0,0 +1,24 @@
1
+ # @zodal/dials-ui-shadcn
2
+
3
+ React renderer for [`@zodal/dials-ui`](https://github.com/i2mint/zodal-dials) — settings panel components that consume the headless config. Plain elements with `zodal-dials-*` classes; swap in shadcn/ui primitives in a consumer.
4
+
5
+ ```bash
6
+ npm i @zodal/dials-ui-shadcn @zodal/dials-core @zodal/dials-ui react react-dom
7
+ ```
8
+
9
+ ```tsx
10
+ import { SettingsPanel } from '@zodal/dials-ui-shadcn';
11
+ import { toSettingsForm, toFieldStates } from '@zodal/dials-ui';
12
+
13
+ const result = dials.resolve(layers, { maskSecrets: true });
14
+ const form = toSettingsForm(dials, { result });
15
+ const states = toFieldStates(form.fields, result, dirtyKeys);
16
+
17
+ <SettingsPanel form={form} states={states} onChange={setValue} onReset={resetKey} />
18
+ ```
19
+
20
+ - `SettingsPanel` (search + facet sections) and `SettingField` (one row, provenance badges + reset).
21
+ - `createShadcnSettingsRegistry()` — a per-widget React control registry with a terminal rawJson fallback; register a higher-priority control to override.
22
+ - Secrets render **masked** (write-only field, masked status) and never echo plaintext.
23
+
24
+ Part of the [zodal-dials](https://github.com/i2mint/zodal-dials) ecosystem.
package/dist/index.cjs ADDED
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ NumberControl: () => NumberControl,
24
+ RadioControl: () => RadioControl,
25
+ RawJsonControl: () => RawJsonControl,
26
+ SecretControl: () => SecretControl,
27
+ SelectControl: () => SelectControl,
28
+ SettingField: () => SettingField,
29
+ SettingsPanel: () => SettingsPanel,
30
+ SliderControl: () => SliderControl,
31
+ SwitchControl: () => SwitchControl,
32
+ TextControl: () => TextControl,
33
+ createShadcnSettingsRegistry: () => createShadcnSettingsRegistry,
34
+ widgetIs: () => widgetIs
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/widgets.ts
39
+ var import_react = require("react");
40
+ var import_dials_core = require("@zodal/dials-core");
41
+ var isDisabled = (p) => p.field.readOnly || p.state.managed;
42
+ var asText = (v) => v == null ? "" : String(v);
43
+ var jsonString = (v) => {
44
+ try {
45
+ return JSON.stringify(v ?? null, null, 2);
46
+ } catch {
47
+ return String(v);
48
+ }
49
+ };
50
+ function decodeNumber(raw) {
51
+ if (raw.trim() === "") return void 0;
52
+ const n = Number(raw);
53
+ return Number.isNaN(n) ? void 0 : n;
54
+ }
55
+ var SwitchControl = (p) => (0, import_react.createElement)("input", {
56
+ type: "checkbox",
57
+ className: "zodal-dials-switch",
58
+ checked: p.state.value === true,
59
+ disabled: isDisabled(p),
60
+ onChange: (e) => p.onChange?.(e.target.checked)
61
+ });
62
+ var TextControl = (p) => (0, import_react.createElement)("input", {
63
+ type: "text",
64
+ className: "zodal-dials-text",
65
+ value: asText(p.state.value),
66
+ // never use a secret's default as a placeholder (defense in depth; secret defaults are omitted upstream)
67
+ placeholder: p.field.sensitivity === "secret" ? void 0 : asText(p.field.defaultValue),
68
+ disabled: isDisabled(p),
69
+ onChange: (e) => p.onChange?.(e.target.value)
70
+ });
71
+ var NumberControl = (p) => (0, import_react.createElement)("input", {
72
+ type: "number",
73
+ className: "zodal-dials-number",
74
+ value: asText(p.state.value),
75
+ min: p.field.bounds?.min,
76
+ max: p.field.bounds?.max,
77
+ disabled: isDisabled(p),
78
+ onChange: (e) => {
79
+ const v = decodeNumber(e.target.value);
80
+ if (v !== void 0) p.onChange?.(v);
81
+ }
82
+ });
83
+ var SliderControl = (p) => (0, import_react.createElement)(
84
+ "span",
85
+ { className: "zodal-dials-slider-wrap" },
86
+ (0, import_react.createElement)("input", {
87
+ type: "range",
88
+ className: "zodal-dials-slider",
89
+ value: asText(p.state.value),
90
+ min: p.field.bounds?.min,
91
+ max: p.field.bounds?.max,
92
+ disabled: isDisabled(p),
93
+ onChange: (e) => {
94
+ const v = decodeNumber(e.target.value);
95
+ if (v !== void 0) p.onChange?.(v);
96
+ }
97
+ }),
98
+ (0, import_react.createElement)("output", { className: "zodal-dials-slider-value" }, asText(p.state.value))
99
+ );
100
+ var SelectControl = (p) => (0, import_react.createElement)(
101
+ "select",
102
+ {
103
+ className: "zodal-dials-select",
104
+ value: asText(p.state.value),
105
+ disabled: isDisabled(p),
106
+ onChange: (e) => p.onChange?.(e.target.value)
107
+ },
108
+ ...(p.field.enumValues ?? []).map((v, i) => (0, import_react.createElement)("option", { key: `${i}:${v}`, value: v }, v))
109
+ );
110
+ var RadioControl = (p) => (0, import_react.createElement)(
111
+ "fieldset",
112
+ { className: "zodal-dials-radio" },
113
+ ...(p.field.enumValues ?? []).map(
114
+ (v, i) => (0, import_react.createElement)(
115
+ "label",
116
+ { key: `${i}:${v}` },
117
+ (0, import_react.createElement)("input", {
118
+ type: "radio",
119
+ name: p.field.key,
120
+ value: v,
121
+ checked: p.state.value === v,
122
+ disabled: isDisabled(p),
123
+ onChange: () => p.onChange?.(v)
124
+ }),
125
+ v
126
+ )
127
+ )
128
+ );
129
+ var SecretControl = (p) => {
130
+ const ref = (0, import_dials_core.isSecretRef)(p.state.value) ? p.state.value : void 0;
131
+ return (0, import_react.createElement)(
132
+ "span",
133
+ { className: "zodal-dials-secret-wrap" },
134
+ (0, import_react.createElement)("span", { className: "zodal-dials-secret-status" }, ref ? ref.masked : "not set"),
135
+ (0, import_react.createElement)("input", {
136
+ type: "password",
137
+ className: "zodal-dials-secret",
138
+ placeholder: "Enter new value",
139
+ disabled: isDisabled(p),
140
+ onChange: (e) => p.onChange?.(e.target.value)
141
+ })
142
+ );
143
+ };
144
+ var RawJsonControl = (p) => {
145
+ if (p.field.sensitivity === "secret" || (0, import_dials_core.isSecretRef)(p.state.value)) return SecretControl(p);
146
+ return (0, import_react.createElement)(
147
+ "span",
148
+ { className: "zodal-dials-json-wrap" },
149
+ (0, import_react.createElement)("small", { className: "zodal-dials-json-note" }, "rendered as raw JSON (no structured editor for this type)"),
150
+ (0, import_react.createElement)("textarea", {
151
+ // key on the serialized value: remounts (re-seeds) the uncontrolled textarea when the upstream
152
+ // value changes (fixes desync), while keeping local typing smooth when it does not.
153
+ key: jsonString(p.state.value),
154
+ className: "zodal-dials-rawjson",
155
+ defaultValue: jsonString(p.state.value),
156
+ disabled: isDisabled(p),
157
+ onChange: (e) => {
158
+ try {
159
+ p.onChange?.(JSON.parse(e.target.value));
160
+ } catch {
161
+ }
162
+ }
163
+ })
164
+ );
165
+ };
166
+
167
+ // src/registry.ts
168
+ var import_dials_ui = require("@zodal/dials-ui");
169
+ function widgetIs(kind) {
170
+ return (_field, ctx) => ctx.widget === kind ? import_dials_ui.PRIORITY.LIBRARY : -1;
171
+ }
172
+ var WIDGET_CONTROLS = [
173
+ ["switch", SwitchControl],
174
+ ["text", TextControl],
175
+ ["textarea", TextControl],
176
+ ["number", NumberControl],
177
+ ["slider", SliderControl],
178
+ ["select", SelectControl],
179
+ ["radio", RadioControl],
180
+ ["color", TextControl],
181
+ ["date", TextControl],
182
+ ["path", TextControl],
183
+ ["object", RawJsonControl],
184
+ ["array", RawJsonControl],
185
+ ["secret", SecretControl]
186
+ ];
187
+ function createShadcnSettingsRegistry() {
188
+ const registry = (0, import_dials_ui.createSettingsRendererRegistry)();
189
+ registry.register({ tester: (0, import_dials_ui.alwaysMatch)(), renderer: RawJsonControl, name: "rawJson" });
190
+ for (const [kind, control] of WIDGET_CONTROLS) registry.register({ tester: widgetIs(kind), renderer: control, name: kind });
191
+ registry.register({ tester: (0, import_dials_ui.secretRoleIs)(), renderer: SecretControl, name: "secret" });
192
+ return registry;
193
+ }
194
+
195
+ // src/components.ts
196
+ var import_react2 = require("react");
197
+ function humanize(id) {
198
+ return id.replace(/^[@_]/, "").replace(/[._-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) || "Other";
199
+ }
200
+ function emptyState() {
201
+ return { value: void 0, managed: false, shadowed: false, dirty: false };
202
+ }
203
+ function SettingField({ field, state, onChange, onReset, registry }) {
204
+ const reg = (0, import_react2.useMemo)(() => registry ?? createShadcnSettingsRegistry(), [registry]);
205
+ const Control = reg.resolve({ zodType: field.zodType }, {
206
+ mode: "form",
207
+ widget: field.widget,
208
+ sensitivity: field.sensitivity
209
+ });
210
+ const badges = [];
211
+ if (state.source && state.source !== "default") {
212
+ badges.push((0, import_react2.createElement)("span", { key: "src", className: "zodal-dials-badge zodal-dials-source", title: `set by ${state.source}` }, state.source));
213
+ }
214
+ if (state.managed) badges.push((0, import_react2.createElement)("span", { key: "mng", className: "zodal-dials-badge zodal-dials-managed", title: "managed by policy" }, "policy"));
215
+ if (state.dirty) badges.push((0, import_react2.createElement)("span", { key: "dty", className: "zodal-dials-badge zodal-dials-dirty" }, "modified"));
216
+ const reset = state.source && state.source !== "default" && !state.managed ? (0, import_react2.createElement)("button", { type: "button", className: "zodal-dials-reset", onClick: () => onReset?.(field.key) }, "Reset") : null;
217
+ const control = Control ? (0, import_react2.createElement)(Control, { field, state, onChange: (value) => onChange?.(field.key, value) }) : (0, import_react2.createElement)("span", null, "(no renderer)");
218
+ return (0, import_react2.createElement)(
219
+ "div",
220
+ { className: "zodal-dials-field", "data-key": field.key, "data-widget": field.widget },
221
+ (0, import_react2.createElement)("label", { className: "zodal-dials-label", htmlFor: field.key }, field.label, ...badges),
222
+ field.description ? (0, import_react2.createElement)("p", { className: "zodal-dials-description" }, field.description) : null,
223
+ (0, import_react2.createElement)("div", { className: "zodal-dials-control" }, control, reset)
224
+ );
225
+ }
226
+ function SettingsPanel({ form, states, onChange, onReset, registry, search = true }) {
227
+ const [query, setQuery] = (0, import_react2.useState)("");
228
+ const reg = (0, import_react2.useMemo)(() => registry ?? createShadcnSettingsRegistry(), [registry]);
229
+ const q = query.trim().toLowerCase();
230
+ const matches = (f) => !q || f.key.toLowerCase().includes(q) || f.label.toLowerCase().includes(q) || (f.description ?? "").toLowerCase().includes(q);
231
+ const primary = (f) => f.facets?.find((x) => !x.startsWith("@")) ?? "_ungrouped";
232
+ const groupMeta = new Map(form.groups.map((g) => [g.id, g]));
233
+ const buckets = /* @__PURE__ */ new Map();
234
+ for (const f of form.fields) {
235
+ const id = primary(f);
236
+ const list = buckets.get(id);
237
+ if (list) list.push(f);
238
+ else buckets.set(id, [f]);
239
+ }
240
+ const sectionIds = [...buckets.keys()].sort(
241
+ (a, b) => (groupMeta.get(a)?.order ?? 500) - (groupMeta.get(b)?.order ?? 500) || a.localeCompare(b)
242
+ );
243
+ const children = [];
244
+ if (search) {
245
+ children.push(
246
+ (0, import_react2.createElement)("input", {
247
+ key: "__search",
248
+ type: "search",
249
+ className: "zodal-dials-search",
250
+ placeholder: "Search settings\u2026",
251
+ value: query,
252
+ onChange: (e) => setQuery(e.target.value)
253
+ })
254
+ );
255
+ }
256
+ for (const id of sectionIds) {
257
+ const fields = (buckets.get(id) ?? []).filter(matches);
258
+ if (fields.length === 0) continue;
259
+ const title = groupMeta.get(id)?.title ?? humanize(id);
260
+ children.push(
261
+ (0, import_react2.createElement)(
262
+ "section",
263
+ { key: id, className: "zodal-dials-group", "data-group": id },
264
+ (0, import_react2.createElement)("h3", { className: "zodal-dials-group-title" }, title),
265
+ ...fields.map((f) => (0, import_react2.createElement)(SettingField, { key: f.key, field: f, state: states[f.key] ?? emptyState(), onChange, onReset, registry: reg }))
266
+ )
267
+ );
268
+ }
269
+ return (0, import_react2.createElement)("div", { className: "zodal-dials-panel" }, ...children);
270
+ }
271
+ // Annotate the CommonJS export names for ESM import in node:
272
+ 0 && (module.exports = {
273
+ NumberControl,
274
+ RadioControl,
275
+ RawJsonControl,
276
+ SecretControl,
277
+ SelectControl,
278
+ SettingField,
279
+ SettingsPanel,
280
+ SliderControl,
281
+ SwitchControl,
282
+ TextControl,
283
+ createShadcnSettingsRegistry,
284
+ widgetIs
285
+ });
286
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/widgets.ts","../src/registry.ts","../src/components.ts"],"sourcesContent":["/**\n * @zodal/dials-ui-shadcn — a React/shadcn renderer for @zodal/dials-ui.\n *\n * React components that turn the headless settings config (`SettingsForm` + field states) into a\n * settings panel: `SettingsPanel` (search + grouped fields) and `SettingField` (one row), with a\n * per-widget control registry (`createShadcnSettingsRegistry`) and a terminal rawJson fallback. The\n * controls are plain elements with `zodal-dials-*` class names — swap in shadcn/ui primitives in a\n * consumer. Secrets render masked and never echo plaintext.\n */\n\nexport type { ControlProps, SettingControl, PanelHandlers } from './types.js';\n\nexport {\n SwitchControl,\n TextControl,\n NumberControl,\n SliderControl,\n SelectControl,\n RadioControl,\n SecretControl,\n RawJsonControl,\n} from './widgets.js';\n\nexport { createShadcnSettingsRegistry, widgetIs } from './registry.js';\n\nexport { SettingField, SettingsPanel } from './components.js';\nexport type { SettingFieldProps, SettingsPanelProps } from './components.js';\n","/**\n * React widget controls for settings — plain elements with `zodal-dials-*` class names (swap in\n * shadcn/ui `Switch`, `Select`, `Slider`, … in a consumer). Uses `React.createElement` (no JSX). A\n * secret renders a masked status + write-only field and never echoes plaintext; structured/unknown\n * values fall back to a raw JSON editor that also refuses to serialize a secret.\n */\n\nimport { createElement as h } from 'react';\nimport type { ChangeEvent } from 'react';\nimport { isSecretRef } from '@zodal/dials-core';\nimport type { ControlProps, SettingControl } from './types.js';\n\nconst isDisabled = (p: ControlProps): boolean => p.field.readOnly || p.state.managed;\nconst asText = (v: unknown): string => (v == null ? '' : String(v));\nconst jsonString = (v: unknown): string => {\n try {\n return JSON.stringify(v ?? null, null, 2);\n } catch {\n return String(v);\n }\n};\n\n/** Empty/non-numeric input → undefined (treated as \"no change\"; use Reset to unset). */\nfunction decodeNumber(raw: string): number | undefined {\n if (raw.trim() === '') return undefined;\n const n = Number(raw);\n return Number.isNaN(n) ? undefined : n;\n}\n\nexport const SwitchControl: SettingControl = (p) =>\n h('input', {\n type: 'checkbox',\n className: 'zodal-dials-switch',\n checked: p.state.value === true,\n disabled: isDisabled(p),\n onChange: (e: ChangeEvent<HTMLInputElement>) => p.onChange?.(e.target.checked),\n });\n\nexport const TextControl: SettingControl = (p) =>\n h('input', {\n type: 'text',\n className: 'zodal-dials-text',\n value: asText(p.state.value),\n // never use a secret's default as a placeholder (defense in depth; secret defaults are omitted upstream)\n placeholder: p.field.sensitivity === 'secret' ? undefined : asText(p.field.defaultValue),\n disabled: isDisabled(p),\n onChange: (e: ChangeEvent<HTMLInputElement>) => p.onChange?.(e.target.value),\n });\n\nexport const NumberControl: SettingControl = (p) =>\n h('input', {\n type: 'number',\n className: 'zodal-dials-number',\n value: asText(p.state.value),\n min: p.field.bounds?.min,\n max: p.field.bounds?.max,\n disabled: isDisabled(p),\n onChange: (e: ChangeEvent<HTMLInputElement>) => {\n const v = decodeNumber(e.target.value);\n if (v !== undefined) p.onChange?.(v);\n },\n });\n\nexport const SliderControl: SettingControl = (p) =>\n h(\n 'span',\n { className: 'zodal-dials-slider-wrap' },\n h('input', {\n type: 'range',\n className: 'zodal-dials-slider',\n value: asText(p.state.value),\n min: p.field.bounds?.min,\n max: p.field.bounds?.max,\n disabled: isDisabled(p),\n onChange: (e: ChangeEvent<HTMLInputElement>) => {\n const v = decodeNumber(e.target.value);\n if (v !== undefined) p.onChange?.(v);\n },\n }),\n h('output', { className: 'zodal-dials-slider-value' }, asText(p.state.value)),\n );\n\nexport const SelectControl: SettingControl = (p) =>\n h(\n 'select',\n {\n className: 'zodal-dials-select',\n value: asText(p.state.value),\n disabled: isDisabled(p),\n onChange: (e: ChangeEvent<HTMLSelectElement>) => p.onChange?.(e.target.value),\n },\n ...(p.field.enumValues ?? []).map((v, i) => h('option', { key: `${i}:${v}`, value: v }, v)),\n );\n\nexport const RadioControl: SettingControl = (p) =>\n h(\n 'fieldset',\n { className: 'zodal-dials-radio' },\n ...(p.field.enumValues ?? []).map((v, i) =>\n h(\n 'label',\n { key: `${i}:${v}` },\n h('input', {\n type: 'radio',\n name: p.field.key,\n value: v,\n checked: p.state.value === v,\n disabled: isDisabled(p),\n onChange: () => p.onChange?.(v),\n }),\n v,\n ),\n ),\n );\n\nexport const SecretControl: SettingControl = (p) => {\n const ref = isSecretRef(p.state.value) ? p.state.value : undefined;\n return h(\n 'span',\n { className: 'zodal-dials-secret-wrap' },\n h('span', { className: 'zodal-dials-secret-status' }, ref ? ref.masked : 'not set'),\n h('input', {\n type: 'password',\n className: 'zodal-dials-secret',\n placeholder: 'Enter new value',\n disabled: isDisabled(p),\n onChange: (e: ChangeEvent<HTMLInputElement>) => p.onChange?.(e.target.value),\n }),\n );\n};\n\nexport const RawJsonControl: SettingControl = (p) => {\n // Defense in depth: never serialize a secret as JSON — always fall back to the masked widget.\n if (p.field.sensitivity === 'secret' || isSecretRef(p.state.value)) return SecretControl(p);\n return h(\n 'span',\n { className: 'zodal-dials-json-wrap' },\n h('small', { className: 'zodal-dials-json-note' }, 'rendered as raw JSON (no structured editor for this type)'),\n h('textarea', {\n // key on the serialized value: remounts (re-seeds) the uncontrolled textarea when the upstream\n // value changes (fixes desync), while keeping local typing smooth when it does not.\n key: jsonString(p.state.value),\n className: 'zodal-dials-rawjson',\n defaultValue: jsonString(p.state.value),\n disabled: isDisabled(p),\n onChange: (e: ChangeEvent<HTMLTextAreaElement>) => {\n try {\n p.onChange?.(JSON.parse(e.target.value));\n } catch {\n // invalid JSON: ignore (a real renderer would surface an error)\n }\n },\n }),\n );\n};\n","/**\n * The shadcn settings renderer registry — a `@zodal/dials-ui` registry of React controls, one per\n * widget kind, the masked secret control at the OVERRIDE band, and a terminal rawJson control for\n * total coverage. Selection is open-closed (register a higher-priority control to override).\n */\n\nimport { createSettingsRendererRegistry, secretRoleIs, alwaysMatch, PRIORITY } from '@zodal/dials-ui';\nimport type { RendererRegistry, RendererTester, WidgetKind } from '@zodal/dials-ui';\nimport type { SettingControl } from './types.js';\nimport {\n SwitchControl,\n TextControl,\n NumberControl,\n SliderControl,\n SelectControl,\n RadioControl,\n SecretControl,\n RawJsonControl,\n} from './widgets.js';\n\n/** Tester matching a field's resolved widget kind (supplied via the render context as `widget`). */\nexport function widgetIs(kind: WidgetKind): RendererTester {\n return (_field, ctx) => (ctx.widget === kind ? PRIORITY.LIBRARY : -1);\n}\n\nconst WIDGET_CONTROLS: Array<[WidgetKind, SettingControl]> = [\n ['switch', SwitchControl],\n ['text', TextControl],\n ['textarea', TextControl],\n ['number', NumberControl],\n ['slider', SliderControl],\n ['select', SelectControl],\n ['radio', RadioControl],\n ['color', TextControl],\n ['date', TextControl],\n ['path', TextControl],\n ['object', RawJsonControl],\n ['array', RawJsonControl],\n ['secret', SecretControl],\n];\n\n/** Create a shadcn/React settings renderer registry. */\nexport function createShadcnSettingsRegistry(): RendererRegistry<SettingControl> {\n const registry = createSettingsRendererRegistry<SettingControl>();\n registry.register({ tester: alwaysMatch(), renderer: RawJsonControl, name: 'rawJson' });\n for (const [kind, control] of WIDGET_CONTROLS) registry.register({ tester: widgetIs(kind), renderer: control, name: kind });\n registry.register({ tester: secretRoleIs(), renderer: SecretControl, name: 'secret' });\n return registry;\n}\n","/**\n * The React settings components: `SettingField` (label + provenance badges + control via the\n * registry + reset) and `SettingsPanel` (a search box + a section per primary facet; each setting\n * renders once). Uses `React.createElement` (no JSX). Plain elements + `zodal-dials-*` classes — swap\n * in shadcn/ui primitives in a consumer.\n */\n\nimport { createElement as h, useMemo, useState } from 'react';\nimport type { ChangeEvent, ReactElement } from 'react';\nimport type { ResolvedFieldAffordance } from '@zodal/core';\nimport type { SettingKey } from '@zodal/dials-core';\nimport type { RendererRegistry, SettingFieldConfig, SettingFieldState, SettingsForm } from '@zodal/dials-ui';\nimport type { PanelHandlers, SettingControl } from './types.js';\nimport { createShadcnSettingsRegistry } from './registry.js';\n\nfunction humanize(id: string): string {\n return id.replace(/^[@_]/, '').replace(/[._-]+/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase()) || 'Other';\n}\n\nfunction emptyState(): SettingFieldState {\n return { value: undefined, managed: false, shadowed: false, dirty: false };\n}\n\nexport interface SettingFieldProps extends PanelHandlers {\n field: SettingFieldConfig;\n state: SettingFieldState;\n registry?: RendererRegistry<SettingControl>;\n}\n\n/** Render one setting as a labeled field row. */\nexport function SettingField({ field, state, onChange, onReset, registry }: SettingFieldProps): ReactElement {\n const reg = useMemo(() => registry ?? createShadcnSettingsRegistry(), [registry]);\n const Control = reg.resolve({ zodType: field.zodType } as unknown as ResolvedFieldAffordance, {\n mode: 'form',\n widget: field.widget,\n sensitivity: field.sensitivity,\n });\n\n const badges: ReactElement[] = [];\n if (state.source && state.source !== 'default') {\n badges.push(h('span', { key: 'src', className: 'zodal-dials-badge zodal-dials-source', title: `set by ${state.source}` }, state.source));\n }\n if (state.managed) badges.push(h('span', { key: 'mng', className: 'zodal-dials-badge zodal-dials-managed', title: 'managed by policy' }, 'policy'));\n if (state.dirty) badges.push(h('span', { key: 'dty', className: 'zodal-dials-badge zodal-dials-dirty' }, 'modified'));\n\n const reset =\n state.source && state.source !== 'default' && !state.managed\n ? h('button', { type: 'button', className: 'zodal-dials-reset', onClick: () => onReset?.(field.key) }, 'Reset')\n : null;\n\n const control = Control\n ? h(Control, { field, state, onChange: (value: unknown) => onChange?.(field.key, value) })\n : h('span', null, '(no renderer)');\n\n return h(\n 'div',\n { className: 'zodal-dials-field', 'data-key': field.key, 'data-widget': field.widget },\n h('label', { className: 'zodal-dials-label', htmlFor: field.key }, field.label, ...badges),\n field.description ? h('p', { className: 'zodal-dials-description' }, field.description) : null,\n h('div', { className: 'zodal-dials-control' }, control, reset),\n );\n}\n\nexport interface SettingsPanelProps extends PanelHandlers {\n form: SettingsForm;\n states: Record<SettingKey, SettingFieldState>;\n registry?: RendererRegistry<SettingControl>;\n /** Include the search box. Default: true. */\n search?: boolean;\n}\n\n/** Render the full settings panel: a search box + a section per primary facet (each setting once). */\nexport function SettingsPanel({ form, states, onChange, onReset, registry, search = true }: SettingsPanelProps): ReactElement {\n const [query, setQuery] = useState('');\n const reg = useMemo(() => registry ?? createShadcnSettingsRegistry(), [registry]);\n const q = query.trim().toLowerCase();\n const matches = (f: SettingFieldConfig): boolean =>\n !q || f.key.toLowerCase().includes(q) || f.label.toLowerCase().includes(q) || (f.description ?? '').toLowerCase().includes(q);\n\n const primary = (f: SettingFieldConfig): string => f.facets?.find((x) => !x.startsWith('@')) ?? '_ungrouped';\n const groupMeta = new Map(form.groups.map((g) => [g.id, g]));\n const buckets = new Map<string, SettingFieldConfig[]>();\n for (const f of form.fields) {\n const id = primary(f);\n const list = buckets.get(id);\n if (list) list.push(f);\n else buckets.set(id, [f]);\n }\n const sectionIds = [...buckets.keys()].sort(\n (a, b) => (groupMeta.get(a)?.order ?? 500) - (groupMeta.get(b)?.order ?? 500) || a.localeCompare(b),\n );\n\n const children: ReactElement[] = [];\n if (search) {\n children.push(\n h('input', {\n key: '__search',\n type: 'search',\n className: 'zodal-dials-search',\n placeholder: 'Search settings…',\n value: query,\n onChange: (e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value),\n }),\n );\n }\n for (const id of sectionIds) {\n const fields = (buckets.get(id) ?? []).filter(matches);\n if (fields.length === 0) continue;\n const title = groupMeta.get(id)?.title ?? humanize(id);\n children.push(\n h(\n 'section',\n { key: id, className: 'zodal-dials-group', 'data-group': id },\n h('h3', { className: 'zodal-dials-group-title' }, title),\n ...fields.map((f) => h(SettingField, { key: f.key, field: f, state: states[f.key] ?? emptyState(), onChange, onReset, registry: reg })),\n ),\n );\n }\n return h('div', { className: 'zodal-dials-panel' }, ...children);\n}\n"],"mappings":"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","names":["h","import_react","h"]}
@@ -0,0 +1,71 @@
1
+ import { ReactElement } from 'react';
2
+ import { SettingFieldConfig, SettingFieldState, RendererRegistry, WidgetKind, RendererTester, SettingsForm } from '@zodal/dials-ui';
3
+ import { SettingKey } from '@zodal/dials-core';
4
+
5
+ /** React render contract for the shadcn settings renderer. */
6
+
7
+ interface ControlProps {
8
+ field: SettingFieldConfig;
9
+ state: SettingFieldState;
10
+ /** Called with the decoded value when the control changes. */
11
+ onChange?: (value: unknown) => void;
12
+ }
13
+ /** A widget control: a React component rendering the input for a setting. */
14
+ type SettingControl = (props: ControlProps) => ReactElement;
15
+ interface PanelHandlers {
16
+ onChange?: (key: SettingKey, value: unknown) => void;
17
+ onReset?: (key: SettingKey) => void;
18
+ }
19
+
20
+ /**
21
+ * React widget controls for settings — plain elements with `zodal-dials-*` class names (swap in
22
+ * shadcn/ui `Switch`, `Select`, `Slider`, … in a consumer). Uses `React.createElement` (no JSX). A
23
+ * secret renders a masked status + write-only field and never echoes plaintext; structured/unknown
24
+ * values fall back to a raw JSON editor that also refuses to serialize a secret.
25
+ */
26
+
27
+ declare const SwitchControl: SettingControl;
28
+ declare const TextControl: SettingControl;
29
+ declare const NumberControl: SettingControl;
30
+ declare const SliderControl: SettingControl;
31
+ declare const SelectControl: SettingControl;
32
+ declare const RadioControl: SettingControl;
33
+ declare const SecretControl: SettingControl;
34
+ declare const RawJsonControl: SettingControl;
35
+
36
+ /**
37
+ * The shadcn settings renderer registry — a `@zodal/dials-ui` registry of React controls, one per
38
+ * widget kind, the masked secret control at the OVERRIDE band, and a terminal rawJson control for
39
+ * total coverage. Selection is open-closed (register a higher-priority control to override).
40
+ */
41
+
42
+ /** Tester matching a field's resolved widget kind (supplied via the render context as `widget`). */
43
+ declare function widgetIs(kind: WidgetKind): RendererTester;
44
+ /** Create a shadcn/React settings renderer registry. */
45
+ declare function createShadcnSettingsRegistry(): RendererRegistry<SettingControl>;
46
+
47
+ /**
48
+ * The React settings components: `SettingField` (label + provenance badges + control via the
49
+ * registry + reset) and `SettingsPanel` (a search box + a section per primary facet; each setting
50
+ * renders once). Uses `React.createElement` (no JSX). Plain elements + `zodal-dials-*` classes — swap
51
+ * in shadcn/ui primitives in a consumer.
52
+ */
53
+
54
+ interface SettingFieldProps extends PanelHandlers {
55
+ field: SettingFieldConfig;
56
+ state: SettingFieldState;
57
+ registry?: RendererRegistry<SettingControl>;
58
+ }
59
+ /** Render one setting as a labeled field row. */
60
+ declare function SettingField({ field, state, onChange, onReset, registry }: SettingFieldProps): ReactElement;
61
+ interface SettingsPanelProps extends PanelHandlers {
62
+ form: SettingsForm;
63
+ states: Record<SettingKey, SettingFieldState>;
64
+ registry?: RendererRegistry<SettingControl>;
65
+ /** Include the search box. Default: true. */
66
+ search?: boolean;
67
+ }
68
+ /** Render the full settings panel: a search box + a section per primary facet (each setting once). */
69
+ declare function SettingsPanel({ form, states, onChange, onReset, registry, search }: SettingsPanelProps): ReactElement;
70
+
71
+ export { type ControlProps, NumberControl, type PanelHandlers, RadioControl, RawJsonControl, SecretControl, SelectControl, type SettingControl, SettingField, type SettingFieldProps, SettingsPanel, type SettingsPanelProps, SliderControl, SwitchControl, TextControl, createShadcnSettingsRegistry, widgetIs };
@@ -0,0 +1,71 @@
1
+ import { ReactElement } from 'react';
2
+ import { SettingFieldConfig, SettingFieldState, RendererRegistry, WidgetKind, RendererTester, SettingsForm } from '@zodal/dials-ui';
3
+ import { SettingKey } from '@zodal/dials-core';
4
+
5
+ /** React render contract for the shadcn settings renderer. */
6
+
7
+ interface ControlProps {
8
+ field: SettingFieldConfig;
9
+ state: SettingFieldState;
10
+ /** Called with the decoded value when the control changes. */
11
+ onChange?: (value: unknown) => void;
12
+ }
13
+ /** A widget control: a React component rendering the input for a setting. */
14
+ type SettingControl = (props: ControlProps) => ReactElement;
15
+ interface PanelHandlers {
16
+ onChange?: (key: SettingKey, value: unknown) => void;
17
+ onReset?: (key: SettingKey) => void;
18
+ }
19
+
20
+ /**
21
+ * React widget controls for settings — plain elements with `zodal-dials-*` class names (swap in
22
+ * shadcn/ui `Switch`, `Select`, `Slider`, … in a consumer). Uses `React.createElement` (no JSX). A
23
+ * secret renders a masked status + write-only field and never echoes plaintext; structured/unknown
24
+ * values fall back to a raw JSON editor that also refuses to serialize a secret.
25
+ */
26
+
27
+ declare const SwitchControl: SettingControl;
28
+ declare const TextControl: SettingControl;
29
+ declare const NumberControl: SettingControl;
30
+ declare const SliderControl: SettingControl;
31
+ declare const SelectControl: SettingControl;
32
+ declare const RadioControl: SettingControl;
33
+ declare const SecretControl: SettingControl;
34
+ declare const RawJsonControl: SettingControl;
35
+
36
+ /**
37
+ * The shadcn settings renderer registry — a `@zodal/dials-ui` registry of React controls, one per
38
+ * widget kind, the masked secret control at the OVERRIDE band, and a terminal rawJson control for
39
+ * total coverage. Selection is open-closed (register a higher-priority control to override).
40
+ */
41
+
42
+ /** Tester matching a field's resolved widget kind (supplied via the render context as `widget`). */
43
+ declare function widgetIs(kind: WidgetKind): RendererTester;
44
+ /** Create a shadcn/React settings renderer registry. */
45
+ declare function createShadcnSettingsRegistry(): RendererRegistry<SettingControl>;
46
+
47
+ /**
48
+ * The React settings components: `SettingField` (label + provenance badges + control via the
49
+ * registry + reset) and `SettingsPanel` (a search box + a section per primary facet; each setting
50
+ * renders once). Uses `React.createElement` (no JSX). Plain elements + `zodal-dials-*` classes — swap
51
+ * in shadcn/ui primitives in a consumer.
52
+ */
53
+
54
+ interface SettingFieldProps extends PanelHandlers {
55
+ field: SettingFieldConfig;
56
+ state: SettingFieldState;
57
+ registry?: RendererRegistry<SettingControl>;
58
+ }
59
+ /** Render one setting as a labeled field row. */
60
+ declare function SettingField({ field, state, onChange, onReset, registry }: SettingFieldProps): ReactElement;
61
+ interface SettingsPanelProps extends PanelHandlers {
62
+ form: SettingsForm;
63
+ states: Record<SettingKey, SettingFieldState>;
64
+ registry?: RendererRegistry<SettingControl>;
65
+ /** Include the search box. Default: true. */
66
+ search?: boolean;
67
+ }
68
+ /** Render the full settings panel: a search box + a section per primary facet (each setting once). */
69
+ declare function SettingsPanel({ form, states, onChange, onReset, registry, search }: SettingsPanelProps): ReactElement;
70
+
71
+ export { type ControlProps, NumberControl, type PanelHandlers, RadioControl, RawJsonControl, SecretControl, SelectControl, type SettingControl, SettingField, type SettingFieldProps, SettingsPanel, type SettingsPanelProps, SliderControl, SwitchControl, TextControl, createShadcnSettingsRegistry, widgetIs };
package/dist/index.js ADDED
@@ -0,0 +1,248 @@
1
+ // src/widgets.ts
2
+ import { createElement as h } from "react";
3
+ import { isSecretRef } from "@zodal/dials-core";
4
+ var isDisabled = (p) => p.field.readOnly || p.state.managed;
5
+ var asText = (v) => v == null ? "" : String(v);
6
+ var jsonString = (v) => {
7
+ try {
8
+ return JSON.stringify(v ?? null, null, 2);
9
+ } catch {
10
+ return String(v);
11
+ }
12
+ };
13
+ function decodeNumber(raw) {
14
+ if (raw.trim() === "") return void 0;
15
+ const n = Number(raw);
16
+ return Number.isNaN(n) ? void 0 : n;
17
+ }
18
+ var SwitchControl = (p) => h("input", {
19
+ type: "checkbox",
20
+ className: "zodal-dials-switch",
21
+ checked: p.state.value === true,
22
+ disabled: isDisabled(p),
23
+ onChange: (e) => p.onChange?.(e.target.checked)
24
+ });
25
+ var TextControl = (p) => h("input", {
26
+ type: "text",
27
+ className: "zodal-dials-text",
28
+ value: asText(p.state.value),
29
+ // never use a secret's default as a placeholder (defense in depth; secret defaults are omitted upstream)
30
+ placeholder: p.field.sensitivity === "secret" ? void 0 : asText(p.field.defaultValue),
31
+ disabled: isDisabled(p),
32
+ onChange: (e) => p.onChange?.(e.target.value)
33
+ });
34
+ var NumberControl = (p) => h("input", {
35
+ type: "number",
36
+ className: "zodal-dials-number",
37
+ value: asText(p.state.value),
38
+ min: p.field.bounds?.min,
39
+ max: p.field.bounds?.max,
40
+ disabled: isDisabled(p),
41
+ onChange: (e) => {
42
+ const v = decodeNumber(e.target.value);
43
+ if (v !== void 0) p.onChange?.(v);
44
+ }
45
+ });
46
+ var SliderControl = (p) => h(
47
+ "span",
48
+ { className: "zodal-dials-slider-wrap" },
49
+ h("input", {
50
+ type: "range",
51
+ className: "zodal-dials-slider",
52
+ value: asText(p.state.value),
53
+ min: p.field.bounds?.min,
54
+ max: p.field.bounds?.max,
55
+ disabled: isDisabled(p),
56
+ onChange: (e) => {
57
+ const v = decodeNumber(e.target.value);
58
+ if (v !== void 0) p.onChange?.(v);
59
+ }
60
+ }),
61
+ h("output", { className: "zodal-dials-slider-value" }, asText(p.state.value))
62
+ );
63
+ var SelectControl = (p) => h(
64
+ "select",
65
+ {
66
+ className: "zodal-dials-select",
67
+ value: asText(p.state.value),
68
+ disabled: isDisabled(p),
69
+ onChange: (e) => p.onChange?.(e.target.value)
70
+ },
71
+ ...(p.field.enumValues ?? []).map((v, i) => h("option", { key: `${i}:${v}`, value: v }, v))
72
+ );
73
+ var RadioControl = (p) => h(
74
+ "fieldset",
75
+ { className: "zodal-dials-radio" },
76
+ ...(p.field.enumValues ?? []).map(
77
+ (v, i) => h(
78
+ "label",
79
+ { key: `${i}:${v}` },
80
+ h("input", {
81
+ type: "radio",
82
+ name: p.field.key,
83
+ value: v,
84
+ checked: p.state.value === v,
85
+ disabled: isDisabled(p),
86
+ onChange: () => p.onChange?.(v)
87
+ }),
88
+ v
89
+ )
90
+ )
91
+ );
92
+ var SecretControl = (p) => {
93
+ const ref = isSecretRef(p.state.value) ? p.state.value : void 0;
94
+ return h(
95
+ "span",
96
+ { className: "zodal-dials-secret-wrap" },
97
+ h("span", { className: "zodal-dials-secret-status" }, ref ? ref.masked : "not set"),
98
+ h("input", {
99
+ type: "password",
100
+ className: "zodal-dials-secret",
101
+ placeholder: "Enter new value",
102
+ disabled: isDisabled(p),
103
+ onChange: (e) => p.onChange?.(e.target.value)
104
+ })
105
+ );
106
+ };
107
+ var RawJsonControl = (p) => {
108
+ if (p.field.sensitivity === "secret" || isSecretRef(p.state.value)) return SecretControl(p);
109
+ return h(
110
+ "span",
111
+ { className: "zodal-dials-json-wrap" },
112
+ h("small", { className: "zodal-dials-json-note" }, "rendered as raw JSON (no structured editor for this type)"),
113
+ h("textarea", {
114
+ // key on the serialized value: remounts (re-seeds) the uncontrolled textarea when the upstream
115
+ // value changes (fixes desync), while keeping local typing smooth when it does not.
116
+ key: jsonString(p.state.value),
117
+ className: "zodal-dials-rawjson",
118
+ defaultValue: jsonString(p.state.value),
119
+ disabled: isDisabled(p),
120
+ onChange: (e) => {
121
+ try {
122
+ p.onChange?.(JSON.parse(e.target.value));
123
+ } catch {
124
+ }
125
+ }
126
+ })
127
+ );
128
+ };
129
+
130
+ // src/registry.ts
131
+ import { createSettingsRendererRegistry, secretRoleIs, alwaysMatch, PRIORITY } from "@zodal/dials-ui";
132
+ function widgetIs(kind) {
133
+ return (_field, ctx) => ctx.widget === kind ? PRIORITY.LIBRARY : -1;
134
+ }
135
+ var WIDGET_CONTROLS = [
136
+ ["switch", SwitchControl],
137
+ ["text", TextControl],
138
+ ["textarea", TextControl],
139
+ ["number", NumberControl],
140
+ ["slider", SliderControl],
141
+ ["select", SelectControl],
142
+ ["radio", RadioControl],
143
+ ["color", TextControl],
144
+ ["date", TextControl],
145
+ ["path", TextControl],
146
+ ["object", RawJsonControl],
147
+ ["array", RawJsonControl],
148
+ ["secret", SecretControl]
149
+ ];
150
+ function createShadcnSettingsRegistry() {
151
+ const registry = createSettingsRendererRegistry();
152
+ registry.register({ tester: alwaysMatch(), renderer: RawJsonControl, name: "rawJson" });
153
+ for (const [kind, control] of WIDGET_CONTROLS) registry.register({ tester: widgetIs(kind), renderer: control, name: kind });
154
+ registry.register({ tester: secretRoleIs(), renderer: SecretControl, name: "secret" });
155
+ return registry;
156
+ }
157
+
158
+ // src/components.ts
159
+ import { createElement as h2, useMemo, useState } from "react";
160
+ function humanize(id) {
161
+ return id.replace(/^[@_]/, "").replace(/[._-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) || "Other";
162
+ }
163
+ function emptyState() {
164
+ return { value: void 0, managed: false, shadowed: false, dirty: false };
165
+ }
166
+ function SettingField({ field, state, onChange, onReset, registry }) {
167
+ const reg = useMemo(() => registry ?? createShadcnSettingsRegistry(), [registry]);
168
+ const Control = reg.resolve({ zodType: field.zodType }, {
169
+ mode: "form",
170
+ widget: field.widget,
171
+ sensitivity: field.sensitivity
172
+ });
173
+ const badges = [];
174
+ if (state.source && state.source !== "default") {
175
+ badges.push(h2("span", { key: "src", className: "zodal-dials-badge zodal-dials-source", title: `set by ${state.source}` }, state.source));
176
+ }
177
+ if (state.managed) badges.push(h2("span", { key: "mng", className: "zodal-dials-badge zodal-dials-managed", title: "managed by policy" }, "policy"));
178
+ if (state.dirty) badges.push(h2("span", { key: "dty", className: "zodal-dials-badge zodal-dials-dirty" }, "modified"));
179
+ const reset = state.source && state.source !== "default" && !state.managed ? h2("button", { type: "button", className: "zodal-dials-reset", onClick: () => onReset?.(field.key) }, "Reset") : null;
180
+ const control = Control ? h2(Control, { field, state, onChange: (value) => onChange?.(field.key, value) }) : h2("span", null, "(no renderer)");
181
+ return h2(
182
+ "div",
183
+ { className: "zodal-dials-field", "data-key": field.key, "data-widget": field.widget },
184
+ h2("label", { className: "zodal-dials-label", htmlFor: field.key }, field.label, ...badges),
185
+ field.description ? h2("p", { className: "zodal-dials-description" }, field.description) : null,
186
+ h2("div", { className: "zodal-dials-control" }, control, reset)
187
+ );
188
+ }
189
+ function SettingsPanel({ form, states, onChange, onReset, registry, search = true }) {
190
+ const [query, setQuery] = useState("");
191
+ const reg = useMemo(() => registry ?? createShadcnSettingsRegistry(), [registry]);
192
+ const q = query.trim().toLowerCase();
193
+ const matches = (f) => !q || f.key.toLowerCase().includes(q) || f.label.toLowerCase().includes(q) || (f.description ?? "").toLowerCase().includes(q);
194
+ const primary = (f) => f.facets?.find((x) => !x.startsWith("@")) ?? "_ungrouped";
195
+ const groupMeta = new Map(form.groups.map((g) => [g.id, g]));
196
+ const buckets = /* @__PURE__ */ new Map();
197
+ for (const f of form.fields) {
198
+ const id = primary(f);
199
+ const list = buckets.get(id);
200
+ if (list) list.push(f);
201
+ else buckets.set(id, [f]);
202
+ }
203
+ const sectionIds = [...buckets.keys()].sort(
204
+ (a, b) => (groupMeta.get(a)?.order ?? 500) - (groupMeta.get(b)?.order ?? 500) || a.localeCompare(b)
205
+ );
206
+ const children = [];
207
+ if (search) {
208
+ children.push(
209
+ h2("input", {
210
+ key: "__search",
211
+ type: "search",
212
+ className: "zodal-dials-search",
213
+ placeholder: "Search settings\u2026",
214
+ value: query,
215
+ onChange: (e) => setQuery(e.target.value)
216
+ })
217
+ );
218
+ }
219
+ for (const id of sectionIds) {
220
+ const fields = (buckets.get(id) ?? []).filter(matches);
221
+ if (fields.length === 0) continue;
222
+ const title = groupMeta.get(id)?.title ?? humanize(id);
223
+ children.push(
224
+ h2(
225
+ "section",
226
+ { key: id, className: "zodal-dials-group", "data-group": id },
227
+ h2("h3", { className: "zodal-dials-group-title" }, title),
228
+ ...fields.map((f) => h2(SettingField, { key: f.key, field: f, state: states[f.key] ?? emptyState(), onChange, onReset, registry: reg }))
229
+ )
230
+ );
231
+ }
232
+ return h2("div", { className: "zodal-dials-panel" }, ...children);
233
+ }
234
+ export {
235
+ NumberControl,
236
+ RadioControl,
237
+ RawJsonControl,
238
+ SecretControl,
239
+ SelectControl,
240
+ SettingField,
241
+ SettingsPanel,
242
+ SliderControl,
243
+ SwitchControl,
244
+ TextControl,
245
+ createShadcnSettingsRegistry,
246
+ widgetIs
247
+ };
248
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/widgets.ts","../src/registry.ts","../src/components.ts"],"sourcesContent":["/**\n * React widget controls for settings — plain elements with `zodal-dials-*` class names (swap in\n * shadcn/ui `Switch`, `Select`, `Slider`, … in a consumer). Uses `React.createElement` (no JSX). A\n * secret renders a masked status + write-only field and never echoes plaintext; structured/unknown\n * values fall back to a raw JSON editor that also refuses to serialize a secret.\n */\n\nimport { createElement as h } from 'react';\nimport type { ChangeEvent } from 'react';\nimport { isSecretRef } from '@zodal/dials-core';\nimport type { ControlProps, SettingControl } from './types.js';\n\nconst isDisabled = (p: ControlProps): boolean => p.field.readOnly || p.state.managed;\nconst asText = (v: unknown): string => (v == null ? '' : String(v));\nconst jsonString = (v: unknown): string => {\n try {\n return JSON.stringify(v ?? null, null, 2);\n } catch {\n return String(v);\n }\n};\n\n/** Empty/non-numeric input → undefined (treated as \"no change\"; use Reset to unset). */\nfunction decodeNumber(raw: string): number | undefined {\n if (raw.trim() === '') return undefined;\n const n = Number(raw);\n return Number.isNaN(n) ? undefined : n;\n}\n\nexport const SwitchControl: SettingControl = (p) =>\n h('input', {\n type: 'checkbox',\n className: 'zodal-dials-switch',\n checked: p.state.value === true,\n disabled: isDisabled(p),\n onChange: (e: ChangeEvent<HTMLInputElement>) => p.onChange?.(e.target.checked),\n });\n\nexport const TextControl: SettingControl = (p) =>\n h('input', {\n type: 'text',\n className: 'zodal-dials-text',\n value: asText(p.state.value),\n // never use a secret's default as a placeholder (defense in depth; secret defaults are omitted upstream)\n placeholder: p.field.sensitivity === 'secret' ? undefined : asText(p.field.defaultValue),\n disabled: isDisabled(p),\n onChange: (e: ChangeEvent<HTMLInputElement>) => p.onChange?.(e.target.value),\n });\n\nexport const NumberControl: SettingControl = (p) =>\n h('input', {\n type: 'number',\n className: 'zodal-dials-number',\n value: asText(p.state.value),\n min: p.field.bounds?.min,\n max: p.field.bounds?.max,\n disabled: isDisabled(p),\n onChange: (e: ChangeEvent<HTMLInputElement>) => {\n const v = decodeNumber(e.target.value);\n if (v !== undefined) p.onChange?.(v);\n },\n });\n\nexport const SliderControl: SettingControl = (p) =>\n h(\n 'span',\n { className: 'zodal-dials-slider-wrap' },\n h('input', {\n type: 'range',\n className: 'zodal-dials-slider',\n value: asText(p.state.value),\n min: p.field.bounds?.min,\n max: p.field.bounds?.max,\n disabled: isDisabled(p),\n onChange: (e: ChangeEvent<HTMLInputElement>) => {\n const v = decodeNumber(e.target.value);\n if (v !== undefined) p.onChange?.(v);\n },\n }),\n h('output', { className: 'zodal-dials-slider-value' }, asText(p.state.value)),\n );\n\nexport const SelectControl: SettingControl = (p) =>\n h(\n 'select',\n {\n className: 'zodal-dials-select',\n value: asText(p.state.value),\n disabled: isDisabled(p),\n onChange: (e: ChangeEvent<HTMLSelectElement>) => p.onChange?.(e.target.value),\n },\n ...(p.field.enumValues ?? []).map((v, i) => h('option', { key: `${i}:${v}`, value: v }, v)),\n );\n\nexport const RadioControl: SettingControl = (p) =>\n h(\n 'fieldset',\n { className: 'zodal-dials-radio' },\n ...(p.field.enumValues ?? []).map((v, i) =>\n h(\n 'label',\n { key: `${i}:${v}` },\n h('input', {\n type: 'radio',\n name: p.field.key,\n value: v,\n checked: p.state.value === v,\n disabled: isDisabled(p),\n onChange: () => p.onChange?.(v),\n }),\n v,\n ),\n ),\n );\n\nexport const SecretControl: SettingControl = (p) => {\n const ref = isSecretRef(p.state.value) ? p.state.value : undefined;\n return h(\n 'span',\n { className: 'zodal-dials-secret-wrap' },\n h('span', { className: 'zodal-dials-secret-status' }, ref ? ref.masked : 'not set'),\n h('input', {\n type: 'password',\n className: 'zodal-dials-secret',\n placeholder: 'Enter new value',\n disabled: isDisabled(p),\n onChange: (e: ChangeEvent<HTMLInputElement>) => p.onChange?.(e.target.value),\n }),\n );\n};\n\nexport const RawJsonControl: SettingControl = (p) => {\n // Defense in depth: never serialize a secret as JSON — always fall back to the masked widget.\n if (p.field.sensitivity === 'secret' || isSecretRef(p.state.value)) return SecretControl(p);\n return h(\n 'span',\n { className: 'zodal-dials-json-wrap' },\n h('small', { className: 'zodal-dials-json-note' }, 'rendered as raw JSON (no structured editor for this type)'),\n h('textarea', {\n // key on the serialized value: remounts (re-seeds) the uncontrolled textarea when the upstream\n // value changes (fixes desync), while keeping local typing smooth when it does not.\n key: jsonString(p.state.value),\n className: 'zodal-dials-rawjson',\n defaultValue: jsonString(p.state.value),\n disabled: isDisabled(p),\n onChange: (e: ChangeEvent<HTMLTextAreaElement>) => {\n try {\n p.onChange?.(JSON.parse(e.target.value));\n } catch {\n // invalid JSON: ignore (a real renderer would surface an error)\n }\n },\n }),\n );\n};\n","/**\n * The shadcn settings renderer registry — a `@zodal/dials-ui` registry of React controls, one per\n * widget kind, the masked secret control at the OVERRIDE band, and a terminal rawJson control for\n * total coverage. Selection is open-closed (register a higher-priority control to override).\n */\n\nimport { createSettingsRendererRegistry, secretRoleIs, alwaysMatch, PRIORITY } from '@zodal/dials-ui';\nimport type { RendererRegistry, RendererTester, WidgetKind } from '@zodal/dials-ui';\nimport type { SettingControl } from './types.js';\nimport {\n SwitchControl,\n TextControl,\n NumberControl,\n SliderControl,\n SelectControl,\n RadioControl,\n SecretControl,\n RawJsonControl,\n} from './widgets.js';\n\n/** Tester matching a field's resolved widget kind (supplied via the render context as `widget`). */\nexport function widgetIs(kind: WidgetKind): RendererTester {\n return (_field, ctx) => (ctx.widget === kind ? PRIORITY.LIBRARY : -1);\n}\n\nconst WIDGET_CONTROLS: Array<[WidgetKind, SettingControl]> = [\n ['switch', SwitchControl],\n ['text', TextControl],\n ['textarea', TextControl],\n ['number', NumberControl],\n ['slider', SliderControl],\n ['select', SelectControl],\n ['radio', RadioControl],\n ['color', TextControl],\n ['date', TextControl],\n ['path', TextControl],\n ['object', RawJsonControl],\n ['array', RawJsonControl],\n ['secret', SecretControl],\n];\n\n/** Create a shadcn/React settings renderer registry. */\nexport function createShadcnSettingsRegistry(): RendererRegistry<SettingControl> {\n const registry = createSettingsRendererRegistry<SettingControl>();\n registry.register({ tester: alwaysMatch(), renderer: RawJsonControl, name: 'rawJson' });\n for (const [kind, control] of WIDGET_CONTROLS) registry.register({ tester: widgetIs(kind), renderer: control, name: kind });\n registry.register({ tester: secretRoleIs(), renderer: SecretControl, name: 'secret' });\n return registry;\n}\n","/**\n * The React settings components: `SettingField` (label + provenance badges + control via the\n * registry + reset) and `SettingsPanel` (a search box + a section per primary facet; each setting\n * renders once). Uses `React.createElement` (no JSX). Plain elements + `zodal-dials-*` classes — swap\n * in shadcn/ui primitives in a consumer.\n */\n\nimport { createElement as h, useMemo, useState } from 'react';\nimport type { ChangeEvent, ReactElement } from 'react';\nimport type { ResolvedFieldAffordance } from '@zodal/core';\nimport type { SettingKey } from '@zodal/dials-core';\nimport type { RendererRegistry, SettingFieldConfig, SettingFieldState, SettingsForm } from '@zodal/dials-ui';\nimport type { PanelHandlers, SettingControl } from './types.js';\nimport { createShadcnSettingsRegistry } from './registry.js';\n\nfunction humanize(id: string): string {\n return id.replace(/^[@_]/, '').replace(/[._-]+/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase()) || 'Other';\n}\n\nfunction emptyState(): SettingFieldState {\n return { value: undefined, managed: false, shadowed: false, dirty: false };\n}\n\nexport interface SettingFieldProps extends PanelHandlers {\n field: SettingFieldConfig;\n state: SettingFieldState;\n registry?: RendererRegistry<SettingControl>;\n}\n\n/** Render one setting as a labeled field row. */\nexport function SettingField({ field, state, onChange, onReset, registry }: SettingFieldProps): ReactElement {\n const reg = useMemo(() => registry ?? createShadcnSettingsRegistry(), [registry]);\n const Control = reg.resolve({ zodType: field.zodType } as unknown as ResolvedFieldAffordance, {\n mode: 'form',\n widget: field.widget,\n sensitivity: field.sensitivity,\n });\n\n const badges: ReactElement[] = [];\n if (state.source && state.source !== 'default') {\n badges.push(h('span', { key: 'src', className: 'zodal-dials-badge zodal-dials-source', title: `set by ${state.source}` }, state.source));\n }\n if (state.managed) badges.push(h('span', { key: 'mng', className: 'zodal-dials-badge zodal-dials-managed', title: 'managed by policy' }, 'policy'));\n if (state.dirty) badges.push(h('span', { key: 'dty', className: 'zodal-dials-badge zodal-dials-dirty' }, 'modified'));\n\n const reset =\n state.source && state.source !== 'default' && !state.managed\n ? h('button', { type: 'button', className: 'zodal-dials-reset', onClick: () => onReset?.(field.key) }, 'Reset')\n : null;\n\n const control = Control\n ? h(Control, { field, state, onChange: (value: unknown) => onChange?.(field.key, value) })\n : h('span', null, '(no renderer)');\n\n return h(\n 'div',\n { className: 'zodal-dials-field', 'data-key': field.key, 'data-widget': field.widget },\n h('label', { className: 'zodal-dials-label', htmlFor: field.key }, field.label, ...badges),\n field.description ? h('p', { className: 'zodal-dials-description' }, field.description) : null,\n h('div', { className: 'zodal-dials-control' }, control, reset),\n );\n}\n\nexport interface SettingsPanelProps extends PanelHandlers {\n form: SettingsForm;\n states: Record<SettingKey, SettingFieldState>;\n registry?: RendererRegistry<SettingControl>;\n /** Include the search box. Default: true. */\n search?: boolean;\n}\n\n/** Render the full settings panel: a search box + a section per primary facet (each setting once). */\nexport function SettingsPanel({ form, states, onChange, onReset, registry, search = true }: SettingsPanelProps): ReactElement {\n const [query, setQuery] = useState('');\n const reg = useMemo(() => registry ?? createShadcnSettingsRegistry(), [registry]);\n const q = query.trim().toLowerCase();\n const matches = (f: SettingFieldConfig): boolean =>\n !q || f.key.toLowerCase().includes(q) || f.label.toLowerCase().includes(q) || (f.description ?? '').toLowerCase().includes(q);\n\n const primary = (f: SettingFieldConfig): string => f.facets?.find((x) => !x.startsWith('@')) ?? '_ungrouped';\n const groupMeta = new Map(form.groups.map((g) => [g.id, g]));\n const buckets = new Map<string, SettingFieldConfig[]>();\n for (const f of form.fields) {\n const id = primary(f);\n const list = buckets.get(id);\n if (list) list.push(f);\n else buckets.set(id, [f]);\n }\n const sectionIds = [...buckets.keys()].sort(\n (a, b) => (groupMeta.get(a)?.order ?? 500) - (groupMeta.get(b)?.order ?? 500) || a.localeCompare(b),\n );\n\n const children: ReactElement[] = [];\n if (search) {\n children.push(\n h('input', {\n key: '__search',\n type: 'search',\n className: 'zodal-dials-search',\n placeholder: 'Search settings…',\n value: query,\n onChange: (e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value),\n }),\n );\n }\n for (const id of sectionIds) {\n const fields = (buckets.get(id) ?? []).filter(matches);\n if (fields.length === 0) continue;\n const title = groupMeta.get(id)?.title ?? humanize(id);\n children.push(\n h(\n 'section',\n { key: id, className: 'zodal-dials-group', 'data-group': id },\n h('h3', { className: 'zodal-dials-group-title' }, title),\n ...fields.map((f) => h(SettingField, { key: f.key, field: f, state: states[f.key] ?? emptyState(), onChange, onReset, registry: reg })),\n ),\n );\n }\n return h('div', { className: 'zodal-dials-panel' }, ...children);\n}\n"],"mappings":"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","names":["h","h"]}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@zodal/dials-ui-shadcn",
3
+ "version": "0.1.0",
4
+ "description": "React/shadcn renderer for @zodal/dials-ui — settings panel components consuming the headless config",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "peerDependencies": {
28
+ "@zodal/core": "^0.1.2",
29
+ "@zodal/dials-core": "^0.1.0",
30
+ "@zodal/dials-ui": "^0.1.0",
31
+ "react": "^18.0.0 || ^19.0.0",
32
+ "react-dom": "^18.0.0 || ^19.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@zodal/core": "^0.1.2",
36
+ "@testing-library/react": "^16.0.0",
37
+ "@types/react": "^18.3.0",
38
+ "@types/react-dom": "^18.3.0",
39
+ "react": "^18.3.1",
40
+ "react-dom": "^18.3.1",
41
+ "jsdom": "^25.0.0",
42
+ "tsup": "^8.0.0",
43
+ "typescript": "^5.7.0",
44
+ "vitest": "^3.0.0",
45
+ "@zodal/dials-core": "0.1.0",
46
+ "@zodal/dials-ui": "0.1.0"
47
+ },
48
+ "scripts": {
49
+ "build": "tsup",
50
+ "test": "vitest run",
51
+ "typecheck": "tsc --noEmit"
52
+ }
53
+ }