@jxsuite/studio 0.19.0 → 0.20.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 (93) hide show
  1. package/dist/studio.css +98 -98
  2. package/dist/studio.js +12577 -12183
  3. package/dist/studio.js.map +146 -144
  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 +17 -6
  8. package/src/browse/browse.js +25 -19
  9. package/src/canvas/canvas-diff.js +24 -16
  10. package/src/canvas/canvas-helpers.js +24 -19
  11. package/src/canvas/canvas-live-render.js +94 -39
  12. package/src/canvas/canvas-render.js +119 -73
  13. package/src/canvas/canvas-utils.js +38 -15
  14. package/src/canvas/nested-site-style.js +50 -0
  15. package/src/editor/component-inline-edit.js +14 -13
  16. package/src/editor/content-inline-edit.js +22 -10
  17. package/src/editor/context-menu.js +1 -1
  18. package/src/editor/convert-to-component.js +12 -10
  19. package/src/editor/inline-edit.js +66 -28
  20. package/src/editor/insertion-helper.js +16 -12
  21. package/src/editor/shortcuts.js +4 -2
  22. package/src/editor/slash-menu.js +6 -4
  23. package/src/files/components.js +1 -1
  24. package/src/files/file-ops.js +37 -16
  25. package/src/files/files.js +78 -52
  26. package/src/github/github-auth.js +122 -0
  27. package/src/github/github-publish.js +115 -0
  28. package/src/markdown/md-convert.js +172 -107
  29. package/src/new-project/new-project-modal.js +204 -0
  30. package/src/panels/activity-bar.js +22 -20
  31. package/src/panels/ai-panel.js +399 -0
  32. package/src/panels/block-action-bar.js +5 -5
  33. package/src/panels/canvas-dnd.js +8 -2
  34. package/src/panels/data-explorer.js +19 -13
  35. package/src/panels/dnd.js +144 -92
  36. package/src/panels/editors.js +55 -35
  37. package/src/panels/elements-panel.js +33 -19
  38. package/src/panels/events-panel.js +15 -10
  39. package/src/panels/git-panel.js +469 -98
  40. package/src/panels/head-panel.js +121 -81
  41. package/src/panels/imports-panel.js +88 -64
  42. package/src/panels/layers-panel.js +64 -43
  43. package/src/panels/left-panel.js +85 -47
  44. package/src/panels/overlays.js +1 -0
  45. package/src/panels/panel-events.js +45 -30
  46. package/src/panels/preview-render.js +23 -18
  47. package/src/panels/properties-panel.js +365 -249
  48. package/src/panels/pseudo-preview.js +2 -2
  49. package/src/panels/quick-search.js +7 -5
  50. package/src/panels/right-panel.js +26 -2
  51. package/src/panels/shared.js +4 -4
  52. package/src/panels/signals-panel.js +295 -166
  53. package/src/panels/statusbar.js +4 -4
  54. package/src/panels/style-inputs.js +67 -35
  55. package/src/panels/style-panel.js +233 -132
  56. package/src/panels/style-utils.js +34 -16
  57. package/src/panels/stylebook-layers-panel.js +12 -10
  58. package/src/panels/stylebook-panel.js +134 -66
  59. package/src/panels/toolbar.js +333 -109
  60. package/src/panels/welcome-screen.js +121 -0
  61. package/src/platform.js +2 -4
  62. package/src/platforms/devserver.js +113 -7
  63. package/src/resize-edges.js +98 -0
  64. package/src/services/cem-export.js +12 -7
  65. package/src/services/code-services.js +30 -12
  66. package/src/settings/content-types-editor.js +7 -7
  67. package/src/settings/css-vars-editor.js +30 -24
  68. package/src/settings/defs-editor.js +12 -7
  69. package/src/settings/general-settings.js +85 -3
  70. package/src/settings/head-editor.js +30 -24
  71. package/src/settings/schema-field-ui.js +58 -39
  72. package/src/site-context.js +41 -31
  73. package/src/state.js +70 -29
  74. package/src/store.js +21 -26
  75. package/src/studio.js +103 -81
  76. package/src/tabs/tab.js +67 -43
  77. package/src/tabs/transact.js +23 -13
  78. package/src/ui/button-group.js +24 -24
  79. package/src/ui/color-selector.js +28 -23
  80. package/src/ui/field-row.js +3 -3
  81. package/src/ui/icons.js +2 -2
  82. package/src/ui/media-picker.js +7 -7
  83. package/src/ui/spectrum.js +5 -1
  84. package/src/ui/unit-selector.js +18 -16
  85. package/src/ui/value-selector.js +21 -15
  86. package/src/ui/widgets.js +22 -19
  87. package/src/utils/canvas-media.js +5 -4
  88. package/src/utils/edit-display.js +31 -20
  89. package/src/utils/google-fonts.js +11 -11
  90. package/src/utils/inherited-style.js +9 -8
  91. package/src/utils/studio-utils.js +7 -7
  92. package/src/view.js +58 -1
  93. package/src/workspace/workspace.js +10 -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>
