@jxsuite/studio 0.10.2 → 0.13.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 (53) hide show
  1. package/README.md +82 -0
  2. package/dist/studio.js +5114 -3424
  3. package/dist/studio.js.map +65 -49
  4. package/package.json +6 -3
  5. package/src/browse/browse.js +27 -3
  6. package/src/canvas/canvas-diff.js +184 -0
  7. package/src/canvas/canvas-helpers.js +10 -14
  8. package/src/canvas/canvas-live-render.js +136 -17
  9. package/src/canvas/canvas-render.js +154 -21
  10. package/src/canvas/canvas-utils.js +21 -23
  11. package/src/editor/component-inline-edit.js +54 -41
  12. package/src/editor/content-inline-edit.js +46 -47
  13. package/src/editor/context-menu.js +63 -39
  14. package/src/editor/convert-to-component.js +11 -14
  15. package/src/editor/insertion-helper.js +8 -10
  16. package/src/editor/shortcuts.js +69 -39
  17. package/src/files/components.js +15 -4
  18. package/src/files/files.js +72 -24
  19. package/src/panels/activity-bar.js +29 -7
  20. package/src/panels/block-action-bar.js +104 -80
  21. package/src/panels/canvas-dnd.js +132 -50
  22. package/src/panels/dnd.js +32 -28
  23. package/src/panels/editors.js +7 -14
  24. package/src/panels/elements-panel.js +9 -3
  25. package/src/panels/events-panel.js +44 -39
  26. package/src/panels/git-panel.js +45 -3
  27. package/src/panels/layers-panel.js +16 -11
  28. package/src/panels/left-panel.js +108 -43
  29. package/src/panels/overlays.js +97 -35
  30. package/src/panels/panel-events.js +18 -11
  31. package/src/panels/properties-panel.js +467 -98
  32. package/src/panels/right-panel.js +85 -37
  33. package/src/panels/shared.js +0 -22
  34. package/src/panels/signals-panel.js +125 -54
  35. package/src/panels/statusbar.js +23 -8
  36. package/src/panels/style-inputs.js +4 -2
  37. package/src/panels/style-panel.js +128 -105
  38. package/src/panels/stylebook-panel.js +42 -17
  39. package/src/panels/tab-strip.js +124 -0
  40. package/src/panels/toolbar.js +39 -9
  41. package/src/reactivity.js +28 -0
  42. package/src/settings/content-types-editor.js +78 -8
  43. package/src/settings/defs-editor.js +56 -7
  44. package/src/settings/schema-field-ui.js +99 -11
  45. package/src/site-context.js +105 -0
  46. package/src/state.js +0 -456
  47. package/src/store.js +105 -124
  48. package/src/studio.js +112 -121
  49. package/src/tabs/tab.js +153 -0
  50. package/src/tabs/transact.js +406 -0
  51. package/src/ui/spectrum.js +2 -0
  52. package/src/view.js +3 -0
  53. package/src/workspace/workspace.js +89 -0
@@ -7,40 +7,94 @@
7
7
  */
8
8
 
9
9
  import { html, render as litRender, nothing } from "lit-html";
10
- import {
11
- getState,
12
- leftPanel,
13
- updateSession,
14
- update,
15
- applyMutation,
16
- updateFrontmatter,
17
- } from "../store.js";
18
- import { ensureLitState } from "./shared.js";
10
+ import { leftPanel, updateSession } from "../store.js";
11
+ import { effect, effectScope } from "../reactivity.js";
12
+ import { activeTab } from "../workspace/workspace.js";
13
+ import { transact, mutateUpdateFrontmatter } from "../tabs/transact.js";
14
+
19
15
  import { renderLayersTemplate } from "./layers-panel.js";
20
16
  import { renderStylebookLayersTemplate } from "./stylebook-layers-panel.js";
21
17
  import { renderElementsTemplate } from "./elements-panel.js";
22
18
  import { selectStylebookTag, stylebookMeta } from "./stylebook-panel.js";
23
19
 
