@jxsuite/studio 0.28.2 → 0.28.4

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 (104) hide show
  1. package/dist/studio.js +86144 -83971
  2. package/dist/studio.js.map +229 -219
  3. package/package.json +5 -5
  4. package/src/browse/browse-modal.ts +9 -3
  5. package/src/browse/browse.ts +139 -82
  6. package/src/canvas/canvas-diff.ts +35 -18
  7. package/src/canvas/canvas-helpers.ts +55 -21
  8. package/src/canvas/canvas-live-render.ts +140 -89
  9. package/src/canvas/canvas-render.ts +127 -82
  10. package/src/canvas/canvas-utils.ts +68 -42
  11. package/src/canvas/nested-site-style.ts +15 -7
  12. package/src/editor/component-inline-edit.ts +90 -51
  13. package/src/editor/content-inline-edit.ts +75 -72
  14. package/src/editor/context-menu.ts +125 -71
  15. package/src/editor/convert-targets.ts +17 -13
  16. package/src/editor/convert-to-component.ts +51 -28
  17. package/src/editor/convert-to-repeater.ts +44 -19
  18. package/src/editor/inline-edit.ts +107 -52
  19. package/src/editor/inline-format.ts +128 -60
  20. package/src/editor/insertion-helper.ts +26 -14
  21. package/src/editor/shortcuts.ts +89 -43
  22. package/src/editor/slash-menu.ts +41 -26
  23. package/src/files/components.ts +9 -7
  24. package/src/files/file-ops.ts +53 -33
  25. package/src/files/files.ts +246 -143
  26. package/src/format/constraints.ts +25 -15
  27. package/src/format/format-host.ts +33 -10
  28. package/src/github/github-auth.ts +24 -14
  29. package/src/github/github-publish.ts +20 -14
  30. package/src/new-project/new-project-modal.ts +27 -18
  31. package/src/panels/activity-bar.ts +27 -26
  32. package/src/panels/ai-panel.ts +100 -44
  33. package/src/panels/block-action-bar.ts +84 -52
  34. package/src/panels/canvas-dnd.ts +47 -28
  35. package/src/panels/data-explorer.ts +24 -11
  36. package/src/panels/dnd.ts +148 -100
  37. package/src/panels/editors.ts +54 -37
  38. package/src/panels/elements-panel.ts +23 -13
  39. package/src/panels/events-panel.ts +48 -38
  40. package/src/panels/git-panel.ts +95 -60
  41. package/src/panels/head-panel.ts +138 -84
  42. package/src/panels/imports-panel.ts +66 -29
  43. package/src/panels/layers-panel.ts +80 -36
  44. package/src/panels/left-panel.ts +86 -53
  45. package/src/panels/overlays.ts +36 -19
  46. package/src/panels/panel-events.ts +48 -27
  47. package/src/panels/panel-scheduler.ts +41 -23
  48. package/src/panels/preview-render.ts +26 -26
  49. package/src/panels/properties-panel.ts +155 -118
  50. package/src/panels/pseudo-preview.ts +31 -15
  51. package/src/panels/quick-search.ts +28 -13
  52. package/src/panels/right-panel.ts +35 -24
  53. package/src/panels/shared.ts +46 -29
  54. package/src/panels/signals-panel.ts +303 -186
  55. package/src/panels/statusbar.ts +27 -12
  56. package/src/panels/style-inputs.ts +30 -24
  57. package/src/panels/style-panel.ts +130 -90
  58. package/src/panels/style-utils.ts +63 -23
  59. package/src/panels/stylebook-layers-panel.ts +27 -30
  60. package/src/panels/stylebook-panel.ts +140 -72
  61. package/src/panels/tab-strip.ts +21 -10
  62. package/src/panels/toolbar.ts +52 -40
  63. package/src/panels/welcome-screen.ts +1 -1
  64. package/src/platform.ts +2 -1
  65. package/src/platforms/devserver.ts +221 -105
  66. package/src/recent-projects.ts +15 -7
  67. package/src/resize-edges.ts +7 -5
  68. package/src/services/cem-export.ts +73 -57
  69. package/src/services/code-services.ts +52 -25
  70. package/src/services/monaco-setup.ts +5 -5
  71. package/src/settings/content-types-editor.ts +133 -62
  72. package/src/settings/css-vars-editor.ts +31 -14
  73. package/src/settings/defs-editor.ts +125 -63
  74. package/src/settings/general-settings.ts +11 -6
  75. package/src/settings/head-editor.ts +30 -15
  76. package/src/settings/schema-field-ui.ts +75 -35
  77. package/src/settings/settings-modal.ts +35 -17
  78. package/src/site-context.ts +80 -47
  79. package/src/state.ts +98 -65
  80. package/src/store.ts +42 -23
  81. package/src/studio.ts +165 -144
  82. package/src/tabs/tab.ts +51 -45
  83. package/src/tabs/transact.ts +230 -109
  84. package/src/types.ts +60 -51
  85. package/src/ui/button-group.ts +3 -2
  86. package/src/ui/color-selector.ts +33 -15
  87. package/src/ui/expression-editor.ts +139 -86
  88. package/src/ui/field-input.ts +35 -18
  89. package/src/ui/field-row.ts +1 -1
  90. package/src/ui/icons.ts +5 -6
  91. package/src/ui/layers.ts +33 -26
  92. package/src/ui/media-picker.ts +13 -7
  93. package/src/ui/panel-resize.ts +39 -20
  94. package/src/ui/spectrum.ts +4 -2
  95. package/src/ui/unit-selector.ts +20 -11
  96. package/src/ui/value-selector.ts +13 -7
  97. package/src/ui/widgets.ts +34 -18
  98. package/src/utils/canvas-media.ts +66 -33
  99. package/src/utils/edit-display.ts +43 -44
  100. package/src/utils/google-fonts.ts +10 -6
  101. package/src/utils/inherited-style.ts +22 -8
  102. package/src/utils/studio-utils.ts +88 -45
  103. package/src/view.ts +9 -5
  104. package/src/workspace/workspace.ts +49 -29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "0.28.2",
