@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.
- package/dist/studio.css +98 -98
- package/dist/studio.js +12577 -12183
- package/dist/studio.js.map +146 -144
- package/dist/workers/editor.worker.js +79 -79
- package/dist/workers/json.worker.js +109 -109
- package/dist/workers/ts.worker.js +82 -82
- package/package.json +17 -6
- package/src/browse/browse.js +25 -19
- package/src/canvas/canvas-diff.js +24 -16
- package/src/canvas/canvas-helpers.js +24 -19
- package/src/canvas/canvas-live-render.js +94 -39
- package/src/canvas/canvas-render.js +119 -73
- package/src/canvas/canvas-utils.js +38 -15
- package/src/canvas/nested-site-style.js +50 -0
- package/src/editor/component-inline-edit.js +14 -13
- package/src/editor/content-inline-edit.js +22 -10
- package/src/editor/context-menu.js +1 -1
- package/src/editor/convert-to-component.js +12 -10
- package/src/editor/inline-edit.js +66 -28
- package/src/editor/insertion-helper.js +16 -12
- package/src/editor/shortcuts.js +4 -2
- package/src/editor/slash-menu.js +6 -4
- package/src/files/components.js +1 -1
- package/src/files/file-ops.js +37 -16
- package/src/files/files.js +78 -52
- package/src/github/github-auth.js +122 -0
- package/src/github/github-publish.js +115 -0
- package/src/markdown/md-convert.js +172 -107
- package/src/new-project/new-project-modal.js +204 -0
- package/src/panels/activity-bar.js +22 -20
- package/src/panels/ai-panel.js +399 -0
- package/src/panels/block-action-bar.js +5 -5
- package/src/panels/canvas-dnd.js +8 -2
- package/src/panels/data-explorer.js +19 -13
- package/src/panels/dnd.js +144 -92
- package/src/panels/editors.js +55 -35
- package/src/panels/elements-panel.js +33 -19
- package/src/panels/events-panel.js +15 -10
- package/src/panels/git-panel.js +469 -98
- package/src/panels/head-panel.js +121 -81
- package/src/panels/imports-panel.js +88 -64
- package/src/panels/layers-panel.js +64 -43
- package/src/panels/left-panel.js +85 -47
- package/src/panels/overlays.js +1 -0
- package/src/panels/panel-events.js +45 -30
- package/src/panels/preview-render.js +23 -18
- package/src/panels/properties-panel.js +365 -249
- package/src/panels/pseudo-preview.js +2 -2
- package/src/panels/quick-search.js +7 -5
- package/src/panels/right-panel.js +26 -2
- package/src/panels/shared.js +4 -4
- package/src/panels/signals-panel.js +295 -166
- package/src/panels/statusbar.js +4 -4
- package/src/panels/style-inputs.js +67 -35
- package/src/panels/style-panel.js +233 -132
- package/src/panels/style-utils.js +34 -16
- package/src/panels/stylebook-layers-panel.js +12 -10
- package/src/panels/stylebook-panel.js +134 -66
- package/src/panels/toolbar.js +333 -109
- package/src/panels/welcome-screen.js +121 -0
- package/src/platform.js +2 -4
- package/src/platforms/devserver.js +113 -7
- package/src/resize-edges.js +98 -0
- package/src/services/cem-export.js +12 -7
- package/src/services/code-services.js +30 -12
- package/src/settings/content-types-editor.js +7 -7
- package/src/settings/css-vars-editor.js +30 -24
- package/src/settings/defs-editor.js +12 -7
- package/src/settings/general-settings.js +85 -3
- package/src/settings/head-editor.js +30 -24
- package/src/settings/schema-field-ui.js +58 -39
- package/src/site-context.js +41 -31
- package/src/state.js +70 -29
- package/src/store.js +21 -26
- package/src/studio.js +103 -81
- package/src/tabs/tab.js +67 -43
- package/src/tabs/transact.js +23 -13
- package/src/ui/button-group.js +24 -24
- package/src/ui/color-selector.js +28 -23
- package/src/ui/field-row.js +3 -3
- package/src/ui/icons.js +2 -2
- package/src/ui/media-picker.js +7 -7
- package/src/ui/spectrum.js +5 -1
- package/src/ui/unit-selector.js +18 -16
- package/src/ui/value-selector.js +21 -15
- package/src/ui/widgets.js +22 -19
- package/src/utils/canvas-media.js +5 -4
- package/src/utils/edit-display.js +31 -20
- package/src/utils/google-fonts.js +11 -11
- package/src/utils/inherited-style.js +9 -8
- package/src/utils/studio-utils.js +7 -7
- package/src/view.js +58 -1
- 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?:
|
|
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 {
|
|
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 {
|
|
93
|
-
const
|
|
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
|
|
96
|
+
else target.value = fieldName;
|
|
96
97
|
}}
|
|
97
|
-
@keydown=${(/** @type {
|
|
98
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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 {
|
|
136
|
+
@change=${(/** @type {Event} */ e) => {
|
|
135
137
|
if (handlers.onChangeRefTarget)
|
|
136
|
-
handlers.onChangeRefTarget(
|
|
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 {
|
|
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 {
|
|
188
|
-
const
|
|
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
|
-
|
|
198
|
+
target.value = childName;
|
|
193
199
|
}
|
|
194
200
|
}}
|
|
195
|
-
@keydown=${(/** @type {
|
|
196
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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 {
|
|
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 {
|
|
259
|
+
@keydown=${(/** @type {KeyboardEvent} */ e) => {
|
|
253
260
|
if (e.key === "Enter") {
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
const
|
|
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
|
-
|
|
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 {
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
const
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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:
|
|
350
|
+
* onInput: (field: string, value: string | boolean) => void;
|
|
334
351
|
* onConfirm: () => void;
|
|
335
352
|
* onCancel: () => void;
|
|
336
353
|
* }} handlers
|
|
337
|
-
* @returns {
|
|
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 {
|
|
347
|
-
|
|
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 {
|
|
377
|
+
@change=${(/** @type {Event} */ e) =>
|
|
378
|
+
handlers.onInput("required", /** @type {HTMLInputElement} */ (e.target).checked)}
|
|
360
379
|
>
|
|
361
380
|
Required
|
|
362
381
|
</sp-switch>
|
package/src/site-context.js
CHANGED
|
@@ -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 {
|
|
15
|
-
*
|
|
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 {
|
|
29
|
-
* @returns {
|
|
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] = {
|
|
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 {
|
|
55
|
-
*
|
|
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 {
|
|
68
|
-
*
|
|
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 {
|
|
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 {
|
|
92
|
-
* @returns {
|
|
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 {
|
|
103
|
+
/** @type {JxHeadEntry[]} */
|
|
101
104
|
const merged = [];
|
|
102
105
|
for (const entry of [...siteHead, ...docHead]) {
|
|
103
|
-
const key =
|
|
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,
|
|
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 {
|
|
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<
|
|
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))
|
|
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 {
|
|
159
|
-
* @param {
|
|
160
|
-
* @returns {
|
|
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 {
|
|
191
|
-
/** @type {Map<string,
|
|
192
|
-
/** @type {
|
|
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 {
|
|
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 {
|
|
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 = {
|
|
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({ ...
|
|
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:
|
|
14
|
+
* @typedef {{ document: JxMutableNode; selection: JxPath | null }} HistorySnapshot
|
|
17
15
|
*
|
|
18
16
|
* @typedef {{
|
|
19
|
-
* document:
|
|
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:
|
|
23
|
+
* fileHandle: FileSystemFileHandle | null;
|
|
26
24
|
* documentPath: string | null;
|
|
27
|
-
* documentStack:
|
|
25
|
+
* documentStack: StudioStackFrame[];
|
|
28
26
|
* handlersSource: string | null;
|
|
29
27
|
* mode: string;
|
|
30
|
-
* content: { frontmatter: Record<string,
|
|
31
|
-
* ui: Record<string,
|
|
32
|
-
* canvas: { status: string; scope:
|
|
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 {
|
|
50
|
+
* @param {JxMutableNode} doc
|
|
42
51
|
* @param {JxPath} path
|
|
43
|
-
* @returns {
|
|
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 {
|
|
126
|
+
* @param {JxMutableNode | string | number | boolean} doc
|
|
118
127
|
* @param {JxPath} [path]
|
|
119
128
|
* @param {number} [depth]
|
|
120
|
-
* @returns {{
|
|
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
|
-
/**
|
|
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 (
|
|
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(
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
252
|
-
* @param {
|
|
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:
|
|
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 {
|
|
337
|
+
/** @type {ProjectState | null} */
|
|
307
338
|
export let projectState = null;
|
|
308
339
|
|
|
309
|
-
/** @param {
|
|
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 {
|
|
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 {
|
|
403
|
+
* @param {JxMutableNode} doc
|
|
363
404
|
* @param {string | null} documentPath
|
|
364
405
|
* @returns {StudioState}
|
|
365
406
|
*/
|