24
- /** @type {any} */
20
+ /** @typedef {import("lit-html").TemplateResult} TemplateResult */
21
+
22
+ /**
23
+ * @typedef {{
24
+ * getCanvasMode: () => string;
25
+ * setCanvasMode: (mode: string) => void;
26
+ * renderImportsTemplate: (...args: any[]) => TemplateResult;
27
+ * renderFilesTemplate: () => TemplateResult;
28
+ * renderSignalsTemplate: (...args: any[]) => TemplateResult;
29
+ * renderDataExplorerTemplate: (...args: any[]) => TemplateResult;
30
+ * renderHeadTemplate: (...args: any[]) => TemplateResult;
31
+ * renderGitPanel: (...args: any[]) => TemplateResult;
32
+ * renderCanvas: () => void;
33
+ * defCategory: (tag: string) => string;
34
+ * defBadgeLabel: (tag: string) => string;
35
+ * navigateToComponent: (path: string) => void;
36
+ * webdata: object;
37
+ * defaultDef: (tag: string) => object;
38
+ * registerLayersDnD: () => void;
39
+ * registerElementsDnD: () => void;
40
+ * registerComponentsDnD: () => void;
41
+ * setupTreeKeyboard: (tree: HTMLElement) => void;
42
+ * }} LeftPanelCtx
43
+ */
44
+
45
+ /** @type {LeftPanelCtx | null} */
25
46
  let _ctx = null;
26
47
 
48
+ /** @type {import("@vue/reactivity").EffectScope | null} */
49
+ let _scope = null;
50
+
51
+ let _rendering = false;
52
+ let _scheduled = false;
53
+
27
54
  /**
28
55
  * Mount the left panel orchestrator.
29
56
  *
30
- * @param {any} ctx — callbacks and references that avoid circular dependencies
57
+ * @param {LeftPanelCtx} ctx
31
58
  */
32
59
  export function mount(ctx) {
33
60
  _ctx = ctx;
61
+ _scope = effectScope();
62
+ _scope.run(() => {
63
+ effect(() => {
64
+ const tab = activeTab.value;
65
+ if (!tab) return;
66
+ // Track properties the left panel reads
67
+ void tab.doc.document;
68
+ void tab.doc.mode;
69
+ void tab.session.selection;
70
+ void tab.session.ui.leftTab;
71
+ void tab.session.ui.settingsTab;
72
+ render();
73
+ });
74
+ });
34
75
  }
35
76
 
36
77
  export function unmount() {
78
+ _scope?.stop();
79
+ _scope = null;
37
80
  _ctx = null;
38
81
  }
39
82
 
40
83
  export function render() {
41
84
  if (!_ctx) return;
85
+ if (_rendering) return;
86
+ if (!_scheduled) {
87
+ _scheduled = true;
88
+ queueMicrotask(_flush);
89
+ }
90
+ }
91
+
92
+ function _flush() {
93
+ _scheduled = false;
94
+ if (!_ctx) return;
95
+ if (_rendering) return;
96
+ _rendering = true;
42
97
  try {
43
- ensureLitState(leftPanel);
44
98
  _render();
45
99
  } catch (e) {
46
100
  console.error("left-panel render error:", e);
@@ -52,90 +106,101 @@ export function render() {
52
106
  } catch (e2) {
53
107
  console.error("left-panel retry failed:", e2);
54
108
  }
109
+ } finally {
110
+ _rendering = false;
55
111
  }
56
112
  }
57
113
 
