@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.
Files changed (99) hide show
  1. package/dist/studio.css +98 -98
  2. package/dist/studio.js +13476 -12330
  3. package/dist/studio.js.map +152 -148
  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 +19 -7
  8. package/src/browse/browse-modal.js +16 -13
  9. package/src/browse/browse.js +44 -35
  10. package/src/canvas/canvas-diff.js +24 -16
  11. package/src/canvas/canvas-helpers.js +24 -19
  12. package/src/canvas/canvas-live-render.js +113 -39
  13. package/src/canvas/canvas-render.js +119 -73
  14. package/src/canvas/canvas-utils.js +51 -21
  15. package/src/canvas/nested-site-style.js +50 -0
  16. package/src/editor/component-inline-edit.js +14 -13
  17. package/src/editor/content-inline-edit.js +22 -10
  18. package/src/editor/context-menu.js +28 -14
  19. package/src/editor/convert-targets.js +60 -0
  20. package/src/editor/convert-to-component.js +20 -19
  21. package/src/editor/convert-to-repeater.js +226 -0
  22. package/src/editor/inline-edit.js +69 -28
  23. package/src/editor/insertion-helper.js +16 -12
  24. package/src/editor/shortcuts.js +14 -4
  25. package/src/editor/slash-menu.js +40 -19
  26. package/src/files/components.js +1 -1
  27. package/src/files/file-ops.js +37 -16
  28. package/src/files/files.js +322 -68
  29. package/src/github/github-auth.js +122 -0
  30. package/src/github/github-publish.js +126 -0
  31. package/src/markdown/md-convert.js +172 -107
  32. package/src/new-project/new-project-modal.js +204 -0
  33. package/src/panels/activity-bar.js +27 -25
  34. package/src/panels/ai-panel.js +406 -0
  35. package/src/panels/block-action-bar.js +77 -28
  36. package/src/panels/canvas-dnd.js +8 -2
  37. package/src/panels/data-explorer.js +19 -13
  38. package/src/panels/dnd.js +144 -92
  39. package/src/panels/editors.js +55 -35
  40. package/src/panels/elements-panel.js +33 -19
  41. package/src/panels/events-panel.js +15 -10
  42. package/src/panels/git-panel.js +475 -99
  43. package/src/panels/head-panel.js +121 -81
  44. package/src/panels/imports-panel.js +94 -65
  45. package/src/panels/layers-panel.js +64 -43
  46. package/src/panels/left-panel.js +105 -48
  47. package/src/panels/overlays.js +1 -0
  48. package/src/panels/panel-events.js +45 -30
  49. package/src/panels/preview-render.js +23 -18
  50. package/src/panels/properties-panel.js +378 -257
  51. package/src/panels/pseudo-preview.js +2 -2
  52. package/src/panels/quick-search.js +11 -9
  53. package/src/panels/right-panel.js +42 -16
  54. package/src/panels/shared.js +4 -4
  55. package/src/panels/signals-panel.js +415 -166
  56. package/src/panels/statusbar.js +10 -6
  57. package/src/panels/style-inputs.js +67 -35
  58. package/src/panels/style-panel.js +239 -134
  59. package/src/panels/style-utils.js +34 -16
  60. package/src/panels/stylebook-layers-panel.js +12 -10
  61. package/src/panels/stylebook-panel.js +134 -66
  62. package/src/panels/tab-strip.js +5 -2
  63. package/src/panels/toolbar.js +333 -109
  64. package/src/panels/welcome-screen.js +121 -0
  65. package/src/platform.js +2 -4
  66. package/src/platforms/devserver.js +113 -7
  67. package/src/resize-edges.js +98 -0
  68. package/src/services/cem-export.js +12 -7
  69. package/src/services/code-services.js +30 -12
  70. package/src/settings/content-types-editor.js +8 -8
  71. package/src/settings/css-vars-editor.js +30 -24
  72. package/src/settings/defs-editor.js +12 -7
  73. package/src/settings/general-settings.js +85 -3
  74. package/src/settings/head-editor.js +30 -24
  75. package/src/settings/schema-field-ui.js +58 -39
  76. package/src/settings/settings-modal.js +12 -9
  77. package/src/site-context.js +41 -31
  78. package/src/state.js +70 -29
  79. package/src/store.js +36 -35
  80. package/src/studio.js +120 -90
  81. package/src/tabs/tab.js +67 -43
  82. package/src/tabs/transact.js +23 -13
  83. package/src/ui/button-group.js +24 -24
  84. package/src/ui/color-selector.js +28 -23
  85. package/src/ui/field-row.js +3 -3
  86. package/src/ui/icons.js +2 -2
  87. package/src/ui/layers.js +31 -1
  88. package/src/ui/media-picker.js +7 -7
  89. package/src/ui/spectrum.js +5 -1
  90. package/src/ui/unit-selector.js +18 -16
  91. package/src/ui/value-selector.js +21 -15
  92. package/src/ui/widgets.js +22 -19
  93. package/src/utils/canvas-media.js +5 -4
  94. package/src/utils/edit-display.js +32 -26
  95. package/src/utils/google-fonts.js +11 -11
  96. package/src/utils/inherited-style.js +9 -8
  97. package/src/utils/studio-utils.js +18 -18
  98. package/src/view.js +58 -1
  99. package/src/workspace/workspace.js +29 -7