3
+ "version": "0.28.4",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,9 +32,9 @@
32
32
  "dependencies": {
33
33
  "@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
34
34
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.2.0",
35
- "@jxsuite/parser": "^0.28.2",
36
- "@jxsuite/runtime": "^0.28.2",
37
- "@jxsuite/schema": "^0.28.2",
35
+ "@jxsuite/parser": "^0.28.4",
36
+ "@jxsuite/runtime": "^0.28.4",
37
+ "@jxsuite/schema": "^0.28.4",
38
38
  "@spectrum-web-components/accordion": "^1.12.1",
39
39
  "@spectrum-web-components/action-bar": "1.12.1",
40
40
  "@spectrum-web-components/action-button": "^1.12.1",
@@ -70,7 +70,7 @@
70
70
  "@spectrum-web-components/toast": "^1.12.1",
71
71
  "@spectrum-web-components/tooltip": "^1.12.1",
72
72
  "@spectrum-web-components/underlay": "^1.12.1",
73
- "@vue/reactivity": "3.5.35",
73
+ "@vue/reactivity": "3.5.38",
74
74
  "lit": "^3.3.3",
75
75
  "lit-html": "^3.3.3",
76
76
  "monaco-editor": "^0.55.1",
@@ -15,10 +15,14 @@ let _handle: ReturnType<typeof openModal> | null = null;
15
15
  let _escHandler: ((e: KeyboardEvent) => void) | null = null;
16
16
 
17
17
  export function openBrowseModal() {
18
- if (_handle) return;
18
+ if (_handle) {
19
+ return;
20
+ }
19
21
 
20
22
  _escHandler = (e: KeyboardEvent) => {
21
- if (e.key === "Escape") closeBrowseModal();
23
+ if (e.key === "Escape") {
24
+ closeBrowseModal();
25
+ }
22
26
  };
23
27
  document.addEventListener("keydown", _escHandler, true);
24
28
 
@@ -53,7 +57,9 @@ export function openBrowseModal() {
53
57
  }
54
58
 
55
59
  export function closeBrowseModal() {
56
- if (!_handle) return;
60
+ if (!_handle) {
61
+ return;
62
+ }
57
63
  if (_escHandler) {
58
64
  document.removeEventListener("keydown", _escHandler, true);
59
65
  _escHandler = null;
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { html, render as litRender, nothing } from "lit-html";
11
+ import { errorMessage } from "@jxsuite/schema/parse";
11
12
  import { ref } from "lit-html/directives/ref.js";
12
13
  import { repeat } from "lit-html/directives/repeat.js";
13
14
  import { getPlatform } from "../platform";
@@ -17,16 +18,16 @@ import { invalidateMediaCache } from "../ui/media-picker";
17
18
  import { statusMessage } from "../panels/statusbar";
18
19
  import { componentRegistry } from "../files/components";
19
20
 
20
- import { showDialog, renderPopover } from "../ui/layers";
21
+ import { renderPopover, showDialog } from "../ui/layers";
21
22
  import { renderComponentPreview } from "../panels/stylebook-panel";
22
- import { renderNode, buildScope, setSkipServerFunctions } from "@jxsuite/runtime";
23
+ import { buildScope, renderNode, setSkipServerFunctions } from "@jxsuite/runtime";
23
24
  import { parseSourceForPath } from "../files/file-ops";
24
25
  import {
25
- loadFormats,
26
- formatForPath,
27
- formatByName,
28
- documentExtensions,
29
26
  defaultContentFormat,
27
+ documentExtensions,
28
+ formatByName,
29
+ formatForPath,
30
+ loadFormats,
30
31
  } from "../format/format-host";
31
32
 
32
33
  import type { ComponentEntry } from "../files/components";
@@ -36,11 +37,11 @@ import type { ContentTypeDef } from "@jxsuite/schema/types";
36
37
 
37
38
  const CATEGORIES = [
38
39
  { key: "all", label: "All" },
39
- { key: "pages", label: "Pages", dir: "pages" },
40
- { key: "layouts", label: "Layouts", dir: "layouts" },
41
- { key: "components", label: "Components", dir: "components" },
42
- { key: "content", label: "Content", dir: "content" },
43
- { key: "media", label: "Media", dir: "public" },
40
+ { dir: "pages", key: "pages", label: "Pages" },
41
+ { dir: "layouts", key: "layouts", label: "Layouts" },
42
+ { dir: "components", key: "components", label: "Components" },
43
+ { dir: "content", key: "content", label: "Content" },
44
+ { dir: "public", key: "media", label: "Media" },
44
45
  ];
45
46
 
46
47
  const MEDIA_EXTENSIONS = new Set([
@@ -106,14 +107,30 @@ function isImage(ext: string) {
106
107
 
107
108
  /** Map a file path to a display category. Media files override by extension. */
108
109
  function categoryFor(dir: string, ext: string) {
109
- if (ext && MEDIA_EXTENSIONS.has(ext)) return "Media";
110
- if (dir.startsWith("pages")) return "Pages";
111
- if (dir.startsWith("layouts")) return "Layouts";
112
- if (dir.startsWith("components")) return "Components";
113
- if (dir.startsWith("content")) return "Content";
114
- if (dir.startsWith("public")) return "Media";
115
- if (dir.startsWith("data")) return "Content";
116
- if (dir.startsWith("styles")) return "Components";
110
+ if (ext && MEDIA_EXTENSIONS.has(ext)) {
111
+ return "Media";
112
+ }
113
+ if (dir.startsWith("pages")) {
114
+ return "Pages";
115
+ }
116
+ if (dir.startsWith("layouts")) {
117
+ return "Layouts";
118
+ }
119
+ if (dir.startsWith("components")) {
120
+ return "Components";
121
+ }
122
+ if (dir.startsWith("content")) {
123
+ return "Content";
124
+ }
125
+ if (dir.startsWith("public")) {
126
+ return "Media";
127
+ }
128
+ if (dir.startsWith("data")) {
129
+ return "Content";
130
+ }
131
+ if (dir.startsWith("styles")) {
132
+ return "Components";
133
+ }
117
134
  return "Other";
118
135
  }
119
136
 
@@ -149,11 +166,11 @@ async function collectFiles(
149
166
  const type =
150
167
  category === "Content" ? contentTypeFor(entry.path) || ext || "file" : ext || "file";
151
168
  results.push({
169
+ category,
170
+ ext,
152
171
  name: entry.name,
153
172
  path: entry.path,
154
173
  type,
155
- category,
156
- ext,
157
174
  });
158
175
  }
159
176
  }
@@ -172,12 +189,16 @@ async function collectFiles(
172
189
  */
173
190
  function contentTypeFor(filePath: string) {
174
191
  const config = projectState?.projectConfig;
175
- if (!config?.contentTypes) return null;
192
+ if (!config?.contentTypes) {
193
+ return null;
194
+ }
176
195
  for (const [name, def] of Object.entries(config.contentTypes)) {
177
196
  const d = def as ContentTypeDef;
178
- if (!d.source) continue;
197
+ if (!d.source) {
198
+ continue;
199
+ }
179
200
  const prefix = d.source.replace(/^\.\//, "").replace(/\/$/, "");
180
- if (filePath.startsWith(prefix + "/") || filePath === prefix) {
201
+ if (filePath.startsWith(`${prefix}/`) || filePath === prefix) {
181
202
  return name.charAt(0).toUpperCase() + name.slice(1);
182
203
  }
183
204
  }
@@ -187,7 +208,9 @@ function contentTypeFor(filePath: string) {
187
208
  // ─── Data loading ────────────────────────────────────────────────────────────
188
209
 
189
210
  async function loadFiles() {
190
- if (!projectState) return;
211
+ if (!projectState) {
212
+ return;
213
+ }
191
214
  loading = true;
192
215
  const platform = getPlatform();
193
216
  const dirs = projectState.projectDirs || [];
@@ -223,10 +246,10 @@ function getEntityTypes() {
223
246
  const pageExt = documentExtensions("page")[0] ?? ".json";
224
247
  const contentExt = defaultContentFormat()?.extensions[0] ?? ".json";
225
248
  return [
226
- { key: "page", label: "Page", dir: "pages", ext: pageExt },
227
- { key: "layout", label: "Layout", dir: "layouts", ext: ".json" },
228
- { key: "component", label: "Component", dir: "components", ext: ".json" },
229
- { key: "content", label: "Content", dir: "content", ext: contentExt },
249
+ { dir: "pages", ext: pageExt, key: "page", label: "Page" },
250
+ { dir: "layouts", ext: ".json", key: "layout", label: "Layout" },
251
+ { dir: "components", ext: ".json", key: "component", label: "Component" },
252
+ { dir: "content", ext: contentExt, key: "content", label: "Content" },
230
253
  ];
231
254
  }
232
255
 
@@ -239,7 +262,9 @@ function getEntityTypes() {
239
262
  function buildFrontmatterYaml(contentTypeName: string) {
240
263
  const config = projectState?.projectConfig;
241
264
  const col = config?.contentTypes?.[contentTypeName];
242
- if (!col?.schema?.properties) return "title: Untitled\n";
265
+ if (!col?.schema?.properties) {
266
+ return "title: Untitled\n";
267
+ }
243
268
 
244
269
  let yaml = "";
245
270
  for (const [field, def] of Object.entries(col.schema.properties)) {
@@ -256,13 +281,14 @@ function buildFrontmatterYaml(contentTypeName: string) {
256
281
  */
257
282
  function getContentTypeTypes() {
258
283
  const config = projectState?.projectConfig;
259
- if (!config?.contentTypes) return [];
284
+ if (!config?.contentTypes) {
285
+ return [];
286
+ }
260
287
  return Object.entries(config.contentTypes).map(([name, def]) => {
261
288
  const d = def as ContentTypeDef;
262
289
  const dir = d.source ? d.source.replace(/^\.\//, "").replace(/\/$/, "") : name;
263
290
  return {
264
- key: `contentType:${name}`,
265
- label: name.charAt(0).toUpperCase() + name.slice(1),
291
+ contentTypeName: name,
266
292
  dir,
267
293
  ext:
268
294
  d.format === "json"
@@ -270,7 +296,8 @@ function getContentTypeTypes() {
270
296
  : (formatByName(d.format)?.extensions[0] ??
271
297
  defaultContentFormat()?.extensions[0] ??
272
298
  ".json"),
273
- contentTypeName: name,
299
+ key: `contentType:${name}`,
300
+ label: name.charAt(0).toUpperCase() + name.slice(1),
274
301
  };
275
302
  });
276
303
  }
@@ -292,27 +319,30 @@ async function handleNewEntity(
292
319
  await loadFormats();
293
320
  const allTypes = [...getEntityTypes(), ...getContentTypeTypes()];
294
321
  const typeInfo = allTypes.find((t) => t.key === typeKey);
295
- if (!typeInfo) return;
322
+ if (!typeInfo) {
323
+ return;
324
+ }
296
325
 
326
+ // oxlint-disable-next-line no-alert -- native prompt is the intended quick-input UX here
297
327
  const name = prompt(`${typeInfo.label} name:`, "untitled");
298
- if (!name) return;
328
+ if (!name) {
329
+ return;
330
+ }
299
331
 
300
332
  const slug = name
301
333
  .toLowerCase()
302
- .replace(/\s+/g, "-")
303
- .replace(/[^a-z0-9-]/g, "");
334
+ .replaceAll(/\s+/g, "-")
335
+ .replaceAll(/[^a-z0-9-]/g, "");
304
336
  const filePath = `${typeInfo.dir}/${slug}${typeInfo.ext}`;
305
337
 
306
338
  let content;
307
339
  const entityFormat = formatForPath(filePath);
308
340
  if (entityFormat) {
309
- if (contentTypeName) {
310
- content = `---\n${buildFrontmatterYaml(contentTypeName)}---\n\n`;
311
- } else {
312
- content = entityFormat.studio?.newFileTemplate ?? "";
313
- }
341
+ content = contentTypeName
342
+ ? `---\n${buildFrontmatterYaml(contentTypeName)}---\n\n`
343
+ : (entityFormat.studio?.newFileTemplate ?? "");
314
344
  } else {
315
- content = JSON.stringify({ tagName: "div", children: [] }, null, "\t");
345
+ content = JSON.stringify({ children: [], tagName: "div" }, null, "\t");
316
346
  }
317
347
 
318
348
  const platform = getPlatform();
@@ -386,21 +416,21 @@ function showBrowseContextMenu(
386
416
 
387
417
  /** @type {{ label: string; action?: () => void; danger?: boolean }[]} */
388
418
  const items = [
389
- { label: "Open", action: () => ctx.openFile(file.path) },
419
+ { action: () => ctx.openFile(file.path), label: "Open" },
390
420
  { label: "\u2014" },
391
421
  {
392
- label: "Rename\u2026",
393
422
  action: () => browseRenameFile(file, container, ctx),
423
+ label: "Rename\u2026",
394
424
  },
395
425
  {
396
- label: "Duplicate",
397
426
  action: () => browseDuplicateFile(file, container, ctx),
427
+ label: "Duplicate",
398
428
  },
399
429
  { label: "\u2014" },
400
430
  {
401
- label: "Delete",
402
431
  action: () => browseDeleteFile(file, container, ctx),
403
432
  danger: true,
433
+ label: "Delete",
404
434
  },
405
435
  ];
406
436
 
@@ -412,13 +442,18 @@ function showBrowseContextMenu(
412
442
  open
413
443
  style="position:fixed;left:${x}px;top:${y}px"
414
444
  ${ref((el) => {
415
- if (!el) return;
445
+ if (!el) {
446
+ return;
447
+ }
416
448
  requestAnimationFrame(() => {
417
449
  const popover = el as HTMLElement;
418
450
  const menuRect = popover.getBoundingClientRect();
419
- if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
420
- if (y + menuRect.height > window.innerHeight)
451
+ if (x + menuRect.width > window.innerWidth) {
452
+ x = window.innerWidth - menuRect.width - 4;
453
+ }
454
+ if (y + menuRect.height > window.innerHeight) {
421
455
  y = window.innerHeight - menuRect.height - 4;
456
+ }
422
457
  popover.style.left = `${x}px`;
423
458
  popover.style.top = `${y}px`;
424
459
  });
@@ -441,10 +476,10 @@ function showBrowseContextMenu(
441
476
  </sp-popover>`,
442
477
  {
443
478
  dismissOnOutsideClick: true,
479
+ layer: "dialog",
444
480
  onDismiss: () => {
445
481
  _browseCtxHandle = null;
446
482
  },
447
- layer: "dialog",
448
483
  },
449
484
  );
450
485
  }
@@ -460,9 +495,11 @@ async function browseRenameFile(
460
495
  ctx: { openFile: (path: string) => void },
461
496
  ) {
462
497
  const newName = await showRenameDialog(file.name);
463
- if (!newName || newName === file.name) return;
498
+ if (!newName || newName === file.name) {
499
+ return;
500
+ }
464
501
  const filePath = file.path.replaceAll("\\", "/");
465
- const parentDir = filePath.includes("/") ? filePath.substring(0, filePath.lastIndexOf("/")) : ".";
502
+ const parentDir = filePath.includes("/") ? filePath.slice(0, filePath.lastIndexOf("/")) : ".";
466
503
  const newPath = parentDir === "." ? newName : `${parentDir}/${newName}`;
467
504
  try {
468
505
  const platform = getPlatform();
@@ -470,8 +507,8 @@ async function browseRenameFile(
470
507
  invalidateBrowseCache();
471
508
  renderBrowse(container, ctx);
472
509
  statusMessage(`Renamed to ${newName}`);
473
- } catch (e) {
474
- statusMessage(`Error: ${(e as Error).message}`);
510
+ } catch (error) {
511
+ statusMessage(`Error: ${errorMessage(error)}`);
475
512
  }
476
513
  }
477
514
 
@@ -486,7 +523,7 @@ async function browseDuplicateFile(
486
523
  ctx: { openFile: (path: string) => void },
487
524
  ) {
488
525
  const filePath = file.path.replaceAll("\\", "/");
489
- const parentDir = filePath.includes("/") ? filePath.substring(0, filePath.lastIndexOf("/")) : ".";
526
+ const parentDir = filePath.includes("/") ? filePath.slice(0, filePath.lastIndexOf("/")) : ".";
490
527
  const baseName = file.name.replace(/(\.[^.]+)$/, "");
491
528
  const ext = file.ext || "";
492
529
  const copyName = `${baseName}-copy${ext}`;
@@ -498,8 +535,8 @@ async function browseDuplicateFile(
498
535
  invalidateBrowseCache();
499
536
  renderBrowse(container, ctx);
500
537
  statusMessage(`Duplicated as ${copyName}`);
501
- } catch (e) {
502
- statusMessage(`Error: ${(e as Error).message}`);
538
+ } catch (error) {
539
+ statusMessage(`Error: ${errorMessage(error)}`);
503
540
  }
504
541
  }
505
542
 
@@ -514,15 +551,17 @@ async function browseDeleteFile(
514
551
  ctx: { openFile: (path: string) => void },
515
552
  ) {
516
553
  const confirmed = await showDeleteDialog(file.name);
517
- if (!confirmed) return;
554
+ if (!confirmed) {
555
+ return;
556
+ }
518
557
  try {
519
558
  const platform = getPlatform();
520
559
  await platform.deleteFile(file.path);
521
560
  invalidateBrowseCache();
522
561
  renderBrowse(container, ctx);
523
562
  statusMessage(`Deleted ${file.name}`);
524
- } catch (e) {
525
- statusMessage(`Error: ${(e as Error).message}`);
563
+ } catch (error) {
564
+ statusMessage(`Error: ${errorMessage(error)}`);
526
565
  }
527
566
  }
528
567
 
@@ -538,7 +577,9 @@ function showRenameDialog(currentName: string): Promise<string | null> {
538
577
  return showDialog<string | null>((done) => {
539
578
  function confirm() {
540
579
  const trimmed = value.trim();
541
- if (!trimmed) return;
580
+ if (!trimmed) {
581
+ return;
582
+ }
542
583
  done(trimmed);
543
584
  }
544
585
 
@@ -561,19 +602,23 @@ function showRenameDialog(currentName: string): Promise<string | null> {
561
602
  value = (e.target as HTMLInputElement).value || "";
562
603
  }}
563
604
  @keydown=${(e: KeyboardEvent) => {
564
- if (e.key === "Enter") confirm();
605
+ if (e.key === "Enter") {
606
+ confirm();
607
+ }
565
608
  }}
566
609
  ></sp-textfield>
567
610
  </sp-dialog-wrapper>
568
611
  `;
569
612
 
570
613
  requestAnimationFrame(() => {
571
- const layer = document.getElementById("layer-dialog");
614
+ const layer = document.querySelector("#layer-dialog");
572
615
  const tf = layer?.querySelector("sp-textfield") as HTMLElement | null;
573
616
  if (tf) {
574
617
  tf.focus();
575
618
  const input = tf.shadowRoot?.querySelector("input");
576
- if (input) input.select();
619
+ if (input) {
620
+ input.select();
621
+ }
577
622
  }
578
623
  });
579
624
 
@@ -607,7 +652,7 @@ function showDeleteDialog(fileName: string) {
607
652
 
608
653
  // ─── Grid view helpers ──────────────────────────────────────────────────────
609
654
 
610
- const _previewCache: Map<string, HTMLElement> = new Map();
655
+ const _previewCache = new Map<string, HTMLElement>();
611
656
 
612
657
  /**
613
658
  * Render a live preview for a page or layout file (JSON or Markdown).
@@ -628,9 +673,9 @@ async function renderDocPreview(filePath: string) {
628
673
  } else {
629
674
  doc = JSON.parse(content);
630
675
  }
631
- const scope = buildScope(doc.state || {});
676
+ const scope = await buildScope(doc, {}, location.href);
632
677
  const el = renderNode(doc, scope);
633
- return el as HTMLElement | null;
678
+ return el instanceof HTMLElement ? el : null;
634
679
  } catch {
635
680
  return null;
636
681
  }
@@ -644,23 +689,27 @@ async function renderDocPreview(filePath: string) {
644
689
  */
645
690
  async function loadPreview(el: Element, file: { path: string; category: string }) {
646
691
  // Already populated
647
- if (el.firstElementChild) return;
692
+ if (el.firstElementChild) {
693
+ return;
694
+ }
648
695
 
649
696
  let preview: HTMLElement | undefined = _previewCache.get(file.path);
650
697
  if (!preview) {
651
698
  try {
652
699
  const comp = componentRegistry.find((c: ComponentEntry) => c.path === file.path);
653
- if (comp) {
654
- preview = (await renderComponentPreview(comp)) as HTMLElement | undefined;
655
- } else {
656
- preview = ((await renderDocPreview(file.path)) as HTMLElement | undefined) || undefined;
700
+ preview = comp
701
+ ? ((await renderComponentPreview(comp)) as HTMLElement | undefined)
702
+ : ((await renderDocPreview(file.path)) as HTMLElement | undefined) || undefined;
703
+ if (preview) {
704
+ _previewCache.set(file.path, /** @type {HTMLElement} */ preview);
657
705
  }
658
- if (preview) _previewCache.set(file.path, /** @type {HTMLElement} */ preview);
659
706
  } catch {
660
707
  return;
661
708
  }
662
709
  }
663
- if (preview) el.appendChild(preview);
710
+ if (preview) {
711
+ el.append(preview);
712
+ }
664
713
  }
665
714
 
666
715
  /**
@@ -698,7 +747,9 @@ function renderCard(
698
747
  class="element-card-preview"
699
748
  ${needsPreview
700
749
  ? ref((el: Element | undefined) => {
701
- if (el) loadPreview(el, file);
750
+ if (el) {
751
+ loadPreview(el, file);
752
+ }
702
753
  })
703
754
  : nothing}
704
755
  >
@@ -730,7 +781,7 @@ export async function renderBrowse(
730
781
  ) {
731
782
  // Re-load when projectDirs changed (e.g. project opened after initial render)
732
783
  const currentKey = (projectState?.projectDirs || []).join(",");
733
- if ((!fileCache.length && !loading) || currentKey !== lastProjectDirsKey) {
784
+ if ((fileCache.length === 0 && !loading) || currentKey !== lastProjectDirsKey) {
734
785
  await loadFiles();
735
786
  }
736
787
 
@@ -777,7 +828,7 @@ export async function renderBrowse(
777
828
  ${getEntityTypes().map(
778
829
  (t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`,
779
830
  )}
780
- ${contentTypeTypes.length
831
+ ${contentTypeTypes.length > 0
781
832
  ? html`<sp-menu-divider></sp-menu-divider> ${contentTypeTypes.map(
782
833
  (t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`,
783
834
  )}`
@@ -789,7 +840,9 @@ export async function renderBrowse(
789
840
  size="s"
790
841
  @click=${() => {
791
842
  const input = container.querySelector(".browse-upload-input") as HTMLInputElement;
792
- if (input) input.click();
843
+ if (input) {
844
+ input.click();
845
+ }
793
846
  }}
794
847
  >
795
848
  <sp-icon-upload slot="icon"></sp-icon-upload> Upload
@@ -802,7 +855,9 @@ export async function renderBrowse(
802
855
  style="display:none"
803
856
  @change=${(e: Event) => {
804
857
  const input = e.target as HTMLInputElement;
805
- if (input.files?.length) handleUpload(input.files, container, ctx);
858
+ if (input.files?.length) {
859
+ handleUpload(input.files, container, ctx);
860
+ }
806
861
  input.value = "";
807
862
  }}
808
863
  />
@@ -900,7 +955,9 @@ export async function renderBrowse(
900
955
  e.preventDefault();
901
956
  (e.currentTarget as HTMLElement).classList.remove("browse-drop-active");
902
957
  const droppedFiles = e.dataTransfer?.files;
903
- if (droppedFiles?.length) handleUpload(droppedFiles, container, ctx);
958
+ if (droppedFiles?.length) {
959
+ handleUpload(droppedFiles, container, ctx);
960
+ }
904
961
  }}
905
962
  >
906
963
  <div class="browse-filter-bar">${filterBar}</div>
@@ -23,13 +23,21 @@ interface DiffResult {
23
23
  * @returns {boolean}
24
24
  */
25
25
  function valuesEqual(a: unknown, b: unknown): boolean {
26
- if (a === b) return true;
27
- if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false;
28
- if (Array.isArray(a) !== Array.isArray(b)) return false;
26
+ if (a === b) {
27
+ return true;
28
+ }
29
+ if (typeof a !== "object" || typeof b !== "object" || !a || !b) {
30
+ return false;
31
+ }
32
+ if (Array.isArray(a) !== Array.isArray(b)) {
33
+ return false;
34
+ }
29
35
 
30
36
  if (Array.isArray(a)) {
31
37
  const bArr = b as unknown[];
32
- if (a.length !== bArr.length) return false;
38
+ if (a.length !== bArr.length) {
39
+ return false;
40
+ }
33
41
  return a.every((v: unknown, i: number) => valuesEqual(v, bArr[i]));
34
42
  }
35
43
 
@@ -39,8 +47,12 @@ function valuesEqual(a: unknown, b: unknown): boolean {
39
47
  const keysA = Object.keys(aObj).filter((k: string) => !k.startsWith("on") && k !== "$ref");
40
48
  const keysB = Object.keys(bObj).filter((k: string) => !k.startsWith("on") && k !== "$ref");
41
49
 
42
- if (keysA.length !== keysB.length) return false;
43
- if (!keysA.every((k: string) => keysB.includes(k))) return false;
50
+ if (keysA.length !== keysB.length) {
51
+ return false;
52
+ }
53
+ if (!keysA.every((k: string) => keysB.includes(k))) {
54
+ return false;
55
+ }
44
56
 
45
57
  return keysA.every((k: string) => valuesEqual(aObj[k], bObj[k]));
46
58
  }
@@ -52,7 +64,9 @@ function valuesEqual(a: unknown, b: unknown): boolean {
52
64
  * @returns {JxMutableNode[]}
53
65
  */
54
66
  function elementChildren(node: JxMutableNode | undefined) {
55
- if (!node?.children || !Array.isArray(node.children)) return [];
67
+ if (!node?.children || !Array.isArray(node.children)) {
68
+ return [];
69
+ }
56
70
  return node.children.filter(
57
71
  (c: JxMutableNode | string) => c != null && typeof c === "object",
58
72
  ) as JxMutableNode[];
@@ -94,8 +108,8 @@ export function computeDocumentDiff(
94
108
  originalDoc: JxMutableNode | undefined,
95
109
  currentDoc: JxMutableNode | undefined,
96
110
  ): DiffResult {
97
- const diffMap: Map<string, DiffStatus> = new Map();
98
- const allPaths: Set<string> = new Set();
111
+ const diffMap = new Map<string, DiffStatus>();
112
+ const allPaths = new Set<string>();
99
113
 
100
114
  /**
101
115
  * Walk both trees in parallel and mark differences.
@@ -107,7 +121,7 @@ export function computeDocumentDiff(
107
121
  const walk = (
108
122
  origNode: JxMutableNode | undefined,
109
123
  currNode: JxMutableNode | undefined,
110
- path: string = "",
124
+ path = "",
111
125
  ) => {
112
126
  const pathKey = path || "/";
113
127
  allPaths.add(pathKey);
@@ -150,7 +164,6 @@ export function computeDocumentDiff(
150
164
  const childPath = pathKey === "/" ? `children/${i}` : `${pathKey}/children/${i}`;
151
165
  markRecursive(children[i], "removed", childPath, diffMap, allPaths);
152
166
  }
153
- return;
154
167
  }
155
168
  };
156
169
 
@@ -180,9 +193,12 @@ export function computeDocumentDiff(
180
193
  };
181
194
 
182
195
  // Propagate upwards until no more changes
183
- while (propagateModified()) {}
196
+ let propagating = true;
197
+ while (propagating) {
198
+ propagating = propagateModified();
199
+ }
184
200
 
185
- return { byPath: diffMap, allPaths };
201
+ return { allPaths, byPath: diffMap };
186
202
  }
187
203
 
188
204
  /**
@@ -191,9 +207,10 @@ export function computeDocumentDiff(
191
207
  * @param {HTMLElement} canvas
192
208
  */
193
209
  export function clearDiffHighlight(canvas: HTMLElement) {
194
- canvas
195
- .querySelectorAll(".element-diff-added, .element-diff-removed, .element-diff-modified")
196
- .forEach((el: Element) => {
197
- el.classList.remove("element-diff-added", "element-diff-removed", "element-diff-modified");
198
- });
210
+ const marked = canvas.querySelectorAll(
211
+ ".element-diff-added, .element-diff-removed, .element-diff-modified",
212
+ );
213
+ for (const el of marked) {
214
+ el.classList.remove("element-diff-added", "element-diff-removed", "element-diff-modified");
215
+ }
199
216
  }