58
114
  function _render() {
59
- const S = getState();
115
+ const ctx = /** @type {LeftPanelCtx} */ (_ctx);
116
+ const aTab = activeTab.value;
117
+ if (!aTab) return;
118
+ const S = /** @type {any} */ ({
119
+ ui: aTab.session.ui,
120
+ document: aTab.doc.document,
121
+ mode: aTab.doc.mode,
122
+ selection: aTab.session.selection,
123
+ });
60
124
  const tab = S.ui.leftTab;
61
125
 
62
- /** @type {any} */
126
+ /** @type {TemplateResult | typeof nothing} */
63
127
  let content;
64
128
  if (tab === "layers")
65
129
  content =
66
- _ctx.getCanvasMode() === "settings"
130
+ ctx.getCanvasMode() === "settings"
67
131
  ? renderStylebookLayersTemplate({
68
132
  selectStylebookTag,
69
133
  stylebookMeta,
70
134
  })
71
135
  : renderLayersTemplate({
72
- navigateToComponent: _ctx.navigateToComponent,
136
+ navigateToComponent: ctx.navigateToComponent,
73
137
  rerender: render,
74
138
  });
75
139
  else if (tab === "imports")
76
- content = _ctx.renderImportsTemplate({
140
+ content = ctx.renderImportsTemplate({
77
141
  renderLeftPanel: render,
78
142
  documentPath: S.documentPath,
79
143
  documentElements: S.document.$elements || [],
80
- applyMutation: (/** @type {any} */ fn) => {
81
- update(applyMutation(getState(), fn));
144
+ applyMutation: (/** @type {(doc: object) => void} */ fn) => {
145
+ transact(activeTab.value, fn);
82
146
  },
83
147
  });
84
- else if (tab === "files") content = _ctx.renderFilesTemplate();
148
+ else if (tab === "files") content = ctx.renderFilesTemplate();
85
149
  else if (tab === "blocks")
86
150
  content = renderElementsTemplate({
87
- webdata: _ctx.webdata,
88
- defaultDef: _ctx.defaultDef,
151
+ webdata: ctx.webdata,
152
+ defaultDef: ctx.defaultDef,
89
153
  rerender: render,
90
154
  });
91
155
  else if (tab === "state")
92
- content = _ctx.renderSignalsTemplate(S, {
156
+ content = ctx.renderSignalsTemplate(S, {
93
157
  renderLeftPanel: render,
94
- renderCanvas: _ctx.renderCanvas,
158
+ renderCanvas: ctx.renderCanvas,
95
159
  updateSession,
96
160
  });
97
161
  else if (tab === "data")
98
- content = _ctx.renderDataExplorerTemplate(S.document.state, S.canvas?.scope ?? null, {
99
- renderCanvas: _ctx.renderCanvas,
162
+ content = ctx.renderDataExplorerTemplate(S.document.state, S.canvas?.scope ?? null, {
163
+ renderCanvas: ctx.renderCanvas,
100
164
  renderLeftPanel: render,
101
- defCategory: _ctx.defCategory,
102
- defBadgeLabel: _ctx.defBadgeLabel,
165
+ defCategory: ctx.defCategory,
166
+ defBadgeLabel: ctx.defBadgeLabel,
103
167
  });
104
168
  else if (tab === "head") {
105
169
  const isContent = S.mode === "content";
106
170
  const fm = S.content?.frontmatter ?? {};
107
171
  const headDoc = isContent ? { ...S.document, title: fm.title, $head: fm.$head } : S.document;
108
- content = _ctx.renderHeadTemplate({
172
+ content = ctx.renderHeadTemplate({
109
173
  document: headDoc,
110
174
  applyMutation: isContent
111
- ? (/** @type {any} */ fn) => {
175
+ ? (/** @type {(doc: object) => void} */ fn) => {
176
+ const tab = activeTab.value;
177
+ const fm = /** @type {Record<string, any>} */ (tab.doc.content?.frontmatter ?? {});
112
178
  const tmp = { title: fm.title, $head: fm.$head ? [...fm.$head] : undefined };
113
179
  fn(tmp);
114
- let s = getState();
115
- if (tmp.title !== fm.title) s = updateFrontmatter(s, "title", tmp.title);
180
+ if (tmp.title !== fm.title)
181
+ mutateUpdateFrontmatter(tab, "title", /** @type {any} */ (tmp.title));
116
182
  const newHead = tmp.$head && tmp.$head.length > 0 ? tmp.$head : undefined;
117
- s = updateFrontmatter(s, "$head", newHead);
118
- update(s);
183
+ mutateUpdateFrontmatter(tab, "$head", newHead);
119
184
  }
120
- : (/** @type {any} */ fn) => {
121
- update(applyMutation(getState(), fn));
185
+ : (/** @type {(doc: object) => void} */ fn) => {
186
+ transact(activeTab.value, fn);
122
187
  },
123
188
  renderLeftPanel: render,
124
189
  });
125
- } else if (tab === "git") content = _ctx.renderGitPanel(S);
190
+ } else if (tab === "git") content = ctx.renderGitPanel(S, ctx);
126
191
  else content = nothing;
127
192
 
128
- litRender(html`<div class="panel-body">${content}</div>`, /** @type {any} */ (leftPanel));
193
+ litRender(html`<div class="panel-body">${content}</div>`, leftPanel);
129
194
 
130
195
  // Post-render side effects
131
- if (tab === "layers" && _ctx.getCanvasMode() !== "settings") _ctx.registerLayersDnD();
196
+ if (tab === "layers" && ctx.getCanvasMode() !== "settings") ctx.registerLayersDnD();
132
197
  else if (tab === "imports") {
133
198
  /* no post-render DnD needed */
134
199
  } else if (tab === "blocks") {
135
- _ctx.registerElementsDnD();
136
- _ctx.registerComponentsDnD();
200
+ ctx.registerElementsDnD();
201
+ ctx.registerComponentsDnD();
137
202
  } else if (tab === "files") {
138
- const tree = /** @type {any} */ (leftPanel)?.querySelector(".file-tree");
139
- if (tree) _ctx.setupTreeKeyboard(tree);
203
+ const tree = /** @type {HTMLElement | null} */ (leftPanel.querySelector(".file-tree"));
204
+ if (tree) ctx.setupTreeKeyboard(tree);
140
205
  }
141
206
  }
@@ -4,41 +4,83 @@
4
4
  */
5
5
 
6
6
  import { html, render as litRender, nothing } from "lit-html";
7
- import { getState, canvasPanels, pathsEqual, subscribe } from "../store.js";
7
+ import { canvasPanels, pathsEqual } from "../store.js";
8
+ import { effect, effectScope } from "../reactivity.js";
9
+ import { activeTab } from "../workspace/workspace.js";
8
10
  import { view } from "../view.js";
9
- import {
10
- findCanvasElement,
11
- getActivePanel,
12
- overlayBoxDescriptor,
13
- } from "../canvas/canvas-helpers.js";
11
+ import { findCanvasElement, getActivePanel, effectiveZoom } from "../canvas/canvas-helpers.js";
12
+ import { layoutElements } from "../canvas/canvas-live-render.js";
14
13
 
15
- /** @type {any} */
14
+ /**
15
+ * @typedef {{
16
+ * cls: string;
17
+ * top: string;
18
+ * left: string;
19
+ * width: string;
20
+ * height: string;
21
+ * border?: string;
22
+ * isLayout?: boolean;
23
+ * }} OverlayBox
24
+ */
25
+
26
+ /**
27
+ * @typedef {{
28
+ * getCanvasMode: () => string;
29
+ * isEditing: () => boolean;
30
+ * renderBlockActionBar: () => void;
31
+ * }} OverlaysCtx
32
+ */
33
+
34
+ /** @type {OverlaysCtx | null} */
16
35
  let _ctx = null;
17
36
 
18
- /** @type {(() => void) | null} */
19
- let _unsub = null;
37
+ /** @type {import("@vue/reactivity").EffectScope | null} */
38
+ let _scope = null;
39
+
40
+ let _scheduled = false;
20
41
 
21
42
  /**
22
43
  * Mount the overlays panel.
23
44
  *
24
- * @param {any} ctx — { getCanvasMode, isEditing, renderBlockActionBar }
45
+ * @param {OverlaysCtx} ctx
25
46
  */
26
47
  export function mount(ctx) {
27
48
  _ctx = ctx;
28
- _unsub = subscribe((change) => {
29
- if (change.selection || change.hover || change.mode || change.ui || change.doc) render();
49
+ _scope = effectScope();
50
+ _scope.run(() => {
51
+ effect(() => {
52
+ const tab = activeTab.value;
53
+ if (!tab) return;
54
+ // Track selection, hover, and mode
55
+ void tab.session.selection;
56
+ void tab.session.hover;
57
+ void tab.doc.mode;
58
+ render();
59
+ });
30
60
  });
31
61
  }
32
62
 
33
63
  export function unmount() {
34
- _unsub?.();
35
- _unsub = null;
64
+ _scope?.stop();
65
+ _scope = null;
36
66
  _ctx = null;
37
67
  }
38
68
 
39
69
  export function render() {
40
70
  if (!_ctx) return;
41
- const S = getState();
71
+ if (!_scheduled) {
72
+ _scheduled = true;
73
+ queueMicrotask(_flush);
74
+ }
75
+ }
76
+
77
+ function _flush() {
78
+ _scheduled = false;
79
+ if (!_ctx) return;
80
+ const tab = activeTab.value;
81
+ if (!tab) return;
82
+ const { selection, hover } = tab.session;
83
+ const { stylebookTab } = tab.session.ui;
42
84
  const canvasMode = _ctx.getCanvasMode();
43
85
 
44
86
  if (canvasMode !== "design" && canvasMode !== "edit" && canvasMode !== "settings") {
@@ -54,7 +96,7 @@ export function render() {
54
96
  }
55
97
 
56
98
  if (canvasMode === "settings") {
57
- const enable = S.ui.stylebookTab === "elements";
99
+ const enable = stylebookTab === "elements";
58
100
  for (const p of canvasPanels) {
59
101
  p.overlayClk.style.pointerEvents = enable ? "" : "none";
60
102
  }
@@ -71,28 +113,46 @@ export function render() {
71
113
  }
72
114
 
73
115
  for (const p of canvasPanels) {
74
- /**
75
- * @type {{
76
- * cls: string;
77
- * top: string;
78
- * left: string;
79
- * width: string;
80
- * height: string;
81
- * border?: string;
82
- * }[]}
83
- */
116
+ /** @type {OverlayBox[]} */
84
117
  const boxes = [];
85
118
 
86
- if (S.hover && !pathsEqual(S.hover, S.selection)) {
87
- const el = findCanvasElement(S.hover, p.canvas);
88
- if (el) boxes.push(overlayBoxDescriptor(el, "hover", p));
119
+ // Batch layout reads: read viewport geometry once per panel
120
+ const vpRect = p.viewport.getBoundingClientRect();
121
+ const scrollTop = p.viewport.scrollTop;
122
+ const scrollLeft = p.viewport.scrollLeft;
123
+ const scale = effectiveZoom();
124
+
125
+ if (hover && !pathsEqual(hover, selection)) {
126
+ const el = findCanvasElement(hover, p.canvas);
127
+ if (el) {
128
+ const elRect = el.getBoundingClientRect();
129
+ /** @type {OverlayBox} */
130
+ const desc = {
131
+ cls: "overlay-box overlay-hover",
132
+ top: `${(elRect.top - vpRect.top + scrollTop) / scale}px`,
133
+ left: `${(elRect.left - vpRect.left + scrollLeft) / scale}px`,
134
+ width: `${elRect.width / scale}px`,
135
+ height: `${elRect.height / scale}px`,
136
+ };
137
+ if (layoutElements.has(el)) desc.isLayout = true;
138
+ boxes.push(desc);
139
+ }
89
140
  }
90
141
 
91
- if (S.selection && p === getActivePanel()) {
92
- const el = findCanvasElement(S.selection, p.canvas);
142
+ if (selection && p === getActivePanel()) {
143
+ const el = findCanvasElement(selection, p.canvas);
93
144
  if (el) {
94
- const desc = overlayBoxDescriptor(el, "selection", p);
95
- if (view.componentInlineEdit || _ctx.isEditing()) /** @type {any} */ (desc).border = "none";
145
+ const elRect = el.getBoundingClientRect();
146
+ /** @type {OverlayBox} */
147
+ const desc = {
148
+ cls: "overlay-box overlay-selection",
149
+ top: `${(elRect.top - vpRect.top + scrollTop) / scale}px`,
150
+ left: `${(elRect.left - vpRect.left + scrollLeft) / scale}px`,
151
+ width: `${elRect.width / scale}px`,
152
+ height: `${elRect.height / scale}px`,
153
+ };
154
+ if (view.componentInlineEdit || _ctx.isEditing()) desc.border = "none";
155
+ if (layoutElements.has(el)) desc.isLayout = true;
96
156
  boxes.push(desc);
97
157
  }
98
158
  }
@@ -103,11 +163,13 @@ export function render() {
103
163
  ${boxes.map(
104
164
  (b) => html`
105
165
  <div
106
- class=${b.cls}
166
+ class="${b.cls}${b.isLayout ? " overlay-layout" : ""}"
107
167
  style="top:${b.top};left:${b.left};width:${b.width};height:${b.height}${b.border
108
168
  ? `;border:${b.border}`
109
169
  : ""}"
110
- ></div>
170
+ >
171
+ ${b.isLayout ? html`<span class="overlay-layout-badge">Layout</span>` : nothing}
172
+ </div>
111
173
  `,
112
174
  )}
113
175
  `,
@@ -5,24 +5,23 @@
5
5
  */
6
6
 
7
7
  import {
8
- update,
9
8
  updateUi,
10
- selectNode,
11
9
  hoverNode,
12
10
  elToPath,
13
11
  pathsEqual,
14
- insertNode,
15
12
  parentElementPath,
16
13
  childIndex,
17
14
  getNodeAtPath,
18
15
  renderOnly,
19
16
  } from "../store.js";
17
+ import { activeTab } from "../workspace/workspace.js";
20
18
  import { view } from "../view.js";
21
19
  import { stopEditing, isEditing, isEditableBlock } from "../editor/inline-edit.js";
22
20
  import { showContextMenu } from "../editor/context-menu.js";
23
21
  import * as insertionHelper from "../editor/insertion-helper.js";
24
22
  import { defaultDef } from "../panels/shared.js";
25
23
  import { bubbleInlinePath, findCanvasElement, effectiveZoom } from "../canvas/canvas-helpers.js";
24
+ import { layoutElements, activeLayoutPath } from "../canvas/canvas-live-render.js";
26
25
 
27
26
  /** @type {any} */
28
27
  let _ctx = null;
@@ -87,6 +86,15 @@ export function registerPanelEvents(panel) {
87
86
 
88
87
  for (const el of elements) {
89
88
  if (canvas.contains(el) && el !== canvas) {
89
+ // Layout element clicked — show layout info instead of selecting in page doc
90
+ if (layoutElements.has(el)) {
91
+ view.layoutSelection = { el, layoutPath: activeLayoutPath };
92
+ activeTab.value.session.selection = null;
93
+ renderOnly("rightPanel");
94
+ return;
95
+ }
96
+ view.layoutSelection = null;
97
+
90
98
  const originalPath = elToPath.get(el);
91
99
  if (originalPath) {
92
100
  let path = bubbleInlinePath(S.document, originalPath);
@@ -107,16 +115,18 @@ export function registerPanelEvents(panel) {
107
115
 
108
116
  if (canvasMode === "design" && S.mode !== "content") {
109
117
  updateUi("pendingInlineEdit", { path, mediaName });
110
- update(selectNode(withMedia, path));
118
+ activeTab.value.session.ui.activeMedia = newMedia;
119
+ activeTab.value.session.selection = path;
111
120
  return;
112
121
  }
113
122
 
114
- update(selectNode(withMedia, path));
123
+ activeTab.value.session.ui.activeMedia = newMedia;
124
+ activeTab.value.session.selection = path;
115
125
  return;
116
126
  }
117
127
  }
118
128
  }
119
- update(selectNode(S, null));
129
+ activeTab.value.session.selection = null;
120
130
  },
121
131
  opts,
122
132
  );
@@ -151,8 +161,8 @@ export function registerPanelEvents(panel) {
151
161
  const resolvedEl = path === originalPath ? el : findCanvasElement(path, canvas) || el;
152
162
  if (isEditableBlock(resolvedEl)) {
153
163
  const newMedia = mediaName === "base" ? null : (mediaName ?? null);
154
- const withMedia = { ...S, ui: { ...S.ui, activeMedia: newMedia } };
155
- update(selectNode(withMedia, path));
164
+ activeTab.value.session.ui.activeMedia = newMedia;
165
+ activeTab.value.session.selection = path;
156
166
  _ctx.enterInlineEdit(resolvedEl, path);
157
167
  return;
158
168
  }
@@ -243,13 +253,10 @@ export function registerPanelEvents(panel) {
243
253
 
244
254
  insertionHelper.mount({
245
255
  getState: _ctx.getState,
246
- update,
247
256
  getCanvasMode: _ctx.getCanvasMode,
248
257
  withPanelPointerEvents,
249
258
  effectiveZoom: effectiveZoom,
250
259
  defaultDef,
251
- insertNode,
252
- selectNode,
253
260
  parentElementPath,
254
261
  childIndex,
255
262
  getNodeAtPath,