@@ -11,6 +11,8 @@ import { canvasWrap, canvasPanels, updateCanvas } from "../store.js";
11
11
  import { activeTab } from "../workspace/workspace.js";
12
12
  import { view } from "../view.js";
13
13
  import { loadMarkdown } from "../files/file-ops.js";
14
+ import { renderWelcome } from "../panels/welcome-screen.js";
15
+ import { projectState } from "../state.js";
14
16
  import {
15
17
  canvasPanelTemplate,
16
18
  applyTransform,
@@ -44,24 +46,43 @@ import { mediaDisplayName } from "../panels/shared.js";
44
46
  import { statusMessage } from "../panels/statusbar.js";
45
47
  import * as overlaysPanel from "../panels/overlays.js";
46
48
 
47
- /** @type {any} */
48
- let _ctx = null;
49
+ /** @typedef {import("../panels/canvas-dnd.js").CanvasPanel} CanvasPanel */
49
50
 
50
- let _prevStylebookFilter = "";
51
- let _prevStylebookCustomizedOnly = false;
51
+ /**
52
+ * @typedef {{
53
+ * filePath: string;
54
+ * originalContent: string;
55
+ * currentContent: string;
56
+ * isMarkdown: boolean;
57
+ * fileStatus: string;
58
+ * originalDoc?: JxMutableNode | null;
59
+ * currentDoc?: JxMutableNode | null;
60
+ * original?: JxMutableNode | null;
61
+ * }} GitDiffState
62
+ */
52
63
 
53
64
  /**
54
- * Initialize the canvas render module.
55
- *
56
- * @param {{
65
+ * @typedef {{
57
66
  * getCanvasMode: () => string;
58
67
  * setCanvasMode: (mode: string) => void;
59
68
  * openFileFromTree: (path: string) => void;
60
69
  * exportFile: () => void;
61
70
  * closeFunctionEditor: () => void;
62
- * gitDiffState: any;
63
- * setGitDiffState: (state: any) => void;
64
- * }} ctx
71
+ * gitDiffState: GitDiffState | null;
72
+ * setGitDiffState: (state: GitDiffState | null) => void;
73
+ * }} CanvasRenderCtx
74
+ */
75
+
76
+ /** @type {CanvasRenderCtx | null} */
77
+ let _ctx = null;
78
+
79
+ let _prevStylebookFilter = "";
80
+ let _prevStylebookCustomizedOnly = false;
81
+
82
+ /**
83
+ * Initialize the canvas render module.
84
+ *
85
+ * @param {CanvasRenderCtx} ctx
65
86
  */
66
87
  export function initCanvasRender(ctx) {
67
88
  _ctx = ctx;
@@ -69,9 +90,17 @@ export function initCanvasRender(ctx) {
69
90
 
70
91
  export function renderCanvas() {
71
92
  const tab = activeTab.value;
72
- if (!tab) return;
93
+ if (!tab) {
94
+ if (!projectState) {
95
+ renderWelcome(canvasWrap);
96
+ } else {
97
+ canvasWrap.textContent = "";
98
+ }
99
+ return;
100
+ }
101
+ const ctx = /** @type {CanvasRenderCtx} */ (_ctx);
73
102
  const S = { document: tab.doc.document, ui: tab.session.ui, mode: tab.doc.mode };
74
- const canvasMode = _ctx.getCanvasMode();
103
+ const canvasMode = ctx.getCanvasMode();
75
104
 
76
105
  // Advance render generation so stale async renders from the previous cycle bail out
77
106
  ++view.renderGeneration;
@@ -92,7 +121,7 @@ export function renderCanvas() {
92
121
 
93
122
  // Function editor mode: editing a function body in Monaco (JS)
94
123
  if (S.ui.editingFunction) {
95
- renderFunctionEditor(_ctx.closeFunctionEditor);
124
+ renderFunctionEditor(ctx.closeFunctionEditor);
96
125
  return;
97
126
  }
98
127
 
@@ -203,7 +232,7 @@ export function renderCanvas() {
203
232
  litRender(
204
233
  html`<div class="source-wrap">
205
234
  <div class="source-toolbar">
206
- <sp-action-button size="s" @click=${_ctx.exportFile}>
235
+ <sp-action-button size="s" @click=${ctx.exportFile}>
207
236
  <sp-icon-export slot="icon"></sp-icon-export>
208
237
  Export
209
238
  </sp-action-button>
@@ -224,25 +253,30 @@ export function renderCanvas() {
224
253
  lang === "json" ? JSON.stringify(S.document, null, 2) : S.document?.toString?.() || "";
225
254
  const modelUri = monaco.Uri.parse("file:///" + filePath);
226
255
  const model = monaco.editor.createModel(content, lang, modelUri);
227
- view.monacoEditor = monaco.editor.create(/** @type {any} */ (editorContainer), {
228
- model,
229
- theme: "vs-dark",
230
- automaticLayout: true,
231
- minimap: { enabled: false },
232
- fontSize: 12,
233
- fontFamily: "'SF Mono', 'Fira Code', 'Consolas', monospace",
234
- lineNumbers: "on",
235
- scrollBeyondLastLine: false,
236
- wordWrap: "on",
237
- tabSize: 2,
238
- });
256
+ view.monacoEditor = monaco.editor.create(
257
+ /** @type {HTMLElement} */ (/** @type {unknown} */ (editorContainer)),
258
+ {
259
+ model,
260
+ theme: "vs-dark",
261
+ automaticLayout: true,
262
+ minimap: { enabled: false },
263
+ fontSize: 12,
264
+ fontFamily: "'SF Mono', 'Fira Code', 'Consolas', monospace",
265
+ lineNumbers: "on",
266
+ scrollBeyondLastLine: false,
267
+ wordWrap: "on",
268
+ tabSize: 2,
269
+ },
270
+ );
239
271
 
240
272
  // Debounced sync back to state
241
- /** @type {any} */
273
+ /** @type {ReturnType<typeof setTimeout> | undefined} */
242
274
  let debounce;
243
275
  view.monacoEditor.onDidChangeModelContent(() => {
244
- if (view.monacoEditor._ignoreNextChange) {
245
- view.monacoEditor._ignoreNextChange = false;
276
+ const editor = view.monacoEditor;
277
+ if (!editor) return;
278
+ if (editor._ignoreNextChange) {
279
+ editor._ignoreNextChange = false;
246
280
  return;
247
281
  }
248
282
  clearTimeout(debounce);
@@ -250,7 +284,7 @@ export function renderCanvas() {
250
284
  const tab = activeTab.value;
251
285
  if (lang === "json") {
252
286
  try {
253
- tab.doc.document = JSON.parse(view.monacoEditor.getValue());
287
+ tab.doc.document = JSON.parse(editor.getValue());
254
288
  tab.doc.dirty = true;
255
289
  } catch {
256
290
  // Invalid JSON — don't update state
@@ -265,8 +299,8 @@ export function renderCanvas() {
265
299
 
266
300
  // Git diff mode — render original (left) and current (right) side-by-side on panzoom surface
267
301
  if (canvasMode === "git-diff") {
268
- if (!_ctx.gitDiffState) {
269
- _ctx.setCanvasMode("design");
302
+ if (!ctx.gitDiffState) {
303
+ ctx.setCanvasMode("design");
270
304
  renderCanvas();
271
305
  return;
272
306
  }
@@ -276,7 +310,7 @@ export function renderCanvas() {
276
310
  canvasWrap.style.overflow = "hidden";
277
311
  }
278
312
 
279
- const gitDiffState = _ctx.gitDiffState;
313
+ const gitDiffState = ctx.gitDiffState;
280
314
  const panelWidth = 800;
281
315
 
282
316
  const { tpl: origTpl, panel: origPanel } = canvasPanelTemplate(
@@ -307,8 +341,8 @@ export function renderCanvas() {
307
341
  canvasWrap,
308
342
  );
309
343
 
310
- canvasPanels.push(/** @type {any} */ (origPanel));
311
- canvasPanels.push(/** @type {any} */ (currPanel));
344
+ canvasPanels.push(/** @type {CanvasPanel} */ (origPanel));
345
+ canvasPanels.push(/** @type {CanvasPanel} */ (currPanel));
312
346
 
313
347
  /** @param {string} content */
314
348
  const parseContent = (content) => {
@@ -329,8 +363,20 @@ export function renderCanvas() {
329
363
  parseContent(gitDiffState.originalContent || ""),
330
364
  parseContent(gitDiffState.currentContent || ""),
331
365
  ]).then(([originalDoc, currentDoc]) => {
332
- renderCanvasIntoPanel(origPanel, new Set(), featureToggles, originalDoc, gitDiffState);
333
- renderCanvasIntoPanel(currPanel, new Set(), featureToggles, currentDoc, gitDiffState);
366
+ renderCanvasIntoPanel(
367
+ /** @type {CanvasPanel} */ (origPanel),
368
+ new Set(),
369
+ featureToggles,
370
+ originalDoc,
371
+ gitDiffState,
372
+ );
373
+ renderCanvasIntoPanel(
374
+ /** @type {CanvasPanel} */ (currPanel),
375
+ new Set(),
376
+ featureToggles,
377
+ currentDoc,
378
+ gitDiffState,
379
+ );
334
380
  });
335
381
 
336
382
  applyTransform();
@@ -355,15 +401,15 @@ export function renderCanvas() {
355
401
  <div
356
402
  class="content-edit-canvas"
357
403
  ${ref((el) => {
358
- panel.scrollContainer = /** @type {HTMLElement | null} */ (el);
404
+ panel.scrollContainer = /** @type {HTMLElement | null} */ (el || null);
359
405
  })}
360
406
  >
361
407
  <div class="content-edit-column" style="max-width:${baseWidth}px">${panelTpl}</div>
362
408
  </div>
363
409
  `;
364
410
  litRender(editTpl, canvasWrap);
365
- canvasPanels.push(/** @type {any} */ (panel));
366
- renderCanvasIntoPanel(panel, new Set(), S.ui.featureToggles);
411
+ canvasPanels.push(/** @type {CanvasPanel} */ (panel));
412
+ renderCanvasIntoPanel(/** @type {CanvasPanel} */ (panel), new Set(), S.ui.featureToggles);
367
413
  return;
368
414
  }
369
415
 
@@ -407,8 +453,8 @@ export function renderCanvas() {
407
453
  `,
408
454
  canvasWrap,
409
455
  );
410
- canvasPanels.push(/** @type {any} */ (panel));
411
- renderCanvasIntoPanel(panel, new Set(), featureToggles);
456
+ canvasPanels.push(/** @type {CanvasPanel} */ (panel));
457
+ renderCanvasIntoPanel(/** @type {CanvasPanel} */ (panel), new Set(), featureToggles);
412
458
  applyTransform();
413
459
  if (modeChanged) {
414
460
  observeCenterUntilStable();
@@ -436,7 +482,6 @@ export function renderCanvas() {
436
482
  });
437
483
  }
438
484
 
439
- /** @type {{ tpl: any; panel: any; activeSet: any }[]} */
440
485
  const panelEntries = allPanelDefs.map((def) => {
441
486
  const label = `${def.displayName} (${def.width}px)`;
442
487
  const { tpl, panel } = canvasPanelTemplate(def.name, label, false, def.width);
@@ -459,8 +504,9 @@ export function renderCanvas() {
459
504
  );
460
505
 
461
506
  for (const { panel, activeSet } of panelEntries) {
462
- canvasPanels.push(/** @type {any} */ (panel));
463
- renderCanvasIntoPanel(panel, activeSet, featureToggles);
507
+ const p = /** @type {CanvasPanel} */ (panel);
508
+ canvasPanels.push(p);
509
+ renderCanvasIntoPanel(p, activeSet, featureToggles);
464
510
  }
465
511
 
466
512
  // Highlight active panel header
@@ -480,13 +526,13 @@ export function renderCanvas() {
480
526
  * Render document into a single canvas panel. Tries runtime rendering first, falls back to
481
527
  * structural preview.
482
528
  *
483
- * @param {any} panel
529
+ * @param {CanvasPanel} panel
484
530
  * @param {Set<string>} activeBreakpoints
485
- * @param {any} featureToggles
486
- * @param {any} [docOverride] - Optional document to render (for diff mode). Uses active tab doc if
487
- * not provided.
488
- * @param {any} [gitDiffState] - Optional diff state. If provided, computes and applies diff
489
- * highlighting.
531
+ * @param {Record<string, boolean>} featureToggles
532
+ * @param {JxMutableNode | null} [docOverride] - Optional document to render (for diff mode). Uses
533
+ * active tab doc if not provided.
534
+ * @param {GitDiffState | null} [gitDiffState] - Optional diff state. If provided, computes and
535
+ * applies diff highlighting.
490
536
  */
491
537
  function renderCanvasIntoPanel(
492
538
  panel,
@@ -497,15 +543,16 @@ function renderCanvasIntoPanel(
497
543
  ) {
498
544
  const gen = view.renderGeneration;
499
545
  const tab = activeTab.value;
500
- const docToRender = docOverride || tab?.doc.document;
546
+ const docToRender = /** @type {JxMutableNode} */ (docOverride || tab?.doc.document);
547
+ const canvas = /** @type {HTMLElement} */ (panel.canvas);
501
548
 
502
- renderCanvasLive(gen, docToRender, panel.canvas)
503
- .then((/** @type {any} */ scope) => {
549
+ renderCanvasLive(gen, docToRender, canvas)
550
+ .then((/** @type {Record<string, unknown> | null} */ scope) => {
504
551
  // Skip post-render setup if a newer render has started
505
552
  if (gen !== view.renderGeneration) return;
506
553
  if (scope) {
507
554
  updateCanvas({ status: "ready", scope, error: null });
508
- applyCanvasMediaOverrides(panel.canvas, activeBreakpoints);
555
+ applyCanvasMediaOverrides(canvas, activeBreakpoints);
509
556
  statusMessage("Runtime render OK", 1500);
510
557
 
511
558
  // Apply diff highlighting if in git-diff mode
@@ -521,51 +568,50 @@ function renderCanvasIntoPanel(
521
568
  // Can't iterate WeakMap, so apply styling by walking the canvas
522
569
  const { elToPath } = scope;
523
570
  if (elToPath instanceof WeakMap) {
524
- applyDiffHighlightToCanvas(panel.canvas, diffMap);
571
+ applyDiffHighlightToCanvas(canvas, diffMap);
525
572
  }
526
573
  }
527
574
  } else {
528
575
  // Fallback to structural preview
529
576
  updateCanvas({ status: "ready", scope: null, error: null });
530
- panel.canvas.innerHTML = "";
531
- renderCanvasNode(docToRender, [], panel.canvas, activeBreakpoints, featureToggles);
577
+ canvas.innerHTML = "";
578
+ renderCanvasNode(docToRender, [], canvas, activeBreakpoints, featureToggles);
532
579
  }
533
580
  try {
534
- registerPanelDnD(panel);
535
- } catch (/** @type {any} */ e) {
536
- console.warn("registerPanelDnD failed:", e.message);
581
+ registerPanelDnD(/** @type {CanvasPanel} */ (panel));
582
+ } catch (/** @type {unknown} */ e) {
583
+ console.warn("registerPanelDnD failed:", /** @type {Error} */ (e).message);
537
584
  }
538
- registerPanelEvents(panel);
585
+ registerPanelEvents(/** @type {CanvasPanel} */ (panel));
539
586
  renderOverlays();
540
587
  updateForcedPseudoPreview();
541
588
 
542
589
  // Process pending inline edit when canvas becomes ready
543
590
  const currentTab = activeTab.value;
544
591
  if (currentTab?.session.ui?.pendingInlineEdit) {
545
- const { path, mediaName: mn } = /** @type {{ path: any; mediaName: string }} */ (
592
+ const { path, mediaName: mn } = /** @type {InlineEditDef} */ (
546
593
  currentTab.session.ui.pendingInlineEdit
547
594
  );
548
595
  currentTab.session.ui.pendingInlineEdit = null;
549
- const targetPanel =
550
- canvasPanels.find((/** @type {any} */ p) => p.mediaName === mn) || canvasPanels[0];
596
+ const targetPanel = canvasPanels.find((p) => p.mediaName === mn) || canvasPanels[0];
551
597
  if (targetPanel) {
552
598
  const el = findCanvasElement(path, targetPanel.canvas);
553
599
  if (el) enterComponentInlineEdit(el, path);
554
600
  }
555
601
  }
556
602
  })
557
- .catch((/** @type {any} */ err) => {
603
+ .catch((/** @type {unknown} */ err) => {
558
604
  if (gen !== view.renderGeneration) return;
559
- console.warn("renderCanvasLive rejected:", err?.message || err);
605
+ console.warn("renderCanvasLive rejected:", err instanceof Error ? err.message : err);
560
606
  updateCanvas({ status: "ready", scope: null, error: null });
561
- panel.canvas.innerHTML = "";
562
- renderCanvasNode(docToRender, [], panel.canvas, activeBreakpoints, featureToggles);
607
+ canvas.innerHTML = "";
608
+ renderCanvasNode(docToRender, [], canvas, activeBreakpoints, featureToggles);
563
609
  try {
564
- registerPanelDnD(panel);
565
- } catch (/** @type {any} */ e) {
566
- console.warn("registerPanelDnD failed:", e.message);
610
+ registerPanelDnD(/** @type {CanvasPanel} */ (panel));
611
+ } catch (/** @type {unknown} */ e) {
612
+ console.warn("registerPanelDnD failed:", /** @type {Error} */ (e).message);
567
613
  }
568
- registerPanelEvents(panel);
614
+ registerPanelEvents(/** @type {CanvasPanel} */ (panel));
569
615
  renderOverlays();
570
616
  updateForcedPseudoPreview();
571
617
  });
@@ -15,8 +15,18 @@ import { view } from "../view.js";
15
15
  import { getLayerSlot } from "../ui/layers.js";
16
16
  import { getActivePanel, findCanvasElement } from "./canvas-helpers.js";
17
17
 
18
- /** @type {any} */
19
- let _ctx = null;
18
+ /**
19
+ * @type {{
20
+ * getCanvasMode: () => string;
21
+ * getZoom: () => number;
22
+ * setZoomDirect: (zoom: number) => void;
23
+ * renderStylebookOverlays: () => void;
24
+ * }}
25
+ */
26
+ let _ctx;
27
+
28
+ /** @type {HTMLElement | null} */
29
+ let _zoomIndicatorEl = null;
20
30
 
21
31
  /**
22
32
  * Initialize the canvas utils module.
@@ -35,10 +45,10 @@ export function initCanvasUtils(ctx) {
35
45
  /**
36
46
  * Create the DOM structure for a single canvas panel.
37
47
  *
38
- * @param {any} mediaName
39
- * @param {any} label
40
- * @param {any} fullWidth
41
- * @param {any} width
48
+ * @param {string | null} mediaName
49
+ * @param {string | null} label
50
+ * @param {boolean} fullWidth
51
+ * @param {number | null} width
42
52
  */
43
53
  export function canvasPanelTemplate(mediaName, label, fullWidth, width = null) {
44
54
  /**
@@ -55,7 +65,7 @@ export function canvasPanelTemplate(mediaName, label, fullWidth, width = null) {
55
65
  * }}
56
66
  */
57
67
  const panel = {
58
- mediaName,
68
+ mediaName: mediaName || "",
59
69
  element: null,
60
70
  canvas: null,
61
71
  overlay: null,
@@ -201,6 +211,15 @@ export function fitToScreen() {
201
211
  applyTransform();
202
212
  }
203
213
 
214
+ /** Reset zoom to 100% and re-center horizontally. */
215
+ export function resetZoom() {
216
+ if (!view.panzoomWrap) return;
217
+ _ctx.setZoomDirect(1);
218
+ centerCanvas();
219
+ applyTransform();
220
+ renderZoomIndicator();
221
+ }
222
+
204
223
  /** Reset the zoom indicator (clear its content). Called when switching to non-panzoom modes. */
205
224
  export function resetZoomIndicator() {
206
225
  litRender(nothing, getLayerSlot("popover", "zoom-indicator"));
@@ -272,15 +291,27 @@ export function renderZoomIndicator() {
272
291
  const host = getLayerSlot("popover", "zoom-indicator");
273
292
  litRender(
274
293
  html`
275
- <div class="zoom-indicator">
294
+ <div
295
+ class="zoom-indicator"
296
+ ${ref((el) => {
297
+ _zoomIndicatorEl = /** @type {HTMLElement | null} */ (el || null);
298
+ })}
299
+ >
300
+ <span class="zoom-indicator-action" title="Reset to 100%" @click=${resetZoom}>
301
+ <svg
302
+ width="14"
303
+ height="14"
304
+ viewBox="0 0 16 16"
305
+ fill="none"
306
+ stroke="currentColor"
307
+ stroke-width="1.5"
308
+ >
309
+ <circle cx="8" cy="8" r="5.5" />
310
+ <path d="M8 5.5v5M5.5 8h5" />
311
+ </svg>
312
+ </span>
276
313
  <span class="zoom-indicator-label">${Math.round(zoom * 100)}%</span>
277
- <sp-action-button
278
- quiet
279
- size="s"
280
- class="zoom-fit-btn"
281
- title="Fit to screen"
282
- @click=${fitToScreen}
283
- >
314
+ <span class="zoom-indicator-action" title="Fit to screen" @click=${fitToScreen}>
284
315
  <svg
285
316
  width="14"
286
317
  height="14"
@@ -292,7 +323,7 @@ export function renderZoomIndicator() {
292
323
  <rect x="2" y="2" width="12" height="12" rx="1" />
293
324
  <path d="M2 6h12M6 2v12" />
294
325
  </svg>
295
- </sp-action-button>
326
+ </span>
296
327
  </div>
297
328
  `,
298
329
  host,
@@ -302,12 +333,11 @@ export function renderZoomIndicator() {
302
333
 
303
334
  /** Position the zoom indicator relative to canvas-wrap bounds. */
304
335
  export function positionZoomIndicator() {
305
- const indicator = /** @type {HTMLElement | null} */ (document.querySelector(".zoom-indicator"));
306
- if (!indicator) return;
336
+ if (!_zoomIndicatorEl) return;
307
337
  const rect = canvasWrap.getBoundingClientRect();
308
- indicator.style.left = `${rect.left + rect.width / 2}px`;
309
- indicator.style.top = `${rect.bottom - 32}px`;
310
- indicator.style.transform = "translateX(-50%)";
338
+ _zoomIndicatorEl.style.left = `${rect.left + rect.width / 2}px`;
339
+ _zoomIndicatorEl.style.top = `${rect.bottom - 32}px`;
340
+ _zoomIndicatorEl.style.transform = "translateX(-50%)";
311
341
  }
312
342
 
313
343
  /** Toggle "active" class on canvas panel headers based on activeMedia. */
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Generate scoped CSS from nested style objects (e.g. `table: { width: "100%", thead: { ... } }`).
3
+ * Returns a CSS string with rules scoped under the given selector.
4
+ *
5
+ * @param {Record<string, any>} styleObj - The full style object (flat + nested)
6
+ * @param {string} scope - The scoping selector (e.g. `[data-jx-site]`)
7
+ * @returns {string} Generated CSS text
8
+ */
9
+ export function buildNestedSiteCSS(styleObj, scope) {
10
+ let css = "";
11
+
12
+ function emit(/** @type {string} */ parentSel, /** @type {Record<string, any>} */ rules) {
13
+ const props = Object.entries(rules)
14
+ .filter(([, val]) => val === null || typeof val !== "object" || Array.isArray(val))
15
+ .map(([p, val]) => `${camelToKebab(p)}: ${val}`)
16
+ .join("; ");
17
+ if (props) css += `${parentSel} { ${props} }\n`;
18
+ for (const [sel, sub] of Object.entries(rules)) {
19
+ if (sub === null || typeof sub !== "object" || Array.isArray(sub)) continue;
20
+ const resolved = sel.startsWith("&")
21
+ ? sel.replace("&", parentSel)
22
+ : sel.startsWith("[") || sel.startsWith(":") || sel.startsWith(".")
23
+ ? `${parentSel}${sel}`
24
+ : `${parentSel} ${sel}`;
25
+ emit(resolved, sub);
26
+ }
27
+ }
28
+
29
+ for (const [k, v] of Object.entries(styleObj)) {
30
+ if (v === null || typeof v !== "object" || Array.isArray(v)) continue;
31
+ const resolved = k.startsWith("&")
32
+ ? k.replace("&", scope)
33
+ : k.startsWith("[") || k.startsWith(":") || k.startsWith(".")
34
+ ? `${scope}${k}`
35
+ : `${scope} ${k}`;
36
+ emit(resolved, v);
37
+ }
38
+
39
+ return css;
40
+ }
41
+
42
+ /**
43
+ * Convert camelCase to kebab-case.
44
+ *
45
+ * @param {string} s
46
+ * @returns {string}
47
+ */
48
+ function camelToKebab(s) {
49
+ return s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
50
+ }
@@ -24,7 +24,7 @@ import { isSlashMenuOpen, showSlashMenu, dismissSlashMenu } from "./slash-menu.j
24
24
  import { renderBlockActionBar } from "../panels/block-action-bar.js";
25
25
  import { defaultDef } from "../panels/shared.js";
26
26
 
27
- /** @type {any} */
27
+ /** @type {{ findCanvasElement: Function } | null} */
28
28
  let _ctx = null;
29
29
 
30
30
  /**
@@ -39,8 +39,8 @@ export function initComponentInlineEdit(ctx) {
39
39
  /**
40
40
  * Enter plaintext inline editing on a canvas element.
41
41
  *
42
- * @param {any} el
43
- * @param {any} path
42
+ * @param {HTMLElement} el
43
+ * @param {JxPath} path
44
44
  */
45
45
  export function enterComponentInlineEdit(el, path) {
46
46
  if (view.componentInlineEdit && view.componentInlineEdit.el === el) {
@@ -56,7 +56,7 @@ export function enterComponentInlineEdit(el, path) {
56
56
  if (node.children && typeof node.children === "object") return;
57
57
  if (tc && typeof tc === "object") return;
58
58
  const voids = new Set(["img", "input", "br", "hr", "video", "audio", "source", "embed", "slot"]);
59
- if (voids.has(node.tagName)) return;
59
+ if (voids.has(node.tagName || "")) return;
60
60
 
61
61
  for (const p of canvasPanels) {
62
62
  const boxes = /** @type {NodeListOf<HTMLElement>} */ (
@@ -96,14 +96,15 @@ export function enterComponentInlineEdit(el, path) {
96
96
  el.addEventListener("keydown", componentInlineKeydown);
97
97
  el.addEventListener("input", componentInlineInput);
98
98
 
99
- const outsideHandler = (/** @type {any} */ evt) => {
99
+ const outsideHandler = (/** @type {MouseEvent} */ evt) => {
100
100
  if (!view.componentInlineEdit) {
101
101
  document.removeEventListener("mousedown", outsideHandler, true);
102
102
  return;
103
103
  }
104
- if (view.componentInlineEdit.el.contains(evt.target)) return;
104
+ if (view.componentInlineEdit.el.contains(/** @type {Node} */ (evt.target))) return;
105
105
  if (isSlashMenuOpen()) return;
106
- if (view.blockActionBarEl && view.blockActionBarEl.contains(evt.target)) return;
106
+ if (view.blockActionBarEl && view.blockActionBarEl.contains(/** @type {Node} */ (evt.target)))
107
+ return;
107
108
  document.removeEventListener("mousedown", outsideHandler, true);
108
109
 
109
110
  /** @type {(string | number)[] | null} */
@@ -185,7 +186,7 @@ export function enterComponentInlineEdit(el, path) {
185
186
  renderBlockActionBar();
186
187
  }
187
188
 
188
- /** @param {any} e */
189
+ /** @param {KeyboardEvent} e */
189
190
  function componentInlineKeydown(e) {
190
191
  if (isSlashMenuOpen()) {
191
192
  if (["ArrowDown", "ArrowUp", "Enter", "Escape"].includes(e.key)) return;
@@ -205,10 +206,10 @@ function splitParagraph() {
205
206
  if (!view.componentInlineEdit) return;
206
207
  const { el, path, mediaName } = view.componentInlineEdit;
207
208
 
208
- const sel = /** @type {any} */ (el.ownerDocument.defaultView?.getSelection());
209
+ const sel = /** @type {Selection | null} */ (el.ownerDocument.defaultView?.getSelection());
209
210
  const fullText = el.textContent || "";
210
211
  let offset = fullText.length;
211
- if (sel.rangeCount) {
212
+ if (sel && sel.rangeCount) {
212
213
  const range = sel.getRangeAt(0);
213
214
  const preRange = document.createRange();
214
215
  preRange.selectNodeContents(el);
@@ -220,7 +221,7 @@ function splitParagraph() {
220
221
  const textAfter = fullText.slice(offset);
221
222
 
222
223
  const tag = "p";
223
- const pPath = /** @type {any} */ (parentElementPath(path));
224
+ const pPath = /** @type {JxPath} */ (parentElementPath(path));
224
225
  const idx = /** @type {number} */ (childIndex(path));
225
226
  if (!pPath) return;
226
227
 
@@ -266,7 +267,7 @@ function cancelComponentInlineEdit() {
266
267
  renderOnly("overlays");
267
268
  }
268
269
 
269
- /** @param {any} el */
270
+ /** @param {HTMLElement} el */
270
271
  function cleanupComponentInlineEdit(el) {
271
272
  el.removeEventListener("keydown", componentInlineKeydown);
272
273
  el.removeEventListener("input", componentInlineInput);
@@ -304,7 +305,7 @@ function componentInlineInput() {
304
305
  }
305
306
  }
306
307
 
307
- /** @param {any} cmd */
308
+ /** @param {{ tag: string; label: string; description: string }} cmd */
308
309
  function handleComponentSlashSelect(cmd) {
309
310
  if (!view.componentInlineEdit) return;
310
311
  const { el, path, mediaName } = view.componentInlineEdit;