@jxsuite/studio 0.37.0 → 1.0.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 (38) hide show
  1. package/dist/studio.js +77654 -76269
  2. package/dist/studio.js.map +128 -119
  3. package/package.json +44 -44
  4. package/src/account-status.ts +39 -0
  5. package/src/browse/browse.ts +10 -14
  6. package/src/canvas/iframe-host.ts +1 -1
  7. package/src/editor/context-menu.ts +1 -1
  8. package/src/editor/repeater-scope.ts +8 -13
  9. package/src/files/files.ts +3 -0
  10. package/src/format/format-host.ts +63 -1
  11. package/src/new-project/add-repo-modal.ts +183 -0
  12. package/src/new-project/new-project-modal.ts +22 -3
  13. package/src/page-params.ts +34 -8
  14. package/src/panels/ai-chat/chat-markdown.ts +2 -2
  15. package/src/panels/ai-panel.ts +61 -4
  16. package/src/panels/data-grid.ts +619 -0
  17. package/src/panels/signals-panel.ts +102 -437
  18. package/src/panels/statusbar.ts +1 -1
  19. package/src/panels/welcome-screen.ts +50 -0
  20. package/src/platform-errors.ts +30 -0
  21. package/src/platforms/cloud.ts +172 -89
  22. package/src/platforms/devserver.ts +172 -0
  23. package/src/services/context-resolver.ts +73 -0
  24. package/src/services/data-service.ts +155 -0
  25. package/src/services/monaco-setup.ts +75 -26
  26. package/src/settings/contributed-section.ts +406 -0
  27. package/src/settings/extension-sections.ts +145 -0
  28. package/src/settings/schema-field-ui.ts +4 -2
  29. package/src/settings/settings-modal.ts +101 -42
  30. package/src/site-context.ts +10 -1
  31. package/src/studio.ts +24 -0
  32. package/src/tabs/transact.ts +1 -1
  33. package/src/types.ts +120 -1
  34. package/src/ui/form-controls.ts +322 -0
  35. package/src/ui/progress-modal.ts +2 -2
  36. package/src/ui/schema-form.ts +524 -0
  37. package/src/utils/studio-utils.ts +4 -3
  38. package/src/settings/content-types-editor.ts +0 -599
@@ -7,9 +7,7 @@
7
7
 
8
8
  import { html, nothing } from "lit-html";
9
9
  import { classMap } from "lit-html/directives/class-map.js";
10
- import { ifDefined } from "lit-html/directives/if-defined.js";
11
10
  import { live } from "lit-html/directives/live.js";
12
- import { styleMap } from "lit-html/directives/style-map.js";
13
11
  import { isRef } from "@jxsuite/schema/guards";
14
12
  import { dynamicRouteParams } from "../page-params";
15
13
  import { projectState } from "../state";
@@ -26,6 +24,9 @@ import { renderFieldRow } from "../ui/field-row";
26
24
  import { rawTextArea, spTextField } from "../ui/field-input";
27
25
  import { expressionHint, renderExpressionEditor } from "../ui/expression-editor";
28
26
  import { renderMediaPicker } from "../ui/media-picker";
27
+ import { registerFormControl, renderForm } from "../ui/schema-form";
28
+ import { resolveContextPointer } from "../services/context-resolver";
29
+ import type { JsonSchema } from "../ui/schema-form";
29
30
  import type { TabUi } from "../tabs/tab";