@@ -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
  }
package/src/state.js CHANGED
@@ -9,28 +9,37 @@
9
9
  */
10
10
 
11
11
  /**
12
- * @typedef {Record<string, any>} JxNode
13
- *
14
12
  * @typedef {(string | number)[]} JxPath
15
13
  *
16
- * @typedef {{ document: JxNode; selection: JxPath | null }} HistorySnapshot
14
+ * @typedef {{ document: JxMutableNode; selection: JxPath | null }} HistorySnapshot
17
15
  *
18
16
  * @typedef {{
19
- * document: JxNode;
17
+ * document: JxMutableNode;
20
18
  * selection: JxPath | null;
21
19
  * hover: JxPath | null;
22
20
  * history: HistorySnapshot[];
23
21
  * historyIndex: number;
24
22
  * dirty: boolean;
25
- * fileHandle: any;
23
+ * fileHandle: FileSystemFileHandle | null;
26
24
  * documentPath: string | null;
27
- * documentStack: any[];
25
+ * documentStack: StudioStackFrame[];
28
26
  * handlersSource: string | null;
29
27
  * mode: string;
30
- * content: { frontmatter: Record<string, any> };
31
- * ui: Record<string, any>;
32
- * canvas: { status: string; scope: any; error: string | null };
28
+ * content: { frontmatter: Record<string, unknown> };
29
+ * ui: Record<string, unknown>;
30
+ * canvas: { status: string; scope: Record<string, unknown> | null; error: string | null };
33
31
  * }} StudioState
32
+ *
33
+ * @typedef {{
34
+ * document: JxMutableNode;
35
+ * selection: JxPath | null;
36
+ * fileHandle: FileSystemFileHandle | null;
37
+ * documentPath: string | null;
38
+ * dirty: boolean;
39
+ * history: HistorySnapshot[];
40
+ * historyIndex: number;
41
+ * mode: string;
42
+ * }} StudioStackFrame
34
43
  */
35
44
 
36
45
  // ─── Path utilities ───────────────────────────────────────────────────────────
@@ -38,14 +47,14 @@
38
47
  /**
39
48
  * Walk the document tree and return the node at the given path.
40
49
  *
41
- * @param {any} doc
50
+ * @param {JxMutableNode} doc
42
51
  * @param {JxPath} path
43
- * @returns {any}
52
+ * @returns {JxMutableNode}
44
53
  */
