@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
@@ -11,26 +11,36 @@ import { html, render as litRender, nothing } from "lit-html";
11
11
  import type { TemplateResult } from "lit-html";
12
12
  import { leftPanel, updateSession } from "../store";
13
13
  import { effect, effectScope } from "../reactivity";
14
- import { createPanelScheduler, type PanelScheduler } from "./panel-scheduler";
14
+ import { createPanelScheduler } from "./panel-scheduler";
15
+ import type { PanelScheduler } from "./panel-scheduler";
15
16
  import { activeTab } from "../workspace/workspace";
16
17
  import { view } from "../view";
17
- import { transact, mutateUpdateFrontmatter } from "../tabs/transact";
18
+ import { mutateUpdateFrontmatter, transact } from "../tabs/transact";
18
19
  import type { GitDiffState, JsonValue } from "../types";
20
+ import type { JxHeadEntry, JxMutableNode } from "@jxsuite/schema/types";
19
21
 
20
22
  import { renderLayersTemplate } from "./layers-panel";
21
23
  import { renderStylebookLayersTemplate } from "./stylebook-layers-panel";
22
24
  import { renderElementsTemplate } from "./elements-panel";
23
25
  import { selectStylebookTag, stylebookMeta } from "./stylebook-panel";
26
+ import type { renderImportsTemplate } from "./imports-panel";
27
+ import type { renderSignalsTemplate } from "./signals-panel";
28
+ import type { renderDataExplorerTemplate } from "./data-explorer";
29
+ import type { renderHeadTemplate } from "./head-panel";
30
+ import type { renderGitPanel } from "./git-panel";
31
+ import type { EffectScope } from "@vue/reactivity";
24
32
 
