@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.
- package/dist/studio.js +77654 -76269
- package/dist/studio.js.map +128 -119
- package/package.json +44 -44
- package/src/account-status.ts +39 -0
- package/src/browse/browse.ts +10 -14
- package/src/canvas/iframe-host.ts +1 -1
- package/src/editor/context-menu.ts +1 -1
- package/src/editor/repeater-scope.ts +8 -13
- package/src/files/files.ts +3 -0
- package/src/format/format-host.ts +63 -1
- package/src/new-project/add-repo-modal.ts +183 -0
- package/src/new-project/new-project-modal.ts +22 -3
- package/src/page-params.ts +34 -8
- package/src/panels/ai-chat/chat-markdown.ts +2 -2
- package/src/panels/ai-panel.ts +61 -4
- package/src/panels/data-grid.ts +619 -0
- package/src/panels/signals-panel.ts +102 -437
- package/src/panels/statusbar.ts +1 -1
- package/src/panels/welcome-screen.ts +50 -0
- package/src/platform-errors.ts +30 -0
- package/src/platforms/cloud.ts +172 -89
- package/src/platforms/devserver.ts +172 -0
- package/src/services/context-resolver.ts +73 -0
- package/src/services/data-service.ts +155 -0
- package/src/services/monaco-setup.ts +75 -26
- package/src/settings/contributed-section.ts +406 -0
- package/src/settings/extension-sections.ts +145 -0
- package/src/settings/schema-field-ui.ts +4 -2
- package/src/settings/settings-modal.ts +101 -42
- package/src/site-context.ts +10 -1
- package/src/studio.ts +24 -0
- package/src/tabs/transact.ts +1 -1
- package/src/types.ts +120 -1
- package/src/ui/form-controls.ts +322 -0
- package/src/ui/progress-modal.ts +2 -2
- package/src/ui/schema-form.ts +524 -0
- package/src/utils/studio-utils.ts +4 -3
- 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
|
|
1238
|
-
*
|
|
1239
|
-
*
|
|
1240
|
-
* string sentinel
|
|
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}
|
|
1288
|
-
* @param {Record<string, unknown>}
|
|
1289
|
-
* @
|
|
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
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
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
|
|
1459
|
-
*
|
|
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
|
|
1473
|
-
|
|
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
|
|
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
|
/**
|
package/src/panels/statusbar.ts
CHANGED
|
@@ -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);
|