30
31
  import type {
31
32
  CemEvent,
@@ -34,6 +35,7 @@ import type {
34
35
  JxStateDefinition,
35
36
  } from "@jxsuite/schema/types";
36
37
  import { fetchPluginSchema, pluginSchemaCache } from "../services/code-services";
38
+ import { getExtensions, loadExtensions } from "../format/format-host";
37
39
  import type { TemplateResult } from "lit-html";
38
40
 
39
41
  interface SignalsPanelState {
@@ -52,30 +54,6 @@ interface SignalsPanelCtx {
52
54
  updateSession: (patch: Record<string, unknown>) => void;
53
55
  }
54
56
 
55
- interface JsonSchema {
56
- type?: string;
57
- properties?: Record<string, SchemaProperty>;
58
- required?: string[];
59
- description?: string;
60
- }
61
-
62
- interface SchemaProperty {
63
- type?: string;
64
- enum?: string[];
65
- default?: unknown;
66
- format?: string;
67
- minimum?: number;
68
- maximum?: number;
69
- description?: string;
70
- examples?: string[];
71
- name?: string;
72
- items?: {
73
- type?: string;
74
- properties?: Record<string, Record<string, unknown>>;
75
- required?: string[];
76
- };
77
- }
78
-
79
57
  export interface SignalDef {
80
58
  $prototype?: string;
81
59
  $src?: string;
@@ -158,6 +136,29 @@ const STUDIO_RESERVED_KEYS = new Set([
158
136
 
159
137
  // ─── Signals / defs helpers ──────────────────────────────────────────────────
160
138
 
139
+ /**
140
+ * Extension-manifest state classes for the add-state picker: plain `$prototype` targets (no
141
+ * admission blocks) across the enabled extensions, each with its `$studio.stateDefaults` hint
142
+ * (specs/extensions.md §10). Manifest classes need no `$src` — the registry resolves them.
143
+ */
144
+ export function extensionStateClasses(): {
145
+ name: string;
146
+ stateDefaults?: Record<string, unknown>;
147
+ }[] {
148
+ const out: { name: string; stateDefaults?: Record<string, unknown> }[] = [];
149
+ for (const ext of getExtensions()) {
150
+ for (const cls of ext.classes ?? []) {
151
+ if (cls.state) {
152
+ out.push({
153
+ name: cls.name,
154
+ ...(cls.stateDefaults === undefined ? {} : { stateDefaults: cls.stateDefaults }),
155
+ });
156
+ }
157
+ }
158
+ }
159
+ return out;
160
+ }
161
+
161
162
  /**
162
163
  * View a state entry through the panel's flattened editing lens. Naked primitive and array entries
163
164
  * surface as an empty view — the renderers guard every field access.
@@ -415,6 +416,10 @@ export function renderSignalsTemplate(S: SignalsPanelState, ctx: SignalsPanelCtx
415
416
  const defs = S.document.state || {};
416
417
  const entries = Object.entries(defs);
417
418
 
419
+ // Warm the extensions payload so manifest state classes appear in the add picker (the panel
420
+ // Re-renders constantly; loadExtensions memoizes, so this is a one-time fetch per project).
421
+ void loadExtensions();
422
+
418
423
  // Group by category
419
424
  const groups = {
420
425
  computed: [],
@@ -504,6 +509,29 @@ export function renderSignalsTemplate(S: SignalsPanelState, ctx: SignalsPanelCtx
504
509
  return;
505
510
  }
506
511
 
512
+ // Extension-manifest state classes ("ext:Session"): no $src needed — the registry
513
+ // Resolves them; the descriptor's stateDefaults seed the def (e.g. timing "client").
514
+ if (type.startsWith("ext:")) {
515
+ const protoName = type.slice(4);
516
+ const cls = extensionStateClasses().find((c) => c.name === protoName);
517
+ let n = `$${protoName.charAt(0).toLowerCase()}${protoName.slice(1)}`;
518
+ let i = 1;
519
+ const base = n;
520
+ while (S.document.state && S.document.state[n]) {
521
+ n = base + i;
522
+ i += 1;
523
+ }
524
+ transactDoc(activeTab.value, (t) =>
525
+ mutateAddDef(t, n, {
526
+ $prototype: protoName,
527
+ ...cls?.stateDefaults,
528
+ } as Record<string, JsonValue>),
529
+ );
530
+ expandedSignal = n;
531
+ ctx.renderLeftPanel();
532
+ return;
533
+ }
534
+
507
535
  // Handle import-based prototypes (e.g., "import:ContentCollection")
508
536
  if (type.startsWith("import:")) {
509
537
  const protoName = type.slice(7);
@@ -576,6 +604,11 @@ export function renderSignalsTemplate(S: SignalsPanelState, ctx: SignalsPanelCtx
576
604
  projectState.projectConfig.imports,
577
605
  ).map((k: string) => html`<sp-menu-item value="import:${k}">${k}</sp-menu-item>`)}`
578
606
  : nothing}
607
+ ${extensionStateClasses().length > 0
608
+ ? html`<sp-menu-divider></sp-menu-divider>${extensionStateClasses().map(
609
+ (cls) => html`<sp-menu-item value="ext:${cls.name}">${cls.name}</sp-menu-item>`,
610
+ )}`
611
+ : nothing}
579
612
  <sp-menu-divider></sp-menu-divider>
580
613
  <sp-menu-item value="expression">Expression</sp-menu-item>
581
614
  <sp-menu-item value="function">Function</sp-menu-item>
@@ -1234,137 +1267,27 @@ function renderEmitsEditorTemplate(S: SignalsPanelState, name: string, def: Sign
1234
1267
  // ─── Plugin schema-driven form rendering ────────────────────────────────────
1235
1268
 
1236
1269
  /**
1237
- * Resolve a schema enum value. Handles: - Plain arrays (pass through) - `$ref` objects pointing to
1238
- * `#/$context/contentTypes` (resolves to project content type keys) - `$ref` objects pointing to
1239
- * `#/$context/contentTypes/{@param}/schema/properties` (dependent enum) - Legacy `"$contentTypes"`
1240
- * string sentinel (deprecated)
1241
- *
1242
- * @param {unknown} enumDef
1243
- * @param {Record<string, unknown>} [parentDef] - Parent def for resolving dependent refs
1244
- * @returns {string[] | undefined}
1245
- */
1246
- function resolveSchemaEnum(
1247
- enumDef: unknown,
1248
- parentDef?: Record<string, unknown>,
1249
- ): string[] | undefined {
1250
- if (Array.isArray(enumDef)) {
1251
- return enumDef;
1252
- }
1253
- if (enumDef && typeof enumDef === "object") {
1254
- const ref = (enumDef as Record<string, unknown>).$ref;
1255
- if (ref === "#/$context/contentTypes") {
1256
- return Object.keys(projectState?.projectConfig?.contentTypes ?? {});
1257
- }
1258
- if (typeof ref === "string" && ref.startsWith("#/$context/contentTypes/{@")) {
1259
- const match = ref.match(/#\/\$context\/contentTypes\/\{@(\w+)\}\/schema\/properties/);
1260
- if (match && parentDef) {
1261
- const [, paramName] = match;
1262
- const typeName = parentDef[paramName!] as string | undefined;
1263
- if (typeName) {
1264
- const ct = projectState?.projectConfig?.contentTypes?.[typeName] as
1265
- | Record<string, unknown>
1266
- | undefined;
1267
- const schema = ct?.schema as Record<string, unknown> | undefined;
1268
- const props = schema?.properties as Record<string, unknown> | undefined;
1269
- if (props) {
1270
- return Object.keys(props);
1271
- }
1272
- }
1273
- }
1274
- return undefined;
1275
- }
1276
- }
1277
- if (enumDef === "$contentTypes") {
1278
- return Object.keys(projectState?.projectConfig?.contentTypes ?? {});
1279
- }
1280
- return undefined;
1281
- }
1282
-
1283
- /**
1284
- * Render a single inline field within an array-of-objects row. Dispatches by schema type: enum →
1285
- * picker, boolean → switch, number → number-field, else → textfield.
1270
+ * Resolve a schema context pointer for signal config forms a thin wrapper over the generic
1271
+ * `resolveContextPointer` making the content-type roots always-present (a missing section resolves
1272
+ * to `{}` empty choices rather than a plain textfield): the parser's real descriptors point at
1273
+ * the `#/$context/content` root, while the deprecated `"$contentTypes"` string sentinel and the
1274
+ * legacy `#/$context/contentTypes` root keep resolving bit-for-bit for old class descriptors.
1286
1275
  *
1287
- * @param {string} key
1288
- * @param {Record<string, unknown>} schema
1289
- * @param {unknown} value
1290
- * @param {(val: unknown) => void} onChange
1291
- * @param {Record<string, unknown>} [parentDef] - Parent def for resolving dependent enum refs
1276
+ * @param {string} pointer
1277
+ * @param {Record<string, unknown>} [scope] - Parent def for `{@param}` substitution
1278
+ * @returns {unknown}
1292
1279
  */
1293
- /** Parse a numeric field value, returning NaN for blank input (so callers can treat it as unset). */
1294
- function parseNumericField(raw: string, integer: boolean): number {
1295
- if (raw.trim() === "") {
1296
- return Number.NaN;
1280
+ function resolveSignalsContextPointer(pointer: string, scope?: Record<string, unknown>): unknown {
1281
+ if (pointer === "#/$context/content") {
1282
+ return projectState?.projectConfig?.content ?? {};
1297
1283
  }
1298
- return integer ? Math.trunc(Number(raw)) : Number(raw);
1299
- }
1300
-
1301
- function renderInlineField(
1302
- key: string,
1303
- schema: Record<string, unknown>,
1304
- value: unknown,
1305
- onChange: (val: unknown) => void,
1306
- parentDef?: Record<string, unknown>,
1307
- ) {
1308
- if (isRef(value)) {
1309
- return html`<sp-textfield
1310
- size="s"
1311
- label=${key}
1312
- placeholder=${key}
1313
- .value=${live(value.$ref)}
1314
- @change=${(e: Event) => {
1315
- const v = (e.target as HTMLInputElement).value.trim();
1316
- onChange(v ? { $ref: v } : undefined);
1317
- }}
1318
- ></sp-textfield>`;
1284
+ if (pointer === "$contentTypes" || pointer === "#/$context/contentTypes") {
1285
+ return projectState?.projectConfig?.contentTypes ?? {};
1319
1286
  }
1320
- const enumValues = resolveSchemaEnum(schema.enum, parentDef);
1321
-
1322
- if (enumValues) {
1323
- return html`<sp-picker
1324
- size="s"
1325
- label=${key}
1326
- value=${value !== undefined ? String(value) : "__none__"}
1327
- @change=${(e: Event) =>
1328
- onChange(
1329
- (e.target as HTMLInputElement).value === "__none__"
1330
- ? undefined
1331
- : (e.target as HTMLInputElement).value,
1332
- )}
1333
- >
1334
- <sp-menu-item value="__none__">—</sp-menu-item>
1335
- ${enumValues.map((v: string) => html`<sp-menu-item value=${v}>${v}</sp-menu-item>`)}
1336
- </sp-picker>`;
1337
- }
1338
- if (schema.type === "boolean") {
1339
- return html`<sp-switch
1340
- size="s"
1341
- ?checked=${Boolean(value)}
1342
- @change=${(e: Event) => onChange((e.target as HTMLInputElement).checked)}
1343
- >${key}</sp-switch
1344
- >`;
1345
- }
1346
- if (schema.type === "integer" || schema.type === "number") {
1347
- return html`<sp-number-field
1348
- size="s"
1349
- label=${key}
1350
- .value=${value !== undefined ? value : nothing}
1351
- step=${schema.type === "integer" ? "1" : nothing}
1352
- @change=${(e: Event) => {
1353
- const parsed = parseNumericField(
1354
- (e.target as HTMLInputElement).value,
1355
- schema.type === "integer",
1356
- );
1357
- onChange(Number.isNaN(parsed) ? undefined : parsed);
1358
- }}
1359
- ></sp-number-field>`;
1360
- }
1361
- return html`<sp-textfield
1362
- size="s"
1363
- label=${key}
1364
- placeholder=${key}
1365
- .value=${value ?? ""}
1366
- @input=${(e: Event) => onChange((e.target as HTMLInputElement).value || undefined)}
1367
- ></sp-textfield>`;
1287
+ return resolveContextPointer(pointer, {
1288
+ projectConfig: (projectState?.projectConfig ?? {}) as Record<string, unknown>,
1289
+ ...(scope !== undefined && { scope }),
1290
+ });
1368
1291
  }
1369
1292
 
1370
1293
  /**
@@ -1424,39 +1347,22 @@ function renderBindingControl(opts: {
1424
1347
  `;
1425
1348
  }
1426
1349
 
1427
- /** Render a debounced multiline JSON text field for array/object schema properties. */
1428
- function renderJsonTextField(
1429
- currentValue: unknown,
1430
- ps: SchemaProperty,
1431
- name: string,
1432
- prop: string,
1433
- ) {
1434
- /** @type {ReturnType<typeof setTimeout> | undefined} */
1435
- let debounce: ReturnType<typeof setTimeout> | undefined;
1436
- return html`<sp-textfield
1437
- multiline
1438
- size="s"
1439
- style="min-height:40px"
1440
- .value=${currentValue !== undefined ? JSON.stringify(currentValue, null, 2) : ""}
1441
- placeholder=${ps.default !== undefined ? JSON.stringify(ps.default) : nothing}
1442
- @input=${(e: Event) => {
1443
- clearTimeout(debounce);
1444
- debounce = setTimeout(() => {
1445
- try {
1446
- transactDoc(activeTab.value, (t) =>
1447
- mutateUpdateDef(t, name, {
1448
- [prop]: JSON.parse((e.target as HTMLInputElement).value) as unknown,
1449
- }),
1450
- );
1451
- } catch {}
1452
- }, 500);
1453
- }}
1454
- ></sp-textfield>`;
1455
- }
1350
+ // The "binding" control registers here rather than in ui/form-controls.ts because it owns
1351
+ // Panel-local ephemeral UI state (bindingCustomOpen) and the route-param picker semantics.
1352
+ registerFormControl("binding", ({ key, value, onChange, ctx, rerender }) =>
1353
+ renderBindingControl({
1354
+ commit: onChange,
1355
+ fieldKey: `${ctx.fieldKeyPrefix ?? ""}.${key}`,
1356
+ params: ctx.params ?? [],
1357
+ refVal: isRef(value) ? value.$ref : "",
1358
+ rerender,
1359
+ }),
1360
+ );
1456
1361
 
1457
1362
  /**
1458
- * Render config form fields from a JSON Schema `properties` object. Maps schema types to
1459
- * appropriate form controls.
1363
+ * Render config form fields from a JSON Schema `properties` object a thin wrapper over the shared
1364
+ * schema-form engine. Skips studio-reserved keys, resolves enum/context refs against the project
1365
+ * config, and commits every patch through transactDoc/mutateUpdateDef.
1460
1366
  */
1461
1367
  export function renderSchemaFieldsTemplate(
1462
1368
  schema: JsonSchema | null | undefined,
@@ -1469,260 +1375,19 @@ export function renderSchemaFieldsTemplate(
1469
1375
  return nothing;
1470
1376
  }
1471
1377
 
1472
- const required = new Set(schema.required);
1473
- const params = dynamicRouteParams(S.documentPath);
1474
-
1475
- const propertyFields = Object.entries(schema.properties)
1476
- .filter(([prop]) => !STUDIO_RESERVED_KEYS.has(prop))
1477
- .map(([prop, ps]) => {
1478
- const currentValue = def[prop];
1479
- const labelText = prop + (required.has(prop) ? " *" : "");
1480
-
1481
- let control;
1482
- const enumValues = resolveSchemaEnum(ps.enum, def);
1483
- if (
1484
- isRef(currentValue) &&
1485
- ps.format !== "json-schema" &&
1486
- ps.type !== "object" &&
1487
- ps.type !== "array"
1488
- ) {
1489
- control = renderBindingControl({
1490
- refVal: currentValue.$ref,
1491
- params,
1492
- fieldKey: `${name}.${prop}`,
1493
- commit: (next) =>
1494
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { [prop]: next })),
1495
- rerender: ctx ? () => ctx.renderLeftPanel() : undefined,
1496
- });
1497
- } else if (enumValues) {
1498
- control = html`
1499
- <sp-picker
1500
- size="s"
1501
- value=${currentValue !== undefined
1502
- ? String(currentValue)
1503
- : ps.default !== undefined
1504
- ? String(ps.default)
1505
- : "__none__"}
1506
- @change=${(e: Event) =>
1507
- transactDoc(activeTab.value, (t) =>
1508
- mutateUpdateDef(t, name, {
1509
- [prop]:
1510
- (e.target as HTMLInputElement).value === "__none__"
1511
- ? undefined
1512
- : (e.target as HTMLInputElement).value,
1513
- }),
1514
- )}
1515
- >
1516
- ${!required.has(prop) ? html`<sp-menu-item value="__none__">—</sp-menu-item>` : nothing}
1517
- ${enumValues.map(
1518
- (val: string) => html`<sp-menu-item value=${val}>${val}</sp-menu-item>`,
1519
- )}
1520
- </sp-picker>
1521
- `;
1522
- } else if (ps.type === "boolean") {
1523
- control = html`<sp-checkbox
1524
- ?checked=${currentValue ?? ps.default ?? false}
1525
- @change=${(e: Event) =>
1526
- transactDoc(activeTab.value, (t) =>
1527
- mutateUpdateDef(t, name, {
1528
- [prop]: (e.target as HTMLInputElement).checked,
1529
- }),
1530
- )}
1531
- ></sp-checkbox>`;
1532
- } else if (ps.type === "integer" || ps.type === "number") {
1533
- /** @type {ReturnType<typeof setTimeout> | undefined} */
1534
- let debounce: ReturnType<typeof setTimeout> | undefined;
1535
- control = html`<sp-number-field
1536
- size="s"
1537
- min=${ifDefined(ps.minimum)}
1538
- max=${ifDefined(ps.maximum)}
1539
- step=${ps.type === "integer" ? "1" : nothing}
1540
- .value=${currentValue !== undefined ? currentValue : nothing}
1541
- placeholder=${ps.default != null ? String(ps.default) : nothing}
1542
- @change=${(e: Event) => {
1543
- clearTimeout(debounce);
1544
- debounce = setTimeout(() => {
1545
- const parsed = parseNumericField(
1546
- (e.target as HTMLInputElement).value,
1547
- ps.type === "integer",
1548
- );
1549
- transactDoc(activeTab.value, (t) =>
1550
- mutateUpdateDef(t, name, {
1551
- [prop]: Number.isNaN(parsed) ? undefined : parsed,
1552
- }),
1553
- );
1554
- }, 400);
1555
- }}
1556
- ></sp-number-field>`;
1557
- } else if (ps.format === "json-schema") {
1558
- const hasValue =
1559
- currentValue && typeof currentValue === "object" && Object.keys(currentValue).length > 0;
1560
- const cv = currentValue as Record<string, unknown>;
1561
- const isSchemaRef = hasValue && cv.$ref;
1562
- /** @type {ReturnType<typeof setTimeout> | undefined} */
1563
- let debounce: ReturnType<typeof setTimeout> | undefined;
1564
- control = html`
1565
- <div class="schema-param-editor">
1566
- ${hasValue && !isSchemaRef && cv.properties
1567
- ? html`
1568
- <div style="display:flex;flex-wrap:wrap;gap:3px;margin-bottom:4px">
1569
- ${Object.entries(cv.properties as Record<string, Record<string, unknown>>).map(
1570
- ([k, v]) => html`
1571
- <span
1572
- style="background:var(--bg);padding:1px 6px;border-radius:var(--radius);font-size:10px;color:var(--fg-dim)"
1573
- >${k}: ${v.type ?? "any"}</span
1574
- >
1575
- `,
1576
- )}
1577
- </div>
1578
- `
1579
- : nothing}
1580
- <sp-textfield
1581
- multiline
1582
- size="s"
1583
- style=${styleMap({
1584
- fontFamily: "monospace",
1585
- fontSize: "11px",
1586
- minHeight: hasValue ? "80px" : "40px",
1587
- })}
1588
- .value=${currentValue !== undefined ? JSON.stringify(currentValue, null, 2) : ""}
1589
- placeholder=${ps.description ?? "JSON Schema defining the data shape\u2026"}
1590
- @input=${(e: Event) => {
1591
- clearTimeout(debounce);
1592
- debounce = setTimeout(() => {
1593
- try {
1594
- transactDoc(activeTab.value, (t) =>
1595
- mutateUpdateDef(t, name, {
1596
- [prop]: JSON.parse((e.target as HTMLInputElement).value) as unknown,
1597
- }),
1598
- );
1599
- } catch {}
1600
- }, 500);
1601
- }}
1602
- ></sp-textfield>
1603
- </div>
1604
- `;
1605
- } else if (ps.type === "array" && ps.items?.type === "object" && ps.items?.properties) {
1606
- // Array of objects with defined schema → multi-row inline form
1607
- const rows: Record<string, unknown>[] = Array.isArray(currentValue)
1608
- ? (currentValue as Record<string, unknown>[])
1609
- : [];
1610
- const itemProps = ps.items.properties as Record<string, Record<string, unknown>>;
1611
- control = html`
1612
- <div class="array-object-field">
1613
- ${rows.map(
1614
- (row: Record<string, unknown>, idx: number) => html`
1615
- <div
1616
- class="array-object-row"
1617
- style="display:flex;gap:4px;align-items:center;margin-bottom:4px"
1618
- >
1619
- ${Object.entries(itemProps).map(([propKey, propSchema]) =>
1620
- renderInlineField(
1621
- propKey,
1622
- propSchema,
1623
- row[propKey],
1624
- (val) => {
1625
- const updated = [...rows];
1626
- updated[idx] = { ...updated[idx], [propKey]: val };
1627
- transactDoc(activeTab.value, (t) =>
1628
- mutateUpdateDef(t, name, { [prop]: updated }),
1629
- );
1630
- },
1631
- def,
1632
- ),
1633
- )}
1634
- <sp-action-button
1635
- quiet
1636
- size="s"
1637
- @click=${() => {
1638
- const updated = rows.filter((_: unknown, i: number) => i !== idx);
1639
- transactDoc(activeTab.value, (t) =>
1640
- mutateUpdateDef(t, name, {
1641
- [prop]: updated.length > 0 ? updated : undefined,
1642
- }),
1643
- );
1644
- ctx?.renderLeftPanel();
1645
- }}
1646
- >
1647
- <sp-icon-delete slot="icon"></sp-icon-delete>
1648
- </sp-action-button>
1649
- </div>
1650
- `,
1651
- )}
1652
- <sp-action-button
1653
- quiet
1654
- size="s"
1655
- @click=${(e: Event) => {
1656
- e.stopPropagation();
1657
- const newRow: Record<string, unknown> = {};
1658
- for (const [k, v] of Object.entries(itemProps)) {
1659
- if ((v as Record<string, unknown>).default !== undefined) {
1660
- newRow[k] = (v as Record<string, unknown>).default;
1661
- }
1662
- }
1663
- transactDoc(activeTab.value, (t) =>
1664
- mutateUpdateDef(t, name, { [prop]: [...rows, newRow] }),
1665
- );
1666
- ctx?.renderLeftPanel();
1667
- }}
1668
- >+ Add</sp-action-button
1669
- >
1670
- </div>
1671
- `;
1672
- } else if (ps.type === "array" || ps.type === "object") {
1673
- control = renderJsonTextField(currentValue, ps, name, prop);
1674
- } else {
1675
- /** @type {ReturnType<typeof setTimeout> | undefined} */
1676
- let debounce: ReturnType<typeof setTimeout> | undefined;
1677
- const ph = ps.default !== undefined ? String(ps.default) : (ps.examples?.[0] ?? "");
1678
- control = html`<div style="display:flex;gap:4px;align-items:center">
1679
- <sp-textfield
1680
- size="s"
1681
- style="flex:1"
1682
- .value=${currentValue ?? ""}
1683
- placeholder=${ph || nothing}
1684
- title=${ps.description || nothing}
1685
- @input=${(e: Event) => {
1686
- clearTimeout(debounce);
1687
- debounce = setTimeout(
1688
- () =>
1689
- transactDoc(activeTab.value, (t) =>
1690
- mutateUpdateDef(t, name, {
1691
- [prop]: (e.target as HTMLInputElement).value || undefined,
1692
- }),
1693
- ),
1694
- 400,
1695
- );
1696
- }}
1697
- ></sp-textfield>
1698
- ${params.length > 0
1699
- ? html`<sp-action-button
1700
- quiet
1701
- size="s"
1702
- title="Bind to route param"
1703
- @click=${() => {
1704
- transactDoc(activeTab.value, (t) =>
1705
- mutateUpdateDef(t, name, {
1706
- [prop]: { $ref: `#/$params/${params[0]}` },
1707
- }),
1708
- );
1709
- ctx?.renderLeftPanel();
1710
- }}
1711
- ><sp-icon-link slot="icon"></sp-icon-link
1712
- ></sp-action-button>`
1713
- : nothing}
1714
- </div>`;
1715
- }
1716
-
1717
- return renderFieldRow({
1718
- hasValue: false,
1719
- label: labelText,
1720
- prop: ps.name || prop,
1721
- widget: control,
1722
- });
1723
- });
1378
+ const properties = Object.fromEntries(
1379
+ Object.entries(schema.properties).filter(([prop]) => !STUDIO_RESERVED_KEYS.has(prop)),
1380
+ );
1724
1381
 
1725
- return html`${propertyFields}`;
1382
+ return renderForm({ ...schema, properties }, def as Record<string, unknown>, {
1383
+ context: {
1384
+ fieldKeyPrefix: name,
1385
+ params: dynamicRouteParams(S.documentPath),
1386
+ resolvePointer: resolveSignalsContextPointer,
1387
+ },
1388
+ onChange: (patch) => transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, patch)),
1389
+ ...(ctx && { rerender: () => ctx.renderLeftPanel() }),
1390
+ });
1726
1391
  }
1727
1392
 
1728
1393
  /**
@@ -75,7 +75,7 @@ export function renderStatusbar() {
75
75
  // Step width varies. Emitting a crumb per node keeps the array pseudo-element ("Repeater") and
76
76
  // Its template both visible instead of collapsing the array into a bare `[index]`.
77
77
  const pathSegments = [];
78
- for (let i = 0; i < sel.length; ) {
78
+ for (let i = 0; i < sel.length;) {
79
79
  const seg = sel[i];
80
80
  const step = seg === "map" ? 1 : 2;
81
81
  const subPath = sel.slice(0, i + step);