@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.
Files changed (86) hide show
  1. package/dist/studio.css +98 -98
  2. package/dist/studio.js +9368 -6586
  3. package/dist/studio.js.map +109 -83
  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 +11 -10
  8. package/src/browse/browse-modal.ts +2 -2
  9. package/src/browse/browse.ts +20 -19
  10. package/src/canvas/canvas-diff.ts +3 -3
  11. package/src/canvas/canvas-helpers.ts +15 -2
  12. package/src/canvas/canvas-live-render.ts +168 -86
  13. package/src/canvas/canvas-patcher.ts +656 -0
  14. package/src/canvas/canvas-perf.ts +68 -0
  15. package/src/canvas/canvas-render.ts +135 -23
  16. package/src/canvas/canvas-subtree-render.ts +113 -0
  17. package/src/canvas/canvas-utils.ts +4 -0
  18. package/src/editor/component-inline-edit.ts +4 -35
  19. package/src/editor/context-menu.ts +96 -76
  20. package/src/editor/convert-to-component.ts +11 -2
  21. package/src/editor/convert-to-repeater.ts +4 -5
  22. package/src/editor/inline-edit.ts +9 -9
  23. package/src/editor/inline-format.ts +11 -11
  24. package/src/editor/merge-tags.ts +120 -0
  25. package/src/editor/shortcuts.ts +4 -4
  26. package/src/files/components.ts +37 -0
  27. package/src/files/file-ops.ts +28 -7
  28. package/src/files/files.ts +68 -24
  29. package/src/files/fs-events.ts +162 -0
  30. package/src/github/github-auth.ts +14 -2
  31. package/src/github/github-publish.ts +12 -2
  32. package/src/panels/activity-bar.ts +1 -1
  33. package/src/panels/ai-panel.ts +67 -39
  34. package/src/panels/block-action-bar.ts +72 -1
  35. package/src/panels/canvas-dnd.ts +55 -26
  36. package/src/panels/data-explorer.ts +5 -3
  37. package/src/panels/dnd.ts +14 -17
  38. package/src/panels/editors.ts +5 -23
  39. package/src/panels/elements-panel.ts +2 -12
  40. package/src/panels/events-panel.ts +1 -1
  41. package/src/panels/git-panel.ts +6 -6
  42. package/src/panels/head-panel.ts +1 -1
  43. package/src/panels/layers-panel.ts +177 -147
  44. package/src/panels/preview-render.ts +15 -0
  45. package/src/panels/properties-panel.ts +16 -27
  46. package/src/panels/quick-search.ts +2 -2
  47. package/src/panels/right-panel.ts +2 -6
  48. package/src/panels/shared.ts +3 -0
  49. package/src/panels/signals-panel.ts +43 -33
  50. package/src/panels/statusbar.ts +15 -6
  51. package/src/panels/style-panel.ts +3 -3
  52. package/src/panels/style-utils.ts +3 -3
  53. package/src/panels/stylebook-panel.ts +4 -4
  54. package/src/panels/tab-bar.ts +198 -0
  55. package/src/panels/tab-strip.ts +2 -2
  56. package/src/panels/toolbar.ts +6 -75
  57. package/src/platforms/devserver.ts +113 -35
  58. package/src/reactivity.ts +2 -0
  59. package/src/services/cem-export.ts +23 -2
  60. package/src/services/code-services.ts +16 -2
  61. package/src/services/monaco-setup.ts +2 -1
  62. package/src/settings/content-types-editor.ts +16 -16
  63. package/src/settings/css-vars-editor.ts +2 -2
  64. package/src/settings/defs-editor.ts +14 -14
  65. package/src/settings/general-settings.ts +6 -6
  66. package/src/settings/head-editor.ts +2 -2
  67. package/src/site-context.ts +1 -1
  68. package/src/state.ts +66 -12
  69. package/src/store.ts +15 -3
  70. package/src/studio.ts +88 -35
  71. package/src/tabs/patch-ops.ts +143 -0
  72. package/src/tabs/tab.ts +15 -1
  73. package/src/tabs/transact.ts +454 -27
  74. package/src/types.ts +71 -1
  75. package/src/ui/color-selector.ts +7 -7
  76. package/src/ui/icons.ts +0 -30
  77. package/src/ui/media-picker.ts +2 -2
  78. package/src/ui/panel-resize.ts +14 -8
  79. package/src/ui/unit-selector.ts +5 -2
  80. package/src/ui/value-selector.ts +3 -3
  81. package/src/utils/canvas-media.ts +6 -6
  82. package/src/utils/edit-display.ts +125 -83
  83. package/src/utils/google-fonts.ts +1 -1
  84. package/src/utils/inherited-style.ts +3 -2
  85. package/src/utils/studio-utils.ts +1 -1
  86. 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 {
@@ -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], childPath, depth + 1));
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
- // $map emit the map container, then recurse into the template
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> = {};