@object-ui/app-shell 6.0.4 → 6.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 (81) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.md +10 -1
  3. package/dist/console/AppContent.js +24 -2
  4. package/dist/console/ai/AiChatPage.d.ts +8 -0
  5. package/dist/console/ai/AiChatPage.js +188 -0
  6. package/dist/console/ai/ConversationsSidebar.d.ts +7 -0
  7. package/dist/console/ai/ConversationsSidebar.js +111 -0
  8. package/dist/console/auth/LoginPage.js +19 -2
  9. package/dist/console/auth/RegisterPage.js +30 -1
  10. package/dist/console/marketplace/MarketplaceAccessDenied.js +3 -1
  11. package/dist/console/marketplace/MarketplaceInstalledPage.js +11 -3
  12. package/dist/console/marketplace/MarketplacePackagePage.js +83 -17
  13. package/dist/console/marketplace/MarketplacePage.js +55 -18
  14. package/dist/console/marketplace/marketplaceApi.d.ts +20 -0
  15. package/dist/console/marketplace/usePackageL10n.d.ts +38 -0
  16. package/dist/console/marketplace/usePackageL10n.js +110 -0
  17. package/dist/console/organizations/CreateWorkspaceDialog.js +29 -1
  18. package/dist/console/organizations/OrganizationsPage.js +24 -3
  19. package/dist/context/FavoritesProvider.d.ts +40 -2
  20. package/dist/context/FavoritesProvider.js +201 -20
  21. package/dist/hooks/index.d.ts +1 -0
  22. package/dist/hooks/index.js +1 -0
  23. package/dist/hooks/useChatConversation.d.ts +7 -0
  24. package/dist/hooks/useChatConversation.js +37 -5
  25. package/dist/hooks/useConversationList.d.ts +25 -0
  26. package/dist/hooks/useConversationList.js +131 -0
  27. package/dist/hooks/useNavPins.d.ts +11 -4
  28. package/dist/hooks/useNavPins.js +104 -53
  29. package/dist/index.d.ts +7 -0
  30. package/dist/index.js +14 -0
  31. package/dist/layout/AppHeader.js +2 -2
  32. package/dist/layout/AppSidebar.js +20 -1
  33. package/dist/layout/UnifiedSidebar.js +1 -1
  34. package/dist/providers/ExpressionProvider.d.ts +11 -1
  35. package/dist/providers/ExpressionProvider.js +11 -6
  36. package/dist/services/builtinComponents.d.ts +1 -0
  37. package/dist/services/builtinComponents.js +166 -0
  38. package/dist/services/componentRegistry.d.ts +63 -0
  39. package/dist/services/componentRegistry.js +36 -0
  40. package/dist/views/ComponentNavView.d.ts +6 -0
  41. package/dist/views/ComponentNavView.js +26 -0
  42. package/dist/views/RecordDetailView.js +72 -0
  43. package/dist/views/RecordFormPage.js +15 -3
  44. package/dist/views/SearchResultsPage.js +4 -0
  45. package/dist/views/metadata-admin/DesignerEditorWrapper.d.ts +58 -0
  46. package/dist/views/metadata-admin/DesignerEditorWrapper.js +140 -0
  47. package/dist/views/metadata-admin/DirectoryPage.d.ts +1 -0
  48. package/dist/views/metadata-admin/DirectoryPage.js +135 -0
  49. package/dist/views/metadata-admin/LayeredDiff.d.ts +6 -0
  50. package/dist/views/metadata-admin/LayeredDiff.js +26 -0
  51. package/dist/views/metadata-admin/PageShell.d.ts +34 -0
  52. package/dist/views/metadata-admin/PageShell.js +33 -0
  53. package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +5 -0
  54. package/dist/views/metadata-admin/PermissionMatrixEditor.js +288 -0
  55. package/dist/views/metadata-admin/QuickFind.d.ts +5 -0
  56. package/dist/views/metadata-admin/QuickFind.js +152 -0
  57. package/dist/views/metadata-admin/ResourceEditPage.d.ts +7 -0
  58. package/dist/views/metadata-admin/ResourceEditPage.js +256 -0
  59. package/dist/views/metadata-admin/ResourceHistoryPage.d.ts +5 -0
  60. package/dist/views/metadata-admin/ResourceHistoryPage.js +97 -0
  61. package/dist/views/metadata-admin/ResourceListPage.d.ts +4 -0
  62. package/dist/views/metadata-admin/ResourceListPage.js +144 -0
  63. package/dist/views/metadata-admin/ResourceRouter.d.ts +10 -0
  64. package/dist/views/metadata-admin/ResourceRouter.js +47 -0
  65. package/dist/views/metadata-admin/SchemaForm.d.ts +99 -0
  66. package/dist/views/metadata-admin/SchemaForm.js +556 -0
  67. package/dist/views/metadata-admin/default-schemas.d.ts +6 -0
  68. package/dist/views/metadata-admin/default-schemas.js +207 -0
  69. package/dist/views/metadata-admin/i18n.d.ts +33 -0
  70. package/dist/views/metadata-admin/i18n.js +303 -0
  71. package/dist/views/metadata-admin/index.d.ts +31 -0
  72. package/dist/views/metadata-admin/index.js +33 -0
  73. package/dist/views/metadata-admin/predicate.d.ts +31 -0
  74. package/dist/views/metadata-admin/predicate.js +150 -0
  75. package/dist/views/metadata-admin/registry.d.ts +125 -0
  76. package/dist/views/metadata-admin/registry.js +48 -0
  77. package/dist/views/metadata-admin/useMetadata.d.ts +37 -0
  78. package/dist/views/metadata-admin/useMetadata.js +96 -0
  79. package/dist/views/metadata-admin/widgets.d.ts +68 -0
  80. package/dist/views/metadata-admin/widgets.js +287 -0
  81. package/package.json +29 -28
