@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.
@@ -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
- return Object.entries(schema.properties)
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
- if (ps.enum) {
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
- ${ps.enum.map(
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 !== undefined ? String(ps.default) : nothing}
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
- if (def.$src && def.$prototype) {
1407
- const cacheKey = `${def.$src}::${def.$prototype}`;
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
- ${signalFieldRow("Source", def.$src || "", (/** @type {string} */ v) => {
1433
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { $src: v || undefined }));
1434
- pluginSchemaCache.delete(`${v}::${def.$prototype}`);
1435
- })}
1436
- ${signalFieldRow("Prototype", def.$prototype || "", (/** @type {string} */ v) => {
1437
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { $prototype: v || undefined }));
1438
- pluginSchemaCache.delete(`${def.$src}::${v}`);
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 an external $prototype + $src module via the server.
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
- if (!def.$src || !def.$prototype) return null;
39
- const cacheKey = `${def.$src}::${def.$prototype}`;
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 = state.documentPath ? `${location.origin}/${state.documentPath}` : undefined;
49
- const schema = await platform.fetchPluginSchema(def.$src, def.$prototype, base);
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.content = "";
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.content = val;
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.content || ""}
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.content || ""}
198
+ .value=${entry.textContent || ""}
199
199
  @input=${onFieldChange("content")}
200
200
  rows="8"
201
201
  spellcheck="false"
@@ -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}`;