45
54
  export function getNodeAtPath(doc, path) {
46
55
  let node = doc;
47
56
  for (const key of path) {
48
- if (node == null) return undefined;
57
+ if (node == null) return /** @type {JxMutableNode} */ (/** @type {unknown} */ (undefined));
49
58
  node = node[key];
50
59
  }
51
60
  return node;
@@ -114,10 +123,15 @@ export function isAncestor(path, descendant) {
114
123
  *
115
124
  * NodeType: 'element' (default) | 'map' | 'case' | 'case-ref'
116
125
  *
117
- * @param {any} doc
126
+ * @param {JxMutableNode | string | number | boolean} doc
118
127
  * @param {JxPath} [path]
119
128
  * @param {number} [depth]
120
- * @returns {{ node: any; path: JxPath; depth: number; nodeType: string }[]}
129
+ * @returns {{
130
+ * node: JxMutableNode | string | number | boolean;
131
+ * path: JxPath;
132
+ * depth: number;
133
+ * nodeType: string;
134
+ * }[]}
121
135
  */
122
136
  export function flattenTree(doc, path = [], depth = 0) {
123
137
  // Text node children: bare primitives get a "text" row
@@ -125,7 +139,14 @@ export function flattenTree(doc, path = [], depth = 0) {
125
139
  return [{ node: doc, path, depth, nodeType: "text" }];
126
140
  }
127
141
 
128
- /** @type {{ node: any; path: JxPath; depth: number; nodeType: string }[]} */
142
+ /**
143
+ * @type {{
144
+ * node: JxMutableNode | string | number | boolean;
145
+ * path: JxPath;
146
+ * depth: number;
147
+ * nodeType: string;
148
+ * }[]}
149
+ */
129
150
  const rows = [{ node: doc, path, depth, nodeType: "element" }];
130
151
 
131
152
  // Custom component instances without user-authored children are atomic in the layer tree
@@ -140,12 +161,22 @@ export function flattenTree(doc, path = [], depth = 0) {
140
161
  const childPath = [...path, "children", i];
141
162
  rows.push(...flattenTree(children[i], childPath, depth + 1));
142
163
  }
143
- } else if (children && typeof children === "object" && children.$prototype === "Array") {
164
+ } else if (
165
+ children &&
166
+ typeof children === "object" &&
167
+ /** @type {JxMutableNode} */ (children).$prototype === "Array"
168
+ ) {
144
169
  // $map — emit the map container, then recurse into the template
145
170
  rows.push({ node: children, path: [...path, "children"], depth: depth + 1, nodeType: "map" });
146
- const mapDef = children.map;
171
+ const mapDef = /** @type {JxMutableNode} */ (children).map;
147
172
  if (mapDef && typeof mapDef === "object") {
148
- rows.push(...flattenTree(mapDef, [...path, "children", "map"], depth + 2));
173
+ rows.push(
174
+ ...flattenTree(
175
+ /** @type {JxMutableNode} */ (mapDef),
176
+ [...path, "children", "map"],
177
+ depth + 2,
178
+ ),
179
+ );
149
180
  }
150
181
  }
151
182
 
@@ -153,7 +184,7 @@ export function flattenTree(doc, path = [], depth = 0) {
153
184
  if (doc.$switch && doc.cases && typeof doc.cases === "object") {
154
185
  for (const [caseName, caseDef] of Object.entries(doc.cases)) {
155
186
  const casePath = [...path, "cases", caseName];
156
- if (caseDef && typeof caseDef === "object" && /** @type {any} */ (caseDef).$ref) {
187
+ if (caseDef && typeof caseDef === "object" && /** @type {JxMutableNode} */ (caseDef).$ref) {
157
188
  rows.push({ node: caseDef, path: casePath, depth: depth + 1, nodeType: "case-ref" });
158
189
  } else if (caseDef && typeof caseDef === "object") {
159
190
  rows.push({ node: caseDef, path: casePath, depth: depth + 1, nodeType: "case" });
@@ -170,7 +201,7 @@ export function flattenTree(doc, path = [], depth = 0) {
170
201
  /**
171
202
  * Get a display label for a node (for layers + overlays).
172
203
  *
173
- * @param {any} node
204
+ * @param {JxMutableNode | null} node
174
205
  * @returns {string}
175
206
  */
176
207
  export function nodeLabel(node) {
@@ -193,7 +224,7 @@ export function nodeLabel(node) {
193
224
  // ─── State factory ────────────────────────────────────────────────────────────
194
225
 
195
226
  /**
196
- * @param {any} doc
227
+ * @param {JxMutableNode} doc
197
228
  * @returns {StudioState}
198
229
  */
199
230
  export function createState(doc) {
@@ -248,19 +279,19 @@ export function createState(doc) {
248
279
  /**
249
280
  * Compose a flat StudioState from separate doc and session slices.
250
281
  *
251
- * @param {any} doc
252
- * @param {any} session
282
+ * @param {Partial<StudioState>} doc
283
+ * @param {Partial<StudioState>} session
253
284
  * @returns {StudioState}
254
285
  */
255
286
  export function toFlat(doc, session) {
256
- return { ...doc, ...session };
287
+ return /** @type {StudioState} */ ({ ...doc, ...session });
257
288
  }
258
289
 
259
290
  /**
260
291
  * Decompose a flat StudioState into doc and session slices.
261
292
  *
262
293
  * @param {StudioState} S
263
- * @returns {{ doc: any; session: any }}
294
+ * @returns {{ doc: Partial<StudioState>; session: Partial<StudioState> }}
264
295
  */
265
296
  export function fromFlat(S) {
266
297
  const {
@@ -303,14 +334,24 @@ export function fromFlat(S) {
303
334
  // selectedPath: string|null, searchQuery: string }
304
335
  // DirEntry: { name, path, type: "file"|"directory", size, modified }
305
336
 
306
- /** @type {any} */
337
+ /** @type {ProjectState | null} */
307
338
  export let projectState = null;
308
339
 
309
- /** @param {any} ps */
340
+ /** @param {ProjectState | null} ps */
310
341
  export function setProjectState(ps) {
311
342
  projectState = ps;
312
343
  }
313
344
 
345
+ /**
346
+ * Return the current project state, asserting it is non-null. Only call when a project is known to
347
+ * be loaded.
348
+ *
349
+ * @returns {ProjectState}
350
+ */
351
+ export function requireProjectState() {
352
+ return /** @type {ProjectState} */ (projectState);
353
+ }
354
+
314
355
  // ─── Frontmatter mutation ───────────────────────────────────────────────────
315
356
 
316
357
  /**
@@ -319,7 +360,7 @@ export function setProjectState(ps) {
319
360
  *
320
361
  * @param {StudioState} state
321
362
  * @param {string} field
322
- * @param {any} value
363
+ * @param {unknown} value
323
364
  * @returns {StudioState}
324
365
  */
325
366
  export function updateFrontmatter(state, field, value) {
@@ -359,7 +400,7 @@ export function hoverNode(state, path) {
359
400
  * Push current document onto the stack and switch to editing a new document.
360
401
  *
361
402
  * @param {StudioState} state
362
- * @param {any} doc
403
+ * @param {JxMutableNode} doc
363
404
  * @param {string | null} documentPath
364
405
  * @returns {StudioState}
365
406
  */