@jxsuite/studio 0.19.0 → 0.21.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.
Files changed (99) hide show
  1. package/dist/studio.css +98 -98
  2. package/dist/studio.js +13476 -12330
  3. package/dist/studio.js.map +152 -148
  4. package/dist/workers/editor.worker.js +79 -79
  5. package/dist/workers/json.worker.js +109 -109
  6. package/dist/workers/ts.worker.js +82 -82
  7. package/package.json +19 -7
  8. package/src/browse/browse-modal.js +16 -13
  9. package/src/browse/browse.js +44 -35
  10. package/src/canvas/canvas-diff.js +24 -16
  11. package/src/canvas/canvas-helpers.js +24 -19
  12. package/src/canvas/canvas-live-render.js +113 -39
  13. package/src/canvas/canvas-render.js +119 -73
  14. package/src/canvas/canvas-utils.js +51 -21
  15. package/src/canvas/nested-site-style.js +50 -0
  16. package/src/editor/component-inline-edit.js +14 -13
  17. package/src/editor/content-inline-edit.js +22 -10
  18. package/src/editor/context-menu.js +28 -14
  19. package/src/editor/convert-targets.js +60 -0
  20. package/src/editor/convert-to-component.js +20 -19
  21. package/src/editor/convert-to-repeater.js +226 -0
  22. package/src/editor/inline-edit.js +69 -28
  23. package/src/editor/insertion-helper.js +16 -12
  24. package/src/editor/shortcuts.js +14 -4
  25. package/src/editor/slash-menu.js +40 -19
  26. package/src/files/components.js +1 -1
  27. package/src/files/file-ops.js +37 -16
  28. package/src/files/files.js +322 -68
  29. package/src/github/github-auth.js +122 -0
  30. package/src/github/github-publish.js +126 -0
  31. package/src/markdown/md-convert.js +172 -107
  32. package/src/new-project/new-project-modal.js +204 -0
  33. package/src/panels/activity-bar.js +27 -25
  34. package/src/panels/ai-panel.js +406 -0
  35. package/src/panels/block-action-bar.js +77 -28
  36. package/src/panels/canvas-dnd.js +8 -2
  37. package/src/panels/data-explorer.js +19 -13
  38. package/src/panels/dnd.js +144 -92
  39. package/src/panels/editors.js +55 -35
  40. package/src/panels/elements-panel.js +33 -19
  41. package/src/panels/events-panel.js +15 -10
  42. package/src/panels/git-panel.js +475 -99
  43. package/src/panels/head-panel.js +121 -81
  44. package/src/panels/imports-panel.js +94 -65
  45. package/src/panels/layers-panel.js +64 -43
  46. package/src/panels/left-panel.js +105 -48
  47. package/src/panels/overlays.js +1 -0
  48. package/src/panels/panel-events.js +45 -30
  49. package/src/panels/preview-render.js +23 -18
  50. package/src/panels/properties-panel.js +378 -257
  51. package/src/panels/pseudo-preview.js +2 -2
  52. package/src/panels/quick-search.js +11 -9
  53. package/src/panels/right-panel.js +42 -16
  54. package/src/panels/shared.js +4 -4
  55. package/src/panels/signals-panel.js +415 -166
  56. package/src/panels/statusbar.js +10 -6
  57. package/src/panels/style-inputs.js +67 -35
  58. package/src/panels/style-panel.js +239 -134
  59. package/src/panels/style-utils.js +34 -16
  60. package/src/panels/stylebook-layers-panel.js +12 -10
  61. package/src/panels/stylebook-panel.js +134 -66
  62. package/src/panels/tab-strip.js +5 -2
  63. package/src/panels/toolbar.js +333 -109
  64. package/src/panels/welcome-screen.js +121 -0
  65. package/src/platform.js +2 -4
  66. package/src/platforms/devserver.js +113 -7
  67. package/src/resize-edges.js +98 -0
  68. package/src/services/cem-export.js +12 -7
  69. package/src/services/code-services.js +30 -12
  70. package/src/settings/content-types-editor.js +8 -8
  71. package/src/settings/css-vars-editor.js +30 -24
  72. package/src/settings/defs-editor.js +12 -7
  73. package/src/settings/general-settings.js +85 -3
  74. package/src/settings/head-editor.js +30 -24
  75. package/src/settings/schema-field-ui.js +58 -39
  76. package/src/settings/settings-modal.js +12 -9
  77. package/src/site-context.js +41 -31
  78. package/src/state.js +70 -29
  79. package/src/store.js +36 -35
  80. package/src/studio.js +120 -90
  81. package/src/tabs/tab.js +67 -43
  82. package/src/tabs/transact.js +23 -13
  83. package/src/ui/button-group.js +24 -24
  84. package/src/ui/color-selector.js +28 -23
  85. package/src/ui/field-row.js +3 -3
  86. package/src/ui/icons.js +2 -2
  87. package/src/ui/layers.js +31 -1
  88. package/src/ui/media-picker.js +7 -7
  89. package/src/ui/spectrum.js +5 -1
  90. package/src/ui/unit-selector.js +18 -16
  91. package/src/ui/value-selector.js +21 -15
  92. package/src/ui/widgets.js +22 -19
  93. package/src/utils/canvas-media.js +5 -4
  94. package/src/utils/edit-display.js +32 -26
  95. package/src/utils/google-fonts.js +11 -11
  96. package/src/utils/inherited-style.js +9 -8
  97. package/src/utils/studio-utils.js +18 -18
  98. package/src/view.js +58 -1
  99. package/src/workspace/workspace.js +29 -7
