@jxsuite/studio 0.21.4 → 0.22.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 +532 -252
- package/dist/studio.js.map +15 -15
- package/package.json +4 -4
- package/src/canvas/canvas-live-render.js +6 -5
- package/src/canvas/canvas-render.js +4 -3
- package/src/editor/context-menu.js +146 -26
- package/src/editor/insertion-helper.js +4 -3
- package/src/markdown/md-convert.js +7 -0
- package/src/panels/head-panel.js +105 -2
- package/src/panels/right-panel.js +142 -90
- package/src/panels/signals-panel.js +246 -129
- package/src/panels/style-panel.js +44 -1
- package/src/services/code-services.js +12 -5
- package/src/settings/head-editor.js +4 -4
- package/src/tabs/transact.js +1 -0
|
@@ -88,6 +88,11 @@ import { fetchPluginSchema, pluginSchemaCache } from "../services/code-services.
|
|
|
88
88
|
* description?: string;
|
|
89
89
|
* examples?: string[];
|
|
90
90
|
* name?: string;
|
|
91
|
+
* items?: {
|
|
92
|
+
* type?: string;
|
|
93
|
+
* properties?: Record<string, Record<string, unknown>>;
|
|
94
|
+
* required?: string[];
|
|
95
|
+
* };
|
|
91
96
|
* }} SchemaProperty
|
|
92
97
|
*/
|
|
93
98
|
|
|
@@ -386,6 +391,36 @@ export function renderSignalsTemplate(S, ctx) {
|
|
|
386
391
|
@change=${(/** @type {Event} */ e) => {
|
|
387
392
|
const type = /** @type {HTMLInputElement} */ (e.target).value;
|
|
388
393
|
if (!type) return;
|
|
394
|
+
|
|
395
|
+
// Handle import-based prototypes (e.g., "import:ContentCollection")
|
|
396
|
+
if (type.startsWith("import:")) {
|
|
397
|
+
const protoName = type.slice(7);
|
|
398
|
+
const src = projectState?.projectConfig?.imports?.[protoName];
|
|
399
|
+
let n = "$" + protoName.charAt(0).toLowerCase() + protoName.slice(1);
|
|
400
|
+
let i = 1;
|
|
401
|
+
const base = n;
|
|
402
|
+
while (S.document.state && S.document.state[n]) {
|
|
403
|
+
n = base + i++;
|
|
404
|
+
}
|
|
405
|
+
transactDoc(activeTab.value, (t) =>
|
|
406
|
+
mutateAddDef(
|
|
407
|
+
t,
|
|
408
|
+
n,
|
|
409
|
+
/** @type {Record<string, JsonValue>} */ ({ $prototype: protoName }),
|
|
410
|
+
),
|
|
411
|
+
);
|
|
412
|
+
expandedSignal = n;
|
|
413
|
+
if (src) {
|
|
414
|
+
fetchPluginSchema(
|
|
415
|
+
{ $prototype: protoName, $src: src },
|
|
416
|
+
/** @type {{ documentPath?: string }} */ (S),
|
|
417
|
+
).then(() => ctx.renderLeftPanel());
|
|
418
|
+
} else {
|
|
419
|
+
ctx.renderLeftPanel();
|
|
420
|
+
}
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
389
424
|
const template = DEF_TEMPLATES[type];
|
|
390
425
|
if (!template) return;
|
|
391
426
|
const isFunction = type === "function";
|
|
@@ -418,6 +453,14 @@ export function renderSignalsTemplate(S, ctx) {
|
|
|
418
453
|
<sp-menu-item value="map">Map</sp-menu-item>
|
|
419
454
|
<sp-menu-item value="formData">FormData</sp-menu-item>
|
|
420
455
|
<sp-menu-item value="external">External Module…</sp-menu-item>
|
|
456
|
+
${projectState?.projectConfig?.imports
|
|
457
|
+
? html`<sp-menu-divider></sp-menu-divider>${Object.keys(
|
|
458
|
+
projectState.projectConfig.imports,
|
|
459
|
+
).map(
|
|
460
|
+
(/** @type {string} */ k) =>
|
|
461
|
+
html`<sp-menu-item value="import:${k}">${k}</sp-menu-item>`,
|
|
462
|
+
)}`
|
|
463
|
+
: nothing}
|
|
421
464
|
<sp-menu-divider></sp-menu-divider>
|
|
422
465
|
<sp-menu-item value="function">Function</sp-menu-item>
|
|
423
466
|
</sp-picker>
|
|
@@ -736,9 +779,6 @@ function renderDataSourceFields(
|
|
|
736
779
|
)}
|
|
737
780
|
`;
|
|
738
781
|
}
|
|
739
|
-
if (proto === "ContentEntry" || proto === "ContentCollection") {
|
|
740
|
-
return renderContentPrototypeFields(name, def, proto);
|
|
741
|
-
}
|
|
742
782
|
if (proto === "Set" || proto === "Map" || proto === "FormData") {
|
|
743
783
|
const fieldName = proto === "FormData" ? "fields" : "default";
|
|
744
784
|
const fieldLabel = proto === "FormData" ? "Fields" : "Default";
|
|
@@ -760,117 +800,6 @@ function renderDataSourceFields(
|
|
|
760
800
|
return renderExternalPrototypeEditorTemplate(S, name, def, ctx);
|
|
761
801
|
}
|
|
762
802
|
|
|
763
|
-
/** ContentEntry/ContentCollection fields for signal editor */
|
|
764
|
-
function renderContentPrototypeFields(
|
|
765
|
-
/** @type {string} */ name,
|
|
766
|
-
/** @type {SignalDef} */ def,
|
|
767
|
-
/** @type {string} */ proto,
|
|
768
|
-
) {
|
|
769
|
-
const contentTypes = projectState?.projectConfig?.contentTypes;
|
|
770
|
-
const typeNames = contentTypes ? Object.keys(contentTypes) : [];
|
|
771
|
-
const currentType = def.contentType || "";
|
|
772
|
-
const schema = /** @type {ContentTypeSchema | undefined} */ (
|
|
773
|
-
currentType && contentTypes?.[currentType]?.schema
|
|
774
|
-
) ||
|
|
775
|
-
undefined;
|
|
776
|
-
|
|
777
|
-
const contentTypeField = typeNames.length
|
|
778
|
-
? html`
|
|
779
|
-
<div class="signal-field-row">
|
|
780
|
-
<label class="signal-field-label">Content Type</label>
|
|
781
|
-
<sp-picker
|
|
782
|
-
size="s"
|
|
783
|
-
value=${currentType}
|
|
784
|
-
style="width:100%"
|
|
785
|
-
@change=${(/** @type {Event} */ e) => {
|
|
786
|
-
const v = /** @type {HTMLSelectElement} */ (e.target).value;
|
|
787
|
-
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { contentType: v }));
|
|
788
|
-
}}
|
|
789
|
-
>
|
|
790
|
-
${typeNames.map((t) => html`<sp-menu-item value=${t}>${t}</sp-menu-item>`)}
|
|
791
|
-
</sp-picker>
|
|
792
|
-
</div>
|
|
793
|
-
`
|
|
794
|
-
: signalFieldRow("Content Type", currentType, (/** @type {string} */ v) =>
|
|
795
|
-
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { contentType: v })),
|
|
796
|
-
);
|
|
797
|
-
|
|
798
|
-
/** @type {import("lit-html").TemplateResult | symbol} */
|
|
799
|
-
let entryFields = nothing;
|
|
800
|
-
if (proto === "ContentEntry") {
|
|
801
|
-
const idVal = def.id;
|
|
802
|
-
const idStr =
|
|
803
|
-
idVal && typeof idVal === "object" && /** @type {Record<string, unknown>} */ (idVal).$ref
|
|
804
|
-
? /** @type {string} */ (/** @type {Record<string, unknown>} */ (idVal).$ref)
|
|
805
|
-
: typeof idVal === "string"
|
|
806
|
-
? idVal
|
|
807
|
-
: "";
|
|
808
|
-
entryFields = signalFieldRow("ID", idStr, (/** @type {string} */ v) => {
|
|
809
|
-
const val = v.startsWith("#/") ? { $ref: v } : v;
|
|
810
|
-
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { id: val }));
|
|
811
|
-
});
|
|
812
|
-
} else {
|
|
813
|
-
const filterStr = def.filter ? JSON.stringify(def.filter) : "";
|
|
814
|
-
const sortStr = def.sort ? `${def.sort.field || ""} ${def.sort.order || ""}`.trim() : "";
|
|
815
|
-
const limitStr = def.limit != null ? String(def.limit) : "";
|
|
816
|
-
entryFields = html`
|
|
817
|
-
${signalFieldRow("Filter", filterStr, (/** @type {string} */ v) => {
|
|
818
|
-
try {
|
|
819
|
-
const parsed = v ? JSON.parse(v) : undefined;
|
|
820
|
-
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { filter: parsed }));
|
|
821
|
-
} catch {}
|
|
822
|
-
})}
|
|
823
|
-
${signalFieldRow("Sort", sortStr, (/** @type {string} */ v) => {
|
|
824
|
-
if (!v) {
|
|
825
|
-
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { sort: undefined }));
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
const [field, order] = v.split(" ");
|
|
829
|
-
transactDoc(activeTab.value, (t) =>
|
|
830
|
-
mutateUpdateDef(t, name, { sort: { field, order: order || "asc" } }),
|
|
831
|
-
);
|
|
832
|
-
})}
|
|
833
|
-
${signalFieldRow("Limit", limitStr, (/** @type {string} */ v) => {
|
|
834
|
-
const num = v ? parseInt(v, 10) : undefined;
|
|
835
|
-
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { limit: num }));
|
|
836
|
-
})}
|
|
837
|
-
`;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
/** @type {import("lit-html").TemplateResult | symbol} */
|
|
841
|
-
let schemaFields = nothing;
|
|
842
|
-
if (schema?.properties) {
|
|
843
|
-
const fields = Object.entries(schema.properties);
|
|
844
|
-
const required = new Set(schema.required || []);
|
|
845
|
-
schemaFields = html`
|
|
846
|
-
<div style="margin-top:8px;padding:6px 0;border-top:1px solid var(--border-dim)">
|
|
847
|
-
<div style="font-size:11px;color:var(--fg-dim);margin-bottom:4px;font-weight:500">
|
|
848
|
-
Fields (${currentType})
|
|
849
|
-
</div>
|
|
850
|
-
${fields.map(
|
|
851
|
-
([field, fieldDef]) => html`
|
|
852
|
-
<div style="font-size:11px;padding:2px 0;display:flex;gap:4px;align-items:center">
|
|
853
|
-
<code style="color:var(--fg-default)">${field}</code>
|
|
854
|
-
<span style="color:var(--fg-dim)">${/** @type {any} */ (fieldDef).type || ""}</span>
|
|
855
|
-
${required.has(field)
|
|
856
|
-
? html`<span style="color:var(--color-accent,#f36f32);font-size:9px">req</span>`
|
|
857
|
-
: nothing}
|
|
858
|
-
</div>
|
|
859
|
-
`,
|
|
860
|
-
)}
|
|
861
|
-
</div>
|
|
862
|
-
`;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
return html`
|
|
866
|
-
${contentTypeField}
|
|
867
|
-
${signalFieldRow("Prototype", proto, (/** @type {string} */ v) =>
|
|
868
|
-
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { $prototype: v })),
|
|
869
|
-
)}
|
|
870
|
-
${entryFields} ${schemaFields}
|
|
871
|
-
`;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
803
|
/** Function fields for signal editor */
|
|
875
804
|
function renderFunctionFields(
|
|
876
805
|
/** @type {SignalsPanelState} */ S,
|
|
@@ -1207,6 +1136,111 @@ function renderEmitsEditorTemplate(
|
|
|
1207
1136
|
|
|
1208
1137
|
// ─── Plugin schema-driven form rendering ────────────────────────────────────
|
|
1209
1138
|
|
|
1139
|
+
/**
|
|
1140
|
+
* Resolve a schema enum value. Handles: - Plain arrays (pass through) - `$ref` objects pointing to
|
|
1141
|
+
* `#/$context/contentTypes` (resolves to project content type keys) - `$ref` objects pointing to
|
|
1142
|
+
* `#/$context/contentTypes/{@param}/schema/properties` (dependent enum) - Legacy `"$contentTypes"`
|
|
1143
|
+
* string sentinel (deprecated)
|
|
1144
|
+
*
|
|
1145
|
+
* @param {unknown} enumDef
|
|
1146
|
+
* @param {Record<string, unknown>} [parentDef] - Parent def for resolving dependent refs
|
|
1147
|
+
* @returns {string[] | undefined}
|
|
1148
|
+
*/
|
|
1149
|
+
function resolveSchemaEnum(enumDef, parentDef) {
|
|
1150
|
+
if (Array.isArray(enumDef)) return enumDef;
|
|
1151
|
+
if (enumDef && typeof enumDef === "object") {
|
|
1152
|
+
const ref = /** @type {Record<string, unknown>} */ (enumDef).$ref;
|
|
1153
|
+
if (ref === "#/$context/contentTypes") {
|
|
1154
|
+
return Object.keys(projectState?.projectConfig?.contentTypes ?? {});
|
|
1155
|
+
}
|
|
1156
|
+
if (typeof ref === "string" && ref.startsWith("#/$context/contentTypes/{@")) {
|
|
1157
|
+
const match = ref.match(/#\/\$context\/contentTypes\/\{@(\w+)\}\/schema\/properties/);
|
|
1158
|
+
if (match && parentDef) {
|
|
1159
|
+
const paramName = match[1];
|
|
1160
|
+
const typeName = /** @type {string | undefined} */ (parentDef[paramName]);
|
|
1161
|
+
if (typeName) {
|
|
1162
|
+
const ct = /** @type {Record<string, unknown> | undefined} */ (
|
|
1163
|
+
projectState?.projectConfig?.contentTypes?.[typeName]
|
|
1164
|
+
);
|
|
1165
|
+
const schema = /** @type {Record<string, unknown> | undefined} */ (ct?.schema);
|
|
1166
|
+
const props = /** @type {Record<string, unknown> | undefined} */ (schema?.properties);
|
|
1167
|
+
if (props) return Object.keys(props);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return undefined;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (enumDef === "$contentTypes") {
|
|
1174
|
+
return Object.keys(projectState?.projectConfig?.contentTypes ?? {});
|
|
1175
|
+
}
|
|
1176
|
+
return undefined;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Render a single inline field within an array-of-objects row. Dispatches by schema type: enum →
|
|
1181
|
+
* picker, boolean → switch, number → number-field, else → textfield.
|
|
1182
|
+
*
|
|
1183
|
+
* @param {string} key
|
|
1184
|
+
* @param {Record<string, unknown>} schema
|
|
1185
|
+
* @param {unknown} value
|
|
1186
|
+
* @param {(val: unknown) => void} onChange
|
|
1187
|
+
* @param {Record<string, unknown>} [parentDef] - Parent def for resolving dependent enum refs
|
|
1188
|
+
*/
|
|
1189
|
+
function renderInlineField(key, schema, value, onChange, parentDef) {
|
|
1190
|
+
const enumValues = resolveSchemaEnum(schema.enum, parentDef);
|
|
1191
|
+
|
|
1192
|
+
if (enumValues) {
|
|
1193
|
+
return html`<sp-picker
|
|
1194
|
+
size="s"
|
|
1195
|
+
label=${key}
|
|
1196
|
+
value=${value !== undefined ? String(value) : "__none__"}
|
|
1197
|
+
@change=${(/** @type {Event} */ e) =>
|
|
1198
|
+
onChange(
|
|
1199
|
+
/** @type {HTMLInputElement} */ (e.target).value === "__none__"
|
|
1200
|
+
? undefined
|
|
1201
|
+
: /** @type {HTMLInputElement} */ (e.target).value,
|
|
1202
|
+
)}
|
|
1203
|
+
>
|
|
1204
|
+
<sp-menu-item value="__none__">—</sp-menu-item>
|
|
1205
|
+
${enumValues.map(
|
|
1206
|
+
(/** @type {string} */ v) => html`<sp-menu-item value=${v}>${v}</sp-menu-item>`,
|
|
1207
|
+
)}
|
|
1208
|
+
</sp-picker>`;
|
|
1209
|
+
}
|
|
1210
|
+
if (schema.type === "boolean") {
|
|
1211
|
+
return html`<sp-switch
|
|
1212
|
+
size="s"
|
|
1213
|
+
?checked=${!!value}
|
|
1214
|
+
@change=${(/** @type {Event} */ e) =>
|
|
1215
|
+
onChange(/** @type {HTMLInputElement} */ (e.target).checked)}
|
|
1216
|
+
>${key}</sp-switch
|
|
1217
|
+
>`;
|
|
1218
|
+
}
|
|
1219
|
+
if (schema.type === "integer" || schema.type === "number") {
|
|
1220
|
+
return html`<sp-number-field
|
|
1221
|
+
size="s"
|
|
1222
|
+
label=${key}
|
|
1223
|
+
.value=${value !== undefined ? value : nothing}
|
|
1224
|
+
step=${schema.type === "integer" ? "1" : nothing}
|
|
1225
|
+
@change=${(/** @type {Event} */ e) => {
|
|
1226
|
+
const parsed =
|
|
1227
|
+
schema.type === "integer"
|
|
1228
|
+
? parseInt(/** @type {HTMLInputElement} */ (e.target).value, 10)
|
|
1229
|
+
: parseFloat(/** @type {HTMLInputElement} */ (e.target).value);
|
|
1230
|
+
onChange(isNaN(parsed) ? undefined : parsed);
|
|
1231
|
+
}}
|
|
1232
|
+
></sp-number-field>`;
|
|
1233
|
+
}
|
|
1234
|
+
return html`<sp-textfield
|
|
1235
|
+
size="s"
|
|
1236
|
+
label=${key}
|
|
1237
|
+
placeholder=${key}
|
|
1238
|
+
.value=${value ?? ""}
|
|
1239
|
+
@input=${(/** @type {Event} */ e) =>
|
|
1240
|
+
onChange(/** @type {HTMLInputElement} */ (e.target).value || undefined)}
|
|
1241
|
+
></sp-textfield>`;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1210
1244
|
/**
|
|
1211
1245
|
* Render config form fields from a JSON Schema `properties` object. Maps schema types to
|
|
1212
1246
|
* appropriate form controls.
|
|
@@ -1216,19 +1250,21 @@ export function renderSchemaFieldsTemplate(
|
|
|
1216
1250
|
/** @type {SignalDef} */ def,
|
|
1217
1251
|
/** @type {string} */ name,
|
|
1218
1252
|
/** @type {SignalsPanelState} */ _S,
|
|
1253
|
+
/** @type {SignalsPanelCtx | null} */ ctx = null,
|
|
1219
1254
|
) {
|
|
1220
1255
|
if (!schema?.properties) return nothing;
|
|
1221
1256
|
|
|
1222
1257
|
const required = new Set(schema.required ?? []);
|
|
1223
1258
|
|
|
1224
|
-
|
|
1259
|
+
const propertyFields = Object.entries(schema.properties)
|
|
1225
1260
|
.filter(([prop]) => !STUDIO_RESERVED_KEYS.has(prop))
|
|
1226
1261
|
.map(([prop, ps]) => {
|
|
1227
1262
|
const currentValue = def[prop];
|
|
1228
1263
|
const labelText = prop + (required.has(prop) ? " *" : "");
|
|
1229
1264
|
|
|
1230
1265
|
let control;
|
|
1231
|
-
|
|
1266
|
+
const enumValues = resolveSchemaEnum(ps.enum, def);
|
|
1267
|
+
if (enumValues) {
|
|
1232
1268
|
control = html`
|
|
1233
1269
|
<sp-picker
|
|
1234
1270
|
size="s"
|
|
@@ -1247,7 +1283,7 @@ export function renderSchemaFieldsTemplate(
|
|
|
1247
1283
|
)}
|
|
1248
1284
|
>
|
|
1249
1285
|
${!required.has(prop) ? html`<sp-menu-item value="__none__">—</sp-menu-item>` : nothing}
|
|
1250
|
-
${
|
|
1286
|
+
${enumValues.map(
|
|
1251
1287
|
(/** @type {string} */ val) => html`<sp-menu-item value=${val}>${val}</sp-menu-item>`,
|
|
1252
1288
|
)}
|
|
1253
1289
|
</sp-picker>
|
|
@@ -1271,7 +1307,7 @@ export function renderSchemaFieldsTemplate(
|
|
|
1271
1307
|
max=${ifDefined(ps.maximum)}
|
|
1272
1308
|
step=${ps.type === "integer" ? "1" : nothing}
|
|
1273
1309
|
.value=${currentValue !== undefined ? currentValue : nothing}
|
|
1274
|
-
placeholder=${ps.default
|
|
1310
|
+
placeholder=${ps.default != null ? String(ps.default) : nothing}
|
|
1275
1311
|
@change=${(/** @type {Event} */ e) => {
|
|
1276
1312
|
clearTimeout(debounce);
|
|
1277
1313
|
debounce = setTimeout(() => {
|
|
@@ -1335,6 +1371,73 @@ export function renderSchemaFieldsTemplate(
|
|
|
1335
1371
|
></sp-textfield>
|
|
1336
1372
|
</div>
|
|
1337
1373
|
`;
|
|
1374
|
+
} else if (ps.type === "array" && ps.items?.type === "object" && ps.items?.properties) {
|
|
1375
|
+
// Array of objects with defined schema → multi-row inline form
|
|
1376
|
+
const rows = Array.isArray(currentValue) ? currentValue : [];
|
|
1377
|
+
const itemProps = /** @type {Record<string, Record<string, unknown>>} */ (
|
|
1378
|
+
ps.items.properties
|
|
1379
|
+
);
|
|
1380
|
+
control = html`
|
|
1381
|
+
<div class="array-object-field">
|
|
1382
|
+
${rows.map(
|
|
1383
|
+
(/** @type {Record<string, unknown>} */ row, /** @type {number} */ idx) => html`
|
|
1384
|
+
<div
|
|
1385
|
+
class="array-object-row"
|
|
1386
|
+
style="display:flex;gap:4px;align-items:center;margin-bottom:4px"
|
|
1387
|
+
>
|
|
1388
|
+
${Object.entries(itemProps).map(([propKey, propSchema]) =>
|
|
1389
|
+
renderInlineField(
|
|
1390
|
+
propKey,
|
|
1391
|
+
propSchema,
|
|
1392
|
+
row[propKey],
|
|
1393
|
+
(val) => {
|
|
1394
|
+
const updated = [...rows];
|
|
1395
|
+
updated[idx] = { ...updated[idx], [propKey]: val };
|
|
1396
|
+
transactDoc(activeTab.value, (t) =>
|
|
1397
|
+
mutateUpdateDef(t, name, { [prop]: updated }),
|
|
1398
|
+
);
|
|
1399
|
+
},
|
|
1400
|
+
def,
|
|
1401
|
+
),
|
|
1402
|
+
)}
|
|
1403
|
+
<sp-action-button
|
|
1404
|
+
quiet
|
|
1405
|
+
size="s"
|
|
1406
|
+
@click=${() => {
|
|
1407
|
+
const updated = rows.filter(
|
|
1408
|
+
(/** @type {unknown} */ _, /** @type {number} */ i) => i !== idx,
|
|
1409
|
+
);
|
|
1410
|
+
transactDoc(activeTab.value, (t) =>
|
|
1411
|
+
mutateUpdateDef(t, name, { [prop]: updated.length ? updated : undefined }),
|
|
1412
|
+
);
|
|
1413
|
+
ctx?.renderLeftPanel();
|
|
1414
|
+
}}
|
|
1415
|
+
>
|
|
1416
|
+
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
1417
|
+
</sp-action-button>
|
|
1418
|
+
</div>
|
|
1419
|
+
`,
|
|
1420
|
+
)}
|
|
1421
|
+
<sp-action-button
|
|
1422
|
+
quiet
|
|
1423
|
+
size="s"
|
|
1424
|
+
@click=${(/** @type {Event} */ e) => {
|
|
1425
|
+
e.stopPropagation();
|
|
1426
|
+
/** @type {Record<string, unknown>} */
|
|
1427
|
+
const newRow = {};
|
|
1428
|
+
for (const [k, v] of Object.entries(itemProps)) {
|
|
1429
|
+
if (/** @type {Record<string, unknown>} */ (v).default !== undefined)
|
|
1430
|
+
newRow[k] = /** @type {Record<string, unknown>} */ (v).default;
|
|
1431
|
+
}
|
|
1432
|
+
transactDoc(activeTab.value, (t) =>
|
|
1433
|
+
mutateUpdateDef(t, name, { [prop]: [...rows, newRow] }),
|
|
1434
|
+
);
|
|
1435
|
+
ctx?.renderLeftPanel();
|
|
1436
|
+
}}
|
|
1437
|
+
>+ Add</sp-action-button
|
|
1438
|
+
>
|
|
1439
|
+
</div>
|
|
1440
|
+
`;
|
|
1338
1441
|
} else if (ps.type === "array" || ps.type === "object") {
|
|
1339
1442
|
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
1340
1443
|
let debounce;
|
|
@@ -1388,6 +1491,8 @@ export function renderSchemaFieldsTemplate(
|
|
|
1388
1491
|
widget: control,
|
|
1389
1492
|
});
|
|
1390
1493
|
});
|
|
1494
|
+
|
|
1495
|
+
return html`${propertyFields}`;
|
|
1391
1496
|
}
|
|
1392
1497
|
|
|
1393
1498
|
/**
|
|
@@ -1403,8 +1508,12 @@ export function renderExternalPrototypeEditorTemplate(
|
|
|
1403
1508
|
// Schema-driven config fields (async with cache)
|
|
1404
1509
|
/** @type {import("lit-html").TemplateResult | typeof nothing} */
|
|
1405
1510
|
let schemaContent = nothing;
|
|
1406
|
-
|
|
1407
|
-
|
|
1511
|
+
const importedPath = def.$prototype
|
|
1512
|
+
? projectState?.projectConfig?.imports?.[def.$prototype]
|
|
1513
|
+
: null;
|
|
1514
|
+
const resolvedSrc = def.$src || importedPath;
|
|
1515
|
+
if (resolvedSrc && def.$prototype) {
|
|
1516
|
+
const cacheKey = `${resolvedSrc}::${def.$prototype}`;
|
|
1408
1517
|
if (pluginSchemaCache.has(cacheKey)) {
|
|
1409
1518
|
const schema = pluginSchemaCache.get(cacheKey);
|
|
1410
1519
|
if (schema) {
|
|
@@ -1412,7 +1521,7 @@ export function renderExternalPrototypeEditorTemplate(
|
|
|
1412
1521
|
${schema.description
|
|
1413
1522
|
? html`<div class="signal-hint" style="padding:4px 0 8px">${schema.description}</div>`
|
|
1414
1523
|
: nothing}
|
|
1415
|
-
${renderSchemaFieldsTemplate(schema, def, name, S)}
|
|
1524
|
+
${renderSchemaFieldsTemplate(schema, def, name, S, ctx)}
|
|
1416
1525
|
`;
|
|
1417
1526
|
}
|
|
1418
1527
|
} else {
|
|
@@ -1429,14 +1538,22 @@ export function renderExternalPrototypeEditorTemplate(
|
|
|
1429
1538
|
}
|
|
1430
1539
|
|
|
1431
1540
|
return html`
|
|
1432
|
-
${
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1541
|
+
${importedPath
|
|
1542
|
+
? html`<div class="signal-hint" style="padding:4px 0 2px;font-size:11px;color:var(--fg-dim)">
|
|
1543
|
+
${def.$prototype}
|
|
1544
|
+
</div>`
|
|
1545
|
+
: html`
|
|
1546
|
+
${signalFieldRow("Source", def.$src || "", (/** @type {string} */ v) => {
|
|
1547
|
+
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { $src: v || undefined }));
|
|
1548
|
+
pluginSchemaCache.delete(`${v}::${def.$prototype}`);
|
|
1549
|
+
})}
|
|
1550
|
+
${signalFieldRow("Prototype", def.$prototype || "", (/** @type {string} */ v) => {
|
|
1551
|
+
transactDoc(activeTab.value, (t) =>
|
|
1552
|
+
mutateUpdateDef(t, name, { $prototype: v || undefined }),
|
|
1553
|
+
);
|
|
1554
|
+
pluginSchemaCache.delete(`${def.$src}::${v}`);
|
|
1555
|
+
})}
|
|
1556
|
+
`}
|
|
1440
1557
|
${def.$export
|
|
1441
1558
|
? signalFieldRow("Export", def.$export || "", (/** @type {string} */ v) =>
|
|
1442
1559
|
transactDoc(activeTab.value, (t) =>
|
|
@@ -592,6 +592,50 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector, effectiveSty
|
|
|
592
592
|
},
|
|
593
593
|
);
|
|
594
594
|
|
|
595
|
+
const isOpen = isFiltering ? true : (tab.session.ui.styleSections[sec.key] ?? false);
|
|
596
|
+
|
|
597
|
+
if (!isOpen) {
|
|
598
|
+
return html`
|
|
599
|
+
<sp-accordion-item
|
|
600
|
+
label=${sec.label}
|
|
601
|
+
.open=${false}
|
|
602
|
+
@sp-accordion-item-toggle=${(/** @type {Event} */ e) => {
|
|
603
|
+
activeTab.value.session.ui.styleSections = {
|
|
604
|
+
...activeTab.value.session.ui.styleSections,
|
|
605
|
+
[sec.key]: /** @type {HTMLElement & { open: boolean }} */ (e.target).open,
|
|
606
|
+
};
|
|
607
|
+
}}
|
|
608
|
+
>
|
|
609
|
+
${sectionActiveProps.length > 0
|
|
610
|
+
? html`
|
|
611
|
+
<span slot="heading" style="display:flex;align-items:center;gap:6px">
|
|
612
|
+
${sec.label}
|
|
613
|
+
<span
|
|
614
|
+
class="set-dot set-dot--section"
|
|
615
|
+
title="Clear all ${sec.label.toLowerCase()} properties"
|
|
616
|
+
@click=${(/** @type {Event} */ e) => {
|
|
617
|
+
e.stopPropagation();
|
|
618
|
+
e.preventDefault();
|
|
619
|
+
transactDoc(activeTab.value, (t) => {
|
|
620
|
+
for (const { prop, entry } of sectionActiveProps) {
|
|
621
|
+
if (activeStyle[prop] !== undefined) commitMutate(t, prop, undefined);
|
|
622
|
+
if (inferInputType(entry) === "shorthand") {
|
|
623
|
+
for (const l of /** @type {CssLonghand[]} */ (getLonghands(prop))) {
|
|
624
|
+
if (activeStyle[l.name] !== undefined)
|
|
625
|
+
commitMutate(t, l.name, undefined);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
}}
|
|
631
|
+
></span>
|
|
632
|
+
</span>
|
|
633
|
+
`
|
|
634
|
+
: nothing}
|
|
635
|
+
</sp-accordion-item>
|
|
636
|
+
`;
|
|
637
|
+
}
|
|
638
|
+
|
|
595
639
|
const rows = [];
|
|
596
640
|
for (const { prop, entry } of entries) {
|
|
597
641
|
const val = activeStyle[prop];
|
|
@@ -644,7 +688,6 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector, effectiveSty
|
|
|
644
688
|
}
|
|
645
689
|
|
|
646
690
|
if (isFiltering && rows.length === 0) return nothing;
|
|
647
|
-
const isOpen = isFiltering ? true : (tab.session.ui.styleSections[sec.key] ?? false);
|
|
648
691
|
|
|
649
692
|
return html`
|
|
650
693
|
<sp-accordion-item
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** OXC code services (server-backed) */
|
|
2
2
|
|
|
3
3
|
import { getPlatform } from "../platform.js";
|
|
4
|
+
import { projectState } from "../state.js";
|
|
4
5
|
import { getNodeAtPath } from "../store.js";
|
|
5
6
|
import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
|
|
6
7
|
|
|
@@ -29,14 +30,19 @@ export async function locateDocument(name) {
|
|
|
29
30
|
export const pluginSchemaCache = new Map();
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
|
-
* Fetch and cache the schema for
|
|
33
|
+
* Fetch and cache the schema for a $prototype module via the server. Works for both external
|
|
34
|
+
* prototypes (with $src) and prototypes resolved from project.json imports.
|
|
33
35
|
*
|
|
34
36
|
* @param {{ $src?: string; $prototype?: string }} def
|
|
35
37
|
* @param {{ documentPath?: string }} state
|
|
36
38
|
*/
|
|
37
39
|
export async function fetchPluginSchema(def, state) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
const importedPath = def.$prototype
|
|
41
|
+
? projectState?.projectConfig?.imports?.[def.$prototype]
|
|
42
|
+
: null;
|
|
43
|
+
const src = def.$src || importedPath;
|
|
44
|
+
if (!src || !def.$prototype) return null;
|
|
45
|
+
const cacheKey = `${src}::${def.$prototype}`;
|
|
40
46
|
if (pluginSchemaCache.has(cacheKey)) return pluginSchemaCache.get(cacheKey);
|
|
41
47
|
|
|
42
48
|
try {
|
|
@@ -45,8 +51,9 @@ export async function fetchPluginSchema(def, state) {
|
|
|
45
51
|
pluginSchemaCache.set(cacheKey, null);
|
|
46
52
|
return null;
|
|
47
53
|
}
|
|
48
|
-
const base =
|
|
49
|
-
|
|
54
|
+
const base =
|
|
55
|
+
!importedPath && state.documentPath ? `${location.origin}/${state.documentPath}` : undefined;
|
|
56
|
+
const schema = await platform.fetchPluginSchema(src, def.$prototype, base);
|
|
50
57
|
pluginSchemaCache.set(cacheKey, schema);
|
|
51
58
|
return schema;
|
|
52
59
|
} catch {
|
|
@@ -37,7 +37,7 @@ export function renderHeadEditor(container) {
|
|
|
37
37
|
} else if (tag === "script") {
|
|
38
38
|
attrs.src = "";
|
|
39
39
|
} else if (tag === "style") {
|
|
40
|
-
entry.
|
|
40
|
+
entry.textContent = "";
|
|
41
41
|
}
|
|
42
42
|
headEntries.push(entry);
|
|
43
43
|
save();
|
|
@@ -57,7 +57,7 @@ export function renderHeadEditor(container) {
|
|
|
57
57
|
) => {
|
|
58
58
|
const entry = headEntries[idx];
|
|
59
59
|
if (key === "content" && (entry.tagName === "script" || entry.tagName === "style")) {
|
|
60
|
-
entry.
|
|
60
|
+
entry.textContent = val;
|
|
61
61
|
} else {
|
|
62
62
|
if (!entry.attributes) entry.attributes = {};
|
|
63
63
|
entry.attributes[key] = val;
|
|
@@ -180,7 +180,7 @@ function renderEntryFields(entry, idx, updateEntry) {
|
|
|
180
180
|
<label class="settings-field-label">Script body</label>
|
|
181
181
|
<textarea
|
|
182
182
|
class="head-code-editor"
|
|
183
|
-
.value=${entry.
|
|
183
|
+
.value=${entry.textContent || ""}
|
|
184
184
|
@input=${onFieldChange("content")}
|
|
185
185
|
rows="6"
|
|
186
186
|
spellcheck="false"
|
|
@@ -195,7 +195,7 @@ function renderEntryFields(entry, idx, updateEntry) {
|
|
|
195
195
|
<label class="settings-field-label">Style body</label>
|
|
196
196
|
<textarea
|
|
197
197
|
class="head-code-editor"
|
|
198
|
-
.value=${entry.
|
|
198
|
+
.value=${entry.textContent || ""}
|
|
199
199
|
@input=${onFieldChange("content")}
|
|
200
200
|
rows="8"
|
|
201
201
|
spellcheck="false"
|
package/src/tabs/transact.js
CHANGED
|
@@ -227,6 +227,7 @@ export function mutateUpdateAttribute(tab, path, attr, value) {
|
|
|
227
227
|
* @param {string | undefined} value
|
|
228
228
|
*/
|
|
229
229
|
export function mutateUpdateMediaStyle(tab, path, mediaName, prop, value) {
|
|
230
|
+
if (!mediaName) return mutateUpdateStyle(tab, path, prop, value);
|
|
230
231
|
const node = getNodeAtPath(tab.doc.document, path);
|
|
231
232
|
if (!node.style) node.style = {};
|
|
232
233
|
const key = `@${mediaName}`;
|