@jxsuite/studio 0.10.1 → 0.11.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.
@@ -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;
@@ -163,6 +163,20 @@ function handleChangeType(fieldName, newType, rerender) {
163
163
  saveProjectConfig();
164
164
  }
165
165
 
166
+ /**
167
+ * @param {string} fieldName
168
+ * @param {string} target
169
+ * @param {() => void} rerender
170
+ */
171
+ function handleChangeRefTarget(fieldName, target, rerender) {
172
+ const schema = getSelectedSchema();
173
+ if (!schema?.properties) return;
174
+
175
+ schema.properties[fieldName] = { $ref: `#/contentTypes/${target}` };
176
+ rerender();
177
+ saveProjectConfig();
178
+ }
179
+
166
180
  // ─── Nested field handlers ───────────────────────────────────────────────────
167
181
 
168
182
  /**
@@ -270,13 +284,13 @@ function handleChangeNestedType(parentName, childName, newType, rerender) {
270
284
  }
271
285
 
272
286
  /** @param {() => void} rerender */
273
- function handleDeleteCollection(rerender) {
274
- if (!selectedCollection) return;
287
+ function handleDeleteContentType(rerender) {
288
+ if (!selectedContentType) return;
275
289
  const config = projectState?.projectConfig;
276
- if (!config?.collections?.[selectedCollection]) return;
290
+ if (!config?.contentTypes?.[selectedContentType]) return;
277
291
 
278
- delete config.collections[selectedCollection];
279
- selectedCollection = null;
292
+ delete config.contentTypes[selectedContentType];
293
+ selectedContentType = null;
280
294
 
281
295
  rerender();
282
296
  saveProjectConfig();
@@ -285,26 +299,26 @@ function handleDeleteCollection(rerender) {
285
299
  // ─── Render ───────────────────────────────────────────────────────────────────
286
300
 
287
301
  /**
288
- * Render the collections editor.
302
+ * Render the content types editor.
289
303
  *
290
304
  * @param {HTMLElement} container
291
305
  */
292
- export function renderCollectionsEditor(container) {
293
- const rerender = () => renderCollectionsEditor(container);
306
+ export function renderContentTypesEditor(container) {
307
+ const rerender = () => renderContentTypesEditor(container);
294
308
  const config = projectState?.projectConfig;
295
- const collections = config?.collections || {};
296
- const collectionNames = Object.keys(collections);
309
+ const contentTypes = config?.contentTypes || {};
310
+ const contentTypeNames = Object.keys(contentTypes);
297
311
 
298
- // Left column — collection list
312
+ // Left column — content type list
299
313
  const listTpl = html`
300
314
  <div class="settings-list-panel">
301
- ${collectionNames.map(
315
+ ${contentTypeNames.map(
302
316
  (name) => html`
303
317
  <sp-action-button
304
318
  size="s"
305
- ?selected=${selectedCollection === name}
319
+ ?selected=${selectedContentType === name}
306
320
  @click=${() => {
307
- selectedCollection = name;
321
+ selectedContentType = name;
308
322
  showAddField = false;
309
323
  rerender();
310
324
  }}
@@ -313,25 +327,25 @@ export function renderCollectionsEditor(container) {
313
327
  </sp-action-button>
314
328
  `,
315
329
  )}
316
- ${showNewCollection
330
+ ${showNewContentType
317
331
  ? html`
318
332
  <div class="settings-inline-form">
319
333
  <sp-textfield
320
334
  size="s"
321
- placeholder="collection-name"
322
- .value=${newCollectionName}
335
+ placeholder="content-type-name"
336
+ .value=${newContentTypeName}
323
337
  @input=${(/** @type {any} */ e) => {
324
- newCollectionName = e.target.value;
338
+ newContentTypeName = e.target.value;
325
339
  }}
326
340
  @keydown=${(/** @type {any} */ e) => {
327
- if (e.key === "Enter") handleNewCollection(rerender);
341
+ if (e.key === "Enter") handleNewContentType(rerender);
328
342
  if (e.key === "Escape") {
329
- showNewCollection = false;
343
+ showNewContentType = false;
330
344
  rerender();
331
345
  }
332
346
  }}
333
347
  ></sp-textfield>
334
- <sp-action-button size="s" @click=${() => handleNewCollection(rerender)}>
348
+ <sp-action-button size="s" @click=${() => handleNewContentType(rerender)}>
335
349
  Create
336
350
  </sp-action-button>
337
351
  </div>
@@ -341,11 +355,11 @@ export function renderCollectionsEditor(container) {
341
355
  size="s"
342
356
  quiet
343
357
  @click=${() => {
344
- showNewCollection = true;
358
+ showNewContentType = true;
345
359
  rerender();
346
360
  }}
347
361
  >
348
- <sp-icon-add slot="icon"></sp-icon-add> New Collection
362
+ <sp-icon-add slot="icon"></sp-icon-add> New Content Type
349
363
  </sp-action-button>
350
364
  `}
351
365
  </div>
@@ -353,10 +367,10 @@ export function renderCollectionsEditor(container) {
353
367
 
354
368
  // Right column — schema editor
355
369
  let editorTpl;
356
- if (!selectedCollection || !collections[selectedCollection]) {
357
- editorTpl = html`<div class="settings-empty-state">Select or create a collection</div>`;
370
+ if (!selectedContentType || !contentTypes[selectedContentType]) {
371
+ editorTpl = html`<div class="settings-empty-state">Select or create a content type</div>`;
358
372
  } else {
359
- const col = collections[selectedCollection];
373
+ const col = contentTypes[selectedContentType];
360
374
  const schema = col.schema || {};
361
375
  const properties = schema.properties || {};
362
376
  const required = schema.required || [];
@@ -367,6 +381,7 @@ export function renderCollectionsEditor(container) {
367
381
  onToggleRequired: (n) => handleToggleRequired(n, rerender),
368
382
  onRename: (oldN, newN) => handleRenameField(oldN, newN, rerender),
369
383
  onChangeType: (n, t) => handleChangeType(n, t, rerender),
384
+ onChangeRefTarget: (n, target) => handleChangeRefTarget(n, target, rerender),
370
385
  onAddNestedField: (p, s) => handleAddNestedField(p, s, rerender),
371
386
  onDeleteNested: (p, c) => handleDeleteNested(p, c, rerender),
372
387
  onToggleNestedRequired: (p, c) => handleToggleNestedRequired(p, c, rerender),
@@ -375,19 +390,25 @@ export function renderCollectionsEditor(container) {
375
390
  };
376
391
 
377
392
  const fieldCards = Object.entries(properties).map(([name, def]) =>
378
- fieldCardTpl(name, /** @type {any} */ (def), required.includes(name), handlers),
393
+ fieldCardTpl(
394
+ name,
395
+ /** @type {any} */ (def),
396
+ required.includes(name),
397
+ handlers,
398
+ contentTypeNames,
399
+ ),
379
400
  );
380
401
 
381
402
  editorTpl = html`
382
403
  <div class="settings-editor-panel">
383
404
  <div class="settings-editor-header">
384
- <h3>${selectedCollection}</h3>
405
+ <h3>${selectedContentType}</h3>
385
406
  <sp-field-label size="s">Source: ${col.source || "—"}</sp-field-label>
386
407
  <sp-action-button
387
408
  size="xs"
388
409
  quiet
389
- title="Delete collection"
390
- @click=${() => handleDeleteCollection(rerender)}
410
+ title="Delete content type"
411
+ @click=${() => handleDeleteContentType(rerender)}
391
412
  >
392
413
  <sp-icon-delete slot="icon"></sp-icon-delete>
393
414
  </sp-action-button>
@@ -1,11 +1,21 @@
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
 
6
6
  import { html, nothing } from "lit-html";
7
7
 
8
- export const FIELD_TYPES = ["string", "number", "boolean", "array", "object", "date"];
8
+ export const FIELD_TYPES = [
9
+ "string",
10
+ "number",
11
+ "boolean",
12
+ "array",
13
+ "object",
14
+ "date",
15
+ "image",
16
+ "gallery",
17
+ "reference",
18
+ ];
9
19
 
10
20
  /**
11
21
  * @typedef {{
@@ -14,6 +24,7 @@ export const FIELD_TYPES = ["string", "number", "boolean", "array", "object", "d
14
24
  * required?: string[];
15
25
  * items?: any;
16
26
  * format?: string;
27
+ * $ref?: string;
17
28
  * }} SchemaProperty
18
29
  */
19
30
 
@@ -23,6 +34,7 @@ export const FIELD_TYPES = ["string", "number", "boolean", "array", "object", "d
23
34
  * onToggleRequired: (name: string) => void;
24
35
  * onRename: (oldName: string, newName: string) => void;
25
36
  * onChangeType: (name: string, newType: string) => void;
37
+ * onChangeRefTarget?: (name: string, target: string) => void;
26
38
  * onAddNestedField?: (
27
39
  * parentName: string,
28
40
  * state: { name: string; type: string; required: boolean },
@@ -34,6 +46,20 @@ export const FIELD_TYPES = ["string", "number", "boolean", "array", "object", "d
34
46
  * }} FieldHandlers
35
47
  */
36
48
 
49
+ /**
50
+ * Detect the studio field type from a JSON Schema property definition.
51
+ *
52
+ * @param {SchemaProperty} schema
53
+ * @returns {string}
54
+ */
55
+ export function detectFieldType(schema) {
56
+ if (schema.$ref) return "reference";
57
+ if (schema.format === "image") return "image";
58
+ if (schema.format === "date") return "date";
59
+ if (schema.type === "array" && schema.items?.format === "image") return "gallery";
60
+ return schema.type || "string";
61
+ }
62
+
37
63
  /**
38
64
  * Render a single schema field as an inline-editable form row.
39
65
  *
@@ -41,13 +67,16 @@ export const FIELD_TYPES = ["string", "number", "boolean", "array", "object", "d
41
67
  * @param {SchemaProperty} fieldSchema — JSON Schema property definition
42
68
  * @param {boolean} isRequired
43
69
  * @param {FieldHandlers} handlers
70
+ * @param {string[]} [contentTypeNames] - Available content type names for reference target picker
44
71
  * @returns {any}
45
72
  */
46
- export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers) {
47
- const type = fieldSchema.format === "date" ? "date" : fieldSchema.type || "string";
73
+ export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers, contentTypeNames = []) {
74
+ const type = detectFieldType(fieldSchema);
48
75
  const isNested = type === "object";
76
+ const isRef = type === "reference";
49
77
  const nestedProps = fieldSchema.properties || {};
50
78
  const nestedRequired = fieldSchema.required || [];
79
+ const refTarget = fieldSchema.$ref ? fieldSchema.$ref.replace("#/contentTypes/", "") : "";
51
80
 
52
81
  return html`
53
82
  <div class="schema-field-card">
@@ -87,6 +116,23 @@ export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers) {
87
116
  <sp-icon-delete slot="icon"></sp-icon-delete>
88
117
  </sp-action-button>
89
118
  </div>
119
+ ${isRef && contentTypeNames.length
120
+ ? html`
121
+ <div class="schema-field-ref-target">
122
+ <sp-picker
123
+ size="s"
124
+ label="Target"
125
+ value=${refTarget}
126
+ @change=${(/** @type {any} */ e) => {
127
+ if (handlers.onChangeRefTarget)
128
+ handlers.onChangeRefTarget(fieldName, e.target.value);
129
+ }}
130
+ >
131
+ ${contentTypeNames.map((n) => html`<sp-menu-item value=${n}>${n}</sp-menu-item>`)}
132
+ </sp-picker>
133
+ </div>
134
+ `
135
+ : nothing}
90
136
  ${isNested
91
137
  ? html`
92
138
  <div class="schema-field-nested">
@@ -119,7 +165,7 @@ export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers) {
119
165
  * @returns {any}
120
166
  */
121
167
  function nestedFieldCardTpl(parentName, childName, childSchema, isRequired, handlers) {
122
- const type = childSchema.format === "date" ? "date" : childSchema.type || "string";
168
+ const type = detectFieldType(childSchema);
123
169
 
124
170
  return html`
125
171
  <div class="schema-field-card schema-field-card--nested">
@@ -300,6 +346,12 @@ export function schemaForType(type) {
300
346
  return { type: "object", properties: {}, required: [] };
301
347
  case "date":
302
348
  return { type: "string", format: "date" };
349
+ case "image":
350
+ return { type: "string", format: "image" };
351
+ case "gallery":
352
+ return { type: "array", items: { type: "string", format: "image" } };
353
+ case "reference":
354
+ return { $ref: "#/contentTypes/" };
303
355
  default:
304
356
  return { type: "string" };
305
357
  }
@@ -314,6 +366,7 @@ export function schemaForType(type) {
314
366
  */
315
367
  export function yamlDefault(type, format) {
316
368
  if (format === "date") return new Date().toISOString().split("T")[0];
369
+ if (format === "image") return '""';
317
370
  switch (type) {
318
371
  case "boolean":
319
372
  return "false";
@@ -109,6 +109,111 @@ export function getEffectiveHead(docHead) {
109
109
  return merged;
110
110
  }
111
111
 
112
+ // ─── Layout resolution ──────────────────────────────────────────────────────
113
+
114
+ /** @type {Map<string, any>} */
115
+ const layoutCache = new Map();
116
+
117
+ export function invalidateLayoutCache() {
118
+ layoutCache.clear();
119
+ }
120
+
121
+ /**
122
+ * Determine the effective layout path for a document.
123
+ *
124
+ * @param {any} docLayout - The document's $layout value (string path, false, or undefined)
125
+ * @returns {string | null} The layout path, or null if no layout applies
126
+ */
127
+ export function getEffectiveLayoutPath(docLayout) {
128
+ if (docLayout === false) return null;
129
+ const defaultLayout = projectState?.projectConfig?.defaults?.layout;
130
+ return docLayout || defaultLayout || null;
131
+ }
132
+
133
+ /**
134
+ * Resolve a layout document by path. Fetches and caches the parsed JSON.
135
+ *
136
+ * @param {string} layoutPath - Relative path to the layout file (e.g., "./layouts/base.json")
137
+ * @returns {Promise<any | null>} The parsed layout document, or null on failure
138
+ */
139
+ export async function resolveLayoutDoc(layoutPath) {
140
+ const normalized = layoutPath.replace(/^\.\//, "");
141
+ if (layoutCache.has(normalized)) return structuredClone(layoutCache.get(normalized));
142
+
143
+ try {
144
+ const platform = getPlatform();
145
+ const content = await platform.readFile(normalized);
146
+ const doc = JSON.parse(content);
147
+ layoutCache.set(normalized, doc);
148
+ return structuredClone(doc);
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Distribute page children into a layout document's <slot> elements. Returns the merged document
156
+ * with page content injected into slots.
157
+ *
158
+ * @param {any} layoutDoc - Deep-cloned layout document
159
+ * @param {any} pageDoc - The page document
160
+ * @returns {any} The merged document
161
+ */
162
+ export function distributePageIntoLayout(layoutDoc, pageDoc) {
163
+ const pageChildren = pageDoc.children ?? [];
164
+ const children = typeof pageChildren === "string" ? [pageChildren] : pageChildren;
165
+
166
+ const named = new Map();
167
+ const defaults = [];
168
+
169
+ for (const child of children) {
170
+ if (child && typeof child === "object" && child.attributes?.slot) {
171
+ const slotName = child.attributes.slot;
172
+ if (!named.has(slotName)) named.set(slotName, []);
173
+ named.get(slotName).push(child);
174
+ } else {
175
+ defaults.push(child);
176
+ }
177
+ }
178
+
179
+ fillSlots(layoutDoc, named, defaults);
180
+
181
+ if (pageDoc.state) layoutDoc.state = { ...layoutDoc.state, ...pageDoc.state };
182
+ if (pageDoc.$media) layoutDoc.$media = { ...layoutDoc.$media, ...pageDoc.$media };
183
+ if (pageDoc.style) layoutDoc.style = { ...layoutDoc.style, ...pageDoc.style };
184
+ if (pageDoc.attributes) layoutDoc.attributes = { ...layoutDoc.attributes, ...pageDoc.attributes };
185
+
186
+ return layoutDoc;
187
+ }
188
+
189
+ function fillSlots(
190
+ /** @type {any} */ node,
191
+ /** @type {Map<string, any[]>} */ named,
192
+ /** @type {any[]} */ defaults,
193
+ ) {
194
+ if (!node || typeof node !== "object") return;
195
+ if (!Array.isArray(node.children)) return;
196
+
197
+ const newChildren = [];
198
+ for (const child of node.children) {
199
+ if (child && typeof child === "object" && child.tagName === "slot") {
200
+ const slotName = child.attributes?.name;
201
+ if (slotName && named.has(slotName)) {
202
+ newChildren.push(.../** @type {any[]} */ (named.get(slotName)));
203
+ named.delete(slotName);
204
+ } else if (!slotName && defaults.length > 0) {
205
+ newChildren.push(...defaults);
206
+ } else if (child.children) {
207
+ newChildren.push(...child.children);
208
+ }
209
+ } else {
210
+ fillSlots(child, named, defaults);
211
+ newChildren.push(child);
212
+ }
213
+ }
214
+ node.children = newChildren;
215
+ }
216
+
112
217
  /**
113
218
  * Update the project's project.json with a partial patch and persist to disk.
114
219
  *
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
package/src/store.js CHANGED
@@ -164,7 +164,15 @@ export function stripEventHandlers(node) {
164
164
  const cases = {};
165
165
  for (const [ck, cv] of Object.entries(v)) cases[ck] = stripEventHandlers(cv);
166
166
  out.cases = cases;
167
- } else if (k === "state" || k === "style" || k === "attributes" || k === "$media") {
167
+ } else if (k === "state" && typeof v === "object" && v !== null) {
168
+ /** @type {Record<string, any>} */
169
+ const state = {};
170
+ for (const [sk, sv] of Object.entries(v)) {
171
+ if (sv && typeof sv === "object" && sv.timing === "server") continue;
172
+ state[sk] = sv;
173
+ }
174
+ out.state = state;
175
+ } else if (k === "style" || k === "attributes" || k === "$media") {
168
176
  out[k] = v;
169
177
  } else {
170
178
  out[k] = v;
package/src/studio.js CHANGED
@@ -543,13 +543,19 @@ if (_openParam) {
543
543
  }
544
544
 
545
545
  // Read and open the file
546
- const fileRelPath = siteCtx.fileRelPath || _openParam;
546
+ const _fileParam = new URLSearchParams(location.search).get("file");
547
+ const fileRelPath = _fileParam || siteCtx.fileRelPath || _openParam;
547
548
  const content = await platform.readFile(fileRelPath);
548
549
  if (content) {
549
- const parsed = JSON.parse(content);
550
- S = createState(parsed);
550
+ if (fileRelPath.endsWith(".md")) {
551
+ await loadMarkdown(content, null);
552
+ S.documentPath = fileRelPath;
553
+ } else {
554
+ const parsed = JSON.parse(content);
555
+ S = createState(parsed);
556
+ S.documentPath = fileRelPath;
557
+ }
551
558
  S.dirty = false;
552
- S.documentPath = fileRelPath;
553
559
  S.ui = { ...S.ui, leftTab: "files" };
554
560
  ({ doc, session } = fromFlat(S));
555
561
  render();
@@ -47,6 +47,7 @@ import { PickerButton } from "@spectrum-web-components/picker-button/src/PickerB
47
47
  import { Accordion } from "@spectrum-web-components/accordion/src/Accordion.js";
48
48
  import { AccordionItem } from "@spectrum-web-components/accordion/src/AccordionItem.js";
49
49
  import { ActionBar } from "@spectrum-web-components/action-bar/src/ActionBar.js";
50
+ import { Toast } from "@spectrum-web-components/toast/src/Toast.js";
50
51
  import { Table } from "@spectrum-web-components/table/src/Table.js";
51
52
  import { TableHead } from "@spectrum-web-components/table/src/TableHead.js";
52
53
  import { TableHeadCell } from "@spectrum-web-components/table/src/TableHeadCell.js";
@@ -180,6 +181,7 @@ const components = [
180
181
  ["sp-accordion", Accordion],
181
182
  ["sp-accordion-item", AccordionItem],
182
183
  ["sp-action-bar", ActionBar],
184
+ ["sp-toast", Toast],
183
185
  ["sp-table", Table],
184
186
  ["sp-table-head", TableHead],
185
187
  ["sp-table-head-cell", TableHeadCell],
@@ -123,17 +123,17 @@ export function inferInputType(entry) {
123
123
  }
124
124
 
125
125
  /**
126
- * Match a document path to a content collection and return its schema. Uses simple directory-prefix
127
- * + extension matching against the collection's `source` glob.
126
+ * Match a document path to a content type and return its schema. Uses simple directory-prefix +
127
+ * extension matching against the content type's `source` glob.
128
128
  *
129
129
  * @param {string | null} documentPath — project-relative path (e.g. "blog/hello.md")
130
130
  * @param {any} projectConfig — parsed project.json
131
131
  * @returns {{ name: string; schema: any } | null}
132
132
  */
133
- export function findCollectionSchema(documentPath, projectConfig) {
134
- if (!documentPath || !projectConfig?.collections) return null;
133
+ export function findContentTypeSchema(documentPath, projectConfig) {
134
+ if (!documentPath || !projectConfig?.contentTypes) return null;
135
135
  for (const [name, def] of Object.entries(
136
- /** @type {Record<string, any>} */ (projectConfig.collections),
136
+ /** @type {Record<string, any>} */ (projectConfig.contentTypes),
137
137
  )) {
138
138
  if (!def.source || !def.schema) continue;
139
139
  const src = def.source.replace(/^\.\//, "");
package/src/view.js CHANGED
@@ -61,6 +61,9 @@ export const view = {
61
61
  showAddBreakpointForm: false,
62
62
  addBreakpointPreview: "",
63
63
 
64
+ // Layout selection (when user clicks a layout element)
65
+ layoutSelection: null,
66
+
64
67
  // Autosave
65
68
  autosaveTimer: null,
66
69
  };