@jxsuite/studio 0.0.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/dist/studio.css +3676 -0
- package/dist/studio.js +188743 -0
- package/dist/studio.js.map +1448 -0
- package/package.json +67 -0
- package/src/editor/context-menu.js +144 -0
- package/src/editor/inline-edit.js +597 -0
- package/src/editor/inline-format.js +572 -0
- package/src/editor/shortcuts.js +275 -0
- package/src/editor/slash-menu.js +167 -0
- package/src/files/components.js +40 -0
- package/src/files/file-ops.js +195 -0
- package/src/files/files.js +569 -0
- package/src/markdown/md-allowlist.js +101 -0
- package/src/markdown/md-convert.js +491 -0
- package/src/panels/activity-bar.js +69 -0
- package/src/panels/data-explorer.js +181 -0
- package/src/panels/events-panel.js +235 -0
- package/src/panels/imports-panel.js +427 -0
- package/src/panels/signals-panel.js +1093 -0
- package/src/panels/statusbar.js +56 -0
- package/src/platform.js +31 -0
- package/src/platforms/devserver.js +293 -0
- package/src/services/cem-export.js +130 -0
- package/src/services/code-services.js +98 -0
- package/src/site-context.js +122 -0
- package/src/state.js +744 -0
- package/src/store.js +332 -0
- package/src/studio.js +7692 -0
- package/src/ui/icons.js +83 -0
- package/src/ui/jx-styled-combobox.js +142 -0
- package/src/ui/spectrum.js +238 -0
- package/src/utils/studio-utils.js +185 -0
|
@@ -0,0 +1,1093 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signals panel — signal/def helpers, signals template, CEM editors, plugin schema forms.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from studio.js to reduce file size.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { html, nothing } from "lit-html";
|
|
8
|
+
import { addDef, removeDef, updateDef, renameDef, update } from "../store.js";
|
|
9
|
+
import { fetchPluginSchema, pluginSchemaCache } from "../services/code-services.js";
|
|
10
|
+
|
|
11
|
+
// ─── Module-local state ─────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/** Expanded signal editor state (persists across renders). */
|
|
14
|
+
/** @type {any} */
|
|
15
|
+
let expandedSignal = null;
|
|
16
|
+
|
|
17
|
+
/** Track which functions have the advanced param editor open. */
|
|
18
|
+
const advancedParamOpen = new Set();
|
|
19
|
+
|
|
20
|
+
/** Default templates for creating new signal definitions. */
|
|
21
|
+
const DEF_TEMPLATES = /** @type {Record<string, any>} */ ({
|
|
22
|
+
state: { type: "string", default: "" },
|
|
23
|
+
computed: { $compute: "", $deps: [] },
|
|
24
|
+
request: { $prototype: "Request", url: "", method: "GET", timing: "client" },
|
|
25
|
+
localStorage: { $prototype: "LocalStorage", key: "", default: null },
|
|
26
|
+
sessionStorage: { $prototype: "SessionStorage", key: "", default: null },
|
|
27
|
+
indexedDB: { $prototype: "IndexedDB", database: "", store: "", version: 1 },
|
|
28
|
+
cookie: { $prototype: "Cookie", name: "", default: "" },
|
|
29
|
+
set: { $prototype: "Set", default: [] },
|
|
30
|
+
map: { $prototype: "Map", default: {} },
|
|
31
|
+
formData: { $prototype: "FormData", fields: {} },
|
|
32
|
+
function: { $prototype: "Function", body: "", parameters: [] },
|
|
33
|
+
external: { $prototype: "", $src: "" },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/** Keys handled by the framework — skip when rendering schema fields. */
|
|
37
|
+
const STUDIO_RESERVED_KEYS = new Set([
|
|
38
|
+
"$prototype",
|
|
39
|
+
"$src",
|
|
40
|
+
"$export",
|
|
41
|
+
"timing",
|
|
42
|
+
"default",
|
|
43
|
+
"description",
|
|
44
|
+
"body",
|
|
45
|
+
"parameters",
|
|
46
|
+
"name",
|
|
47
|
+
"attribute",
|
|
48
|
+
"reflects",
|
|
49
|
+
"deprecated",
|
|
50
|
+
"emits",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
// ─── Signals / defs helpers ──────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Classify a state entry into a category string.
|
|
57
|
+
*
|
|
58
|
+
* @param {any} def
|
|
59
|
+
*/
|
|
60
|
+
export function defCategory(def) {
|
|
61
|
+
if (!def) return "state";
|
|
62
|
+
if (def.$handler || def.$prototype === "Function") return "function";
|
|
63
|
+
if (def.$compute) return "computed";
|
|
64
|
+
if (def.$prototype) return "data";
|
|
65
|
+
return "state";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Badge label for a def category.
|
|
70
|
+
*
|
|
71
|
+
* @param {any} def
|
|
72
|
+
*/
|
|
73
|
+
export function defBadgeLabel(def) {
|
|
74
|
+
if (!def) return "S";
|
|
75
|
+
if (def.$handler || def.$prototype === "Function") return "F";
|
|
76
|
+
if (def.$compute) return "C";
|
|
77
|
+
if (def.$prototype) return def.$prototype.charAt(0);
|
|
78
|
+
return "S";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Hint text for a signal row.
|
|
83
|
+
*
|
|
84
|
+
* @param {any} name
|
|
85
|
+
* @param {any} def
|
|
86
|
+
*/
|
|
87
|
+
export function defHint(name, def) {
|
|
88
|
+
if (!def) return "";
|
|
89
|
+
if (def.$prototype === "Function") {
|
|
90
|
+
if (def.body) return def.body.length > 20 ? def.body.slice(0, 20) + "..." : def.body;
|
|
91
|
+
if (def.$src) return def.$src;
|
|
92
|
+
return "function";
|
|
93
|
+
}
|
|
94
|
+
if (def.$handler) return "handler (legacy)";
|
|
95
|
+
if (def.$compute)
|
|
96
|
+
return "=" + (def.$compute.length > 20 ? def.$compute.slice(0, 20) + "..." : def.$compute);
|
|
97
|
+
if (def.$prototype === "Request") return def.method + " " + (def.url || "").slice(0, 20);
|
|
98
|
+
if (def.$prototype === "LocalStorage" || def.$prototype === "SessionStorage")
|
|
99
|
+
return def.key || "";
|
|
100
|
+
if (def.$prototype === "IndexedDB") return def.database || "";
|
|
101
|
+
if (def.$prototype === "Cookie") return def.name || "";
|
|
102
|
+
if (def.$prototype) return def.$prototype;
|
|
103
|
+
if (def.attribute) return `[${def.attribute}] ${def.type || ""}`;
|
|
104
|
+
return def.type || "";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether the current document defines a custom element (hyphenated tagName).
|
|
109
|
+
*
|
|
110
|
+
* @param {any} S
|
|
111
|
+
*/
|
|
112
|
+
export function isCustomElementDoc(S) {
|
|
113
|
+
return (S.document.tagName || "").includes("-");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Recursively collect CSS `part` attributes from the document tree.
|
|
118
|
+
*
|
|
119
|
+
* @param {any} node
|
|
120
|
+
* @param {any[]} [parts]
|
|
121
|
+
*/
|
|
122
|
+
export function collectCssParts(node, parts = []) {
|
|
123
|
+
if (node?.attributes?.part)
|
|
124
|
+
parts.push({ name: node.attributes.part, tag: node.tagName || "div" });
|
|
125
|
+
if (Array.isArray(node?.children))
|
|
126
|
+
node.children.forEach((/** @type {any} */ c) => collectCssParts(c, parts));
|
|
127
|
+
return parts;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resolve a $ref value to a display string using signal defaults. Used by the canvas to show real
|
|
132
|
+
* values instead of raw refs.
|
|
133
|
+
*
|
|
134
|
+
* @param {any} value
|
|
135
|
+
* @param {any} defs
|
|
136
|
+
*/
|
|
137
|
+
export function resolveDefaultForCanvas(value, defs) {
|
|
138
|
+
if (!value || typeof value !== "object" || !value.$ref) return value;
|
|
139
|
+
const ref = value.$ref;
|
|
140
|
+
/** @type {any} */
|
|
141
|
+
let defName;
|
|
142
|
+
if (ref.startsWith("#/state/")) defName = ref.slice(8);
|
|
143
|
+
else if (ref.startsWith("$")) defName = ref;
|
|
144
|
+
else return `{${ref}}`;
|
|
145
|
+
|
|
146
|
+
const def = defs?.[defName];
|
|
147
|
+
if (!def) return `{${defName}}`;
|
|
148
|
+
|
|
149
|
+
// State signal → use default
|
|
150
|
+
if (!def.$compute && !def.$prototype) {
|
|
151
|
+
if (def.default !== undefined && def.default !== null) {
|
|
152
|
+
if (typeof def.default === "object") return JSON.stringify(def.default);
|
|
153
|
+
return String(def.default);
|
|
154
|
+
}
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
// Computed → expression indicator
|
|
158
|
+
if (def.$compute) return `\u0192(${defName})`;
|
|
159
|
+
// Request → URL hint
|
|
160
|
+
if (def.$prototype === "Request") return `\u27F3 ${def.url || "fetch"}`;
|
|
161
|
+
// Storage → use default or key
|
|
162
|
+
if (def.$prototype === "LocalStorage" || def.$prototype === "SessionStorage") {
|
|
163
|
+
if (def.default !== undefined && def.default !== null) {
|
|
164
|
+
if (typeof def.default === "object") return JSON.stringify(def.default);
|
|
165
|
+
return String(def.default);
|
|
166
|
+
}
|
|
167
|
+
return `[${def.key || "storage"}]`;
|
|
168
|
+
}
|
|
169
|
+
if (def.$prototype) return `{${def.$prototype}}`;
|
|
170
|
+
return `{${defName}}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── Simple field row ────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/** Simple field row for signal editors — vertical stacked layout. */
|
|
176
|
+
export function signalFieldRow(
|
|
177
|
+
/** @type {any} */ label,
|
|
178
|
+
/** @type {any} */ value,
|
|
179
|
+
/** @type {any} */ onChange,
|
|
180
|
+
) {
|
|
181
|
+
/** @type {any} */
|
|
182
|
+
let debounce;
|
|
183
|
+
return html`
|
|
184
|
+
<div class="style-row">
|
|
185
|
+
<div class="style-row-label">
|
|
186
|
+
<sp-field-label size="s">${label}</sp-field-label>
|
|
187
|
+
</div>
|
|
188
|
+
<sp-textfield
|
|
189
|
+
size="s"
|
|
190
|
+
value=${value}
|
|
191
|
+
@input=${(/** @type {any} */ e) => {
|
|
192
|
+
clearTimeout(debounce);
|
|
193
|
+
debounce = setTimeout(() => onChange(e.target.value), 400);
|
|
194
|
+
}}
|
|
195
|
+
></sp-textfield>
|
|
196
|
+
</div>
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Normalize a parameter entry to a CEM object. */
|
|
201
|
+
export function normParam(/** @type {any} */ p) {
|
|
202
|
+
return typeof p === "string" ? { name: p } : p;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── Left panel: Signals ─────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @param {any} S
|
|
209
|
+
* @param {{ renderLeftPanel: Function; renderCanvas: Function }} ctx
|
|
210
|
+
*/
|
|
211
|
+
export function renderSignalsTemplate(S, { renderLeftPanel, renderCanvas }) {
|
|
212
|
+
const defs = S.document.state || {};
|
|
213
|
+
const entries = Object.entries(defs);
|
|
214
|
+
|
|
215
|
+
// Group by category
|
|
216
|
+
const groups = /** @type {Record<string, any[]>} */ ({
|
|
217
|
+
state: [],
|
|
218
|
+
computed: [],
|
|
219
|
+
data: [],
|
|
220
|
+
function: [],
|
|
221
|
+
});
|
|
222
|
+
for (const [name, def] of entries) {
|
|
223
|
+
groups[defCategory(def)].push([name, def]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const categories = [
|
|
227
|
+
{ key: "state", label: "State", items: groups.state },
|
|
228
|
+
{ key: "computed", label: "Computed", items: groups.computed },
|
|
229
|
+
{ key: "data", label: "Data", items: groups.data },
|
|
230
|
+
{ key: "function", label: "Functions", items: groups.function },
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
const collapsedCats = S._collapsedSignalCats || (S._collapsedSignalCats = new Set());
|
|
234
|
+
|
|
235
|
+
const catTemplates = categories
|
|
236
|
+
.filter((c) => c.items.length > 0)
|
|
237
|
+
.map(
|
|
238
|
+
({ key, label, items }) => html`
|
|
239
|
+
<sp-accordion-item
|
|
240
|
+
label="${label} (${items.length})"
|
|
241
|
+
?open=${!collapsedCats.has(key)}
|
|
242
|
+
@sp-accordion-item-toggle=${() => {
|
|
243
|
+
if (collapsedCats.has(key)) collapsedCats.delete(key);
|
|
244
|
+
else collapsedCats.add(key);
|
|
245
|
+
renderLeftPanel();
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
${items.map(([name, def]) => {
|
|
249
|
+
/** @type {any} */
|
|
250
|
+
const isExpanded = expandedSignal === name;
|
|
251
|
+
return html`
|
|
252
|
+
<div
|
|
253
|
+
class="signal-row${isExpanded ? " expanded" : ""}"
|
|
254
|
+
@click=${() => {
|
|
255
|
+
expandedSignal = isExpanded ? null : name;
|
|
256
|
+
renderLeftPanel();
|
|
257
|
+
}}
|
|
258
|
+
>
|
|
259
|
+
<span class="signal-badge ${defCategory(def)}">${defBadgeLabel(def)}</span>
|
|
260
|
+
<span class="signal-name">${name}</span>
|
|
261
|
+
<span class="signal-hint">${defHint(name, def)}</span>
|
|
262
|
+
<sp-action-button
|
|
263
|
+
quiet
|
|
264
|
+
size="xs"
|
|
265
|
+
class="signal-del"
|
|
266
|
+
@click=${(/** @type {any} */ e) => {
|
|
267
|
+
e.stopPropagation();
|
|
268
|
+
update(removeDef(S, name));
|
|
269
|
+
}}
|
|
270
|
+
>
|
|
271
|
+
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
272
|
+
</sp-action-button>
|
|
273
|
+
</div>
|
|
274
|
+
${isExpanded
|
|
275
|
+
? html`<div class="signal-editor">
|
|
276
|
+
${renderSignalEditorTemplate(S, name, def, { renderLeftPanel, renderCanvas })}
|
|
277
|
+
</div>`
|
|
278
|
+
: nothing}
|
|
279
|
+
`;
|
|
280
|
+
})}
|
|
281
|
+
</sp-accordion-item>
|
|
282
|
+
`,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
return html`
|
|
286
|
+
<div class="signals-panel">
|
|
287
|
+
<sp-accordion allow-multiple size="s"> ${catTemplates} </sp-accordion>
|
|
288
|
+
${entries.length === 0 ? html`<div class="empty-state">No state defined</div>` : nothing}
|
|
289
|
+
<div class="signals-add">
|
|
290
|
+
<sp-picker
|
|
291
|
+
size="s"
|
|
292
|
+
label="+ Add…"
|
|
293
|
+
placeholder="+ Add…"
|
|
294
|
+
@change=${(/** @type {any} */ e) => {
|
|
295
|
+
const type = e.target.value;
|
|
296
|
+
if (!type) return;
|
|
297
|
+
const template = DEF_TEMPLATES[type];
|
|
298
|
+
if (!template) return;
|
|
299
|
+
const isFunction = type === "function";
|
|
300
|
+
let nameBase = isFunction ? "newFunction" : "$newSignal";
|
|
301
|
+
let n = nameBase;
|
|
302
|
+
let i = 1;
|
|
303
|
+
while (S.document.state && S.document.state[n]) {
|
|
304
|
+
n = nameBase + i++;
|
|
305
|
+
}
|
|
306
|
+
update(addDef(S, n, structuredClone(template)));
|
|
307
|
+
expandedSignal = n;
|
|
308
|
+
renderLeftPanel();
|
|
309
|
+
}}
|
|
310
|
+
>
|
|
311
|
+
<sp-menu-item value="state">State Signal</sp-menu-item>
|
|
312
|
+
<sp-menu-item value="computed">Computed</sp-menu-item>
|
|
313
|
+
<sp-menu-divider></sp-menu-divider>
|
|
314
|
+
<sp-menu-item value="request">Fetch (Request)</sp-menu-item>
|
|
315
|
+
<sp-menu-item value="localStorage">LocalStorage</sp-menu-item>
|
|
316
|
+
<sp-menu-item value="sessionStorage">SessionStorage</sp-menu-item>
|
|
317
|
+
<sp-menu-item value="indexedDB">IndexedDB</sp-menu-item>
|
|
318
|
+
<sp-menu-item value="cookie">Cookie</sp-menu-item>
|
|
319
|
+
<sp-menu-item value="set">Set</sp-menu-item>
|
|
320
|
+
<sp-menu-item value="map">Map</sp-menu-item>
|
|
321
|
+
<sp-menu-item value="formData">FormData</sp-menu-item>
|
|
322
|
+
<sp-menu-item value="external">External Module…</sp-menu-item>
|
|
323
|
+
<sp-menu-divider></sp-menu-divider>
|
|
324
|
+
<sp-menu-item value="function">Function</sp-menu-item>
|
|
325
|
+
</sp-picker>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Render inline editor fields for a specific signal/def type. */
|
|
332
|
+
function renderSignalEditorTemplate(
|
|
333
|
+
/** @type {any} */ S,
|
|
334
|
+
/** @type {any} */ name,
|
|
335
|
+
/** @type {any} */ def,
|
|
336
|
+
/** @type {{ renderLeftPanel: Function; renderCanvas: Function }} */ ctx,
|
|
337
|
+
) {
|
|
338
|
+
const cat = defCategory(def);
|
|
339
|
+
|
|
340
|
+
// Helper for picker rows
|
|
341
|
+
const pickerRow = (
|
|
342
|
+
/** @type {any} */ label,
|
|
343
|
+
/** @type {any} */ options,
|
|
344
|
+
/** @type {any} */ currentVal,
|
|
345
|
+
/** @type {any} */ onChange,
|
|
346
|
+
) => {
|
|
347
|
+
return html`
|
|
348
|
+
<div class="style-row">
|
|
349
|
+
<div class="style-row-label">
|
|
350
|
+
<sp-field-label size="s">${label}</sp-field-label>
|
|
351
|
+
</div>
|
|
352
|
+
<sp-picker
|
|
353
|
+
size="s"
|
|
354
|
+
value=${currentVal}
|
|
355
|
+
@change=${(/** @type {any} */ e) => onChange(e.target.value)}
|
|
356
|
+
>
|
|
357
|
+
${options.map(
|
|
358
|
+
(/** @type {any} */ opt) => html`<sp-menu-item value=${opt}>${opt}</sp-menu-item>`,
|
|
359
|
+
)}
|
|
360
|
+
</sp-picker>
|
|
361
|
+
</div>
|
|
362
|
+
`;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// Helper for textarea rows
|
|
366
|
+
const textareaRow = (
|
|
367
|
+
/** @type {any} */ label,
|
|
368
|
+
/** @type {any} */ value,
|
|
369
|
+
/** @type {any} */ onChange,
|
|
370
|
+
/** @type {any} */ opts = {},
|
|
371
|
+
) => {
|
|
372
|
+
/** @type {any} */
|
|
373
|
+
let debounce;
|
|
374
|
+
return html`
|
|
375
|
+
<div class="style-row">
|
|
376
|
+
<div class="style-row-label">
|
|
377
|
+
<sp-field-label size="s">${label}</sp-field-label>
|
|
378
|
+
</div>
|
|
379
|
+
<textarea
|
|
380
|
+
class="field-input"
|
|
381
|
+
style="min-height:${opts.minHeight || "40px"};${opts.mono
|
|
382
|
+
? "font-family:'SF Mono','Fira Code','Consolas',monospace;font-size:11px;"
|
|
383
|
+
: ""}"
|
|
384
|
+
.value=${value}
|
|
385
|
+
@input=${(/** @type {any} */ e) => {
|
|
386
|
+
clearTimeout(debounce);
|
|
387
|
+
debounce = setTimeout(() => onChange(e.target.value), 500);
|
|
388
|
+
}}
|
|
389
|
+
></textarea>
|
|
390
|
+
</div>
|
|
391
|
+
`;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// Name field (common to all)
|
|
395
|
+
const nameField = signalFieldRow("Name", name, (/** @type {any} */ v) => {
|
|
396
|
+
if (v && v !== name && !(S.document.state && S.document.state[v])) {
|
|
397
|
+
expandedSignal = v;
|
|
398
|
+
update(renameDef(S, name, v));
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
/** @type {any} */
|
|
403
|
+
let fields = nothing;
|
|
404
|
+
|
|
405
|
+
if (cat === "state") {
|
|
406
|
+
const defaultVal =
|
|
407
|
+
def.default !== undefined && def.default !== null
|
|
408
|
+
? typeof def.default === "object"
|
|
409
|
+
? JSON.stringify(def.default)
|
|
410
|
+
: String(def.default)
|
|
411
|
+
: "";
|
|
412
|
+
|
|
413
|
+
const cemFields = isCustomElementDoc(S)
|
|
414
|
+
? html`
|
|
415
|
+
${signalFieldRow("Attribute", def.attribute || "", (/** @type {any} */ v) =>
|
|
416
|
+
update(updateDef(S, name, { attribute: v || undefined })),
|
|
417
|
+
)}
|
|
418
|
+
<div class="style-row">
|
|
419
|
+
<div class="style-row-label">
|
|
420
|
+
<sp-field-label size="s">Reflects</sp-field-label>
|
|
421
|
+
</div>
|
|
422
|
+
<sp-checkbox
|
|
423
|
+
class="field-check"
|
|
424
|
+
?checked=${!!def.reflects}
|
|
425
|
+
@change=${(/** @type {any} */ e) =>
|
|
426
|
+
update(updateDef(S, name, { reflects: e.target.checked || undefined }))}
|
|
427
|
+
></sp-checkbox>
|
|
428
|
+
</div>
|
|
429
|
+
${signalFieldRow(
|
|
430
|
+
"Deprecated",
|
|
431
|
+
typeof def.deprecated === "string" ? def.deprecated : "",
|
|
432
|
+
(/** @type {any} */ v) => update(updateDef(S, name, { deprecated: v || undefined })),
|
|
433
|
+
)}
|
|
434
|
+
`
|
|
435
|
+
: nothing;
|
|
436
|
+
|
|
437
|
+
fields = html`
|
|
438
|
+
${pickerRow(
|
|
439
|
+
"Type",
|
|
440
|
+
["string", "integer", "number", "boolean", "array", "object"],
|
|
441
|
+
def.type || "string",
|
|
442
|
+
(/** @type {any} */ v) => update(updateDef(S, name, { type: v })),
|
|
443
|
+
)}
|
|
444
|
+
${signalFieldRow("Default", defaultVal, (/** @type {any} */ v) => {
|
|
445
|
+
let parsed = v;
|
|
446
|
+
if (def.type === "integer") parsed = parseInt(v, 10) || 0;
|
|
447
|
+
else if (def.type === "number") parsed = parseFloat(v) || 0;
|
|
448
|
+
else if (def.type === "boolean") parsed = v === "true";
|
|
449
|
+
else if (def.type === "array" || def.type === "object") {
|
|
450
|
+
try {
|
|
451
|
+
parsed = JSON.parse(v);
|
|
452
|
+
} catch {
|
|
453
|
+
parsed = v;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
update(updateDef(S, name, { default: parsed }));
|
|
457
|
+
})}
|
|
458
|
+
${signalFieldRow("Description", def.description || "", (/** @type {any} */ v) =>
|
|
459
|
+
update(updateDef(S, name, { description: v || undefined })),
|
|
460
|
+
)}
|
|
461
|
+
${cemFields}
|
|
462
|
+
`;
|
|
463
|
+
} else if (cat === "computed") {
|
|
464
|
+
/** @type {any} */
|
|
465
|
+
let debounce;
|
|
466
|
+
fields = html`
|
|
467
|
+
<div class="style-row">
|
|
468
|
+
<div class="style-row-label">
|
|
469
|
+
<sp-field-label size="s">Expression</sp-field-label>
|
|
470
|
+
</div>
|
|
471
|
+
<textarea
|
|
472
|
+
class="field-input"
|
|
473
|
+
style="min-height:40px"
|
|
474
|
+
.value=${def.$compute || ""}
|
|
475
|
+
@input=${(/** @type {any} */ e) => {
|
|
476
|
+
clearTimeout(debounce);
|
|
477
|
+
debounce = setTimeout(() => {
|
|
478
|
+
const expr = e.target.value;
|
|
479
|
+
const depMatches = expr.match(/\$[a-zA-Z_]\w*/g) || [];
|
|
480
|
+
const deps = [...new Set(depMatches)].map((d) => `#/state/${d}`);
|
|
481
|
+
update(updateDef(S, name, { $compute: expr, $deps: deps }));
|
|
482
|
+
}, 500);
|
|
483
|
+
}}
|
|
484
|
+
></textarea>
|
|
485
|
+
</div>
|
|
486
|
+
${def.$deps && def.$deps.length > 0
|
|
487
|
+
? html`
|
|
488
|
+
<div class="style-row">
|
|
489
|
+
<div class="style-row-label">
|
|
490
|
+
<sp-field-label size="s">Dependencies</sp-field-label>
|
|
491
|
+
</div>
|
|
492
|
+
<span class="signal-hint" style="flex:1;max-width:none"
|
|
493
|
+
>${def.$deps
|
|
494
|
+
.map((/** @type {any} */ d) => d.replace("#/state/", ""))
|
|
495
|
+
.join(", ")}</span
|
|
496
|
+
>
|
|
497
|
+
</div>
|
|
498
|
+
`
|
|
499
|
+
: nothing}
|
|
500
|
+
`;
|
|
501
|
+
} else if (cat === "data") {
|
|
502
|
+
fields = renderDataSourceFields(S, name, def, textareaRow, pickerRow, ctx);
|
|
503
|
+
} else if (cat === "function") {
|
|
504
|
+
fields = renderFunctionFields(S, name, def, textareaRow, ctx);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return html`${nameField}${fields}`;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** Data source fields for signal editor */
|
|
511
|
+
function renderDataSourceFields(
|
|
512
|
+
/** @type {any} */ S,
|
|
513
|
+
/** @type {any} */ name,
|
|
514
|
+
/** @type {any} */ def,
|
|
515
|
+
/** @type {any} */ textareaRow,
|
|
516
|
+
/** @type {any} */ pickerRow,
|
|
517
|
+
/** @type {{ renderLeftPanel: Function; renderCanvas: Function }} */ ctx,
|
|
518
|
+
) {
|
|
519
|
+
const proto = def.$prototype;
|
|
520
|
+
|
|
521
|
+
if (proto === "Request") {
|
|
522
|
+
return html`
|
|
523
|
+
${signalFieldRow("URL", def.url || "", (/** @type {any} */ v) =>
|
|
524
|
+
update(updateDef(S, name, { url: v })),
|
|
525
|
+
)}
|
|
526
|
+
${pickerRow(
|
|
527
|
+
"Method",
|
|
528
|
+
["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
529
|
+
def.method || "GET",
|
|
530
|
+
(/** @type {any} */ v) => update(updateDef(S, name, { method: v })),
|
|
531
|
+
)}
|
|
532
|
+
${pickerRow("Timing", ["client", "server"], def.timing || "client", (/** @type {any} */ v) =>
|
|
533
|
+
update(updateDef(S, name, { timing: v })),
|
|
534
|
+
)}
|
|
535
|
+
`;
|
|
536
|
+
}
|
|
537
|
+
if (proto === "LocalStorage" || proto === "SessionStorage") {
|
|
538
|
+
const defaultStr =
|
|
539
|
+
def.default !== undefined && def.default !== null
|
|
540
|
+
? typeof def.default === "object"
|
|
541
|
+
? JSON.stringify(def.default, null, 2)
|
|
542
|
+
: String(def.default)
|
|
543
|
+
: "";
|
|
544
|
+
return html`
|
|
545
|
+
${signalFieldRow("Key", def.key || "", (/** @type {any} */ v) =>
|
|
546
|
+
update(updateDef(S, name, { key: v })),
|
|
547
|
+
)}
|
|
548
|
+
${textareaRow("Default", defaultStr, (/** @type {any} */ v) => {
|
|
549
|
+
try {
|
|
550
|
+
update(updateDef(S, name, { default: JSON.parse(v) }));
|
|
551
|
+
} catch {
|
|
552
|
+
update(updateDef(S, name, { default: v }));
|
|
553
|
+
}
|
|
554
|
+
})}
|
|
555
|
+
`;
|
|
556
|
+
}
|
|
557
|
+
if (proto === "IndexedDB") {
|
|
558
|
+
return html`
|
|
559
|
+
${signalFieldRow("Database", def.database || "", (/** @type {any} */ v) =>
|
|
560
|
+
update(updateDef(S, name, { database: v })),
|
|
561
|
+
)}
|
|
562
|
+
${signalFieldRow("Store", def.store || "", (/** @type {any} */ v) =>
|
|
563
|
+
update(updateDef(S, name, { store: v })),
|
|
564
|
+
)}
|
|
565
|
+
${signalFieldRow("Version", String(def.version || 1), (/** @type {any} */ v) =>
|
|
566
|
+
update(updateDef(S, name, { version: parseInt(v, 10) || 1 })),
|
|
567
|
+
)}
|
|
568
|
+
`;
|
|
569
|
+
}
|
|
570
|
+
if (proto === "Cookie") {
|
|
571
|
+
return html`
|
|
572
|
+
${signalFieldRow("Cookie", def.name || "", (/** @type {any} */ v) =>
|
|
573
|
+
update(updateDef(S, name, { name: v })),
|
|
574
|
+
)}
|
|
575
|
+
${signalFieldRow("Default", def.default || "", (/** @type {any} */ v) =>
|
|
576
|
+
update(updateDef(S, name, { default: v })),
|
|
577
|
+
)}
|
|
578
|
+
`;
|
|
579
|
+
}
|
|
580
|
+
if (proto === "Set" || proto === "Map" || proto === "FormData") {
|
|
581
|
+
const fieldName = proto === "FormData" ? "fields" : "default";
|
|
582
|
+
const fieldLabel = proto === "FormData" ? "Fields" : "Default";
|
|
583
|
+
const defaultStr =
|
|
584
|
+
def.default !== undefined && def.default !== null
|
|
585
|
+
? JSON.stringify(def.default, null, 2)
|
|
586
|
+
: proto === "FormData"
|
|
587
|
+
? JSON.stringify(def.fields || {}, null, 2)
|
|
588
|
+
: "";
|
|
589
|
+
return textareaRow(fieldLabel, defaultStr, (/** @type {any} */ v) => {
|
|
590
|
+
try {
|
|
591
|
+
update(updateDef(S, name, { [fieldName]: JSON.parse(v) }));
|
|
592
|
+
} catch {}
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
// Schema-driven fallback
|
|
596
|
+
return renderExternalPrototypeEditorTemplate(S, name, def, ctx);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Function fields for signal editor */
|
|
600
|
+
function renderFunctionFields(
|
|
601
|
+
/** @type {any} */ S,
|
|
602
|
+
/** @type {any} */ name,
|
|
603
|
+
/** @type {any} */ def,
|
|
604
|
+
/** @type {any} */ textareaRow,
|
|
605
|
+
/** @type {{ renderLeftPanel: Function; renderCanvas: Function }} */ ctx,
|
|
606
|
+
) {
|
|
607
|
+
const srcFields = def.$src
|
|
608
|
+
? html`
|
|
609
|
+
${signalFieldRow("Source", def.$src || "", (/** @type {any} */ v) =>
|
|
610
|
+
update(updateDef(S, name, { $src: v || undefined })),
|
|
611
|
+
)}
|
|
612
|
+
${signalFieldRow("Export", def.$export || "", (/** @type {any} */ v) =>
|
|
613
|
+
update(updateDef(S, name, { $export: v || undefined })),
|
|
614
|
+
)}
|
|
615
|
+
`
|
|
616
|
+
: textareaRow(
|
|
617
|
+
"Body",
|
|
618
|
+
def.body || "",
|
|
619
|
+
(/** @type {any} */ v) => update(updateDef(S, name, { body: v })),
|
|
620
|
+
{ minHeight: "60px", mono: true },
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
return html`
|
|
624
|
+
${srcFields} ${renderParameterEditorTemplate(S, name, def, ctx)}
|
|
625
|
+
${isCustomElementDoc(S) ? renderEmitsEditorTemplate(S, name, def) : nothing}
|
|
626
|
+
${!def.$src
|
|
627
|
+
? html`
|
|
628
|
+
<button
|
|
629
|
+
class="kv-add"
|
|
630
|
+
style="margin-top:4px"
|
|
631
|
+
@click=${() => {
|
|
632
|
+
S = { ...S, ui: { ...S.ui, editingFunction: { type: "def", defName: name } } };
|
|
633
|
+
ctx.renderCanvas();
|
|
634
|
+
}}
|
|
635
|
+
>
|
|
636
|
+
Open in editor
|
|
637
|
+
</button>
|
|
638
|
+
`
|
|
639
|
+
: nothing}
|
|
640
|
+
${signalFieldRow("Description", def.description || "", (/** @type {any} */ v) =>
|
|
641
|
+
update(updateDef(S, name, { description: v || undefined })),
|
|
642
|
+
)}
|
|
643
|
+
`;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ─── CEM Editors ─────────────────────────────────────────────────────────────
|
|
647
|
+
|
|
648
|
+
/** Render CEM parameter editor with basic/advanced toggle. */
|
|
649
|
+
function renderParameterEditorTemplate(
|
|
650
|
+
/** @type {any} */ S,
|
|
651
|
+
/** @type {any} */ name,
|
|
652
|
+
/** @type {any} */ def,
|
|
653
|
+
/** @type {{ renderLeftPanel: Function; renderCanvas: Function }} */ ctx,
|
|
654
|
+
) {
|
|
655
|
+
const params = (def.parameters || []).map(normParam);
|
|
656
|
+
const isAdvanced = advancedParamOpen.has(name);
|
|
657
|
+
|
|
658
|
+
if (!isAdvanced) {
|
|
659
|
+
// Basic mode: name chips
|
|
660
|
+
return html`
|
|
661
|
+
<div class="style-row">
|
|
662
|
+
<div class="style-row-label">
|
|
663
|
+
<sp-field-label size="s">Parameters</sp-field-label>
|
|
664
|
+
</div>
|
|
665
|
+
<div style="display:flex;flex-wrap:wrap;gap:4px;align-items:center">
|
|
666
|
+
${params.map(
|
|
667
|
+
(/** @type {any} */ p, /** @type {any} */ i) => html`
|
|
668
|
+
<span
|
|
669
|
+
style="display:inline-flex;align-items:center;gap:2px;padding:1px 6px;border-radius:3px;background:var(--bg-hover);font-size:11px;font-family:monospace"
|
|
670
|
+
>
|
|
671
|
+
${p.name || "?"}
|
|
672
|
+
<span
|
|
673
|
+
style="cursor:pointer;opacity:0.5;margin-left:2px"
|
|
674
|
+
@click=${() => {
|
|
675
|
+
update(
|
|
676
|
+
updateDef(S, name, {
|
|
677
|
+
parameters: params.filter(
|
|
678
|
+
(/** @type {any} */ _, /** @type {any} */ j) => j !== i,
|
|
679
|
+
).length
|
|
680
|
+
? params.filter((/** @type {any} */ _, /** @type {any} */ j) => j !== i)
|
|
681
|
+
: undefined,
|
|
682
|
+
}),
|
|
683
|
+
);
|
|
684
|
+
}}
|
|
685
|
+
>×</span
|
|
686
|
+
>
|
|
687
|
+
</span>
|
|
688
|
+
`,
|
|
689
|
+
)}
|
|
690
|
+
<input
|
|
691
|
+
class="field-input"
|
|
692
|
+
style="width:60px;flex:0 0 auto;font-size:11px"
|
|
693
|
+
placeholder="+"
|
|
694
|
+
@keydown=${(/** @type {any} */ e) => {
|
|
695
|
+
if (e.key === "Enter" && e.target.value.trim()) {
|
|
696
|
+
update(
|
|
697
|
+
updateDef(S, name, { parameters: [...params, { name: e.target.value.trim() }] }),
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
}}
|
|
701
|
+
/>
|
|
702
|
+
</div>
|
|
703
|
+
<span
|
|
704
|
+
style="font-size:10px;color:var(--fg-dim);cursor:pointer;width:100%;margin-top:2px"
|
|
705
|
+
@click=${() => {
|
|
706
|
+
advancedParamOpen.add(name);
|
|
707
|
+
ctx.renderLeftPanel();
|
|
708
|
+
}}
|
|
709
|
+
>▸ Advanced</span
|
|
710
|
+
>
|
|
711
|
+
</div>
|
|
712
|
+
`;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Advanced mode: full rows
|
|
716
|
+
return html`
|
|
717
|
+
<div class="style-row">
|
|
718
|
+
<div class="style-row-label">
|
|
719
|
+
<sp-field-label size="s">Parameters</sp-field-label>
|
|
720
|
+
</div>
|
|
721
|
+
<div style="display:flex;flex-direction:column;gap:4px">
|
|
722
|
+
${params.map(
|
|
723
|
+
(/** @type {any} */ p, /** @type {any} */ i) => html`
|
|
724
|
+
<div style="display:flex;gap:4px;align-items:center">
|
|
725
|
+
<input
|
|
726
|
+
class="field-input"
|
|
727
|
+
.value=${p.name || ""}
|
|
728
|
+
placeholder="name"
|
|
729
|
+
style="flex:1"
|
|
730
|
+
@change=${(/** @type {any} */ e) => {
|
|
731
|
+
const next = [...params];
|
|
732
|
+
next[i] = { ...next[i], name: e.target.value };
|
|
733
|
+
update(updateDef(S, name, { parameters: next }));
|
|
734
|
+
}}
|
|
735
|
+
/>
|
|
736
|
+
<input
|
|
737
|
+
class="field-input"
|
|
738
|
+
.value=${p.type?.text || ""}
|
|
739
|
+
placeholder="type"
|
|
740
|
+
style="flex:1"
|
|
741
|
+
@change=${(/** @type {any} */ e) => {
|
|
742
|
+
const next = [...params];
|
|
743
|
+
next[i] = {
|
|
744
|
+
...next[i],
|
|
745
|
+
type: e.target.value ? { text: e.target.value } : undefined,
|
|
746
|
+
};
|
|
747
|
+
update(updateDef(S, name, { parameters: next }));
|
|
748
|
+
}}
|
|
749
|
+
/>
|
|
750
|
+
<input
|
|
751
|
+
class="field-input"
|
|
752
|
+
.value=${p.description || ""}
|
|
753
|
+
placeholder="desc"
|
|
754
|
+
style="flex:2"
|
|
755
|
+
@change=${(/** @type {any} */ e) => {
|
|
756
|
+
const next = [...params];
|
|
757
|
+
next[i] = { ...next[i], description: e.target.value || undefined };
|
|
758
|
+
update(updateDef(S, name, { parameters: next }));
|
|
759
|
+
}}
|
|
760
|
+
/>
|
|
761
|
+
<input
|
|
762
|
+
type="checkbox"
|
|
763
|
+
title="optional"
|
|
764
|
+
.checked=${!!p.optional}
|
|
765
|
+
@change=${(/** @type {any} */ e) => {
|
|
766
|
+
const next = [...params];
|
|
767
|
+
next[i] = { ...next[i], optional: e.target.checked || undefined };
|
|
768
|
+
update(updateDef(S, name, { parameters: next }));
|
|
769
|
+
}}
|
|
770
|
+
/>
|
|
771
|
+
<span
|
|
772
|
+
style="cursor:pointer;opacity:0.5"
|
|
773
|
+
@click=${() => {
|
|
774
|
+
const next = params.filter(
|
|
775
|
+
(/** @type {any} */ _, /** @type {any} */ j) => j !== i,
|
|
776
|
+
);
|
|
777
|
+
update(updateDef(S, name, { parameters: next.length ? next : undefined }));
|
|
778
|
+
}}
|
|
779
|
+
>×</span
|
|
780
|
+
>
|
|
781
|
+
</div>
|
|
782
|
+
`,
|
|
783
|
+
)}
|
|
784
|
+
<button
|
|
785
|
+
class="kv-add"
|
|
786
|
+
@click=${() => update(updateDef(S, name, { parameters: [...params, { name: "" }] }))}
|
|
787
|
+
>
|
|
788
|
+
+ Add parameter
|
|
789
|
+
</button>
|
|
790
|
+
</div>
|
|
791
|
+
<span
|
|
792
|
+
style="font-size:10px;color:var(--fg-dim);cursor:pointer;width:100%;margin-top:2px"
|
|
793
|
+
@click=${() => {
|
|
794
|
+
advancedParamOpen.delete(name);
|
|
795
|
+
ctx.renderLeftPanel();
|
|
796
|
+
}}
|
|
797
|
+
>▾ Basic</span
|
|
798
|
+
>
|
|
799
|
+
</div>
|
|
800
|
+
`;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/** Render CEM emits editor for function state entries. */
|
|
804
|
+
function renderEmitsEditorTemplate(
|
|
805
|
+
/** @type {any} */ S,
|
|
806
|
+
/** @type {any} */ name,
|
|
807
|
+
/** @type {any} */ def,
|
|
808
|
+
) {
|
|
809
|
+
const emits = def.emits || [];
|
|
810
|
+
if (emits.length === 0 && !isCustomElementDoc(S)) return nothing;
|
|
811
|
+
|
|
812
|
+
return html`
|
|
813
|
+
<div
|
|
814
|
+
style="font-size:11px;font-weight:600;color:var(--fg-dim);margin:8px 0 4px;text-transform:uppercase;letter-spacing:0.05em"
|
|
815
|
+
>
|
|
816
|
+
Emits
|
|
817
|
+
</div>
|
|
818
|
+
${emits.map(
|
|
819
|
+
(/** @type {any} */ ev, /** @type {any} */ i) => html`
|
|
820
|
+
<div style="display:flex;gap:4px;align-items:center;margin-bottom:4px">
|
|
821
|
+
<input
|
|
822
|
+
class="field-input"
|
|
823
|
+
.value=${ev.name || ""}
|
|
824
|
+
placeholder="event name"
|
|
825
|
+
style="flex:1"
|
|
826
|
+
@change=${(/** @type {any} */ e) => {
|
|
827
|
+
const next = [...emits];
|
|
828
|
+
next[i] = { ...next[i], name: e.target.value };
|
|
829
|
+
update(updateDef(S, name, { emits: next }));
|
|
830
|
+
}}
|
|
831
|
+
/>
|
|
832
|
+
<input
|
|
833
|
+
class="field-input"
|
|
834
|
+
.value=${ev.type?.text || ""}
|
|
835
|
+
placeholder="type"
|
|
836
|
+
style="flex:1"
|
|
837
|
+
@change=${(/** @type {any} */ e) => {
|
|
838
|
+
const next = [...emits];
|
|
839
|
+
next[i] = { ...next[i], type: e.target.value ? { text: e.target.value } : undefined };
|
|
840
|
+
update(updateDef(S, name, { emits: next }));
|
|
841
|
+
}}
|
|
842
|
+
/>
|
|
843
|
+
<input
|
|
844
|
+
class="field-input"
|
|
845
|
+
.value=${ev.description || ""}
|
|
846
|
+
placeholder="description"
|
|
847
|
+
style="flex:2"
|
|
848
|
+
@change=${(/** @type {any} */ e) => {
|
|
849
|
+
const next = [...emits];
|
|
850
|
+
next[i] = { ...next[i], description: e.target.value || undefined };
|
|
851
|
+
update(updateDef(S, name, { emits: next }));
|
|
852
|
+
}}
|
|
853
|
+
/>
|
|
854
|
+
<span
|
|
855
|
+
style="cursor:pointer;opacity:0.5"
|
|
856
|
+
@click=${() => {
|
|
857
|
+
update(
|
|
858
|
+
updateDef(S, name, {
|
|
859
|
+
emits: emits.filter((/** @type {any} */ _, /** @type {any} */ j) => j !== i)
|
|
860
|
+
.length
|
|
861
|
+
? emits.filter((/** @type {any} */ _, /** @type {any} */ j) => j !== i)
|
|
862
|
+
: undefined,
|
|
863
|
+
}),
|
|
864
|
+
);
|
|
865
|
+
}}
|
|
866
|
+
>×</span
|
|
867
|
+
>
|
|
868
|
+
</div>
|
|
869
|
+
`,
|
|
870
|
+
)}
|
|
871
|
+
<button
|
|
872
|
+
class="kv-add"
|
|
873
|
+
@click=${() => update(updateDef(S, name, { emits: [...emits, { name: "" }] }))}
|
|
874
|
+
>
|
|
875
|
+
+ Add event
|
|
876
|
+
</button>
|
|
877
|
+
`;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// ─── Plugin schema-driven form rendering ────────────────────────────────────
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Render config form fields from a JSON Schema `properties` object. Maps schema types to
|
|
884
|
+
* appropriate form controls.
|
|
885
|
+
*/
|
|
886
|
+
export function renderSchemaFieldsTemplate(
|
|
887
|
+
/** @type {any} */ schema,
|
|
888
|
+
/** @type {any} */ def,
|
|
889
|
+
/** @type {any} */ name,
|
|
890
|
+
/** @type {any} */ S,
|
|
891
|
+
) {
|
|
892
|
+
if (!schema?.properties) return nothing;
|
|
893
|
+
|
|
894
|
+
const required = new Set(schema.required ?? []);
|
|
895
|
+
|
|
896
|
+
return Object.entries(schema.properties)
|
|
897
|
+
.filter(([prop]) => !STUDIO_RESERVED_KEYS.has(prop))
|
|
898
|
+
.map(([prop, ps]) => {
|
|
899
|
+
const currentValue = def[prop];
|
|
900
|
+
const labelText = prop + (required.has(prop) ? " *" : "");
|
|
901
|
+
|
|
902
|
+
let control;
|
|
903
|
+
if (ps.enum) {
|
|
904
|
+
control = html`
|
|
905
|
+
<sp-picker
|
|
906
|
+
size="s"
|
|
907
|
+
value=${currentValue !== undefined
|
|
908
|
+
? String(currentValue)
|
|
909
|
+
: ps.default !== undefined
|
|
910
|
+
? String(ps.default)
|
|
911
|
+
: "__none__"}
|
|
912
|
+
@change=${(/** @type {any} */ e) =>
|
|
913
|
+
update(
|
|
914
|
+
updateDef(S, name, {
|
|
915
|
+
[prop]: e.target.value === "__none__" ? undefined : e.target.value,
|
|
916
|
+
}),
|
|
917
|
+
)}
|
|
918
|
+
>
|
|
919
|
+
${!required.has(prop) ? html`<sp-menu-item value="__none__">—</sp-menu-item>` : nothing}
|
|
920
|
+
${ps.enum.map(
|
|
921
|
+
(/** @type {any} */ val) => html`<sp-menu-item value=${val}>${val}</sp-menu-item>`,
|
|
922
|
+
)}
|
|
923
|
+
</sp-picker>
|
|
924
|
+
`;
|
|
925
|
+
} else if (ps.type === "boolean") {
|
|
926
|
+
control = html`<sp-checkbox
|
|
927
|
+
?checked=${currentValue ?? ps.default ?? false}
|
|
928
|
+
@change=${(/** @type {any} */ e) =>
|
|
929
|
+
update(updateDef(S, name, { [prop]: e.target.checked }))}
|
|
930
|
+
></sp-checkbox>`;
|
|
931
|
+
} else if (ps.type === "integer" || ps.type === "number") {
|
|
932
|
+
/** @type {any} */
|
|
933
|
+
let debounce;
|
|
934
|
+
control = html`<sp-number-field
|
|
935
|
+
size="s"
|
|
936
|
+
min=${ps.minimum !== undefined ? ps.minimum : nothing}
|
|
937
|
+
max=${ps.maximum !== undefined ? ps.maximum : nothing}
|
|
938
|
+
step=${ps.type === "integer" ? "1" : nothing}
|
|
939
|
+
.value=${currentValue !== undefined ? currentValue : nothing}
|
|
940
|
+
placeholder=${ps.default !== undefined ? String(ps.default) : nothing}
|
|
941
|
+
@change=${(/** @type {any} */ e) => {
|
|
942
|
+
clearTimeout(debounce);
|
|
943
|
+
debounce = setTimeout(() => {
|
|
944
|
+
const parsed =
|
|
945
|
+
ps.type === "integer" ? parseInt(e.target.value, 10) : parseFloat(e.target.value);
|
|
946
|
+
update(updateDef(S, name, { [prop]: isNaN(parsed) ? undefined : parsed }));
|
|
947
|
+
}, 400);
|
|
948
|
+
}}
|
|
949
|
+
></sp-number-field>`;
|
|
950
|
+
} else if (ps.format === "json-schema") {
|
|
951
|
+
const hasValue =
|
|
952
|
+
currentValue && typeof currentValue === "object" && Object.keys(currentValue).length > 0;
|
|
953
|
+
const isRef = currentValue && typeof currentValue === "object" && currentValue.$ref;
|
|
954
|
+
/** @type {any} */
|
|
955
|
+
let debounce;
|
|
956
|
+
control = html`
|
|
957
|
+
<div class="schema-param-editor">
|
|
958
|
+
${hasValue && !isRef && currentValue.properties
|
|
959
|
+
? html`
|
|
960
|
+
<div style="display:flex;flex-wrap:wrap;gap:3px;margin-bottom:4px">
|
|
961
|
+
${Object.entries(currentValue.properties).map(
|
|
962
|
+
([k, v]) => html`
|
|
963
|
+
<span
|
|
964
|
+
style="background:var(--bg-alt);padding:1px 6px;border-radius:3px;font-size:10px;color:var(--fg-dim)"
|
|
965
|
+
>${k}: ${v.type ?? "any"}</span
|
|
966
|
+
>
|
|
967
|
+
`,
|
|
968
|
+
)}
|
|
969
|
+
</div>
|
|
970
|
+
`
|
|
971
|
+
: nothing}
|
|
972
|
+
<sp-textfield
|
|
973
|
+
multiline
|
|
974
|
+
size="s"
|
|
975
|
+
style="min-height:${hasValue ? "80px" : "40px"};font-family:monospace;font-size:11px"
|
|
976
|
+
.value=${currentValue !== undefined ? JSON.stringify(currentValue, null, 2) : ""}
|
|
977
|
+
placeholder=${ps.description ?? "JSON Schema defining the data shape\u2026"}
|
|
978
|
+
@input=${(/** @type {any} */ e) => {
|
|
979
|
+
clearTimeout(debounce);
|
|
980
|
+
debounce = setTimeout(() => {
|
|
981
|
+
try {
|
|
982
|
+
update(updateDef(S, name, { [prop]: JSON.parse(e.target.value) }));
|
|
983
|
+
} catch {}
|
|
984
|
+
}, 500);
|
|
985
|
+
}}
|
|
986
|
+
></sp-textfield>
|
|
987
|
+
</div>
|
|
988
|
+
`;
|
|
989
|
+
} else if (ps.type === "array" || ps.type === "object") {
|
|
990
|
+
/** @type {any} */
|
|
991
|
+
let debounce;
|
|
992
|
+
control = html`<sp-textfield
|
|
993
|
+
multiline
|
|
994
|
+
size="s"
|
|
995
|
+
style="min-height:40px"
|
|
996
|
+
.value=${currentValue !== undefined ? JSON.stringify(currentValue, null, 2) : ""}
|
|
997
|
+
placeholder=${ps.default !== undefined ? JSON.stringify(ps.default) : nothing}
|
|
998
|
+
@input=${(/** @type {any} */ e) => {
|
|
999
|
+
clearTimeout(debounce);
|
|
1000
|
+
debounce = setTimeout(() => {
|
|
1001
|
+
try {
|
|
1002
|
+
update(updateDef(S, name, { [prop]: JSON.parse(e.target.value) }));
|
|
1003
|
+
} catch {}
|
|
1004
|
+
}, 500);
|
|
1005
|
+
}}
|
|
1006
|
+
></sp-textfield>`;
|
|
1007
|
+
} else {
|
|
1008
|
+
/** @type {any} */
|
|
1009
|
+
let debounce;
|
|
1010
|
+
const ph = ps.default !== undefined ? String(ps.default) : (ps.examples?.[0] ?? "");
|
|
1011
|
+
control = html`<sp-textfield
|
|
1012
|
+
size="s"
|
|
1013
|
+
.value=${currentValue ?? ""}
|
|
1014
|
+
placeholder=${ph || nothing}
|
|
1015
|
+
title=${ps.description || nothing}
|
|
1016
|
+
@input=${(/** @type {any} */ e) => {
|
|
1017
|
+
clearTimeout(debounce);
|
|
1018
|
+
debounce = setTimeout(
|
|
1019
|
+
() => update(updateDef(S, name, { [prop]: e.target.value || undefined })),
|
|
1020
|
+
400,
|
|
1021
|
+
);
|
|
1022
|
+
}}
|
|
1023
|
+
></sp-textfield>`;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
return html`
|
|
1027
|
+
<div class="style-row">
|
|
1028
|
+
<div class="style-row-label">
|
|
1029
|
+
<sp-field-label size="s" title=${ps.description || nothing}
|
|
1030
|
+
>${labelText}</sp-field-label
|
|
1031
|
+
>
|
|
1032
|
+
</div>
|
|
1033
|
+
${control}
|
|
1034
|
+
</div>
|
|
1035
|
+
`;
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Render editor fields for an external $prototype + $src plugin. Shows $src/$export inputs plus
|
|
1041
|
+
* schema-driven config fields.
|
|
1042
|
+
*/
|
|
1043
|
+
export function renderExternalPrototypeEditorTemplate(
|
|
1044
|
+
/** @type {any} */ S,
|
|
1045
|
+
/** @type {any} */ name,
|
|
1046
|
+
/** @type {any} */ def,
|
|
1047
|
+
/** @type {{ renderLeftPanel: Function; renderCanvas: Function }} */ ctx,
|
|
1048
|
+
) {
|
|
1049
|
+
// Schema-driven config fields (async with cache)
|
|
1050
|
+
/** @type {any} */
|
|
1051
|
+
let schemaContent = nothing;
|
|
1052
|
+
if (def.$src && def.$prototype) {
|
|
1053
|
+
const cacheKey = `${def.$src}::${def.$prototype}`;
|
|
1054
|
+
if (pluginSchemaCache.has(cacheKey)) {
|
|
1055
|
+
const schema = pluginSchemaCache.get(cacheKey);
|
|
1056
|
+
if (schema) {
|
|
1057
|
+
schemaContent = html`
|
|
1058
|
+
${schema.description
|
|
1059
|
+
? html`<div class="signal-hint" style="padding:4px 0 8px">${schema.description}</div>`
|
|
1060
|
+
: nothing}
|
|
1061
|
+
${renderSchemaFieldsTemplate(schema, def, name, S)}
|
|
1062
|
+
`;
|
|
1063
|
+
}
|
|
1064
|
+
} else {
|
|
1065
|
+
// Trigger async load — will re-render when cached
|
|
1066
|
+
schemaContent = html`<div
|
|
1067
|
+
style="padding:4px 0;font-size:11px;color:var(--fg-dim);font-style:italic"
|
|
1068
|
+
>
|
|
1069
|
+
Loading schema…
|
|
1070
|
+
</div>`;
|
|
1071
|
+
fetchPluginSchema(def, S).then((schema) => {
|
|
1072
|
+
if (schema) ctx.renderLeftPanel();
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
return html`
|
|
1078
|
+
${signalFieldRow("Source", def.$src || "", (/** @type {any} */ v) => {
|
|
1079
|
+
update(updateDef(S, name, { $src: v || undefined }));
|
|
1080
|
+
pluginSchemaCache.delete(`${v}::${def.$prototype}`);
|
|
1081
|
+
})}
|
|
1082
|
+
${signalFieldRow("Prototype", def.$prototype || "", (/** @type {any} */ v) => {
|
|
1083
|
+
update(updateDef(S, name, { $prototype: v || undefined }));
|
|
1084
|
+
pluginSchemaCache.delete(`${def.$src}::${v}`);
|
|
1085
|
+
})}
|
|
1086
|
+
${def.$export
|
|
1087
|
+
? signalFieldRow("Export", def.$export || "", (/** @type {any} */ v) =>
|
|
1088
|
+
update(updateDef(S, name, { $export: v || undefined })),
|
|
1089
|
+
)
|
|
1090
|
+
: nothing}
|
|
1091
|
+
${schemaContent}
|
|
1092
|
+
`;
|
|
1093
|
+
}
|