@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.
- package/dist/studio.css +98 -98
- package/dist/studio.js +13476 -12330
- package/dist/studio.js.map +152 -148
- 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 +19 -7
- package/src/browse/browse-modal.js +16 -13
- package/src/browse/browse.js +44 -35
- package/src/canvas/canvas-diff.js +24 -16
- package/src/canvas/canvas-helpers.js +24 -19
- package/src/canvas/canvas-live-render.js +113 -39
- package/src/canvas/canvas-render.js +119 -73
- package/src/canvas/canvas-utils.js +51 -21
- 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 +28 -14
- package/src/editor/convert-targets.js +60 -0
- package/src/editor/convert-to-component.js +20 -19
- package/src/editor/convert-to-repeater.js +226 -0
- package/src/editor/inline-edit.js +69 -28
- package/src/editor/insertion-helper.js +16 -12
- package/src/editor/shortcuts.js +14 -4
- package/src/editor/slash-menu.js +40 -19
- package/src/files/components.js +1 -1
- package/src/files/file-ops.js +37 -16
- package/src/files/files.js +322 -68
- package/src/github/github-auth.js +122 -0
- package/src/github/github-publish.js +126 -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 +27 -25
- package/src/panels/ai-panel.js +406 -0
- package/src/panels/block-action-bar.js +77 -28
- 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 +475 -99
- package/src/panels/head-panel.js +121 -81
- package/src/panels/imports-panel.js +94 -65
- package/src/panels/layers-panel.js +64 -43
- package/src/panels/left-panel.js +105 -48
- 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 +378 -257
- package/src/panels/pseudo-preview.js +2 -2
- package/src/panels/quick-search.js +11 -9
- package/src/panels/right-panel.js +42 -16
- package/src/panels/shared.js +4 -4
- package/src/panels/signals-panel.js +415 -166
- package/src/panels/statusbar.js +10 -6
- package/src/panels/style-inputs.js +67 -35
- package/src/panels/style-panel.js +239 -134
- 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/tab-strip.js +5 -2
- 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 +8 -8
- 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/settings/settings-modal.js +12 -9
- package/src/site-context.js +41 -31
- package/src/state.js +70 -29
- package/src/store.js +36 -35
- package/src/studio.js +120 -90
- 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/layers.js +31 -1
- 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 +32 -26
- package/src/utils/google-fonts.js +11 -11
- package/src/utils/inherited-style.js +9 -8
- package/src/utils/studio-utils.js +18 -18
- package/src/view.js +58 -1
- 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?:
|
|
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>
|
|
@@ -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
|
-
|
|
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(
|
|
104
|
+
renderGeneralSettings(_contentEl);
|
|
102
105
|
break;
|
|
103
106
|
case "head":
|
|
104
|
-
renderHeadEditor(
|
|
107
|
+
renderHeadEditor(_contentEl);
|
|
105
108
|
break;
|
|
106
109
|
case "cssVars":
|
|
107
|
-
renderCssVarsEditor(
|
|
110
|
+
renderCssVarsEditor(_contentEl);
|
|
108
111
|
break;
|
|
109
112
|
case "definitions":
|
|
110
|
-
renderDefsEditor(
|
|
113
|
+
renderDefsEditor(_contentEl);
|
|
111
114
|
break;
|
|
112
115
|
case "contentTypes":
|
|
113
|
-
renderContentTypesEditor(
|
|
116
|
+
renderContentTypesEditor(_contentEl);
|
|
114
117
|
break;
|
|
115
118
|
}
|
|
116
119
|
}
|
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
|
}
|