@jxsuite/studio 0.28.5 → 0.30.1
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 +9368 -6586
- package/dist/studio.js.map +109 -83
- 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 +11 -10
- package/src/browse/browse-modal.ts +2 -2
- package/src/browse/browse.ts +20 -19
- package/src/canvas/canvas-diff.ts +3 -3
- package/src/canvas/canvas-helpers.ts +15 -2
- package/src/canvas/canvas-live-render.ts +168 -86
- package/src/canvas/canvas-patcher.ts +656 -0
- package/src/canvas/canvas-perf.ts +68 -0
- package/src/canvas/canvas-render.ts +135 -23
- package/src/canvas/canvas-subtree-render.ts +113 -0
- package/src/canvas/canvas-utils.ts +4 -0
- package/src/editor/component-inline-edit.ts +4 -35
- package/src/editor/context-menu.ts +96 -76
- package/src/editor/convert-to-component.ts +11 -2
- package/src/editor/convert-to-repeater.ts +4 -5
- package/src/editor/inline-edit.ts +9 -9
- package/src/editor/inline-format.ts +11 -11
- package/src/editor/merge-tags.ts +120 -0
- package/src/editor/shortcuts.ts +4 -4
- package/src/files/components.ts +37 -0
- package/src/files/file-ops.ts +28 -7
- package/src/files/files.ts +68 -24
- package/src/files/fs-events.ts +162 -0
- package/src/github/github-auth.ts +14 -2
- package/src/github/github-publish.ts +12 -2
- package/src/panels/activity-bar.ts +1 -1
- package/src/panels/ai-panel.ts +67 -39
- package/src/panels/block-action-bar.ts +72 -1
- package/src/panels/canvas-dnd.ts +55 -26
- package/src/panels/data-explorer.ts +5 -3
- package/src/panels/dnd.ts +14 -17
- package/src/panels/editors.ts +5 -23
- package/src/panels/elements-panel.ts +2 -12
- package/src/panels/events-panel.ts +1 -1
- package/src/panels/git-panel.ts +6 -6
- package/src/panels/head-panel.ts +1 -1
- package/src/panels/layers-panel.ts +177 -147
- package/src/panels/preview-render.ts +15 -0
- package/src/panels/properties-panel.ts +16 -27
- package/src/panels/quick-search.ts +2 -2
- package/src/panels/right-panel.ts +2 -6
- package/src/panels/shared.ts +3 -0
- package/src/panels/signals-panel.ts +43 -33
- package/src/panels/statusbar.ts +15 -6
- package/src/panels/style-panel.ts +3 -3
- package/src/panels/style-utils.ts +3 -3
- package/src/panels/stylebook-panel.ts +4 -4
- package/src/panels/tab-bar.ts +198 -0
- package/src/panels/tab-strip.ts +2 -2
- package/src/panels/toolbar.ts +6 -75
- package/src/platforms/devserver.ts +113 -35
- package/src/reactivity.ts +2 -0
- package/src/services/cem-export.ts +23 -2
- package/src/services/code-services.ts +16 -2
- package/src/services/monaco-setup.ts +2 -1
- package/src/settings/content-types-editor.ts +16 -16
- package/src/settings/css-vars-editor.ts +2 -2
- package/src/settings/defs-editor.ts +14 -14
- package/src/settings/general-settings.ts +6 -6
- package/src/settings/head-editor.ts +2 -2
- package/src/site-context.ts +1 -1
- package/src/state.ts +66 -12
- package/src/store.ts +15 -3
- package/src/studio.ts +88 -35
- package/src/tabs/patch-ops.ts +143 -0
- package/src/tabs/tab.ts +15 -1
- package/src/tabs/transact.ts +454 -27
- package/src/types.ts +71 -1
- package/src/ui/color-selector.ts +7 -7
- package/src/ui/icons.ts +0 -30
- package/src/ui/media-picker.ts +2 -2
- package/src/ui/panel-resize.ts +14 -8
- package/src/ui/unit-selector.ts +5 -2
- package/src/ui/value-selector.ts +3 -3
- package/src/utils/canvas-media.ts +6 -6
- package/src/utils/edit-display.ts +125 -83
- package/src/utils/google-fonts.ts +1 -1
- package/src/utils/inherited-style.ts +3 -2
- package/src/utils/studio-utils.ts +1 -1
- package/src/view.ts +4 -1
|
@@ -80,7 +80,7 @@ function handleNewContentType(rerender: () => void) {
|
|
|
80
80
|
rerender();
|
|
81
81
|
|
|
82
82
|
// Persist in background
|
|
83
|
-
saveProjectConfig().then(async () => {
|
|
83
|
+
void saveProjectConfig().then(async () => {
|
|
84
84
|
const platform = getPlatform();
|
|
85
85
|
await platform.writeFile(`content/${slug}/.gitkeep`, "");
|
|
86
86
|
});
|
|
@@ -116,7 +116,7 @@ function handleAddField(rerender: () => void) {
|
|
|
116
116
|
showAddField = false;
|
|
117
117
|
newFieldState = { format: "", name: "", required: false, type: "string" };
|
|
118
118
|
rerender();
|
|
119
|
-
saveProjectConfig();
|
|
119
|
+
void saveProjectConfig();
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
@@ -135,7 +135,7 @@ function handleDeleteField(fieldName: string, rerender: () => void) {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
rerender();
|
|
138
|
-
saveProjectConfig();
|
|
138
|
+
void saveProjectConfig();
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/**
|
|
@@ -159,7 +159,7 @@ function handleToggleRequired(fieldName: string, rerender: () => void) {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
rerender();
|
|
162
|
-
saveProjectConfig();
|
|
162
|
+
void saveProjectConfig();
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/**
|
|
@@ -185,7 +185,7 @@ function handleRenameField(oldName: string, newName: string, rerender: () => voi
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
rerender();
|
|
188
|
-
saveProjectConfig();
|
|
188
|
+
void saveProjectConfig();
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
/**
|
|
@@ -205,7 +205,7 @@ function handleChangeType(fieldName: string, newType: string, rerender: () => vo
|
|
|
205
205
|
: undefined;
|
|
206
206
|
schema.properties[fieldName] = schemaForType(newType, oldFormat || undefined);
|
|
207
207
|
rerender();
|
|
208
|
-
saveProjectConfig();
|
|
208
|
+
void saveProjectConfig();
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
/**
|
|
@@ -223,7 +223,7 @@ function handleChangeFormat(fieldName: string, format: string, rerender: () => v
|
|
|
223
223
|
const type = prop.type || "string";
|
|
224
224
|
schema.properties[fieldName] = schemaForType(type, format || undefined);
|
|
225
225
|
rerender();
|
|
226
|
-
saveProjectConfig();
|
|
226
|
+
void saveProjectConfig();
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
/**
|
|
@@ -239,7 +239,7 @@ function handleChangeRefTarget(fieldName: string, target: string, rerender: () =
|
|
|
239
239
|
|
|
240
240
|
schema.properties[fieldName] = { $ref: `#/contentTypes/${target}` };
|
|
241
241
|
rerender();
|
|
242
|
-
saveProjectConfig();
|
|
242
|
+
void saveProjectConfig();
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
// ─── Nested field handlers ───────────────────────────────────────────────────
|
|
@@ -280,7 +280,7 @@ function handleAddNestedField(
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
rerender();
|
|
283
|
-
saveProjectConfig();
|
|
283
|
+
void saveProjectConfig();
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
/**
|
|
@@ -301,7 +301,7 @@ function handleDeleteNested(parentName: string, childName: string, rerender: ()
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
rerender();
|
|
304
|
-
saveProjectConfig();
|
|
304
|
+
void saveProjectConfig();
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
/**
|
|
@@ -327,7 +327,7 @@ function handleToggleNestedRequired(parentName: string, childName: string, reren
|
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
rerender();
|
|
330
|
-
saveProjectConfig();
|
|
330
|
+
void saveProjectConfig();
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
/**
|
|
@@ -360,7 +360,7 @@ function handleRenameNested(
|
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
rerender();
|
|
363
|
-
saveProjectConfig();
|
|
363
|
+
void saveProjectConfig();
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
/**
|
|
@@ -387,7 +387,7 @@ function handleChangeNestedType(
|
|
|
387
387
|
: undefined;
|
|
388
388
|
parent.properties[childName] = schemaForType(newType, oldFormat || undefined);
|
|
389
389
|
rerender();
|
|
390
|
-
saveProjectConfig();
|
|
390
|
+
void saveProjectConfig();
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
/**
|
|
@@ -412,7 +412,7 @@ function handleChangeNestedFormat(
|
|
|
412
412
|
const type = prop.type || "string";
|
|
413
413
|
parent.properties[childName] = schemaForType(type, format || undefined);
|
|
414
414
|
rerender();
|
|
415
|
-
saveProjectConfig();
|
|
415
|
+
void saveProjectConfig();
|
|
416
416
|
}
|
|
417
417
|
|
|
418
418
|
/** @param {() => void} rerender */
|
|
@@ -429,7 +429,7 @@ function handleDeleteContentType(rerender: () => void) {
|
|
|
429
429
|
selectedContentType = null;
|
|
430
430
|
|
|
431
431
|
rerender();
|
|
432
|
-
saveProjectConfig();
|
|
432
|
+
void saveProjectConfig();
|
|
433
433
|
}
|
|
434
434
|
|
|
435
435
|
// ─── Render ───────────────────────────────────────────────────────────────────
|
|
@@ -508,7 +508,7 @@ export function renderContentTypesEditor(container: HTMLElement) {
|
|
|
508
508
|
if (!selectedContentType || !contentTypes[selectedContentType]) {
|
|
509
509
|
editorTpl = html`<div class="settings-empty-state">Select or create a content type</div>`;
|
|
510
510
|
} else {
|
|
511
|
-
const col = contentTypes[selectedContentType]
|
|
511
|
+
const col = contentTypes[selectedContentType]!;
|
|
512
512
|
const schema = (col.schema || {}) as ContentTypeSchema;
|
|
513
513
|
const properties = schema.properties || {};
|
|
514
514
|
const required = schema.required || [];
|
|
@@ -53,7 +53,7 @@ export function renderCssVarsEditor(container: HTMLElement) {
|
|
|
53
53
|
const mediaNames = media ? Object.keys(media).filter((m) => m !== "--") : [];
|
|
54
54
|
|
|
55
55
|
const save = () => {
|
|
56
|
-
updateSiteConfig({ style: { ...rootStyle } });
|
|
56
|
+
void updateSiteConfig({ style: { ...rootStyle } });
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
const updateVar = (name: string, val: string) => {
|
|
@@ -297,7 +297,7 @@ function renderMediaOverrides(varName: string, rootStyle: JxStyle, mediaNames: s
|
|
|
297
297
|
(rootStyle[`@${o.mediaName}`] as Record<string, unknown>)[varName] = (
|
|
298
298
|
e.target as HTMLInputElement
|
|
299
299
|
).value;
|
|
300
|
-
updateSiteConfig({ style: { ...rootStyle } });
|
|
300
|
+
void updateSiteConfig({ style: { ...rootStyle } });
|
|
301
301
|
}}
|
|
302
302
|
style="max-width:120px"
|
|
303
303
|
></sp-textfield>
|
|
@@ -73,7 +73,7 @@ function handleNewDef(rerender: () => void) {
|
|
|
73
73
|
showNewDef = false;
|
|
74
74
|
newDefName = "";
|
|
75
75
|
rerender();
|
|
76
|
-
saveProjectConfig();
|
|
76
|
+
void saveProjectConfig();
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/** @param {() => void} rerender */
|
|
@@ -105,7 +105,7 @@ function handleAddField(rerender: () => void) {
|
|
|
105
105
|
showAddField = false;
|
|
106
106
|
newFieldState = { format: "", name: "", required: false, type: "string" };
|
|
107
107
|
rerender();
|
|
108
|
-
saveProjectConfig();
|
|
108
|
+
void saveProjectConfig();
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
@@ -124,7 +124,7 @@ function handleDeleteField(fieldName: string, rerender: () => void) {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
rerender();
|
|
127
|
-
saveProjectConfig();
|
|
127
|
+
void saveProjectConfig();
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
/**
|
|
@@ -148,7 +148,7 @@ function handleToggleRequired(fieldName: string, rerender: () => void) {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
rerender();
|
|
151
|
-
saveProjectConfig();
|
|
151
|
+
void saveProjectConfig();
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
/**
|
|
@@ -173,7 +173,7 @@ function handleRenameField(oldName: string, newName: string, rerender: () => voi
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
rerender();
|
|
176
|
-
saveProjectConfig();
|
|
176
|
+
void saveProjectConfig();
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
/**
|
|
@@ -193,7 +193,7 @@ function handleChangeType(fieldName: string, newType: string, rerender: () => vo
|
|
|
193
193
|
: undefined;
|
|
194
194
|
def.properties[fieldName] = schemaForType(newType, oldFormat || undefined);
|
|
195
195
|
rerender();
|
|
196
|
-
saveProjectConfig();
|
|
196
|
+
void saveProjectConfig();
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
/**
|
|
@@ -211,7 +211,7 @@ function handleChangeFormat(fieldName: string, format: string, rerender: () => v
|
|
|
211
211
|
const type = prop.type || "string";
|
|
212
212
|
def.properties[fieldName] = schemaForType(type, format || undefined);
|
|
213
213
|
rerender();
|
|
214
|
-
saveProjectConfig();
|
|
214
|
+
void saveProjectConfig();
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
// ─── Nested field handlers ───────────────────────────────────────────────────
|
|
@@ -247,7 +247,7 @@ function handleAddNestedField(
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
rerender();
|
|
250
|
-
saveProjectConfig();
|
|
250
|
+
void saveProjectConfig();
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
/**
|
|
@@ -268,7 +268,7 @@ function handleDeleteNested(parentName: string, childName: string, rerender: ()
|
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
rerender();
|
|
271
|
-
saveProjectConfig();
|
|
271
|
+
void saveProjectConfig();
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
/**
|
|
@@ -294,7 +294,7 @@ function handleToggleNestedRequired(parentName: string, childName: string, reren
|
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
rerender();
|
|
297
|
-
saveProjectConfig();
|
|
297
|
+
void saveProjectConfig();
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
/**
|
|
@@ -326,7 +326,7 @@ function handleRenameNested(
|
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
rerender();
|
|
329
|
-
saveProjectConfig();
|
|
329
|
+
void saveProjectConfig();
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
/**
|
|
@@ -353,7 +353,7 @@ function handleChangeNestedType(
|
|
|
353
353
|
: undefined;
|
|
354
354
|
parent.properties[childName] = schemaForType(newType, oldFormat || undefined);
|
|
355
355
|
rerender();
|
|
356
|
-
saveProjectConfig();
|
|
356
|
+
void saveProjectConfig();
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
/**
|
|
@@ -378,7 +378,7 @@ function handleChangeNestedFormat(
|
|
|
378
378
|
const type = prop.type || "string";
|
|
379
379
|
parent.properties[childName] = schemaForType(type, format || undefined);
|
|
380
380
|
rerender();
|
|
381
|
-
saveProjectConfig();
|
|
381
|
+
void saveProjectConfig();
|
|
382
382
|
}
|
|
383
383
|
|
|
384
384
|
/** @param {() => void} rerender */
|
|
@@ -395,7 +395,7 @@ function handleDeleteDef(rerender: () => void) {
|
|
|
395
395
|
selectedDef = null;
|
|
396
396
|
|
|
397
397
|
rerender();
|
|
398
|
-
saveProjectConfig();
|
|
398
|
+
void saveProjectConfig();
|
|
399
399
|
}
|
|
400
400
|
|
|
401
401
|
// ─── Render ───────────────────────────────────────────────────────────────────
|
|
@@ -31,7 +31,7 @@ export function renderGeneralSettings(container: HTMLElement) {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
const onAdapterChange = (e: Event) => {
|
|
34
|
-
updateSiteConfig({
|
|
34
|
+
void updateSiteConfig({
|
|
35
35
|
build: { ...config.build, adapter: (e.target as HTMLInputElement).value },
|
|
36
36
|
});
|
|
37
37
|
};
|
|
@@ -40,7 +40,7 @@ export function renderGeneralSettings(container: HTMLElement) {
|
|
|
40
40
|
// Lazy import breaks the general-settings ↔ settings-modal module cycle
|
|
41
41
|
const { closeSettingsModal } = await import("./settings-modal");
|
|
42
42
|
closeSettingsModal();
|
|
43
|
-
openFileInTab("project.json");
|
|
43
|
+
void openFileInTab("project.json");
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
// ─── Breakpoints ($media) ───────────────────────────────────────────────────
|
|
@@ -50,7 +50,7 @@ export function renderGeneralSettings(container: HTMLElement) {
|
|
|
50
50
|
|
|
51
51
|
const onMediaValueChange = (key: string) => (e: Event) => {
|
|
52
52
|
const updated = { ...media, [key]: (e.target as HTMLInputElement).value };
|
|
53
|
-
updateSiteConfig({ $media: updated });
|
|
53
|
+
void updateSiteConfig({ $media: updated });
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
const onMediaNameChange = (oldKey: string) => (e: Event) => {
|
|
@@ -63,20 +63,20 @@ export function renderGeneralSettings(container: HTMLElement) {
|
|
|
63
63
|
for (const [k, v] of Object.entries(media)) {
|
|
64
64
|
updated[k === oldKey ? newKey : k] = v;
|
|
65
65
|
}
|
|
66
|
-
updateSiteConfig({ $media: updated });
|
|
66
|
+
void updateSiteConfig({ $media: updated });
|
|
67
67
|
renderGeneralSettings(container);
|
|
68
68
|
};
|
|
69
69
|
|
|
70
70
|
const onRemoveBreakpoint = (key: string) => () => {
|
|
71
71
|
const updated = { ...media };
|
|
72
72
|
delete updated[key];
|
|
73
|
-
updateSiteConfig({ $media: updated });
|
|
73
|
+
void updateSiteConfig({ $media: updated });
|
|
74
74
|
renderGeneralSettings(container);
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
const onAddBreakpoint = () => {
|
|
78
78
|
const updated = { ...media, "--new": "(max-width: 480px)" };
|
|
79
|
-
updateSiteConfig({ $media: updated });
|
|
79
|
+
void updateSiteConfig({ $media: updated });
|
|
80
80
|
renderGeneralSettings(container);
|
|
81
81
|
};
|
|
82
82
|
|
|
@@ -23,7 +23,7 @@ export function renderHeadEditor(container: HTMLElement) {
|
|
|
23
23
|
const headEntries = config.$head || ([] as JxHeadEntry[]);
|
|
24
24
|
|
|
25
25
|
const save = () => {
|
|
26
|
-
updateSiteConfig({ $head: headEntries });
|
|
26
|
+
void updateSiteConfig({ $head: headEntries });
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
const addEntry = (tag: string) => {
|
|
@@ -52,7 +52,7 @@ export function renderHeadEditor(container: HTMLElement) {
|
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
const updateEntry = (idx: number, key: string, val: string) => {
|
|
55
|
-
const entry = headEntries[idx]
|
|
55
|
+
const entry = headEntries[idx]!;
|
|
56
56
|
if (key === "content" && (entry.tagName === "script" || entry.tagName === "style")) {
|
|
57
57
|
entry.textContent = val;
|
|
58
58
|
} else {
|
package/src/site-context.ts
CHANGED
|
@@ -170,7 +170,7 @@ export async function resolveLayoutDoc(layoutPath: string) {
|
|
|
170
170
|
try {
|
|
171
171
|
const platform = getPlatform();
|
|
172
172
|
const content = await platform.readFile(normalized);
|
|
173
|
-
const doc = JSON.parse(content);
|
|
173
|
+
const doc = JSON.parse(content) as JxMutableNode;
|
|
174
174
|
layoutCache.set(normalized, doc);
|
|
175
175
|
return structuredClone(doc);
|
|
176
176
|
} catch {
|
package/src/state.ts
CHANGED
|
@@ -84,6 +84,52 @@ export function childList(node: JxMutableNode | null | undefined): (JxMutableNod
|
|
|
84
84
|
return Array.isArray(node?.children) ? node.children : [];
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Normalize a document in place to the canonical array-member form: a legacy whole-children
|
|
89
|
+
* repeater (`children: { $prototype: "Array", … }`) becomes a single member of a children array
|
|
90
|
+
* (`children: [{ $prototype: "Array", … }]`). Recurses through children, repeater templates, and
|
|
91
|
+
* `$switch` cases. Runs once when a document is loaded into a tab so every in-studio doc — and its
|
|
92
|
+
* history checkpoints — uses the member form before any mutation.
|
|
93
|
+
*
|
|
94
|
+
* @param {unknown} node
|
|
95
|
+
* @returns {unknown} The same node (mutated)
|
|
96
|
+
*/
|
|
97
|
+
export function normalizeArrayChildren(node: unknown): unknown {
|
|
98
|
+
if (!node || typeof node !== "object") {
|
|
99
|
+
return node;
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(node)) {
|
|
102
|
+
for (const child of node) {
|
|
103
|
+
normalizeArrayChildren(child);
|
|
104
|
+
}
|
|
105
|
+
return node;
|
|
106
|
+
}
|
|
107
|
+
const n = node as JxMutableNode;
|
|
108
|
+
const { children } = n;
|
|
109
|
+
if (
|
|
110
|
+
children &&
|
|
111
|
+
typeof children === "object" &&
|
|
112
|
+
!Array.isArray(children) &&
|
|
113
|
+
(children as JxMutableNode).$prototype === "Array"
|
|
114
|
+
) {
|
|
115
|
+
n.children = [children as JxMutableNode];
|
|
116
|
+
}
|
|
117
|
+
if (Array.isArray(n.children)) {
|
|
118
|
+
for (const child of n.children) {
|
|
119
|
+
normalizeArrayChildren(child);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (n.$prototype === "Array" && n.map && typeof n.map === "object") {
|
|
123
|
+
normalizeArrayChildren(n.map);
|
|
124
|
+
}
|
|
125
|
+
if (n.cases && typeof n.cases === "object") {
|
|
126
|
+
for (const caseDef of Object.values(n.cases)) {
|
|
127
|
+
normalizeArrayChildren(caseDef);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return node;
|
|
131
|
+
}
|
|
132
|
+
|
|
87
133
|
/**
|
|
88
134
|
* Return the path to the parent element (strips trailing 'children' + index).
|
|
89
135
|
*
|
|
@@ -180,6 +226,19 @@ export function flattenTree(
|
|
|
180
226
|
return [{ depth, node: doc, nodeType: "text", path }];
|
|
181
227
|
}
|
|
182
228
|
|
|
229
|
+
// Array pseudo-element (repeater): a first-class node at its own path. Emit the "map" row, then
|
|
230
|
+
// Recurse into its single template at `[...path, "map"]`. This is reached both when the array is
|
|
231
|
+
// A member of a children array (path `[…, "children", i]`) and the legacy whole-children form
|
|
232
|
+
// (path `[…, "children"]`).
|
|
233
|
+
if ((doc as JxMutableNode).$prototype === "Array") {
|
|
234
|
+
const rows: FlatRow[] = [{ depth, node: doc, nodeType: "map", path }];
|
|
235
|
+
const mapDef = (doc as JxMutableNode).map;
|
|
236
|
+
if (mapDef && typeof mapDef === "object") {
|
|
237
|
+
rows.push(...flattenTree(mapDef as JxMutableNode, [...path, "map"], depth + 1));
|
|
238
|
+
}
|
|
239
|
+
return rows;
|
|
240
|
+
}
|
|
241
|
+
|
|
183
242
|
const rows: FlatRow[] = [{ depth, node: doc, nodeType: "element", path }];
|
|
184
243
|
|
|
185
244
|
// Custom component instances without user-authored children are atomic in the layer tree
|
|
@@ -192,24 +251,15 @@ export function flattenTree(
|
|
|
192
251
|
if (Array.isArray(children)) {
|
|
193
252
|
for (let i = 0; i < children.length; i++) {
|
|
194
253
|
const childPath = [...path, "children", i];
|
|
195
|
-
rows.push(...flattenTree(children[i]
|
|
254
|
+
rows.push(...flattenTree(children[i]!, childPath, depth + 1));
|
|
196
255
|
}
|
|
197
256
|
} else if (
|
|
198
257
|
children &&
|
|
199
258
|
typeof children === "object" &&
|
|
200
259
|
(children as JxMutableNode).$prototype === "Array"
|
|
201
260
|
) {
|
|
202
|
-
//
|
|
203
|
-
rows.push(
|
|
204
|
-
depth: depth + 1,
|
|
205
|
-
node: children as JxMutableNode,
|
|
206
|
-
nodeType: "map",
|
|
207
|
-
path: [...path, "children"],
|
|
208
|
-
});
|
|
209
|
-
const mapDef = (children as JxMutableNode).map;
|
|
210
|
-
if (mapDef && typeof mapDef === "object") {
|
|
211
|
-
rows.push(...flattenTree(mapDef as JxMutableNode, [...path, "children", "map"], depth + 2));
|
|
212
|
-
}
|
|
261
|
+
// Legacy whole-children repeater: the array occupies the children slot itself.
|
|
262
|
+
rows.push(...flattenTree(children as JxMutableNode, [...path, "children"], depth + 1));
|
|
213
263
|
}
|
|
214
264
|
|
|
215
265
|
// $switch — emit each case as a virtual child
|
|
@@ -262,6 +312,10 @@ export function nodeLabel(node: JxMutableNode | null) {
|
|
|
262
312
|
if (node.$id) {
|
|
263
313
|
return node.$id;
|
|
264
314
|
}
|
|
315
|
+
if (node.tagName === "slot") {
|
|
316
|
+
const name = node.attributes?.name;
|
|
317
|
+
return typeof name === "string" && name.trim() ? `slot: ${name.trim()}` : "slot";
|
|
318
|
+
}
|
|
265
319
|
const tag = node.tagName ?? "div";
|
|
266
320
|
const suffix = node.$switch ? " ⇆" : "";
|
|
267
321
|
if (typeof node.textContent === "string" && node.textContent.length > 0) {
|
package/src/store.ts
CHANGED
|
@@ -54,6 +54,18 @@ export function initShellRefs() {
|
|
|
54
54
|
|
|
55
55
|
export const elToPath = new WeakMap<Element, JxPath>();
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Canvas element → the runtime scope its children render with. Captured during live renders so the
|
|
59
|
+
* canvas patcher can re-render a subtree in isolation with the same prototype-chained scope.
|
|
60
|
+
*/
|
|
61
|
+
export const elToScope = new WeakMap<Element, Record<string, unknown>>();
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Subtree root element → the effect scope created for its surgical render. Stopped when the subtree
|
|
65
|
+
* is replaced or removed so render-time reactive effects don't accumulate.
|
|
66
|
+
*/
|
|
67
|
+
export const elToRenderScope = new WeakMap<Element, { stop: () => void }>();
|
|
68
|
+
|
|
57
69
|
export const canvasPanels: CanvasPanel[] = [];
|
|
58
70
|
|
|
59
71
|
// ─── Shared constants ────────────────────────────────────────────────────────
|
|
@@ -97,7 +109,7 @@ export function isNestedSelector(k: string) {
|
|
|
97
109
|
|
|
98
110
|
// ─── Shared utilities ────────────────────────────────────────────────────────
|
|
99
111
|
|
|
100
|
-
const _styleDebounceTimers = new Map();
|
|
112
|
+
const _styleDebounceTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
101
113
|
|
|
102
114
|
/**
|
|
103
115
|
* @param {string} prop
|
|
@@ -139,7 +151,7 @@ export function stripEventHandlers(node: JxMutableNode): JxMutableNode {
|
|
|
139
151
|
}
|
|
140
152
|
if (Array.isArray(node)) {
|
|
141
153
|
// Arrays of nodes round-trip element-wise; the array itself is not a node.
|
|
142
|
-
return node.map((n) => stripEventHandlers(n)) as unknown as JxMutableNode;
|
|
154
|
+
return node.map((n: JxMutableNode) => stripEventHandlers(n)) as unknown as JxMutableNode;
|
|
143
155
|
}
|
|
144
156
|
const out: Record<string, unknown> = {};
|
|
145
157
|
for (const [k, v] of Object.entries(node)) {
|
|
@@ -148,7 +160,7 @@ export function stripEventHandlers(node: JxMutableNode): JxMutableNode {
|
|
|
148
160
|
}
|
|
149
161
|
if (k === "children") {
|
|
150
162
|
out.children = Array.isArray(v)
|
|
151
|
-
? v.map((c) => stripEventHandlers(c))
|
|
163
|
+
? v.map((c: JxMutableNode) => stripEventHandlers(c))
|
|
152
164
|
: stripEventHandlers(v as JxMutableNode);
|
|
153
165
|
} else if (k === "cases" && typeof v === "object") {
|
|
154
166
|
const cases: Record<string, unknown> = {};
|