@jxsuite/studio 0.10.1 → 0.10.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,35 +30,35 @@
30
30
  "dependencies": {
31
31
  "@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
32
32
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
33
- "@jxsuite/runtime": "^0.10.0",
34
- "@spectrum-web-components/accordion": "^1.12.0",
35
- "@spectrum-web-components/action-bar": "1.12.0",
36
- "@spectrum-web-components/action-button": "^1.12.0",
37
- "@spectrum-web-components/action-group": "^1.12.0",
38
- "@spectrum-web-components/checkbox": "^1.12.0",
39
- "@spectrum-web-components/color-area": "^1.12.0",
40
- "@spectrum-web-components/color-handle": "^1.12.0",
41
- "@spectrum-web-components/color-slider": "^1.12.0",
42
- "@spectrum-web-components/combobox": "^1.12.0",
43
- "@spectrum-web-components/dialog": "^1.12.0",
44
- "@spectrum-web-components/divider": "^1.12.0",
45
- "@spectrum-web-components/icon": "^1.12.0",
46
- "@spectrum-web-components/icons-workflow": "^1.12.0",
47
- "@spectrum-web-components/menu": "^1.12.0",
48
- "@spectrum-web-components/number-field": "^1.12.0",
49
- "@spectrum-web-components/overlay": "^1.12.0",
33
+ "@jxsuite/runtime": "^0.10.1",
34
+ "@spectrum-web-components/accordion": "^1.12.1",
35
+ "@spectrum-web-components/action-bar": "1.12.1",
36
+ "@spectrum-web-components/action-button": "^1.12.1",
37
+ "@spectrum-web-components/action-group": "^1.12.1",
38
+ "@spectrum-web-components/checkbox": "^1.12.1",
39
+ "@spectrum-web-components/color-area": "^1.12.1",
40
+ "@spectrum-web-components/color-handle": "^1.12.1",
41
+ "@spectrum-web-components/color-slider": "^1.12.1",
42
+ "@spectrum-web-components/combobox": "^1.12.1",
43
+ "@spectrum-web-components/dialog": "^1.12.1",
44
+ "@spectrum-web-components/divider": "^1.12.1",
45
+ "@spectrum-web-components/icon": "^1.12.1",
46
+ "@spectrum-web-components/icons-workflow": "^1.12.1",
47
+ "@spectrum-web-components/menu": "^1.12.1",
48
+ "@spectrum-web-components/number-field": "^1.12.1",
49
+ "@spectrum-web-components/overlay": "^1.12.1",
50
50
  "@spectrum-web-components/overlay-trigger": "^0.1.7",
51
- "@spectrum-web-components/picker": "^1.12.0",
52
- "@spectrum-web-components/picker-button": "^1.12.0",
53
- "@spectrum-web-components/popover": "^1.12.0",
54
- "@spectrum-web-components/search": "^1.12.0",
55
- "@spectrum-web-components/swatch": "^1.12.0",
56
- "@spectrum-web-components/switch": "^1.12.0",
57
- "@spectrum-web-components/table": "^1.12.0",
58
- "@spectrum-web-components/tabs": "^1.12.0",
59
- "@spectrum-web-components/textfield": "^1.12.0",
60
- "@spectrum-web-components/theme": "^1.12.0",
61
- "@spectrum-web-components/tooltip": "^1.12.0",
51
+ "@spectrum-web-components/picker": "^1.12.1",
52
+ "@spectrum-web-components/picker-button": "^1.12.1",
53
+ "@spectrum-web-components/popover": "^1.12.1",
54
+ "@spectrum-web-components/search": "^1.12.1",
55
+ "@spectrum-web-components/swatch": "^1.12.1",
56
+ "@spectrum-web-components/switch": "^1.12.1",
57
+ "@spectrum-web-components/table": "^1.12.1",
58
+ "@spectrum-web-components/tabs": "^1.12.1",
59
+ "@spectrum-web-components/textfield": "^1.12.1",
60
+ "@spectrum-web-components/theme": "^1.12.1",
61
+ "@spectrum-web-components/tooltip": "^1.12.1",
62
62
  "lit-html": "^3.3.3",
63
63
  "monaco-editor": "^0.55.1",
64
64
  "remark-directive": "^4.0.0",
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Displays pages, layouts, components, content, and media in a filterable table grid. Fills the
5
5
  * center canvas area as a parallel state to Edit/Design/Preview/Code/Settings. Includes a "New +"
6
- * button with type-aware entity creation (including collections from project.json).
6
+ * button with type-aware entity creation (including content types from project.json).
7
7
  */
8
8
 
9
9
  import { html, render as litRender } from "lit-html";
@@ -153,14 +153,14 @@ const ENTITY_TYPES = [
153
153
  ];
154
154
 
155
155
  /**
156
- * Build frontmatter YAML from a collection's schema properties.
156
+ * Build frontmatter YAML from a content type's schema properties.
157
157
  *
158
- * @param {string} collectionName
158
+ * @param {string} contentTypeName
159
159
  * @returns {string}
160
160
  */
161
- function buildFrontmatterYaml(collectionName) {
161
+ function buildFrontmatterYaml(contentTypeName) {
162
162
  const config = projectState?.projectConfig;
163
- const col = config?.collections?.[collectionName];
163
+ const col = config?.contentTypes?.[contentTypeName];
164
164
  if (!col?.schema?.properties) return "title: Untitled\n";
165
165
 
166
166
  let yaml = "";
@@ -172,22 +172,22 @@ function buildFrontmatterYaml(collectionName) {
172
172
  }
173
173
 
174
174
  /**
175
- * Get collection-derived entity types from project config.
175
+ * Get content-type-derived entity types from project config.
176
176
  *
177
- * @returns {{ key: string; label: string; dir: string; ext: string; collectionName: string }[]}
177
+ * @returns {{ key: string; label: string; dir: string; ext: string; contentTypeName: string }[]}
178
178
  */
179
- function getCollectionTypes() {
179
+ function getContentTypeTypes() {
180
180
  const config = projectState?.projectConfig;
181
- if (!config?.collections) return [];
182
- return Object.entries(config.collections).map(([name, def]) => {
181
+ if (!config?.contentTypes) return [];
182
+ return Object.entries(config.contentTypes).map(([name, def]) => {
183
183
  const d = /** @type {any} */ (def);
184
184
  const dir = d.source ? d.source.replace(/^\.\//, "").split("/")[0] : name;
185
185
  return {
186
- key: `collection:${name}`,
186
+ key: `contentType:${name}`,
187
187
  label: name.charAt(0).toUpperCase() + name.slice(1),
188
188
  dir,
189
189
  ext: ".md",
190
- collectionName: name,
190
+ contentTypeName: name,
191
191
  };
192
192
  });
193
193
  }
@@ -200,9 +200,9 @@ function getCollectionTypes() {
200
200
  * @param {{ openFile: (path: string) => void }} ctx
201
201
  */
202
202
  async function handleNewEntity(typeKey, container, ctx) {
203
- const isCollection = typeKey.startsWith("collection:");
204
- const collectionName = isCollection ? typeKey.slice("collection:".length) : null;
205
- const allTypes = [...ENTITY_TYPES, ...getCollectionTypes()];
203
+ const isContentType = typeKey.startsWith("contentType:");
204
+ const contentTypeName = isContentType ? typeKey.slice("contentType:".length) : null;
205
+ const allTypes = [...ENTITY_TYPES, ...getContentTypeTypes()];
206
206
  const typeInfo = allTypes.find((t) => t.key === typeKey);
207
207
  if (!typeInfo) return;
208
208
 
@@ -217,7 +217,9 @@ async function handleNewEntity(typeKey, container, ctx) {
217
217
 
218
218
  let content;
219
219
  if (typeInfo.ext === ".md") {
220
- const frontmatter = collectionName ? buildFrontmatterYaml(collectionName) : "title: Untitled\n";
220
+ const frontmatter = contentTypeName
221
+ ? buildFrontmatterYaml(contentTypeName)
222
+ : "title: Untitled\n";
221
223
  content = `---\n${frontmatter}---\n\n`;
222
224
  } else {
223
225
  content = JSON.stringify({ tagName: "div", children: [] }, null, "\t");
@@ -278,7 +280,7 @@ export async function renderBrowse(container, ctx) {
278
280
 
279
281
  const files = filteredFiles();
280
282
 
281
- const collectionTypes = getCollectionTypes();
283
+ const contentTypeTypes = getContentTypeTypes();
282
284
 
283
285
  const filterBar = html`
284
286
  <div class="browse-filter-bar">
@@ -317,8 +319,8 @@ export async function renderBrowse(container, ctx) {
317
319
  @change=${(/** @type {any} */ e) => handleNewEntity(e.target.value, container, ctx)}
318
320
  >
319
321
  ${ENTITY_TYPES.map((t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`)}
320
- ${collectionTypes.length
321
- ? html`<sp-menu-divider></sp-menu-divider> ${collectionTypes.map(
322
+ ${contentTypeTypes.length
323
+ ? html`<sp-menu-divider></sp-menu-divider> ${contentTypeTypes.map(
322
324
  (t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`,
323
325
  )}`
324
326
  : ""}
@@ -127,7 +127,13 @@ export async function renderCanvasLive(gen, doc, canvasEl) {
127
127
  console.warn("Studio: failed to import package", entry, e);
128
128
  }
129
129
  } else if (entry?.$ref) {
130
- const href = new URL(entry.$ref, docBase).href;
130
+ let href;
131
+ try {
132
+ href = new URL(entry.$ref, docBase).href;
133
+ } catch (/** @type {any} */ urlErr) {
134
+ console.warn("Studio: invalid element URL", { ref: entry.$ref, docBase }, urlErr);
135
+ continue;
136
+ }
131
137
  try {
132
138
  await defineElement(href);
133
139
  } catch (/** @type {any} */ e) {
@@ -66,12 +66,15 @@ export function renderCanvas() {
66
66
  // Advance render generation so stale async renders from the previous cycle bail out
67
67
  ++view.renderGeneration;
68
68
 
69
- // Always clear Lit's internal state so it builds fresh DOM. Stale async
70
- // renderCanvasLive calls from a previous cycle can corrupt nested ChildPart
71
- // markers (Comment nodes inside panzoom-wrap) in ways the root-only
72
- // ensureLitState check cannot detect.
69
+ // Detect whether this is a mode transition or a content-only re-render
70
+ const modeChanged = canvasMode !== view.prevCanvasMode;
71
+
72
+ // Only clear Lit's internal state on mode transitions (structural panel changes).
73
+ // For content re-renders in the same mode, Lit's template diffing preserves
74
+ // the panel structure. Bailed async renders can't corrupt the DOM because
75
+ // renderCanvasLive uses atomic clear (innerHTML = "" right before appendChild).
73
76
  // @ts-ignore
74
- if (canvasWrap["_$litPart$"]) {
77
+ if (modeChanged && canvasWrap["_$litPart$"]) {
75
78
  canvasWrap.textContent = "";
76
79
  // @ts-ignore
77
80
  delete canvasWrap["_$litPart$"];
@@ -101,7 +104,6 @@ export function renderCanvas() {
101
104
  }
102
105
 
103
106
  // Detect whether this is a mode transition or a content-only re-render
104
- const modeChanged = canvasMode !== view.prevCanvasMode;
105
107
  view.prevCanvasMode = canvasMode;
106
108
 
107
109
  // DnD handlers are registered on inner canvas elements that get replaced on every
@@ -564,6 +564,8 @@ export async function openFileFromTree(ctx, path) {
564
564
  if (path.endsWith(".md")) {
565
565
  await ctx.loadMarkdown(content, null);
566
566
  ctx.S.documentPath = path;
567
+ ctx.S.dirty = false;
568
+ ctx.commit(ctx.S);
567
569
  } else {
568
570
  const doc = JSON.parse(content);
569
571
  const newS = createState(doc);
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * When editing project.json: shows Class Imports, Dependencies (add/remove packages), and
5
5
  * per-package component toggles for cherry-picking individual elements. When editing a
6
- * page/layout/component/collection: shows Component Imports ($ref picker) and per-package component
7
- * toggles.
6
+ * page/layout/component/content type: shows Component Imports ($ref picker) and per-package
7
+ * component toggles.
8
8
  */
9
9
 
10
10
  import { html, nothing } from "lit-html";
@@ -27,7 +27,7 @@ import { renderFieldRow } from "../ui/field-row.js";
27
27
  import {
28
28
  attrLabel,
29
29
  inferInputType,
30
- findCollectionSchema,
30
+ findContentTypeSchema,
31
31
  friendlyNameToVar,
32
32
  camelToLabel,
33
33
  parseCemType,
@@ -210,7 +210,7 @@ function kvRow(
210
210
  function renderFrontmatterOnlyPanel() {
211
211
  const S = getState();
212
212
  const fm = S.content?.frontmatter || {};
213
- const col = findCollectionSchema(S.documentPath, projectState?.projectConfig);
213
+ const col = findContentTypeSchema(S.documentPath, projectState?.projectConfig);
214
214
  const schemaProps = col?.schema?.properties;
215
215
  const requiredFields = new Set(col?.schema?.required || []);
216
216
 
@@ -1276,7 +1276,7 @@ export function renderPropertiesPanelTemplate(ctx) {
1276
1276
  S.mode === "content"
1277
1277
  ? (() => {
1278
1278
  const fm = S.content?.frontmatter || {};
1279
- const col = findCollectionSchema(S.documentPath, projectState?.projectConfig);
1279
+ const col = findContentTypeSchema(S.documentPath, projectState?.projectConfig);
1280
1280
  const schemaProps = col?.schema?.properties;
1281
1281
  const requiredFields = new Set(col?.schema?.required || []);
1282
1282
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Stylebook panel — renders the Settings mode canvas (element catalog, design variables,
3
- * definitions, collections). Extracted from studio.js Phase 4e.
3
+ * definitions, content types). Extracted from studio.js Phase 4e.
4
4
  */
5
5
 
6
6
  import { html, render as litRender, nothing } from "lit-html";
@@ -26,7 +26,7 @@ import { parseMediaEntries, activeBreakpointsForWidth } from "../utils/canvas-me
26
26
  import { mediaDisplayName } from "./shared.js";
27
27
  import { friendlyNameToVar, varDisplayName } from "../utils/studio-utils.js";
28
28
  import { renderDefsEditor } from "../settings/defs-editor.js";
29
- import { renderCollectionsEditor } from "../settings/collections-editor.js";
29
+ import { renderContentTypesEditor } from "../settings/content-types-editor.js";
30
30
  import stylebookMeta from "../../data/stylebook-meta.json";
31
31
 
32
32
  export { stylebookMeta };
@@ -67,12 +67,12 @@ export function renderStylebookMode(ctx) {
67
67
  >
68
68
  <sp-tab label="Stylebook" value="stylebook"></sp-tab>
69
69
  <sp-tab label="Definitions" value="definitions"></sp-tab>
70
- <sp-tab label="Collections" value="collections"></sp-tab>
70
+ <sp-tab label="Content Types" value="contentTypes"></sp-tab>
71
71
  </sp-tabs>
72
72
  </div>
73
73
  `;
74
74
 
75
- if (settingsTab === "definitions" || settingsTab === "collections") {
75
+ if (settingsTab === "definitions" || settingsTab === "contentTypes") {
76
76
  /** @type {any} */ (canvasWrap).style.overflow = "hidden";
77
77
 
78
78
  litRender(
@@ -88,7 +88,7 @@ export function renderStylebookMode(ctx) {
88
88
  canvasWrap.querySelector(".settings-editor-container")
89
89
  );
90
90
  if (settingsTab === "definitions") renderDefsEditor(container);
91
- else renderCollectionsEditor(container);
91
+ else renderContentTypesEditor(container);
92
92
  return;
93
93
  }
94
94
 
@@ -261,12 +261,6 @@ export function selectStylebookTag(tag, media) {
261
261
  },
262
262
  });
263
263
  renderStylebookOverlays();
264
- requestAnimationFrame(() => {
265
- if (canvasPanels.length > 0) {
266
- const el = findStylebookEl(canvasPanels[0].canvas, tag);
267
- if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
268
- }
269
- });
270
264
  }
271
265
 
272
266
  /** Draw selection + hover overlays for stylebook elements */
@@ -324,7 +318,7 @@ export function renderStylebookOverlays() {
324
318
  }
325
319
  }
326
320
 
327
- // ─── Internal helpers ─────────────────────────────────────────────────────────
321
+ // ─── Internal helpers (exported for testing) ─────────────────────────────────
328
322
 
329
323
  /**
330
324
  * Build a DOM element tree from a stylebook-meta.json entry.
@@ -332,8 +326,9 @@ export function renderStylebookOverlays() {
332
326
  * @param {any} entry
333
327
  * @param {any} rootStyle
334
328
  * @param {any} activeBreakpoints
329
+ * @param {string | null} [parentTag]
335
330
  */
336
- function buildStylebookElement(entry, rootStyle, activeBreakpoints) {
331
+ export function buildStylebookElement(entry, rootStyle, activeBreakpoints, parentTag = null) {
337
332
  const el = document.createElement(entry.tag);
338
333
  if (entry.text) el.textContent = entry.text;
339
334
  if (entry.attributes) {
@@ -344,7 +339,9 @@ function buildStylebookElement(entry, rootStyle, activeBreakpoints) {
344
339
  }
345
340
  }
346
341
  if (entry.style) el.style.cssText = entry.style;
347
- const tagStyle = rootStyle[`& ${entry.tag}`];
342
+ const compoundSelector =
343
+ parentTag && parentTag !== entry.tag ? `& ${parentTag} ${entry.tag}` : null;
344
+ const tagStyle = (compoundSelector && rootStyle[compoundSelector]) || rootStyle[`& ${entry.tag}`];
348
345
  if (tagStyle) {
349
346
  for (const [prop, val] of Object.entries(tagStyle)) {
350
347
  if (typeof val === "string" || typeof val === "number") {
@@ -372,7 +369,7 @@ function buildStylebookElement(entry, rootStyle, activeBreakpoints) {
372
369
  }
373
370
  if (entry.children) {
374
371
  for (const child of entry.children) {
375
- el.appendChild(buildStylebookElement(child, rootStyle, activeBreakpoints));
372
+ el.appendChild(buildStylebookElement(child, rootStyle, activeBreakpoints, entry.tag));
376
373
  }
377
374
  }
378
375
  return el;
@@ -429,13 +426,19 @@ function hasTagStyle(rootStyle, tag) {
429
426
  * @param {any} customizedOnly
430
427
  * @param {any} activeBreakpoints
431
428
  */
432
- function renderStylebookElementsIntoCanvas(
429
+ export function renderStylebookElementsIntoCanvas(
433
430
  canvasEl,
434
431
  rootStyle,
435
432
  filter,
436
433
  customizedOnly,
437
434
  activeBreakpoints,
438
435
  ) {
436
+ for (const [k, v] of Object.entries(rootStyle)) {
437
+ if (k.startsWith("--") && (typeof v === "string" || typeof v === "number")) {
438
+ canvasEl.style.setProperty(k, String(v));
439
+ }
440
+ }
441
+
439
442
  /** @type {import("lit-html").TemplateResult[]} */
440
443
  const sectionTemplates = [];
441
444
 
@@ -462,10 +465,11 @@ function renderStylebookElementsIntoCanvas(
462
465
  view.stylebookElToTag.set(card, entry.tag);
463
466
  elToPath.set(card, ["__sb", entry.tag]);
464
467
  for (const child of el.querySelectorAll("*")) {
465
- const tag = child.tagName.toLowerCase();
468
+ const childTag = child.tagName.toLowerCase();
466
469
  if (!view.stylebookElToTag.has(child)) {
467
- view.stylebookElToTag.set(child, tag);
468
- elToPath.set(child, ["__sb", tag]);
470
+ const compound = childTag === entry.tag ? entry.tag : `${entry.tag} ${childTag}`;
471
+ view.stylebookElToTag.set(child, compound);
472
+ elToPath.set(child, ["__sb", compound]);
469
473
  }
470
474
  }
471
475
  })}
@@ -473,7 +477,10 @@ function renderStylebookElementsIntoCanvas(
473
477
  <div
474
478
  class="element-card-preview"
475
479
  ${ref((c) => {
476
- if (c && !c.firstChild) c.appendChild(el);
480
+ if (c) {
481
+ c.textContent = "";
482
+ c.appendChild(el);
483
+ }
477
484
  })}
478
485
  ></div>
479
486
  <div class="element-card-label">&lt;${entry.tag}&gt;</div>
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Collections Editor — visual schema builder for project content collections.
2
+ * Content Types Editor — visual schema builder for project content types.
3
3
  *
4
- * Renders inside the Settings view "Collections" tab. Two-column layout: left column lists
5
- * collection names, right column edits the selected collection's schema.
4
+ * Renders inside the Settings view "Content Types" tab. Two-column layout: left column lists
5
+ * content type names, right column edits the selected content type's schema.
6
6
  */
7
7
 
8
8
  import { html, render as litRender } from "lit-html";
@@ -13,11 +13,11 @@ import { fieldCardTpl, addFieldFormTpl, schemaForType } from "./schema-field-ui.
13
13
  // ─── Module state ─────────────────────────────────────────────────────────────
14
14
 
15
15
  /** @type {string | null} */
16
- let selectedCollection = null;
16
+ let selectedContentType = null;
17
17
  let showAddField = false;
18
18
  let newFieldState = { name: "", type: "string", required: false };
19
- let showNewCollection = false;
20
- let newCollectionName = "";
19
+ let showNewContentType = false;
20
+ let newContentTypeName = "";
21
21
 
22
22
  // ─── Persistence ──────────────────────────────────────────────────────────────
23
23
 
@@ -29,17 +29,17 @@ async function saveProjectConfig() {
29
29
 
30
30
  // ─── Helpers ─────────────────────────────────────────────────────────────────
31
31
 
32
- /** Get the schema object for the selected collection. */
32
+ /** Get the schema object for the selected content type. */
33
33
  function getSelectedSchema() {
34
34
  const config = projectState?.projectConfig;
35
- return config?.collections?.[/** @type {string} */ (selectedCollection)]?.schema;
35
+ return config?.contentTypes?.[/** @type {string} */ (selectedContentType)]?.schema;
36
36
  }
37
37
 
38
38
  // ─── Handlers ─────────────────────────────────────────────────────────────────
39
39
 
40
40
  /** @param {() => void} rerender */
41
- function handleNewCollection(rerender) {
42
- const slug = newCollectionName
41
+ function handleNewContentType(rerender) {
42
+ const slug = newContentTypeName
43
43
  .toLowerCase()
44
44
  .replace(/\s+/g, "-")
45
45
  .replace(/[^a-z0-9-]/g, "");
@@ -47,30 +47,30 @@ function handleNewCollection(rerender) {
47
47
 
48
48
  const config = projectState?.projectConfig;
49
49
  if (!config) return;
50
- if (!config.collections) config.collections = {};
51
- if (config.collections[slug]) return; // already exists
50
+ if (!config.contentTypes) config.contentTypes = {};
51
+ if (config.contentTypes[slug]) return; // already exists
52
52
 
53
- config.collections[slug] = {
54
- source: `./${slug}/**/*.md`,
53
+ config.contentTypes[slug] = {
54
+ source: `./content/${slug}/**/*.md`,
55
55
  schema: { type: "object", properties: {}, required: [] },
56
56
  };
57
57
 
58
- selectedCollection = slug;
59
- showNewCollection = false;
60
- newCollectionName = "";
58
+ selectedContentType = slug;
59
+ showNewContentType = false;
60
+ newContentTypeName = "";
61
61
  rerender();
62
62
 
63
63
  // Persist in background
64
64
  saveProjectConfig().then(async () => {
65
65
  const platform = getPlatform();
66
- await platform.writeFile(`${slug}/.gitkeep`, "");
66
+ await platform.writeFile(`content/${slug}/.gitkeep`, "");
67
67
  });
68
68
  }
69
69
 
70
70
  /** @param {() => void} rerender */
71
71
  function handleAddField(rerender) {
72
72
  const name = newFieldState.name.trim();
73
- if (!name || !selectedCollection) return;
73
+ if (!name || !selectedContentType) return;
74
74
 
75
75
  const schema = getSelectedSchema();
76
76
  if (!schema) return;
@@ -270,13 +270,13 @@ function handleChangeNestedType(parentName, childName, newType, rerender) {
270
270
  }
271
271
 
272
272
  /** @param {() => void} rerender */
273
- function handleDeleteCollection(rerender) {
274
- if (!selectedCollection) return;
273
+ function handleDeleteContentType(rerender) {
274
+ if (!selectedContentType) return;
275
275
  const config = projectState?.projectConfig;
276
- if (!config?.collections?.[selectedCollection]) return;
276
+ if (!config?.contentTypes?.[selectedContentType]) return;
277
277
 
278
- delete config.collections[selectedCollection];
279
- selectedCollection = null;
278
+ delete config.contentTypes[selectedContentType];
279
+ selectedContentType = null;
280
280
 
281
281
  rerender();
282
282
  saveProjectConfig();
@@ -285,26 +285,26 @@ function handleDeleteCollection(rerender) {
285
285
  // ─── Render ───────────────────────────────────────────────────────────────────
286
286
 
287
287
  /**
288
- * Render the collections editor.
288
+ * Render the content types editor.
289
289
  *
290
290
  * @param {HTMLElement} container
291
291
  */
292
- export function renderCollectionsEditor(container) {
293
- const rerender = () => renderCollectionsEditor(container);
292
+ export function renderContentTypesEditor(container) {
293
+ const rerender = () => renderContentTypesEditor(container);
294
294
  const config = projectState?.projectConfig;
295
- const collections = config?.collections || {};
296
- const collectionNames = Object.keys(collections);
295
+ const contentTypes = config?.contentTypes || {};
296
+ const contentTypeNames = Object.keys(contentTypes);
297
297
 
298
- // Left column — collection list
298
+ // Left column — content type list
299
299
  const listTpl = html`
300
300
  <div class="settings-list-panel">
301
- ${collectionNames.map(
301
+ ${contentTypeNames.map(
302
302
  (name) => html`
303
303
  <sp-action-button
304
304
  size="s"
305
- ?selected=${selectedCollection === name}
305
+ ?selected=${selectedContentType === name}
306
306
  @click=${() => {
307
- selectedCollection = name;
307
+ selectedContentType = name;
308
308
  showAddField = false;
309
309
  rerender();
310
310
  }}
@@ -313,25 +313,25 @@ export function renderCollectionsEditor(container) {
313
313
  </sp-action-button>
314
314
  `,
315
315
  )}
316
- ${showNewCollection
316
+ ${showNewContentType
317
317
  ? html`
318
318
  <div class="settings-inline-form">
319
319
  <sp-textfield
320
320
  size="s"
321
- placeholder="collection-name"
322
- .value=${newCollectionName}
321
+ placeholder="content-type-name"
322
+ .value=${newContentTypeName}
323
323
  @input=${(/** @type {any} */ e) => {
324
- newCollectionName = e.target.value;
324
+ newContentTypeName = e.target.value;
325
325
  }}
326
326
  @keydown=${(/** @type {any} */ e) => {
327
- if (e.key === "Enter") handleNewCollection(rerender);
327
+ if (e.key === "Enter") handleNewContentType(rerender);
328
328
  if (e.key === "Escape") {
329
- showNewCollection = false;
329
+ showNewContentType = false;
330
330
  rerender();
331
331
  }
332
332
  }}
333
333
  ></sp-textfield>
334
- <sp-action-button size="s" @click=${() => handleNewCollection(rerender)}>
334
+ <sp-action-button size="s" @click=${() => handleNewContentType(rerender)}>
335
335
  Create
336
336
  </sp-action-button>
337
337
  </div>
@@ -341,11 +341,11 @@ export function renderCollectionsEditor(container) {
341
341
  size="s"
342
342
  quiet
343
343
  @click=${() => {
344
- showNewCollection = true;
344
+ showNewContentType = true;
345
345
  rerender();
346
346
  }}
347
347
  >
348
- <sp-icon-add slot="icon"></sp-icon-add> New Collection
348
+ <sp-icon-add slot="icon"></sp-icon-add> New Content Type
349
349
  </sp-action-button>
350
350
  `}
351
351
  </div>
@@ -353,10 +353,10 @@ export function renderCollectionsEditor(container) {
353
353
 
354
354
  // Right column — schema editor
355
355
  let editorTpl;
356
- if (!selectedCollection || !collections[selectedCollection]) {
357
- editorTpl = html`<div class="settings-empty-state">Select or create a collection</div>`;
356
+ if (!selectedContentType || !contentTypes[selectedContentType]) {
357
+ editorTpl = html`<div class="settings-empty-state">Select or create a content type</div>`;
358
358
  } else {
359
- const col = collections[selectedCollection];
359
+ const col = contentTypes[selectedContentType];
360
360
  const schema = col.schema || {};
361
361
  const properties = schema.properties || {};
362
362
  const required = schema.required || [];
@@ -381,13 +381,13 @@ export function renderCollectionsEditor(container) {
381
381
  editorTpl = html`
382
382
  <div class="settings-editor-panel">
383
383
  <div class="settings-editor-header">
384
- <h3>${selectedCollection}</h3>
384
+ <h3>${selectedContentType}</h3>
385
385
  <sp-field-label size="s">Source: ${col.source || "—"}</sp-field-label>
386
386
  <sp-action-button
387
387
  size="xs"
388
388
  quiet
389
- title="Delete collection"
390
- @click=${() => handleDeleteCollection(rerender)}
389
+ title="Delete content type"
390
+ @click=${() => handleDeleteContentType(rerender)}
391
391
  >
392
392
  <sp-icon-delete slot="icon"></sp-icon-delete>
393
393
  </sp-action-button>
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Schema field UI — shared field-card and add-field-dialog templates for the collections and
2
+ * Schema field UI — shared field-card and add-field-dialog templates for the content types and
3
3
  * definitions editors.
4
4
  */
5
5
 
package/src/state.js CHANGED
@@ -229,7 +229,7 @@ export function createState(doc) {
229
229
  stylebookTab: "elements", // "elements" | "variables"
230
230
  stylebookFilter: "", // search filter text
231
231
  stylebookCustomizedOnly: false, // show only customized elements
232
- settingsTab: "stylebook", // "stylebook" | "definitions" | "collections"
232
+ settingsTab: "stylebook", // "stylebook" | "definitions" | "contentTypes"
233
233
  gitStatus: null, // { branch, ahead, behind, files: [] }
234
234
  gitBranches: null, // { current, branches: [] }
235
235
  gitCommitMessage: "", // commit message input