@object-ui/app-shell 6.1.0 → 6.2.1
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/CHANGELOG.md +129 -0
- package/README.md +10 -1
- package/dist/console/AppContent.js +53 -2
- package/dist/console/ai/AiChatPage.d.ts +8 -0
- package/dist/console/ai/AiChatPage.js +188 -0
- package/dist/console/ai/ConversationsSidebar.d.ts +7 -0
- package/dist/console/ai/ConversationsSidebar.js +111 -0
- package/dist/console/auth/LoginPage.js +19 -2
- package/dist/console/auth/RegisterPage.js +30 -1
- package/dist/console/marketplace/MarketplaceAccessDenied.js +3 -1
- package/dist/console/marketplace/MarketplaceInstalledPage.js +11 -3
- package/dist/console/marketplace/MarketplacePackagePage.js +57 -19
- package/dist/console/marketplace/MarketplacePage.js +55 -18
- package/dist/console/marketplace/marketplaceApi.d.ts +20 -0
- package/dist/console/marketplace/usePackageL10n.d.ts +38 -0
- package/dist/console/marketplace/usePackageL10n.js +110 -0
- package/dist/console/organizations/CreateWorkspaceDialog.js +29 -1
- package/dist/console/organizations/OrganizationsPage.js +24 -3
- package/dist/context/FavoritesProvider.d.ts +40 -2
- package/dist/context/FavoritesProvider.js +201 -20
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useChatConversation.d.ts +7 -0
- package/dist/hooks/useChatConversation.js +37 -5
- package/dist/hooks/useConversationList.d.ts +25 -0
- package/dist/hooks/useConversationList.js +131 -0
- package/dist/hooks/useNavPins.d.ts +11 -4
- package/dist/hooks/useNavPins.js +104 -53
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/layout/AppHeader.js +2 -2
- package/dist/layout/AppSidebar.js +20 -1
- package/dist/layout/UnifiedSidebar.js +1 -1
- package/dist/providers/ExpressionProvider.d.ts +11 -1
- package/dist/providers/ExpressionProvider.js +11 -6
- package/dist/services/builtinComponents.d.ts +1 -0
- package/dist/services/builtinComponents.js +169 -0
- package/dist/services/componentRegistry.d.ts +63 -0
- package/dist/services/componentRegistry.js +36 -0
- package/dist/views/ComponentNavView.d.ts +6 -0
- package/dist/views/ComponentNavView.js +26 -0
- package/dist/views/RecordDetailView.js +66 -6
- package/dist/views/RecordFormPage.js +15 -3
- package/dist/views/SearchResultsPage.js +4 -0
- package/dist/views/metadata-admin/DesignerEditorWrapper.d.ts +68 -0
- package/dist/views/metadata-admin/DesignerEditorWrapper.js +158 -0
- package/dist/views/metadata-admin/DirectoryPage.d.ts +1 -0
- package/dist/views/metadata-admin/DirectoryPage.js +135 -0
- package/dist/views/metadata-admin/LayeredDiff.d.ts +6 -0
- package/dist/views/metadata-admin/LayeredDiff.js +26 -0
- package/dist/views/metadata-admin/MetadataDetailDrawer.d.ts +13 -0
- package/dist/views/metadata-admin/MetadataDetailDrawer.js +52 -0
- package/dist/views/metadata-admin/PageShell.d.ts +34 -0
- package/dist/views/metadata-admin/PageShell.js +40 -0
- package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +5 -0
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +288 -0
- package/dist/views/metadata-admin/QuickFind.d.ts +5 -0
- package/dist/views/metadata-admin/QuickFind.js +152 -0
- package/dist/views/metadata-admin/RelatedPanel.d.ts +33 -0
- package/dist/views/metadata-admin/RelatedPanel.js +171 -0
- package/dist/views/metadata-admin/ResourceEditPage.d.ts +13 -0
- package/dist/views/metadata-admin/ResourceEditPage.js +302 -0
- package/dist/views/metadata-admin/ResourceHistoryPage.d.ts +5 -0
- package/dist/views/metadata-admin/ResourceHistoryPage.js +100 -0
- package/dist/views/metadata-admin/ResourceListPage.d.ts +4 -0
- package/dist/views/metadata-admin/ResourceListPage.js +146 -0
- package/dist/views/metadata-admin/ResourceRouter.d.ts +10 -0
- package/dist/views/metadata-admin/ResourceRouter.js +47 -0
- package/dist/views/metadata-admin/SchemaForm.d.ts +99 -0
- package/dist/views/metadata-admin/SchemaForm.js +565 -0
- package/dist/views/metadata-admin/anchors.d.ts +1 -0
- package/dist/views/metadata-admin/anchors.js +229 -0
- package/dist/views/metadata-admin/default-schemas.d.ts +6 -0
- package/dist/views/metadata-admin/default-schemas.js +207 -0
- package/dist/views/metadata-admin/i18n.d.ts +33 -0
- package/dist/views/metadata-admin/i18n.js +303 -0
- package/dist/views/metadata-admin/index.d.ts +33 -0
- package/dist/views/metadata-admin/index.js +39 -0
- package/dist/views/metadata-admin/predicate.d.ts +31 -0
- package/dist/views/metadata-admin/predicate.js +150 -0
- package/dist/views/metadata-admin/registry.d.ts +232 -0
- package/dist/views/metadata-admin/registry.js +106 -0
- package/dist/views/metadata-admin/useMetadata.d.ts +37 -0
- package/dist/views/metadata-admin/useMetadata.js +96 -0
- package/dist/views/metadata-admin/widgets.d.ts +68 -0
- package/dist/views/metadata-admin/widgets.js +287 -0
- package/package.json +27 -26
|
@@ -0,0 +1,565 @@
|
|
|
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
|
+
//
|
|
325
|
+
// We must also honor `widget === 'switch'` (resolved by inferWidget from
|
|
326
|
+
// `fieldSpec.type === 'boolean'` / `'toggle'`), because for composite
|
|
327
|
+
// sub-fields the JSON schema fragment is often `{}` — the parent declares
|
|
328
|
+
// `additionalProperties: true` and no per-property `properties`, so
|
|
329
|
+
// `schema?.type` is undefined even though the form spec clearly marks
|
|
330
|
+
// the sub-field as boolean. Without this, capability toggles inside the
|
|
331
|
+
// Object editor's "Capabilities" section fell through to RawJsonEditor
|
|
332
|
+
// and rendered as empty textareas.
|
|
333
|
+
if (schema?.type === 'boolean' || widget === 'switch' || fieldSpec?.type === 'boolean' || fieldSpec?.type === 'toggle') {
|
|
334
|
+
return (_jsx(Switch, { id: id, checked: !!value, onCheckedChange: (c) => onChange(c), disabled: readOnly }));
|
|
335
|
+
}
|
|
336
|
+
// Number / integer → numeric input with min/max from fieldSpec.
|
|
337
|
+
if (schema?.type === 'number' || schema?.type === 'integer') {
|
|
338
|
+
const min = fieldSpec?.min;
|
|
339
|
+
const max = fieldSpec?.max;
|
|
340
|
+
return (_jsx(Input, { id: id, type: "number", value: value == null ? '' : String(value), min: min, max: max, onChange: (e) => {
|
|
341
|
+
const raw = e.target.value;
|
|
342
|
+
if (raw === '')
|
|
343
|
+
return onChange(undefined);
|
|
344
|
+
const n = schema.type === 'integer' ? parseInt(raw, 10) : Number(raw);
|
|
345
|
+
onChange(Number.isFinite(n) ? n : undefined);
|
|
346
|
+
}, readOnly: readOnly }));
|
|
347
|
+
}
|
|
348
|
+
// String → Input (or Textarea if it looks long), with maxLength from fieldSpec.
|
|
349
|
+
if (schema?.type === 'string') {
|
|
350
|
+
const maxLength = fieldSpec?.maxLength;
|
|
351
|
+
const long = schema?.format === 'multiline' ||
|
|
352
|
+
schema?.contentMediaType === 'text/markdown' ||
|
|
353
|
+
(typeof value === 'string' && value.length > 80);
|
|
354
|
+
if (long) {
|
|
355
|
+
return (_jsx(Textarea, { id: id, rows: 4, value: value ?? '', maxLength: maxLength, onChange: (e) => onChange(e.target.value || undefined), readOnly: readOnly }));
|
|
356
|
+
}
|
|
357
|
+
return (_jsx(Input, { id: id, value: value ?? '', maxLength: maxLength, onChange: (e) => onChange(e.target.value || undefined), readOnly: readOnly }));
|
|
358
|
+
}
|
|
359
|
+
// Array of primitives → comma-separated tag editor (MVP).
|
|
360
|
+
if (schema?.type === 'array') {
|
|
361
|
+
const itemsSchema = schema?.items ?? {};
|
|
362
|
+
const isPrimitive = itemsSchema.type === 'string' ||
|
|
363
|
+
itemsSchema.type === 'number' ||
|
|
364
|
+
itemsSchema.type === 'integer';
|
|
365
|
+
if (isPrimitive) {
|
|
366
|
+
const arr = Array.isArray(value) ? value : [];
|
|
367
|
+
return (_jsx(Input, { id: id, value: arr.map(String).join(', '), placeholder: "comma, separated, values", onChange: (e) => {
|
|
368
|
+
const raw = e.target.value;
|
|
369
|
+
const parts = raw
|
|
370
|
+
.split(',')
|
|
371
|
+
.map((s) => s.trim())
|
|
372
|
+
.filter(Boolean);
|
|
373
|
+
if (itemsSchema.type === 'number' || itemsSchema.type === 'integer') {
|
|
374
|
+
onChange(parts.map((p) => Number(p)).filter((n) => Number.isFinite(n)));
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
onChange(parts);
|
|
378
|
+
}
|
|
379
|
+
}, readOnly: readOnly }));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// Object / complex → JSON fallback so admins can still edit.
|
|
383
|
+
return _jsx(RawJsonEditor, { value: value, onChange: onChange, readOnly: readOnly, small: true });
|
|
384
|
+
}
|
|
385
|
+
/* ----- composite / repeater (embedded structured values) ----------------- */
|
|
386
|
+
/**
|
|
387
|
+
* Resolve the JSONSchema fragment for a sub-field of a composite/repeater.
|
|
388
|
+
* Looks under parent `schema.properties[subName]` (composite) or
|
|
389
|
+
* `schema.items.properties[subName]` (repeater). Falls back to `{}`.
|
|
390
|
+
*/
|
|
391
|
+
function pickSubSchema(parent, kind, subName) {
|
|
392
|
+
if (!parent)
|
|
393
|
+
return {};
|
|
394
|
+
const props = kind === 'composite'
|
|
395
|
+
? parent.properties
|
|
396
|
+
: parent.items?.properties;
|
|
397
|
+
return props?.[subName] ?? {};
|
|
398
|
+
}
|
|
399
|
+
function CompositeField({ value, fields, schema, readOnly, widgetContext, onChange, }) {
|
|
400
|
+
const obj = (value && typeof value === 'object' && !Array.isArray(value))
|
|
401
|
+
? value
|
|
402
|
+
: {};
|
|
403
|
+
const specs = fields.map(normaliseField);
|
|
404
|
+
return (_jsx("div", { className: "rounded-md border border-border/50 bg-muted/20 p-3 space-y-3", children: specs.map((spec) => {
|
|
405
|
+
const subSchema = pickSubSchema(schema, 'composite', spec.field);
|
|
406
|
+
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));
|
|
407
|
+
}) }));
|
|
408
|
+
}
|
|
409
|
+
function RepeaterField({ value, fields, schema, readOnly, widgetContext, widget, onChange, }) {
|
|
410
|
+
const rows = Array.isArray(value) ? value : [];
|
|
411
|
+
const specs = fields.map(normaliseField);
|
|
412
|
+
const [openIdx, setOpenIdx] = React.useState(null);
|
|
413
|
+
// Default to card layout (one fieldset per row). `widget: 'grid'` opts
|
|
414
|
+
// into compact inline-table layout for short, atomic sub-fields.
|
|
415
|
+
const useGrid = widget === 'grid' || widget === 'table';
|
|
416
|
+
const update = (i, patch) => {
|
|
417
|
+
const next = rows.map((r, idx) => (idx === i ? { ...r, ...patch } : r));
|
|
418
|
+
onChange(next);
|
|
419
|
+
};
|
|
420
|
+
const remove = (i) => onChange(rows.filter((_, idx) => idx !== i));
|
|
421
|
+
const add = () => {
|
|
422
|
+
const blank = {};
|
|
423
|
+
specs.forEach((s) => { blank[s.field] = undefined; });
|
|
424
|
+
onChange([...rows, blank]);
|
|
425
|
+
setOpenIdx(rows.length);
|
|
426
|
+
};
|
|
427
|
+
if (useGrid) {
|
|
428
|
+
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) => {
|
|
429
|
+
const sub = pickSubSchema(schema, 'repeater', s.field);
|
|
430
|
+
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));
|
|
431
|
+
}), !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"] }))] }));
|
|
432
|
+
}
|
|
433
|
+
// Card layout — one collapsible fieldset per row.
|
|
434
|
+
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) => {
|
|
435
|
+
const isOpen = openIdx === idx;
|
|
436
|
+
const summary = specs
|
|
437
|
+
.map((s) => row?.[s.field])
|
|
438
|
+
.find((v) => v != null && v !== '');
|
|
439
|
+
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) => {
|
|
440
|
+
const sub = pickSubSchema(schema, 'repeater', s.field);
|
|
441
|
+
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));
|
|
442
|
+
}) }))] }, idx));
|
|
443
|
+
}), !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"] }))] }));
|
|
444
|
+
}
|
|
445
|
+
/* ----- raw JSON fallback -------------------------------------------------- */
|
|
446
|
+
function RawJsonEditor({ value, onChange, readOnly, small, }) {
|
|
447
|
+
const [text, setText] = React.useState(() => safeStringify(value));
|
|
448
|
+
const [error, setError] = React.useState(null);
|
|
449
|
+
// Re-sync when external value changes (e.g. Reset Overlay).
|
|
450
|
+
React.useEffect(() => {
|
|
451
|
+
setText(safeStringify(value));
|
|
452
|
+
setError(null);
|
|
453
|
+
}, [JSON.stringify(value)]); // intentional: stringify-deep-equal
|
|
454
|
+
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) => {
|
|
455
|
+
const next = e.target.value;
|
|
456
|
+
setText(next);
|
|
457
|
+
if (!next.trim()) {
|
|
458
|
+
setError(null);
|
|
459
|
+
onChange(undefined);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
const parsed = JSON.parse(next);
|
|
464
|
+
setError(null);
|
|
465
|
+
onChange(parsed);
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
setError(err?.message ?? 'Invalid JSON');
|
|
469
|
+
}
|
|
470
|
+
} }), error && _jsx("div", { className: "text-xs text-destructive", children: error })] }));
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Synthesize a minimal JSON Schema by introspecting a runtime value.
|
|
474
|
+
*
|
|
475
|
+
* Used as the fallback when the framework hasn't shipped a Zod schema
|
|
476
|
+
* for a metadata type (e.g. `hook`, `trigger`, `validation`). The
|
|
477
|
+
* resulting schema lets `SchemaForm` render a real labelled form
|
|
478
|
+
* (respecting `readOnly`) instead of bailing out to a raw JSON dump.
|
|
479
|
+
*
|
|
480
|
+
* Types are guessed conservatively from the value: scalars become
|
|
481
|
+
* `string` / `number` / `boolean`; arrays of strings become string
|
|
482
|
+
* tags; arrays of objects become master-detail tables; objects become
|
|
483
|
+
* nested JSON regions. Anything indeterminate falls back to `string`
|
|
484
|
+
* so the field still renders.
|
|
485
|
+
*/
|
|
486
|
+
function inferSchemaFromValue(value) {
|
|
487
|
+
const properties = {};
|
|
488
|
+
for (const [k, v] of Object.entries(value)) {
|
|
489
|
+
if (k.startsWith('_'))
|
|
490
|
+
continue;
|
|
491
|
+
if (v === null || v === undefined) {
|
|
492
|
+
properties[k] = { type: 'string' };
|
|
493
|
+
}
|
|
494
|
+
else if (typeof v === 'string') {
|
|
495
|
+
properties[k] = v.length > 80 || v.includes('\n')
|
|
496
|
+
? { type: 'string', format: 'multiline' }
|
|
497
|
+
: { type: 'string' };
|
|
498
|
+
}
|
|
499
|
+
else if (typeof v === 'number') {
|
|
500
|
+
properties[k] = { type: Number.isInteger(v) ? 'integer' : 'number' };
|
|
501
|
+
}
|
|
502
|
+
else if (typeof v === 'boolean') {
|
|
503
|
+
properties[k] = { type: 'boolean' };
|
|
504
|
+
}
|
|
505
|
+
else if (Array.isArray(v)) {
|
|
506
|
+
if (v.length > 0 && typeof v[0] === 'string') {
|
|
507
|
+
properties[k] = { type: 'array', items: { type: 'string' } };
|
|
508
|
+
}
|
|
509
|
+
else if (v.length > 0 && typeof v[0] === 'object' && v[0] !== null) {
|
|
510
|
+
const sample = v[0];
|
|
511
|
+
const itemProps = {};
|
|
512
|
+
for (const key of Object.keys(sample)) {
|
|
513
|
+
itemProps[key] = { type: 'string' };
|
|
514
|
+
}
|
|
515
|
+
properties[k] = {
|
|
516
|
+
type: 'array',
|
|
517
|
+
items: { type: 'object', properties: itemProps },
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
properties[k] = { type: 'array', items: { type: 'string' } };
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
else if (typeof v === 'object') {
|
|
525
|
+
properties[k] = { type: 'object', additionalProperties: true };
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
properties[k] = { type: 'string' };
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return { type: 'object', properties, additionalProperties: true };
|
|
532
|
+
}
|
|
533
|
+
/* ----- helpers ------------------------------------------------------------ */
|
|
534
|
+
function safeStringify(v) {
|
|
535
|
+
if (v === undefined)
|
|
536
|
+
return '';
|
|
537
|
+
try {
|
|
538
|
+
return JSON.stringify(v, null, 2);
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
return String(v);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function prettify(key) {
|
|
545
|
+
return key
|
|
546
|
+
.replace(/[_-]+/g, ' ')
|
|
547
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
548
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
549
|
+
}
|
|
550
|
+
function orderKeys(keys, preferred) {
|
|
551
|
+
if (!preferred.length)
|
|
552
|
+
return keys;
|
|
553
|
+
const set = new Set(keys);
|
|
554
|
+
const head = preferred.filter((k) => set.has(k));
|
|
555
|
+
const tail = keys.filter((k) => !preferred.includes(k));
|
|
556
|
+
return [...head, ...tail];
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Derive a fields[] list for `composite` / `repeater` from a JSON schema.
|
|
560
|
+
* Used when the form author hasn't explicitly enumerated sub-fields.
|
|
561
|
+
*/
|
|
562
|
+
function derivePropertyNames(schema) {
|
|
563
|
+
const props = (schema?.properties ?? {});
|
|
564
|
+
return Object.keys(props);
|
|
565
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function registerBuiltinAnchors(): void;
|