@@ -14,7 +14,7 @@ export const FORMAT_OPTIONS = ["", "image", "date", "color"];
14
14
  * type?: string;
15
15
  * properties?: Record<string, SchemaProperty>;
16
16
  * required?: string[];
17
- * items?: any;
17
+ * items?: SchemaProperty;
18
18
  * format?: string;
19
19
  * $ref?: string;
20
20
  * }} SchemaProperty
@@ -70,7 +70,7 @@ export function detectFieldFormat(schema) {
70
70
  * @param {boolean} isRequired
71
71
  * @param {FieldHandlers} handlers
72
72
  * @param {string[]} [contentTypeNames] - Available content type names for reference target picker
73
- * @returns {any}
73
+ * @returns {import("lit-html").TemplateResult}
74
74
  */
75
75
  export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers, contentTypeNames = []) {
76
76
  const type = detectFieldType(fieldSchema);
@@ -89,16 +89,18 @@ export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers, conte
89
89
  quiet
90
90
  value=${fieldName}
91
91
  class="schema-field-name-input"
92
- @change=${(/** @type {any} */ e) => {
93
- const newName = e.target.value.trim();
92
+ @change=${(/** @type {Event} */ e) => {
93
+ const target = /** @type {HTMLInputElement} */ (e.target);
94
+ const newName = target.value.trim();
94
95
  if (newName && newName !== fieldName) handlers.onRename(fieldName, newName);
95
- else e.target.value = fieldName;
96
+ else target.value = fieldName;
96
97
  }}
97
- @keydown=${(/** @type {any} */ e) => {
98
- if (e.key === "Enter") e.target.blur();
98
+ @keydown=${(/** @type {KeyboardEvent} */ e) => {
99
+ const target = /** @type {HTMLInputElement} */ (e.target);
100
+ if (e.key === "Enter") target.blur();
99
101
  if (e.key === "Escape") {
100
- e.target.value = fieldName;
101
- e.target.blur();
102
+ target.value = fieldName;
103
+ target.blur();
102
104
  }
103
105
  }}
104
106
  ></sp-textfield>
@@ -131,9 +133,12 @@ export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers, conte
131
133
  size="s"
132
134
  label="Target"
133
135
  value=${refTarget}
134
- @change=${(/** @type {any} */ e) => {
136
+ @change=${(/** @type {Event} */ e) => {
135
137
  if (handlers.onChangeRefTarget)
136
- handlers.onChangeRefTarget(fieldName, e.target.value);
138
+ handlers.onChangeRefTarget(
139
+ fieldName,
140
+ /** @type {HTMLInputElement} */ (e.target).value,
141
+ );
137
142
  }}
138
143
  >
139
144
  ${contentTypeNames.map((n) => html`<sp-menu-item value=${n}>${n}</sp-menu-item>`)}
@@ -170,7 +175,7 @@ export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers, conte
170
175
  * @param {SchemaProperty} childSchema
171
176
  * @param {boolean} isRequired
172
177
  * @param {FieldHandlers} handlers
173
- * @returns {any}
178
+ * @returns {import("lit-html").TemplateResult}
174
179
  */
175
180
  function nestedFieldCardTpl(parentName, childName, childSchema, isRequired, handlers) {
176
181
  const type = detectFieldType(childSchema);
@@ -184,19 +189,21 @@ function nestedFieldCardTpl(parentName, childName, childSchema, isRequired, hand
184
189
  quiet
185
190
  value=${childName}
186
191
  class="schema-field-name-input"
187
- @change=${(/** @type {any} */ e) => {
188
- const newName = e.target.value.trim();
192
+ @change=${(/** @type {Event} */ e) => {
193
+ const target = /** @type {HTMLInputElement} */ (e.target);
194
+ const newName = target.value.trim();
189
195
  if (newName && newName !== childName && handlers.onRenameNested) {
190
196
  handlers.onRenameNested(parentName, childName, newName);
191
197
  } else {
192
- e.target.value = childName;
198
+ target.value = childName;
193
199
  }
194
200
  }}
195
- @keydown=${(/** @type {any} */ e) => {
196
- if (e.key === "Enter") e.target.blur();
201
+ @keydown=${(/** @type {KeyboardEvent} */ e) => {
202
+ const target = /** @type {HTMLInputElement} */ (e.target);
203
+ if (e.key === "Enter") target.blur();
197
204
  if (e.key === "Escape") {
198
- e.target.value = childName;
199
- e.target.blur();
205
+ target.value = childName;
206
+ target.blur();
200
207
  }
201
208
  }}
202
209
  ></sp-textfield>
@@ -240,7 +247,7 @@ function nestedFieldCardTpl(parentName, childName, childSchema, isRequired, hand
240
247
  *
241
248
  * @param {string} parentName
242
249
  * @param {FieldHandlers} handlers
243
- * @returns {any}
250
+ * @returns {import("lit-html").TemplateResult}
244
251
  */
245
252
  function nestedAddFieldTpl(parentName, handlers) {
246
253
  return html`
@@ -249,15 +256,18 @@ function nestedAddFieldTpl(parentName, handlers) {
249
256
  size="s"
250
257
  placeholder="field name"
251
258
  class="schema-nested-add-name"
252
- @keydown=${(/** @type {any} */ e) => {
259
+ @keydown=${(/** @type {KeyboardEvent} */ e) => {
253
260
  if (e.key === "Enter") {
254
- const row = e.target.closest(".schema-nested-add");
255
- const name = e.target.value.trim();
256
- const typePicker = row?.querySelector("sp-picker");
261
+ const target = /** @type {HTMLInputElement} */ (e.target);
262
+ const row = target.closest(".schema-nested-add");
263
+ const name = target.value.trim();
264
+ const typePicker = /** @type {HTMLInputElement | null} */ (
265
+ row?.querySelector("sp-picker")
266
+ );
257
267
  const type = typePicker?.value || "string";
258
268
  if (name && handlers.onAddNestedField) {
259
269
  handlers.onAddNestedField(parentName, { name, type, required: false });
260
- e.target.value = "";
270
+ target.value = "";
261
271
  }
262
272
  }
263
273
  }}
@@ -267,15 +277,20 @@ function nestedAddFieldTpl(parentName, handlers) {
267
277
  size="xs"
268
278
  quiet
269
279
  title="Add nested field"
270
- @click=${(/** @type {any} */ e) => {
271
- const row = e.target.closest(".schema-nested-add");
272
- const nameInput = /** @type {any} */ (row?.querySelector(".schema-nested-add-name"));
273
- const typePicker = /** @type {any} */ (row?.querySelector("sp-picker"));
280
+ @click=${(/** @type {Event} */ e) => {
281
+ const target = /** @type {HTMLElement} */ (e.target);
282
+ const row = target.closest(".schema-nested-add");
283
+ const nameInput = /** @type {HTMLInputElement | null} */ (
284
+ row?.querySelector(".schema-nested-add-name")
285
+ );
286
+ const typePicker = /** @type {HTMLInputElement | null} */ (
287
+ row?.querySelector("sp-picker")
288
+ );
274
289
  const name = nameInput?.value?.trim();
275
290
  const type = typePicker?.value || "string";
276
291
  if (name && handlers.onAddNestedField) {
277
292
  handlers.onAddNestedField(parentName, { name, type, required: false });
278
- nameInput.value = "";
293
+ if (nameInput) nameInput.value = "";
279
294
  }
280
295
  }}
281
296
  >
@@ -290,7 +305,7 @@ function nestedAddFieldTpl(parentName, handlers) {
290
305
  *
291
306
  * @param {string} value
292
307
  * @param {(type: string) => void} onChange
293
- * @returns {any}
308
+ * @returns {import("lit-html").TemplateResult}
294
309
  */
295
310
  export function typePickerTpl(value, onChange) {
296
311
  return html`
@@ -298,7 +313,8 @@ export function typePickerTpl(value, onChange) {
298
313
  size="s"
299
314
  label="Type"
300
315
  value=${value}
301
- @change=${(/** @type {any} */ e) => onChange(e.target.value)}
316
+ @change=${(/** @type {Event} */ e) =>
317
+ onChange(/** @type {HTMLInputElement} */ (e.target).value)}
302
318
  >
303
319
  ${FIELD_TYPES.map((t) => html`<sp-menu-item value=${t}>${t}</sp-menu-item>`)}
304
320
  </sp-picker>
@@ -310,7 +326,7 @@ export function typePickerTpl(value, onChange) {
310
326
  *
311
327
  * @param {string} value
312
328
  * @param {(format: string) => void} onChange
313
- * @returns {any}
329
+ * @returns {import("lit-html").TemplateResult}
314
330
  */
315
331
  export function formatPickerTpl(value, onChange) {
316
332
  return html`
@@ -318,7 +334,8 @@ export function formatPickerTpl(value, onChange) {
318
334
  size="s"
319
335
  label="Format"
320
336
  value=${value}
321
- @change=${(/** @type {any} */ e) => onChange(e.target.value)}
337
+ @change=${(/** @type {Event} */ e) =>
338
+ onChange(/** @type {HTMLInputElement} */ (e.target).value)}
322
339
  >
323
340
  ${FORMAT_OPTIONS.map((f) => html`<sp-menu-item value=${f}>${f || "(none)"}</sp-menu-item>`)}
324
341
  </sp-picker>
@@ -330,11 +347,11 @@ export function formatPickerTpl(value, onChange) {
330
347
  *
331
348
  * @param {{ name: string; type: string; format: string; required: boolean }} state
332
349
  * @param {{
333
- * onInput: (field: string, value: any) => void;
350
+ * onInput: (field: string, value: string | boolean) => void;
334
351
  * onConfirm: () => void;
335
352
  * onCancel: () => void;
336
353
  * }} handlers
337
- * @returns {any}
354
+ * @returns {import("lit-html").TemplateResult}
338
355
  */
339
356
  export function addFieldFormTpl(state, handlers) {
340
357
  return html`
@@ -343,8 +360,9 @@ export function addFieldFormTpl(state, handlers) {
343
360
  size="s"
344
361
  placeholder="Field name"
345
362
  .value=${state.name}
346
- @input=${(/** @type {any} */ e) => handlers.onInput("name", e.target.value)}
347
- @keydown=${(/** @type {any} */ e) => {
363
+ @input=${(/** @type {Event} */ e) =>
364
+ handlers.onInput("name", /** @type {HTMLInputElement} */ (e.target).value)}
365
+ @keydown=${(/** @type {KeyboardEvent} */ e) => {
348
366
  if (e.key === "Enter") handlers.onConfirm();
349
367
  if (e.key === "Escape") handlers.onCancel();
350
368
  }}
@@ -356,7 +374,8 @@ export function addFieldFormTpl(state, handlers) {
356
374
  <sp-switch
357
375
  size="s"
358
376
  ?checked=${state.required}
359
- @change=${(/** @type {any} */ e) => handlers.onInput("required", e.target.checked)}
377
+ @change=${(/** @type {Event} */ e) =>
378
+ handlers.onInput("required", /** @type {HTMLInputElement} */ (e.target).checked)}
360
379
  >
361
380
  Required
362
381
  </sp-switch>
@@ -20,6 +20,9 @@ let _handle = null;
20
20
  /** @type {string} */
21
21
  let _activeSection = "general";
22
22
 
23
+ /** @type {HTMLElement | null} */
24
+ let _contentEl = null;
25
+
23
26
  const sections = [
24
27
  { key: "general", label: "General", icon: "sp-icon-properties" },
25
28
  { key: "head", label: "Head", icon: "sp-icon-file-single-web-page" },
@@ -38,6 +41,7 @@ export function closeSettingsModal() {
38
41
  if (!_handle) return;
39
42
  _handle.close();
40
43
  _handle = null;
44
+ _contentEl = null;
41
45
  }
42
46
 
43
47
  function renderModal() {
@@ -77,7 +81,8 @@ function renderModal() {
77
81
  <div
78
82
  class="settings-modal-content"
79
83
  ${ref((el) => {
80
- if (el) requestAnimationFrame(() => renderActiveSection());
84
+ _contentEl = /** @type {HTMLElement | null} */ (el || null);
85
+ if (_contentEl) requestAnimationFrame(() => renderActiveSection());
81
86
  })}
82
87
  ></div>
83
88
  </div>
@@ -92,25 +97,23 @@ function renderModal() {
92
97
  }
93
98
 
94
99
  function renderActiveSection() {
95
- if (!_handle) return;
96
- const container = _handle.host.querySelector(".settings-modal-content");
97
- if (!container) return;
100
+ if (!_handle || !_contentEl) return;
98
101
 
99
102
  switch (_activeSection) {
100
103
  case "general":
101
- renderGeneralSettings(/** @type {HTMLElement} */ (container));
104
+ renderGeneralSettings(_contentEl);
102
105
  break;
103
106
  case "head":
104
- renderHeadEditor(/** @type {HTMLElement} */ (container));
107
+ renderHeadEditor(_contentEl);
105
108
  break;
106
109
  case "cssVars":
107
- renderCssVarsEditor(/** @type {HTMLElement} */ (container));
110
+ renderCssVarsEditor(_contentEl);
108
111
  break;
109
112
  case "definitions":
110
- renderDefsEditor(/** @type {HTMLElement} */ (container));
113
+ renderDefsEditor(_contentEl);
111
114
  break;
112
115
  case "contentTypes":
113
- renderContentTypesEditor(/** @type {HTMLElement} */ (container));
116
+ renderContentTypesEditor(_contentEl);
114
117
  break;
115
118
  }
116
119
  }
@@ -5,14 +5,15 @@
5
5
  * definitions merge on top (file wins on conflict).
6
6
  */
7
7
 
8
- import { projectState, setProjectState } from "./store.js";
8
+ import { projectState, setProjectState, requireProjectState } from "./store.js";
9
9
  import { getPlatform } from "./platform.js";
10
10
 
11
11
  /**
12
12
  * Merge site $media with document $media. Document keys win on conflict.
13
13
  *
14
- * @param {any} docMedia - The current document's $media (may be undefined)
15
- * @returns {any}
14
+ * @param {Record<string, string> | undefined} docMedia - The current document's $media (may be
15
+ * undefined)
16
+ * @returns {Record<string, string>}
16
17
  */
17
18
  export function getEffectiveMedia(docMedia) {
18
19
  const siteMedia = projectState?.projectConfig?.$media;
@@ -25,8 +26,8 @@ export function getEffectiveMedia(docMedia) {
25
26
  * Merge site style with document style. Document keys win on conflict. Nested selector objects
26
27
  * (e.g. `& li`) are shallow-merged individually.
27
28
  *
28
- * @param {any} docStyle - The current document's style (may be undefined)
29
- * @returns {any}
29
+ * @param {JxStyle | undefined} docStyle - The current document's style (may be undefined)
30
+ * @returns {JxStyle}
30
31
  */
31
32
  export function getEffectiveStyle(docStyle) {
32
33
  const siteStyle = projectState?.projectConfig?.style;
@@ -40,7 +41,7 @@ export function getEffectiveStyle(docStyle) {
40
41
  typeof merged[k] === "object" &&
41
42
  merged[k] !== null
42
43
  ) {
43
- merged[k] = { ...merged[k], ...v };
44
+ merged[k] = { .../** @type {JxStyle} */ (merged[k]), .../** @type {JxStyle} */ (v) };
44
45
  } else {
45
46
  merged[k] = v;
46
47
  }
@@ -51,8 +52,9 @@ export function getEffectiveStyle(docStyle) {
51
52
  /**
52
53
  * Merge site imports with document imports. Document keys win on conflict.
53
54
  *
54
- * @param {any} docImports - The current document's imports (may be undefined)
55
- * @returns {any}
55
+ * @param {Record<string, string> | undefined} docImports - The current document's imports (may be
56
+ * undefined)
57
+ * @returns {Record<string, string>}
56
58
  */
57
59
  export function getEffectiveImports(docImports) {
58
60
  const siteImports = projectState?.projectConfig?.imports;
@@ -64,8 +66,9 @@ export function getEffectiveImports(docImports) {
64
66
  /**
65
67
  * Merge site $elements with document $elements. Union with dedup by $ref or string value.
66
68
  *
67
- * @param {any[]} [docElements] - The current document's $elements (may be undefined)
68
- * @returns {any[]}
69
+ * @param {(JxElement | string)[]} [docElements] - The current document's $elements (may be
70
+ * undefined)
71
+ * @returns {(JxElement | string)[]}
69
72
  */
70
73
  export function getEffectiveElements(docElements) {
71
74
  const siteElements = projectState?.projectConfig?.$elements;
@@ -73,7 +76,7 @@ export function getEffectiveElements(docElements) {
73
76
  if (!docElements?.length) return [...siteElements];
74
77
  /** @type {Set<string>} */
75
78
  const seen = new Set();
76
- /** @type {any[]} */
79
+ /** @type {(JxElement | string)[]} */
77
80
  const merged = [];
78
81
  for (const entry of [...siteElements, ...docElements]) {
79
82
  const key = typeof entry === "string" ? entry : entry?.$ref;
@@ -88,8 +91,8 @@ export function getEffectiveElements(docElements) {
88
91
  /**
89
92
  * Merge site $head with document $head. Union with dedup by href/src.
90
93
  *
91
- * @param {any[]} [docHead] - The current document's $head (may be undefined)
92
- * @returns {any[]}
94
+ * @param {JxHeadEntry[]} [docHead] - The current document's $head (may be undefined)
95
+ * @returns {JxHeadEntry[]}
93
96
  */
94
97
  export function getEffectiveHead(docHead) {
95
98
  const siteHead = projectState?.projectConfig?.$head;
@@ -97,10 +100,11 @@ export function getEffectiveHead(docHead) {
97
100
  if (!docHead?.length) return [...siteHead];
98
101
  /** @type {Set<string>} */
99
102
  const seen = new Set();
100
- /** @type {any[]} */
103
+ /** @type {JxHeadEntry[]} */
101
104
  const merged = [];
102
105
  for (const entry of [...siteHead, ...docHead]) {
103
- const key = entry?.attributes?.href || entry?.attributes?.src || JSON.stringify(entry);
106
+ const key =
107
+ String(entry?.attributes?.href || entry?.attributes?.src || "") || JSON.stringify(entry);
104
108
  if (!seen.has(key)) {
105
109
  seen.add(key);
106
110
  merged.push(entry);
@@ -111,7 +115,7 @@ export function getEffectiveHead(docHead) {
111
115
 
112
116
  // ─── Layout resolution ──────────────────────────────────────────────────────
113
117
 
114
- /** @type {Map<string, any>} */
118
+ /** @type {Map<string, JxMutableNode>} */
115
119
  const layoutCache = new Map();
116
120
 
117
121
  export function invalidateLayoutCache() {
@@ -121,7 +125,8 @@ export function invalidateLayoutCache() {
121
125
  /**
122
126
  * Determine the effective layout path for a document.
123
127
  *
124
- * @param {any} docLayout - The document's $layout value (string path, false, or undefined)
128
+ * @param {string | false | undefined} docLayout - The document's $layout value (string path, false,
129
+ * or undefined)
125
130
  * @returns {string | null} The layout path, or null if no layout applies
126
131
  */
127
132
  export function getEffectiveLayoutPath(docLayout) {
@@ -134,11 +139,12 @@ export function getEffectiveLayoutPath(docLayout) {
134
139
  * Resolve a layout document by path. Fetches and caches the parsed JSON.
135
140
  *
136
141
  * @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
142
+ * @returns {Promise<JxMutableNode | null>} The parsed layout document, or null on failure
138
143
  */
139
144
  export async function resolveLayoutDoc(layoutPath) {
140
145
  const normalized = layoutPath.replace(/^\.\//, "");
141
- if (layoutCache.has(normalized)) return structuredClone(layoutCache.get(normalized));
146
+ if (layoutCache.has(normalized))
147
+ return structuredClone(/** @type {JxMutableNode} */ (layoutCache.get(normalized)));
142
148
 
143
149
  try {
144
150
  const platform = getPlatform();
@@ -155,9 +161,9 @@ export async function resolveLayoutDoc(layoutPath) {
155
161
  * Distribute page children into a layout document's <slot> elements. Returns the merged document
156
162
  * with page content injected into slots.
157
163
  *
158
- * @param {any} layoutDoc - Deep-cloned layout document
159
- * @param {any} pageDoc - The page document
160
- * @returns {any} The merged document
164
+ * @param {JxMutableNode} layoutDoc - Deep-cloned layout document
165
+ * @param {JxMutableNode} pageDoc - The page document
166
+ * @returns {JxMutableNode} The merged document
161
167
  */
162
168
  export function distributePageIntoLayout(layoutDoc, pageDoc) {
163
169
  const pageChildren = pageDoc.children ?? [];
@@ -187,19 +193,20 @@ export function distributePageIntoLayout(layoutDoc, pageDoc) {
187
193
  }
188
194
 
189
195
  function fillSlots(
190
- /** @type {any} */ node,
191
- /** @type {Map<string, any[]>} */ named,
192
- /** @type {any[]} */ defaults,
196
+ /** @type {JxMutableNode} */ node,
197
+ /** @type {Map<string, JxMutableNode[]>} */ named,
198
+ /** @type {(JxMutableNode | string)[]} */ defaults,
193
199
  ) {
194
200
  if (!node || typeof node !== "object") return;
195
201
  if (!Array.isArray(node.children)) return;
196
202
 
203
+ /** @type {(JxMutableNode | string)[]} */
197
204
  const newChildren = [];
198
205
  for (const child of node.children) {
199
206
  if (child && typeof child === "object" && child.tagName === "slot") {
200
207
  const slotName = child.attributes?.name;
201
208
  if (slotName && named.has(slotName)) {
202
- newChildren.push(.../** @type {any[]} */ (named.get(slotName)));
209
+ newChildren.push(.../** @type {JxMutableNode[]} */ (named.get(slotName)));
203
210
  named.delete(slotName);
204
211
  } else if (!slotName && defaults.length > 0) {
205
212
  newChildren.push(...defaults);
@@ -207,21 +214,24 @@ function fillSlots(
207
214
  newChildren.push(...child.children);
208
215
  }
209
216
  } else {
210
- fillSlots(child, named, defaults);
217
+ if (typeof child !== "string") fillSlots(child, named, defaults);
211
218
  newChildren.push(child);
212
219
  }
213
220
  }
214
- node.children = newChildren;
221
+ node.children = /** @type {JxMutableNode[]} */ (newChildren);
215
222
  }
216
223
 
217
224
  /**
218
225
  * Update the project's project.json with a partial patch and persist to disk.
219
226
  *
220
- * @param {Record<string, any>} patch - Fields to merge into the current projectConfig
227
+ * @param {Partial<ProjectConfig>} patch - Fields to merge into the current projectConfig
221
228
  */
222
229
  export async function updateSiteConfig(patch) {
223
230
  const platform = getPlatform();
224
- const config = { ...projectState.projectConfig, ...patch };
231
+ const config = /** @type {ProjectConfig} */ ({
232
+ ...requireProjectState().projectConfig,
233
+ ...patch,
234
+ });
225
235
  await platform.writeFile("project.json", JSON.stringify(config, null, 2));
226
- setProjectState({ ...projectState, projectConfig: config });
236
+ setProjectState({ ...requireProjectState(), projectConfig: config });
227
237
  }