25
33
  interface LeftPanelCtx {
26
34
  getCanvasMode: () => string;
27
35
  setCanvasMode: (mode: string) => void;
28
- renderImportsTemplate: (...args: any[]) => TemplateResult;
29
- renderFilesTemplate: (...args: any[]) => TemplateResult;
30
- renderSignalsTemplate: (...args: any[]) => TemplateResult;
31
- renderDataExplorerTemplate: (...args: any[]) => TemplateResult;
32
- renderHeadTemplate: (...args: any[]) => TemplateResult;
33
- renderGitPanel: (...args: any[]) => TemplateResult;
36
+ // Renderers injected from studio.ts (dependency inversion avoids circular imports);
37
+ // Typed against their implementations so call sites stay checked.
38
+ renderImportsTemplate: typeof renderImportsTemplate;
39
+ renderFilesTemplate: () => TemplateResult;
40
+ renderSignalsTemplate: typeof renderSignalsTemplate;
41
+ renderDataExplorerTemplate: typeof renderDataExplorerTemplate;
42
+ renderHeadTemplate: typeof renderHeadTemplate;
43
+ renderGitPanel: typeof renderGitPanel;
34
44
  renderCanvas: () => void;
35
45
  defCategory: (def: unknown) => string;
36
46
  defBadgeLabel: (def: unknown) => string;
@@ -48,7 +58,7 @@ interface LeftPanelCtx {
48
58
 
49
59
  let _ctx: LeftPanelCtx | null = null;
50
60
 
51
- let _scope: import("@vue/reactivity").EffectScope | null = null;
61
+ let _scope: EffectScope | null = null;
52
62
 
53
63
  let _scheduler: PanelScheduler | null = null;
54
64
 
@@ -59,7 +69,7 @@ let _scheduler: PanelScheduler | null = null;
59
69
  */
60
70
  export function mount(ctx: LeftPanelCtx) {
61
71
  _ctx = ctx;
62
- _scheduler = createPanelScheduler({ root: leftPanel, render: _doRender });
72
+ _scheduler = createPanelScheduler({ render: _doRender, root: leftPanel });
63
73
  _scheduler.bindFocus();
64
74
  _scope = effectScope();
65
75
  _scope.run(() => {
@@ -98,27 +108,42 @@ export function render() {
98
108
 
99
109
  /** Actual DOM paint, invoked by the scheduler. Includes a Lit-marker-corruption recovery retry. */
100
110
  function _doRender() {
101
- if (!_ctx) return;
111
+ if (!_ctx) {
112
+ return;
113
+ }
102
114
  try {
103
115
  _render();
104
- } catch (e) {
105
- console.error("left-panel render error:", e);
116
+ } catch (error) {
117
+ console.error("left-panel render error:", error);
106
118
  try {
107
119
  leftPanel.textContent = "";
108
- // @ts-ignore — clear Lit's internal state to recover from marker corruption
120
+ // @ts-expect-error — clear Lit's internal state to recover from marker corruption
109
121
  delete leftPanel["_$litPart$"];
110
122
  _render();
111
- } catch (e2) {
112
- console.error("left-panel retry failed:", e2);
123
+ } catch (retryError) {
124
+ console.error("left-panel retry failed:", retryError);
113
125
  }
114
126
  }
115
127
 
116
128
  if (view.leftTab === "layers") {
117
129
  const sel = leftPanel.querySelector(".layer-row.selected");
118
- if (sel) sel.scrollIntoView({ block: "nearest", behavior: "smooth" });
130
+ if (sel) {
131
+ sel.scrollIntoView({ behavior: "smooth", block: "nearest" });
132
+ }
119
133
  }
120
134
  }
121
135
 
136
+ /** Overlay content-mode frontmatter title/$head onto the document for the head panel. */
137
+ function buildHeadDoc(doc: JxMutableNode, fm: Record<string, unknown>): JxMutableNode {
138
+ const title = fm.title as string | undefined;
139
+ const $head = fm.$head as JxHeadEntry[] | undefined;
140
+ return {
141
+ ...doc,
142
+ ...(title !== undefined ? { title } : {}),
143
+ ...($head !== undefined ? { $head } : {}),
144
+ };
145
+ }
146
+
122
147
  function _render() {
123
148
  const ctx = _ctx as LeftPanelCtx;
124
149
  const tab = view.leftTab;
@@ -128,23 +153,25 @@ function _render() {
128
153
  if (tab === "files") {
129
154
  litRender(html`<div class="panel-body">${ctx.renderFilesTemplate()}</div>`, leftPanel);
130
155
  const tree = leftPanel.querySelector(".file-tree") as HTMLElement | null;
131
- if (tree) ctx.setupTreeKeyboard(tree);
156
+ if (tree) {
157
+ ctx.setupTreeKeyboard(tree);
158
+ }
132
159
  ctx.registerFileTreeDnD({ renderLeftPanel: render });
133
160
  return;
134
161
  }
135
162
 
136
163
  if (tab === "git") {
137
164
  const aTab = activeTab.value;
138
- const S = aTab ? { ui: aTab.session.ui } : { ui: {} };
139
- litRender(html`<div class="panel-body">${ctx.renderGitPanel(S, ctx)}</div>`, leftPanel);
165
+ const ui: Parameters<LeftPanelCtx["renderGitPanel"]>[0]["ui"] = aTab ? aTab.session.ui : {};
166
+ litRender(html`<div class="panel-body">${ctx.renderGitPanel({ ui }, ctx)}</div>`, leftPanel);
140
167
  return;
141
168
  }
142
169
 
143
170
  if (tab === "blocks") {
144
171
  const content = renderElementsTemplate({
145
- webdata: ctx.webdata,
146
172
  defaultDef: ctx.defaultDef,
147
173
  rerender: render,
174
+ webdata: ctx.webdata,
148
175
  } as Parameters<typeof renderElementsTemplate>[0]);
149
176
  litRender(html`<div class="panel-body">${content}</div>`, leftPanel);
150
177
  ctx.registerElementsDnD();
@@ -171,18 +198,18 @@ function _render() {
171
198
  * documentPath?: string;
172
199
  * }}
173
200
  */ {
174
- ui: aTab.session.ui,
175
- document: aTab.doc.document,
176
- mode: aTab.doc.mode,
177
- selection: aTab.session.selection,
178
201
  canvas: aTab.session.canvas,
179
202
  content: aTab.doc.content,
203
+ document: aTab.doc.document,
180
204
  documentPath: aTab.documentPath,
205
+ mode: aTab.doc.mode,
206
+ selection: aTab.session.selection,
207
+ ui: aTab.session.ui,
181
208
  };
182
209
 
183
210
  /** @type {TemplateResult | typeof nothing} */
184
211
  let content;
185
- if (tab === "layers")
212
+ if (tab === "layers") {
186
213
  content =
187
214
  ctx.getCanvasMode() === "stylebook"
188
215
  ? renderStylebookLayersTemplate({
@@ -193,59 +220,65 @@ function _render() {
193
220
  navigateToComponent: ctx.navigateToComponent,
194
221
  rerender: render,
195
222
  });
196
- else if (tab === "imports")
223
+ } else if (tab === "imports") {
197
224
  content = ctx.renderImportsTemplate({
198
- renderLeftPanel: render,
199
- documentPath: S.documentPath,
200
- documentElements: S.document.$elements || [],
201
- applyMutation: (fn: (doc: object) => void) => {
225
+ applyMutation: (fn: (doc: JxMutableNode) => void) => {
202
226
  transact(activeTab.value, fn);
203
227
  },
228
+ documentElements: S.document.$elements || [],
229
+ documentPath: S.documentPath,
230
+ renderLeftPanel: render,
204
231
  });
205
- else if (tab === "state")
232
+ } else if (tab === "state") {
206
233
  content = ctx.renderSignalsTemplate(S, {
207
- renderLeftPanel: render,
208
234
  renderCanvas: ctx.renderCanvas,
235
+ renderLeftPanel: render,
209
236
  updateSession,
210
237
  });
211
- else if (tab === "data")
212
- content = ctx.renderDataExplorerTemplate(S.document.state, S.canvas?.scope ?? null, {
238
+ } else if (tab === "data") {
239
+ content = ctx.renderDataExplorerTemplate(S.document.state ?? {}, S.canvas?.scope ?? null, {
240
+ defBadgeLabel: ctx.defBadgeLabel,
241
+ defCategory: ctx.defCategory,
213
242
  renderCanvas: ctx.renderCanvas,
214
243
  renderLeftPanel: render,
215
- defCategory: ctx.defCategory,
216
- defBadgeLabel: ctx.defBadgeLabel,
217
244
  });
218
- else if (tab === "head") {
245
+ } else if (tab === "head") {
219
246
  const isContent = S.mode === "content";
220
247
  const fm = S.content?.frontmatter ?? {};
221
- const headDoc = isContent ? { ...S.document, title: fm.title, $head: fm.$head } : S.document;
248
+ const headDoc = isContent ? buildHeadDoc(S.document, fm) : S.document;
222
249
  content = ctx.renderHeadTemplate({
223
- document: headDoc,
224
250
  applyMutation: isContent
225
- ? (fn: (doc: object) => void) => {
226
- const tab = activeTab.value!;
227
- const fm = (tab.doc.content?.frontmatter ?? {}) as Record<string, unknown>;
228
- const fmHead = fm.$head as unknown[] | undefined;
229
- const tmp = {
230
- title: fm.title,
231
- $head: fmHead ? [...fmHead] : undefined,
251
+ ? (fn: (doc: JxMutableNode) => void) => {
252
+ const tabNow = activeTab.value!;
253
+ const fmNow = (tabNow.doc.content?.frontmatter ?? {}) as Record<string, unknown>;
254
+ const fmHead = fmNow.$head as JxHeadEntry[] | undefined;
255
+ const tmp: JxMutableNode = {
256
+ ...(typeof fmNow.title === "string" ? { title: fmNow.title } : {}),
257
+ ...(fmHead ? { $head: [...fmHead] } : {}),
232
258
  };
233
259
  fn(tmp);
234
- if (tmp.title !== fm.title)
235
- mutateUpdateFrontmatter(tab, "title", tmp.title as JsonValue);
260
+ if (tmp.title !== fmNow.title) {
261
+ mutateUpdateFrontmatter(tabNow, "title", tmp.title as JsonValue);
262
+ }
236
263
  const newHead = tmp.$head && tmp.$head.length > 0 ? tmp.$head : undefined;
237
- mutateUpdateFrontmatter(tab, "$head", /** @type {JsonValue} */ newHead);
264
+ // JxHeadEntry[] is JSON document content by construction.
265
+ mutateUpdateFrontmatter(tabNow, "$head", newHead as JsonValue);
238
266
  render();
239
267
  }
240
- : (fn: (doc: object) => void) => {
268
+ : (fn: (doc: JxMutableNode) => void) => {
241
269
  transact(activeTab.value, fn);
242
270
  },
271
+ document: headDoc,
243
272
  renderLeftPanel: render,
244
273
  });
245
- } else content = nothing;
274
+ } else {
275
+ content = nothing;
276
+ }
246
277
 
247
278
  litRender(html`<div class="panel-body">${content}</div>`, leftPanel);
248
279
 
249
280
  // Post-render side effects
250
- if (tab === "layers" && ctx.getCanvasMode() !== "stylebook") ctx.registerLayersDnD();
281
+ if (tab === "layers" && ctx.getCanvasMode() !== "stylebook") {
282
+ ctx.registerLayersDnD();
283
+ }
251
284
  }
@@ -10,8 +10,9 @@ import { canvasPanels, pathsEqual } from "../store";
10
10
  import { effect, effectScope } from "../reactivity";
11
11
  import { activeTab } from "../workspace/workspace";
12
12
  import { view } from "../view";
13
- import { findCanvasElement, getActivePanel, effectiveZoom } from "../canvas/canvas-helpers";
13
+ import { effectiveZoom, findCanvasElement, getActivePanel } from "../canvas/canvas-helpers";
14
14
  import { layoutElements } from "../canvas/canvas-live-render";
15
+ import type { EffectScope } from "@vue/reactivity";
15
16
 
16
17
  interface OverlayBox {
17
18
  cls: string;
@@ -31,7 +32,7 @@ interface OverlaysCtx {
31
32
 
32
33
  let _ctx: OverlaysCtx | null = null;
33
34
 
34
- let _scope: import("@vue/reactivity").EffectScope | null = null;
35
+ let _scope: EffectScope | null = null;
35
36
 
36
37
  let _scheduled = false;
37
38
 
@@ -46,7 +47,9 @@ export function mount(ctx: OverlaysCtx) {
46
47
  _scope.run(() => {
47
48
  effect(() => {
48
49
  const tab = activeTab.value;
49
- if (!tab) return;
50
+ if (!tab) {
51
+ return;
52
+ }
50
53
  // Track selection, hover, and mode
51
54
  void tab.session.selection;
52
55
  void tab.session.hover;
@@ -63,7 +66,9 @@ export function unmount() {
63
66
  }
64
67
 
65
68
  export function render() {
66
- if (!_ctx) return;
69
+ if (!_ctx) {
70
+ return;
71
+ }
67
72
  if (!_scheduled) {
68
73
  _scheduled = true;
69
74
  queueMicrotask(_flush);
@@ -72,9 +77,13 @@ export function render() {
72
77
 
73
78
  function _flush() {
74
79
  _scheduled = false;
75
- if (!_ctx) return;
80
+ if (!_ctx) {
81
+ return;
82
+ }
76
83
  const tab = activeTab.value;
77
- if (!tab) return;
84
+ if (!tab) {
85
+ return;
86
+ }
78
87
  const { selection, hover } = tab.session;
79
88
  const { stylebookTab } = tab.session.ui;
80
89
  const canvasMode = _ctx.getCanvasMode();
@@ -112,10 +121,12 @@ function _flush() {
112
121
  const boxes: OverlayBox[] = [];
113
122
 
114
123
  // Batch layout reads: read viewport geometry once per panel
115
- if (!p.viewport) continue;
124
+ if (!p.viewport) {
125
+ continue;
126
+ }
116
127
  const vpRect = p.viewport.getBoundingClientRect();
117
- const scrollTop = p.viewport.scrollTop;
118
- const scrollLeft = p.viewport.scrollLeft;
128
+ const { scrollTop } = p.viewport;
129
+ const { scrollLeft } = p.viewport;
119
130
  const scale = effectiveZoom();
120
131
 
121
132
  if (hover && !pathsEqual(hover, selection)) {
@@ -124,12 +135,14 @@ function _flush() {
124
135
  const elRect = el.getBoundingClientRect();
125
136
  const desc: OverlayBox = {
126
137
  cls: "overlay-box overlay-hover",
127
- top: `${(elRect.top - vpRect.top + scrollTop) / scale}px`,
138
+ height: `${elRect.height / scale}px`,
128
139
  left: `${(elRect.left - vpRect.left + scrollLeft) / scale}px`,
140
+ top: `${(elRect.top - vpRect.top + scrollTop) / scale}px`,
129
141
  width: `${elRect.width / scale}px`,
130
- height: `${elRect.height / scale}px`,
131
142
  };
132
- if (layoutElements.has(el)) desc.isLayout = true;
143
+ if (layoutElements.has(el)) {
144
+ desc.isLayout = true;
145
+ }
133
146
  boxes.push(desc);
134
147
  }
135
148
  }
@@ -140,13 +153,17 @@ function _flush() {
140
153
  const elRect = el.getBoundingClientRect();
141
154
  const desc: OverlayBox = {
142
155
  cls: "overlay-box overlay-selection",
143
- top: `${(elRect.top - vpRect.top + scrollTop) / scale}px`,
156
+ height: `${elRect.height / scale}px`,
144
157
  left: `${(elRect.left - vpRect.left + scrollLeft) / scale}px`,
158
+ top: `${(elRect.top - vpRect.top + scrollTop) / scale}px`,
145
159
  width: `${elRect.width / scale}px`,
146
- height: `${elRect.height / scale}px`,
147
160
  };
148
- if (view.componentInlineEdit || _ctx.isEditing()) desc.border = "none";
149
- if (layoutElements.has(el)) desc.isLayout = true;
161
+ if (view.componentInlineEdit || _ctx.isEditing()) {
162
+ desc.border = "none";
163
+ }
164
+ if (layoutElements.has(el)) {
165
+ desc.isLayout = true;
166
+ }
150
167
  boxes.push(desc);
151
168
  }
152
169
  }
@@ -159,11 +176,11 @@ function _flush() {
159
176
  <div
160
177
  class="${b.cls}${b.isLayout ? " overlay-layout" : ""}"
161
178
  style=${styleMap({
162
- top: b.top,
179
+ border: b.border,
180
+ height: b.height,
163
181
  left: b.left,
182
+ top: b.top,
164
183
  width: b.width,
165
- height: b.height,
166
- border: b.border,
167
184
  })}
168
185
  >
169
186
  ${b.isLayout ? html`<span class="overlay-layout-badge">Layout</span>` : nothing}
@@ -6,22 +6,27 @@
6
6
  */
7
7
 
8
8
  import {
9
- updateUi,
10
- elToPath,
11
- pathsEqual,
12
- parentElementPath,
13
9
  childIndex,
10
+ elToPath,
14
11
  getNodeAtPath,
12
+ parentElementPath,
13
+ pathsEqual,
15
14
  renderOnly,
15
+ updateUi,
16
16
  } from "../store";
17
17
  import { activeTab } from "../workspace/workspace";
18
18
  import { view } from "../view";
19
- import { stopEditing, isEditing, isEditableBlock } from "../editor/inline-edit";
19
+ import { isEditableBlock, isEditing, stopEditing } from "../editor/inline-edit";
20
20
  import { showContextMenu } from "../editor/context-menu";
21
21
  import * as insertionHelper from "../editor/insertion-helper";
22
22
  import { defaultDef } from "../panels/shared";
23
- import { bubbleInlinePath, findCanvasElement, effectiveZoom } from "../canvas/canvas-helpers";
24
- import { layoutElements, activeLayoutPath } from "../canvas/canvas-live-render";
23
+ import {
24
+ bubbleInlinePath,
25
+ effectiveZoom,
26
+ findCanvasElement,
27
+ panelMediaToActiveMedia,
28
+ } from "../canvas/canvas-helpers";
29
+ import { activeLayoutPath, layoutElements } from "../canvas/canvas-live-render";
25
30
  import type { CanvasPanel } from "./canvas-dnd";
26
31
  import type { JxPath } from "../state";
27
32
 
@@ -55,11 +60,15 @@ export function registerPanelEvents(panel: CanvasPanel) {
55
60
  /** @param {() => unknown} fn */
56
61
  function withPanelPointerEvents(fn: () => unknown) {
57
62
  const els = canvas.querySelectorAll("*") as NodeListOf<HTMLElement>;
58
- for (const el of els) el.style.pointerEvents = "auto";
63
+ for (const el of els) {
64
+ el.style.pointerEvents = "auto";
65
+ }
59
66
  overlayClk.style.display = "none";
60
67
  const result = fn();
61
68
  overlayClk.style.display = "";
62
- for (const el of els) el.style.pointerEvents = "none";
69
+ for (const el of els) {
70
+ el.style.pointerEvents = "none";
71
+ }
63
72
  return result;
64
73
  }
65
74
 
@@ -74,8 +83,9 @@ export function registerPanelEvents(panel: CanvasPanel) {
74
83
  e.clientX <= r.right &&
75
84
  e.clientY >= r.top &&
76
85
  e.clientY <= r.bottom
77
- )
86
+ ) {
78
87
  return;
88
+ }
79
89
  }
80
90
  if (isEditing()) {
81
91
  stopEditing();
@@ -88,7 +98,9 @@ export function registerPanelEvents(panel: CanvasPanel) {
88
98
  document.elementsFromPoint(e.clientX, e.clientY),
89
99
  ) as Element[];
90
100
 
91
- if (!tab) return;
101
+ if (!tab) {
102
+ return;
103
+ }
92
104
 
93
105
  for (const el of elements) {
94
106
  if (canvas.contains(el) && el !== canvas) {
@@ -103,8 +115,8 @@ export function registerPanelEvents(panel: CanvasPanel) {
103
115
 
104
116
  const originalPath = elToPath.get(el);
105
117
  if (originalPath) {
106
- let path = bubbleInlinePath(tab.doc.document, originalPath);
107
- const newMedia = mediaName === "base" ? null : (mediaName ?? null);
118
+ const path = bubbleInlinePath(tab.doc.document, originalPath);
119
+ const newMedia = panelMediaToActiveMedia(mediaName);
108
120
 
109
121
  const resolvedEl = (
110
122
  path === originalPath
@@ -123,7 +135,7 @@ export function registerPanelEvents(panel: CanvasPanel) {
123
135
  }
124
136
 
125
137
  if (canvasMode === "design" && tab.doc.mode !== "content") {
126
- updateUi("pendingInlineEdit", { path, mediaName });
138
+ updateUi("pendingInlineEdit", { mediaName, path });
127
139
  tab.session.ui.activeMedia = newMedia;
128
140
  tab.session.selection = path;
129
141
  return;
@@ -151,18 +163,23 @@ export function registerPanelEvents(panel: CanvasPanel) {
151
163
  e.clientX <= r.right &&
152
164
  e.clientY >= r.top &&
153
165
  e.clientY <= r.bottom
154
- )
166
+ ) {
155
167
  return;
168
+ }
156
169
  }
157
170
  const canvasMode = ctx.getCanvasMode();
158
- if (canvasMode !== "edit" && canvasMode !== "design") return;
171
+ if (canvasMode !== "edit" && canvasMode !== "design") {
172
+ return;
173
+ }
159
174
 
160
175
  const tab = activeTab.value;
161
176
  const elements = withPanelPointerEvents(() =>
162
177
  document.elementsFromPoint(e.clientX, e.clientY),
163
178
  ) as Element[];
164
179
 
165
- if (!tab) return;
180
+ if (!tab) {
181
+ return;
182
+ }
166
183
 
167
184
  for (const el of elements) {
168
185
  if (canvas.contains(el) && el !== canvas) {
@@ -175,7 +192,7 @@ export function registerPanelEvents(panel: CanvasPanel) {
175
192
  : (findCanvasElement(path, canvas) as HTMLElement | null) || el
176
193
  ) as HTMLElement;
177
194
  if (isEditableBlock(resolvedEl)) {
178
- const newMedia = mediaName === "base" ? null : (mediaName ?? null);
195
+ const newMedia = panelMediaToActiveMedia(mediaName);
179
196
  tab.session.ui.activeMedia = newMedia;
180
197
  tab.session.selection = path;
181
198
  ctx.enterInlineEdit(resolvedEl, path);
@@ -199,8 +216,9 @@ export function registerPanelEvents(panel: CanvasPanel) {
199
216
  e.clientX <= r.right &&
200
217
  e.clientY >= r.top &&
201
218
  e.clientY <= r.bottom
202
- )
219
+ ) {
203
220
  return;
221
+ }
204
222
  }
205
223
  const tab = activeTab.value;
206
224
  const elements = withPanelPointerEvents(() =>
@@ -234,11 +252,14 @@ export function registerPanelEvents(panel: CanvasPanel) {
234
252
  e.clientX <= r.right &&
235
253
  e.clientY >= r.top &&
236
254
  e.clientY <= r.bottom
237
- )
255
+ ) {
238
256
  return;
257
+ }
239
258
  }
240
259
  const tab = activeTab.value;
241
- if (!tab) return;
260
+ if (!tab) {
261
+ return;
262
+ }
242
263
  const el = withPanelPointerEvents(() =>
243
264
  document.elementFromPoint(e.clientX, e.clientY),
244
265
  ) as Element | null;
@@ -271,15 +292,15 @@ export function registerPanelEvents(panel: CanvasPanel) {
271
292
  );
272
293
 
273
294
  insertionHelper.mount({
274
- getCanvasMode: ctx.getCanvasMode,
275
- withPanelPointerEvents,
276
- effectiveZoom: effectiveZoom,
295
+ childIndex: (path: JxPath) => childIndex(path) ?? 0,
277
296
  defaultDef,
278
- parentElementPath,
279
- childIndex,
280
- getNodeAtPath,
297
+ effectiveZoom,
281
298
  elToPath,
299
+ getCanvasMode: ctx.getCanvasMode,
300
+ getNodeAtPath,
282
301
  panel: panel as unknown as Parameters<typeof insertionHelper.mount>[0]["panel"],
302
+ parentElementPath,
303
+ withPanelPointerEvents,
283
304
  });
284
305
  view.canvasEventCleanups.push(() => insertionHelper.unmount());
285
306
  }
@@ -17,25 +17,33 @@
17
17
  * @returns {boolean}
18
18
  */
19
19
  export function isTextInput(el: Element | null): boolean {
20
- if (!el) return false;
20
+ if (!el) {
21
+ return false;
22
+ }
21
23
  const tag = el.tagName.toLowerCase();
22
- if (tag === "input" || tag === "textarea") return true;
23
- if (tag === "sp-textfield" || tag === "sp-number-field" || tag === "sp-search") return true;
24
- if (el.shadowRoot?.activeElement) return isTextInput(el.shadowRoot.activeElement);
24
+ if (tag === "input" || tag === "textarea") {
25
+ return true;
26
+ }
27
+ if (tag === "sp-textfield" || tag === "sp-number-field" || tag === "sp-search") {
28
+ return true;
29
+ }
30
+ if (el.shadowRoot?.activeElement) {
31
+ return isTextInput(el.shadowRoot.activeElement);
32
+ }
25
33
  return false;
26
34
  }
27
35
 
28
36
  export interface PanelScheduler {
29
37
  /** Request a render. Coalesced; deferred while a text input in the panel is focused. */
30
- schedule(): void;
38
+ schedule: () => void;
31
39
  /** True when a render would currently be deferred (text input focused or blockWhile()). */
32
- isEditing(): boolean;
40
+ isEditing: () => boolean;
33
41
  /** Attach focusin/focusout listeners to the panel root. */
34
- bindFocus(): void;
42
+ bindFocus: () => void;
35
43
  /** Detach listeners and cancel any pending frame. */
36
- unbind(): void;
44
+ unbind: () => void;
37
45
  /** Render immediately, bypassing the rAF coalescing (still respects the focus guard). */
38
- flushNow(): void;
46
+ flushNow: () => void;
39
47
  }
40
48
 
41
49
  /**
@@ -67,7 +75,9 @@ export function createPanelScheduler(opts: {
67
75
  function flush() {
68
76
  scheduled = false;
69
77
  rafId = 0;
70
- if (rendering) return;
78
+ if (rendering) {
79
+ return;
80
+ }
71
81
  // Defer while a field is focused — flushed later by focusout (or the next schedule()).
72
82
  if (blocked()) {
73
83
  pending = true;
@@ -83,7 +93,9 @@ export function createPanelScheduler(opts: {
83
93
  }
84
94
 
85
95
  function schedule() {
86
- if (scheduled) return;
96
+ if (scheduled) {
97
+ return;
98
+ }
87
99
  scheduled = true;
88
100
  rafId = requestAnimationFrame(flush);
89
101
  }
@@ -94,33 +106,39 @@ export function createPanelScheduler(opts: {
94
106
 
95
107
  function onFocusOut() {
96
108
  // Clear the editing flag and schedule a flush. If focus is merely moving to another field in
97
- // the same panel, the synchronous focusin that follows re-sets `editing` before the rAF runs,
98
- // so the deferred render keeps waiting (no mid-edit clobber).
109
+ // The same panel, the synchronous focusin that follows re-sets `editing` before the rAF runs,
110
+ // So the deferred render keeps waiting (no mid-edit clobber).
99
111
  editing = false;
100
- if (pending) schedule();
112
+ if (pending) {
113
+ schedule();
114
+ }
101
115
  }
102
116
 
103
117
  return {
104
- schedule,
105
- isEditing: () => blocked(),
106
118
  bindFocus() {
107
119
  root.addEventListener("focusin", onFocusIn);
108
120
  root.addEventListener("focusout", onFocusOut);
109
121
  },
122
+ flushNow() {
123
+ if (rafId) {
124
+ cancelAnimationFrame(rafId);
125
+ }
126
+ rafId = 0;
127
+ scheduled = false;
128
+ flush();
129
+ },
130
+ isEditing: () => blocked(),
131
+ schedule,
110
132
  unbind() {
111
133
  root.removeEventListener("focusin", onFocusIn);
112
134
  root.removeEventListener("focusout", onFocusOut);
113
- if (rafId) cancelAnimationFrame(rafId);
135
+ if (rafId) {
136
+ cancelAnimationFrame(rafId);
137
+ }
114
138
  scheduled = false;
115
139
  pending = false;
116
140
  editing = false;
117
141
  rendering = false;
118
142
  },
119
- flushNow() {
120
- if (rafId) cancelAnimationFrame(rafId);
121
- rafId = 0;
122
- scheduled = false;
123
- flush();
124
- },
125
143
  };
126
144
  }