@@ -0,0 +1,556 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * SchemaForm — minimal JSONSchema-driven form (Phase 3c).
5
+ *
6
+ * The framework's `/meta/types` endpoint returns a `schema` field per
7
+ * type, generated from Zod via `zod-to-json-schema`. We render that
8
+ * schema as a form so admins can edit *any* metadata type without
9
+ * the platform having to write a bespoke editor for each.
10
+ *
11
+ * Scope (MVP):
12
+ * • string → Input (or Textarea if `format: 'multiline'`)
13
+ * • number → Input type="number"
14
+ * • boolean → Switch
15
+ * • enum → Select
16
+ * • array of strings → tag editor (comma-separated for MVP)
17
+ * • object → recursive collapsed section
18
+ * • anyOf / oneOf / unknown → JSON textarea fallback
19
+ *
20
+ * NOT covered (yet) — those types use bespoke editors registered via
21
+ * `registerMetadataResource()`:
22
+ * • Permission matrix (rows × columns × actions)
23
+ * • Object/Field designers
24
+ * • View / dashboard / page canvas designers
25
+ *
26
+ * Error display:
27
+ * • Pass `issues` in the shape `[{ path: 'a.b', message: '...' }, ...]`
28
+ * to render inline error chips next to the offending fields.
29
+ * • Matches the framework's `error.issues` envelope from `sendError`.
30
+ */
31
+ import * as React from 'react';
32
+ import { Input } from '@object-ui/components';
33
+ import { Textarea } from '@object-ui/components';
34
+ import { Label } from '@object-ui/components';
35
+ import { Switch } from '@object-ui/components';
36
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@object-ui/components';
37
+ import { Button } from '@object-ui/components';
38
+ import { Plus, Trash2, ChevronDown, ChevronRight } from 'lucide-react';
39
+ import { Tabs, TabsList, TabsTrigger, TabsContent, } from '@object-ui/components';
40
+ import { evaluatePredicate } from './predicate';
41
+ import { WIDGETS } from './widgets';
42
+ /** Widgets that don't need a custom renderer — they overlay on the
43
+ * existing default control (textarea/input/etc) and just act as a hint. */
44
+ const KNOWN_PASSTHROUGH_WIDGETS = new Set([
45
+ 'text',
46
+ 'textarea',
47
+ 'number',
48
+ 'switch',
49
+ 'select',
50
+ 'json',
51
+ ]);
52
+ /**
53
+ * Infer widget name from FormFieldSpec.type (Data.FieldType) and schema.
54
+ * Priority: explicit widget > type-based inference > schema-based inference > default.
55
+ */
56
+ function inferWidget(fieldSpec, schema) {
57
+ // 1. Explicit widget always wins
58
+ if (fieldSpec?.widget)
59
+ return fieldSpec.widget;
60
+ // 2. Infer from Data.FieldType
61
+ if (fieldSpec?.type) {
62
+ const t = fieldSpec.type;
63
+ // Text types
64
+ if (t === 'text' || t === 'email' || t === 'url' || t === 'phone' || t === 'password')
65
+ return 'text';
66
+ if (t === 'textarea' || t === 'markdown' || t === 'html' || t === 'richtext')
67
+ return 'textarea';
68
+ // Number types
69
+ if (t === 'number' || t === 'currency' || t === 'percent')
70
+ return 'number';
71
+ // Date/time
72
+ if (t === 'date' || t === 'datetime' || t === 'time')
73
+ return 'date-picker';
74
+ // Boolean
75
+ if (t === 'boolean' || t === 'toggle')
76
+ return 'switch';
77
+ // Selection
78
+ if (t === 'select' || t === 'radio')
79
+ return fieldSpec.multiple ? 'multiselect' : 'select';
80
+ if (t === 'multiselect' || t === 'checkboxes' || t === 'tags')
81
+ return 'string-tags';
82
+ // Embedded structured (composite/repeater handled natively in FieldControl
83
+ // BEFORE the WIDGETS registry — return the type name so the badge is
84
+ // accurate; FieldControl short-circuits before widget lookup).
85
+ if (t === 'composite')
86
+ return 'composite';
87
+ if (t === 'repeater')
88
+ return 'repeater';
89
+ // Relational
90
+ if (t === 'lookup' || t === 'master_detail')
91
+ return 'ref-object';
92
+ if (t === 'tree')
93
+ return 'ref-object';
94
+ // Media
95
+ if (t === 'image' || t === 'file' || t === 'avatar' || t === 'video' || t === 'audio')
96
+ return 'file-upload';
97
+ // Code/JSON
98
+ if (t === 'code')
99
+ return 'code';
100
+ if (t === 'json')
101
+ return 'json';
102
+ // Enhanced
103
+ if (t === 'location' || t === 'address')
104
+ return 'json';
105
+ if (t === 'color')
106
+ return 'color-picker';
107
+ if (t === 'rating')
108
+ return 'number';
109
+ if (t === 'slider')
110
+ return 'slider';
111
+ if (t === 'signature')
112
+ return 'signature';
113
+ if (t === 'qrcode')
114
+ return 'qrcode';
115
+ if (t === 'progress')
116
+ return 'number';
117
+ // Calculated
118
+ if (t === 'formula' || t === 'summary' || t === 'autonumber')
119
+ return 'text';
120
+ // Vector
121
+ if (t === 'vector')
122
+ return 'json';
123
+ }
124
+ // 3. Infer from JSON Schema
125
+ if (schema) {
126
+ const type = schema.type;
127
+ // Array of strings → string-tags
128
+ if (type === 'array' && schema.items?.type === 'string')
129
+ return 'string-tags';
130
+ // Array of objects → master-detail
131
+ if (type === 'array' && schema.items?.type === 'object')
132
+ return 'master-detail';
133
+ // Object → object-fields
134
+ if (type === 'object')
135
+ return 'object-fields';
136
+ // Boolean → switch
137
+ if (type === 'boolean')
138
+ return 'switch';
139
+ // Number → number
140
+ if (type === 'number' || type === 'integer')
141
+ return 'number';
142
+ // Enum → select
143
+ if (Array.isArray(schema.enum))
144
+ return 'select';
145
+ // String with format
146
+ if (type === 'string') {
147
+ if (schema.format === 'date' || schema.format === 'date-time')
148
+ return 'date-picker';
149
+ if (schema.format === 'email' || schema.format === 'uri' || schema.format === 'uri-reference')
150
+ return 'text';
151
+ if (schema.format === 'multiline')
152
+ return 'textarea';
153
+ }
154
+ }
155
+ // 4. Default fallback
156
+ return undefined;
157
+ }
158
+ export function SchemaForm({ schema, form, value, onChange, issues = [], hiddenFields = [], fieldOrder = [], readOnly = false, createMode = false, widgetContext, }) {
159
+ // No schema → synthesize one from the value's top-level keys so the
160
+ // form renderer can still produce a structured, labelled view (with
161
+ // proper read-only semantics) instead of falling back to a raw JSON
162
+ // dump. This handles metadata types the framework hasn't yet shipped
163
+ // a Zod schema for (`hook`, `trigger`, `validation`, etc.).
164
+ //
165
+ // Editable + truly unknown shape → keep the raw JSON editor as a
166
+ // last resort, since we can't safely guess primitive types for
167
+ // fields the user might add.
168
+ let effectiveSchema = schema;
169
+ if (!effectiveSchema || typeof effectiveSchema !== 'object') {
170
+ if (value && typeof value === 'object') {
171
+ effectiveSchema = inferSchemaFromValue(value);
172
+ }
173
+ else {
174
+ return (_jsx(RawJsonEditor, { value: value, onChange: onChange, readOnly: readOnly }));
175
+ }
176
+ }
177
+ // Resolve top-level object properties.
178
+ const props = (effectiveSchema.properties ?? {});
179
+ const required = Array.isArray(effectiveSchema.required) ? effectiveSchema.required : [];
180
+ const keys = orderKeys(Object.keys(props), fieldOrder).filter((k) => !hiddenFields.includes(k));
181
+ const issuesByPath = React.useMemo(() => {
182
+ var _a;
183
+ const map = {};
184
+ for (const i of issues) {
185
+ (map[_a = i.path] ?? (map[_a] = [])).push(i.message);
186
+ }
187
+ return map;
188
+ }, [issues]);
189
+ const v = value ?? {};
190
+ function setField(key, fieldValue) {
191
+ const next = { ...v, [key]: fieldValue };
192
+ if (fieldValue === undefined || fieldValue === '') {
193
+ delete next[key];
194
+ }
195
+ onChange(next);
196
+ }
197
+ // If the framework provided a FormView layout, render sections (tabbed
198
+ // or simple). Otherwise fall through to the flat property list.
199
+ if (form?.sections?.length) {
200
+ return (_jsx(SectionedSchemaForm, { form: form, props: props, required: required, hiddenFields: hiddenFields, issuesByPath: issuesByPath, value: v, readOnly: readOnly, createMode: createMode, widgetContext: widgetContext, onChange: setField }));
201
+ }
202
+ return (_jsx("div", { className: "space-y-4", children: keys.map((key) => (_jsx(FieldRow, { name: key, schema: props[key], value: v[key], required: required.includes(key), issues: issuesByPath[key], readOnly: readOnly, widgetContext: widgetContext, formData: v, onChange: (val) => setField(key, val) }, key))) }));
203
+ }
204
+ /* ----- sectioned layout (FormView spec) ---------------------------------- */
205
+ function normaliseField(f) {
206
+ return typeof f === 'string' ? { field: f } : f;
207
+ }
208
+ function SectionedSchemaForm({ form, props, required, hiddenFields, issuesByPath, value, readOnly, createMode, widgetContext, onChange, }) {
209
+ const sections = (form.sections ?? []).filter((s) => !s.visibleOn || evaluatePredicate(s.visibleOn, { data: value }));
210
+ // Decide whether to render as tabs or stacked sections.
211
+ const isTabbed = form.type === 'tabbed' && sections.length > 1;
212
+ const renderSection = (s, idx) => {
213
+ const fields = s.fields
214
+ .map(normaliseField)
215
+ .filter((f) => {
216
+ if (f.hidden)
217
+ return false;
218
+ if (hiddenFields.includes(f.field))
219
+ return false;
220
+ if (f.visibleOn && !evaluatePredicate(f.visibleOn, { data: value })) {
221
+ return false;
222
+ }
223
+ return true;
224
+ });
225
+ if (fields.length === 0)
226
+ return null;
227
+ const cols = s.columns ?? 1;
228
+ return (_jsxs("section", { className: "space-y-3 rounded-md border border-border/40 bg-card/30 p-4", children: [s.label && (_jsxs("header", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground/90", children: s.label }), s.description && (_jsx("p", { className: "text-xs text-muted-foreground", children: s.description }))] })), _jsx("div", { className: "grid gap-4", style: {
229
+ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
230
+ }, children: fields.map((f) => {
231
+ const propSchema = props[f.field];
232
+ if (!propSchema) {
233
+ return (_jsxs("div", { className: "rounded border border-dashed border-amber-500/40 bg-amber-500/5 p-2 text-xs text-amber-700 dark:text-amber-300", style: { gridColumn: `span ${f.colSpan ?? 1}` }, children: ["\u26A0\uFE0F Field ", _jsx("code", { children: f.field }), " declared in form layout but missing from schema. Skipping."] }, f.field));
234
+ }
235
+ return (_jsx("div", { style: { gridColumn: `span ${f.colSpan ?? 1}` }, children: _jsx(FieldRow, { name: f.field, schema: {
236
+ ...propSchema,
237
+ ...(f.label ? { title: f.label } : {}),
238
+ ...(f.helpText ? { description: f.helpText } : {}),
239
+ ...(f.placeholder ? { placeholder: f.placeholder } : {}),
240
+ }, value: value[f.field], required: f.required ?? required.includes(f.field), issues: issuesByPath[f.field], readOnly: readOnly || f.readonly || (f.immutable && !createMode), fieldSpec: f, widgetContext: widgetContext, formData: value, onChange: (val) => onChange(f.field, val) }) }, f.field));
241
+ }) })] }, idx));
242
+ };
243
+ if (isTabbed) {
244
+ const tabSections = sections.filter((s) => s.fields
245
+ .map(normaliseField)
246
+ .some((f) => !f.hidden &&
247
+ !hiddenFields.includes(f.field) &&
248
+ (!f.visibleOn ||
249
+ evaluatePredicate(f.visibleOn, { data: value }))));
250
+ if (tabSections.length === 0)
251
+ return null;
252
+ const defaultTab = (tabSections[0].label ?? 'section-0').toLowerCase();
253
+ return (_jsxs(Tabs, { defaultValue: defaultTab, className: "w-full", children: [_jsx(TabsList, { className: "flex flex-wrap gap-1", children: tabSections.map((s, i) => (_jsx(TabsTrigger, { value: (s.label ?? `section-${i}`).toLowerCase(), children: s.label ?? `Section ${i + 1}` }, i))) }), tabSections.map((s, i) => (_jsx(TabsContent, { value: (s.label ?? `section-${i}`).toLowerCase(), className: "mt-4", children: renderSection(s, i) }, i)))] }));
254
+ }
255
+ return _jsx("div", { className: "space-y-4", children: sections.map(renderSection) });
256
+ }
257
+ /* ----- inner field row ---------------------------------------------------- */
258
+ function FieldRow({ name, schema, value, required, issues, readOnly, fieldSpec, widgetContext, formData, onChange, }) {
259
+ const label = schema?.title || prettify(name);
260
+ const description = schema?.description;
261
+ const id = `mdf-${name}`;
262
+ // Auto-infer widget from fieldSpec.type or schema
263
+ const widget = inferWidget(fieldSpec, schema);
264
+ // Booleans with a schema default are never *missing* — don't show the
265
+ // required asterisk (which would otherwise lie about user obligation).
266
+ const isBoolean = schema?.type === 'boolean' || widget === 'switch';
267
+ const hasDefault = schema?.default !== undefined;
268
+ const showRequiredStar = required && !(isBoolean && hasDefault);
269
+ // Only show the machine name when it materially differs from the
270
+ // prettified label (e.g. `is_active` → "Is Active" matches, hide it;
271
+ // `rls` → "Rls" doesn't, show it). Cuts ~50% of the visual noise.
272
+ const labelMatchesName = prettify(name).toLowerCase() === label.toLowerCase();
273
+ // Booleans render inline (label · description · switch) on one row to
274
+ // save vertical space and feel like a real settings panel.
275
+ if (isBoolean) {
276
+ return (_jsxs("div", { className: "flex items-start justify-between gap-3 py-1.5", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs(Label, { htmlFor: id, className: "text-sm font-medium cursor-pointer", children: [label, showRequiredStar && _jsx("span", { className: "text-destructive ml-0.5", children: "*" })] }), description && (_jsx("div", { className: "text-xs text-muted-foreground mt-0.5", children: description })), issues?.map((m, i) => (_jsx("div", { className: "text-xs text-destructive mt-0.5", children: m }, i)))] }), _jsx(FieldControl, { id: id, schema: schema, value: value, onChange: onChange, readOnly: readOnly, widget: widget, fieldSpec: fieldSpec, widgetContext: widgetContext, formData: formData })] }));
277
+ }
278
+ return (_jsxs("div", { className: "space-y-1.5", children: [_jsx("div", { className: "flex items-center justify-between gap-2", children: _jsxs(Label, { htmlFor: id, className: "text-sm font-medium", children: [label, showRequiredStar && _jsx("span", { className: "text-destructive ml-0.5", children: "*" }), !labelMatchesName && (_jsx("code", { className: "ml-2 text-[10px] font-mono text-muted-foreground/70", title: "Machine name", children: name }))] }) }), _jsx(FieldControl, { id: id, schema: schema, value: value, onChange: onChange, readOnly: readOnly, widget: widget, fieldSpec: fieldSpec, widgetContext: widgetContext, formData: formData }), description && (_jsx("div", { className: "text-xs text-muted-foreground", children: description })), issues?.map((m, i) => (_jsx("div", { className: "text-xs text-destructive", children: m }, i)))] }));
279
+ }
280
+ function FieldControl({ id, schema, value, onChange, readOnly, widget, fieldSpec, widgetContext, formData, }) {
281
+ // Composite/repeater are first-class structured types — render natively
282
+ // with recursive FieldRow calls so all UI features (widgets, options,
283
+ // visibility, readonly) work uniformly at every nesting level.
284
+ // When `fields` is omitted, fall back to schema-derived sub-fields
285
+ // (all schema.properties / items.properties) so authors don't have to
286
+ // enumerate every sub-property by hand.
287
+ if (fieldSpec?.type === 'composite') {
288
+ const fields = fieldSpec.fields?.length
289
+ ? fieldSpec.fields
290
+ : derivePropertyNames(schema);
291
+ return (_jsx(CompositeField, { value: value, fields: fields, schema: schema, readOnly: readOnly, widgetContext: widgetContext, onChange: onChange }));
292
+ }
293
+ if (fieldSpec?.type === 'repeater') {
294
+ const itemSchema = schema?.items ?? {};
295
+ const fields = fieldSpec.fields?.length
296
+ ? fieldSpec.fields
297
+ : derivePropertyNames(itemSchema);
298
+ return (_jsx(RepeaterField, { value: value, fields: fields, schema: schema, readOnly: readOnly, widgetContext: widgetContext, widget: fieldSpec.widget, onChange: onChange }));
299
+ }
300
+ // Widget hint takes precedence: try the registry first, then the
301
+ // passthrough hint list, then fall back to JSON with an inline hint.
302
+ if (widget) {
303
+ const Renderer = WIDGETS[widget];
304
+ if (Renderer) {
305
+ return (_jsx(Renderer, { id: id, schema: schema, value: value, onChange: onChange, readOnly: readOnly, context: widgetContext, fieldSpec: fieldSpec, formData: formData }));
306
+ }
307
+ if (!KNOWN_PASSTHROUGH_WIDGETS.has(widget)) {
308
+ return (_jsxs("div", { className: "space-y-1", children: [_jsx(RawJsonEditor, { value: value, onChange: (v) => onChange(v), readOnly: readOnly }), _jsxs("div", { className: "text-[10px] text-muted-foreground", children: ["widget ", _jsx("code", { className: "font-mono", children: widget }), " \u2014 falling back to JSON until a custom renderer is registered."] })] }));
309
+ }
310
+ }
311
+ // Enum / Select — fieldSpec.options takes precedence over schema.enum.
312
+ const options = fieldSpec?.options;
313
+ const enumValues = schema?.enum ?? undefined;
314
+ if (Array.isArray(options) && options.length > 0) {
315
+ // Render from fieldSpec.options (Data.SelectOption[])
316
+ return (_jsxs(Select, { value: value == null ? '' : String(value), onValueChange: (v) => onChange(v), disabled: readOnly, children: [_jsx(SelectTrigger, { id: id, children: _jsx(SelectValue, { placeholder: "Select\u2026" }) }), _jsx(SelectContent, { children: options.map((opt) => (_jsxs(SelectItem, { value: opt.value, children: [opt.label, opt.color && (_jsx("span", { className: "ml-2 inline-block h-3 w-3 rounded", style: { backgroundColor: opt.color } }))] }, opt.value))) })] }));
317
+ }
318
+ if (Array.isArray(enumValues) && enumValues.length > 0) {
319
+ // Fallback to schema.enum
320
+ return (_jsxs(Select, { value: value == null ? '' : String(value), onValueChange: (v) => onChange(v), disabled: readOnly, children: [_jsx(SelectTrigger, { id: id, children: _jsx(SelectValue, { placeholder: "Select\u2026" }) }), _jsx(SelectContent, { children: enumValues.map((opt) => (_jsx(SelectItem, { value: String(opt), children: String(opt) }, String(opt)))) })] }));
321
+ }
322
+ // Boolean → Switch (no redundant "true/false" text; the toggle state
323
+ // already conveys the value).
324
+ if (schema?.type === 'boolean') {
325
+ return (_jsx(Switch, { id: id, checked: !!value, onCheckedChange: (c) => onChange(c), disabled: readOnly }));
326
+ }
327
+ // Number / integer → numeric input with min/max from fieldSpec.
328
+ if (schema?.type === 'number' || schema?.type === 'integer') {
329
+ const min = fieldSpec?.min;
330
+ const max = fieldSpec?.max;
331
+ return (_jsx(Input, { id: id, type: "number", value: value == null ? '' : String(value), min: min, max: max, onChange: (e) => {
332
+ const raw = e.target.value;
333
+ if (raw === '')
334
+ return onChange(undefined);
335
+ const n = schema.type === 'integer' ? parseInt(raw, 10) : Number(raw);
336
+ onChange(Number.isFinite(n) ? n : undefined);
337
+ }, readOnly: readOnly }));
338
+ }
339
+ // String → Input (or Textarea if it looks long), with maxLength from fieldSpec.
340
+ if (schema?.type === 'string') {
341
+ const maxLength = fieldSpec?.maxLength;
342
+ const long = schema?.format === 'multiline' ||
343
+ schema?.contentMediaType === 'text/markdown' ||
344
+ (typeof value === 'string' && value.length > 80);
345
+ if (long) {
346
+ return (_jsx(Textarea, { id: id, rows: 4, value: value ?? '', maxLength: maxLength, onChange: (e) => onChange(e.target.value || undefined), readOnly: readOnly }));
347
+ }
348
+ return (_jsx(Input, { id: id, value: value ?? '', maxLength: maxLength, onChange: (e) => onChange(e.target.value || undefined), readOnly: readOnly }));
349
+ }
350
+ // Array of primitives → comma-separated tag editor (MVP).
351
+ if (schema?.type === 'array') {
352
+ const itemsSchema = schema?.items ?? {};
353
+ const isPrimitive = itemsSchema.type === 'string' ||
354
+ itemsSchema.type === 'number' ||
355
+ itemsSchema.type === 'integer';
356
+ if (isPrimitive) {
357
+ const arr = Array.isArray(value) ? value : [];
358
+ return (_jsx(Input, { id: id, value: arr.map(String).join(', '), placeholder: "comma, separated, values", onChange: (e) => {
359
+ const raw = e.target.value;
360
+ const parts = raw
361
+ .split(',')
362
+ .map((s) => s.trim())
363
+ .filter(Boolean);
364
+ if (itemsSchema.type === 'number' || itemsSchema.type === 'integer') {
365
+ onChange(parts.map((p) => Number(p)).filter((n) => Number.isFinite(n)));
366
+ }
367
+ else {
368
+ onChange(parts);
369
+ }
370
+ }, readOnly: readOnly }));
371
+ }
372
+ }
373
+ // Object / complex → JSON fallback so admins can still edit.
374
+ return _jsx(RawJsonEditor, { value: value, onChange: onChange, readOnly: readOnly, small: true });
375
+ }
376
+ /* ----- composite / repeater (embedded structured values) ----------------- */
377
+ /**
378
+ * Resolve the JSONSchema fragment for a sub-field of a composite/repeater.
379
+ * Looks under parent `schema.properties[subName]` (composite) or
380
+ * `schema.items.properties[subName]` (repeater). Falls back to `{}`.
381
+ */
382
+ function pickSubSchema(parent, kind, subName) {
383
+ if (!parent)
384
+ return {};
385
+ const props = kind === 'composite'
386
+ ? parent.properties
387
+ : parent.items?.properties;
388
+ return props?.[subName] ?? {};
389
+ }
390
+ function CompositeField({ value, fields, schema, readOnly, widgetContext, onChange, }) {
391
+ const obj = (value && typeof value === 'object' && !Array.isArray(value))
392
+ ? value
393
+ : {};
394
+ const specs = fields.map(normaliseField);
395
+ return (_jsx("div", { className: "rounded-md border border-border/50 bg-muted/20 p-3 space-y-3", children: specs.map((spec) => {
396
+ const subSchema = pickSubSchema(schema, 'composite', spec.field);
397
+ return (_jsx(FieldRow, { name: spec.field, schema: subSchema, value: obj[spec.field], required: Boolean(spec.required), readOnly: readOnly || spec.readonly, fieldSpec: spec, widgetContext: widgetContext, formData: obj, onChange: (v) => onChange({ ...obj, [spec.field]: v }) }, spec.field));
398
+ }) }));
399
+ }
400
+ function RepeaterField({ value, fields, schema, readOnly, widgetContext, widget, onChange, }) {
401
+ const rows = Array.isArray(value) ? value : [];
402
+ const specs = fields.map(normaliseField);
403
+ const [openIdx, setOpenIdx] = React.useState(null);
404
+ // Default to card layout (one fieldset per row). `widget: 'grid'` opts
405
+ // into compact inline-table layout for short, atomic sub-fields.
406
+ const useGrid = widget === 'grid' || widget === 'table';
407
+ const update = (i, patch) => {
408
+ const next = rows.map((r, idx) => (idx === i ? { ...r, ...patch } : r));
409
+ onChange(next);
410
+ };
411
+ const remove = (i) => onChange(rows.filter((_, idx) => idx !== i));
412
+ const add = () => {
413
+ const blank = {};
414
+ specs.forEach((s) => { blank[s.field] = undefined; });
415
+ onChange([...rows, blank]);
416
+ setOpenIdx(rows.length);
417
+ };
418
+ if (useGrid) {
419
+ return (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "overflow-x-auto rounded-md border border-border/50", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/40", children: _jsxs("tr", { children: [specs.map((s) => (_jsxs("th", { className: "px-2 py-1.5 text-left text-xs font-medium", children: [s.label || prettify(s.field), s.required && _jsx("span", { className: "text-destructive ml-0.5", children: "*" })] }, s.field))), !readOnly && _jsx("th", { className: "w-8" })] }) }), _jsxs("tbody", { children: [rows.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: specs.length + 1, className: "px-2 py-3 text-center text-xs text-muted-foreground", children: "No items. Click + to add." }) })), rows.map((row, idx) => (_jsxs("tr", { className: "border-t border-border/30 align-top", children: [specs.map((s) => {
420
+ const sub = pickSubSchema(schema, 'repeater', s.field);
421
+ return (_jsx("td", { className: "p-1.5", children: _jsx(FieldControl, { id: `rep-${idx}-${s.field}`, schema: sub, value: row?.[s.field], readOnly: readOnly || s.readonly, widget: inferWidget(s, sub), fieldSpec: s, widgetContext: widgetContext, formData: row, onChange: (v) => update(idx, { [s.field]: v }) }) }, s.field));
422
+ }), !readOnly && (_jsx("td", { className: "p-1.5 text-right", children: _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => remove(idx), className: "h-7 w-7 p-0", "aria-label": "Remove", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }) }))] }, idx)))] })] }) }), !readOnly && (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: add, children: [_jsx(Plus, { className: "h-3.5 w-3.5 mr-1" }), " Add"] }))] }));
423
+ }
424
+ // Card layout — one collapsible fieldset per row.
425
+ return (_jsxs("div", { className: "space-y-2", children: [rows.length === 0 && (_jsx("div", { className: "rounded-md border border-dashed border-border/50 px-3 py-4 text-center text-xs text-muted-foreground", children: "No items yet." })), rows.map((row, idx) => {
426
+ const isOpen = openIdx === idx;
427
+ const summary = specs
428
+ .map((s) => row?.[s.field])
429
+ .find((v) => v != null && v !== '');
430
+ return (_jsxs("div", { className: "rounded-md border border-border/50 bg-muted/10", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-2 py-1.5 border-b border-border/30", children: [_jsxs("button", { type: "button", onClick: () => setOpenIdx(isOpen ? null : idx), className: "flex items-center gap-1.5 text-sm font-medium text-left flex-1 min-w-0", children: [isOpen ? _jsx(ChevronDown, { className: "h-3.5 w-3.5" }) : _jsx(ChevronRight, { className: "h-3.5 w-3.5" }), _jsxs("span", { className: "truncate", children: ["#", idx + 1, summary != null ? ` — ${String(summary)}` : ''] })] }), !readOnly && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => remove(idx), className: "h-7 w-7 p-0", "aria-label": "Remove", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }), isOpen && (_jsx("div", { className: "p-3 space-y-3", children: specs.map((s) => {
431
+ const sub = pickSubSchema(schema, 'repeater', s.field);
432
+ return (_jsx(FieldRow, { name: s.field, schema: sub, value: row?.[s.field], required: Boolean(s.required), readOnly: readOnly || s.readonly, fieldSpec: s, widgetContext: widgetContext, formData: row, onChange: (v) => update(idx, { [s.field]: v }) }, s.field));
433
+ }) }))] }, idx));
434
+ }), !readOnly && (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: add, children: [_jsx(Plus, { className: "h-3.5 w-3.5 mr-1" }), " Add item"] }))] }));
435
+ }
436
+ /* ----- raw JSON fallback -------------------------------------------------- */
437
+ function RawJsonEditor({ value, onChange, readOnly, small, }) {
438
+ const [text, setText] = React.useState(() => safeStringify(value));
439
+ const [error, setError] = React.useState(null);
440
+ // Re-sync when external value changes (e.g. Reset Overlay).
441
+ React.useEffect(() => {
442
+ setText(safeStringify(value));
443
+ setError(null);
444
+ }, [JSON.stringify(value)]); // intentional: stringify-deep-equal
445
+ return (_jsxs("div", { className: "space-y-1", children: [_jsx(Textarea, { rows: small ? 4 : 12, className: "font-mono text-xs", value: text, readOnly: readOnly, onChange: (e) => {
446
+ const next = e.target.value;
447
+ setText(next);
448
+ if (!next.trim()) {
449
+ setError(null);
450
+ onChange(undefined);
451
+ return;
452
+ }
453
+ try {
454
+ const parsed = JSON.parse(next);
455
+ setError(null);
456
+ onChange(parsed);
457
+ }
458
+ catch (err) {
459
+ setError(err?.message ?? 'Invalid JSON');
460
+ }
461
+ } }), error && _jsx("div", { className: "text-xs text-destructive", children: error })] }));
462
+ }
463
+ /**
464
+ * Synthesize a minimal JSON Schema by introspecting a runtime value.
465
+ *
466
+ * Used as the fallback when the framework hasn't shipped a Zod schema
467
+ * for a metadata type (e.g. `hook`, `trigger`, `validation`). The
468
+ * resulting schema lets `SchemaForm` render a real labelled form
469
+ * (respecting `readOnly`) instead of bailing out to a raw JSON dump.
470
+ *
471
+ * Types are guessed conservatively from the value: scalars become
472
+ * `string` / `number` / `boolean`; arrays of strings become string
473
+ * tags; arrays of objects become master-detail tables; objects become
474
+ * nested JSON regions. Anything indeterminate falls back to `string`
475
+ * so the field still renders.
476
+ */
477
+ function inferSchemaFromValue(value) {
478
+ const properties = {};
479
+ for (const [k, v] of Object.entries(value)) {
480
+ if (k.startsWith('_'))
481
+ continue;
482
+ if (v === null || v === undefined) {
483
+ properties[k] = { type: 'string' };
484
+ }
485
+ else if (typeof v === 'string') {
486
+ properties[k] = v.length > 80 || v.includes('\n')
487
+ ? { type: 'string', format: 'multiline' }
488
+ : { type: 'string' };
489
+ }
490
+ else if (typeof v === 'number') {
491
+ properties[k] = { type: Number.isInteger(v) ? 'integer' : 'number' };
492
+ }
493
+ else if (typeof v === 'boolean') {
494
+ properties[k] = { type: 'boolean' };
495
+ }
496
+ else if (Array.isArray(v)) {
497
+ if (v.length > 0 && typeof v[0] === 'string') {
498
+ properties[k] = { type: 'array', items: { type: 'string' } };
499
+ }
500
+ else if (v.length > 0 && typeof v[0] === 'object' && v[0] !== null) {
501
+ const sample = v[0];
502
+ const itemProps = {};
503
+ for (const key of Object.keys(sample)) {
504
+ itemProps[key] = { type: 'string' };
505
+ }
506
+ properties[k] = {
507
+ type: 'array',
508
+ items: { type: 'object', properties: itemProps },
509
+ };
510
+ }
511
+ else {
512
+ properties[k] = { type: 'array', items: { type: 'string' } };
513
+ }
514
+ }
515
+ else if (typeof v === 'object') {
516
+ properties[k] = { type: 'object', additionalProperties: true };
517
+ }
518
+ else {
519
+ properties[k] = { type: 'string' };
520
+ }
521
+ }
522
+ return { type: 'object', properties, additionalProperties: true };
523
+ }
524
+ /* ----- helpers ------------------------------------------------------------ */
525
+ function safeStringify(v) {
526
+ if (v === undefined)
527
+ return '';
528
+ try {
529
+ return JSON.stringify(v, null, 2);
530
+ }
531
+ catch {
532
+ return String(v);
533
+ }
534
+ }
535
+ function prettify(key) {
536
+ return key
537
+ .replace(/[_-]+/g, ' ')
538
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
539
+ .replace(/\b\w/g, (c) => c.toUpperCase());
540
+ }
541
+ function orderKeys(keys, preferred) {
542
+ if (!preferred.length)
543
+ return keys;
544
+ const set = new Set(keys);
545
+ const head = preferred.filter((k) => set.has(k));
546
+ const tail = keys.filter((k) => !preferred.includes(k));
547
+ return [...head, ...tail];
548
+ }
549
+ /**
550
+ * Derive a fields[] list for `composite` / `repeater` from a JSON schema.
551
+ * Used when the form author hasn't explicitly enumerated sub-fields.
552
+ */
553
+ function derivePropertyNames(schema) {
554
+ const props = (schema?.properties ?? {});
555
+ return Object.keys(props);
556
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Register fallback schemas for every writable type. Idempotent — uses
3
+ * `registerMetadataResource` so any prior registration (e.g. a bespoke
4
+ * EditPage from PermissionMatrixEditor) is merged via the engine.
5
+ */
6
+ export declare function registerDefaultMetadataSchemas(): void;