@jxsuite/studio 0.11.0 → 0.14.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 (55) hide show
  1. package/dist/studio.js +6517 -5870
  2. package/dist/studio.js.map +57 -51
  3. package/package.json +5 -3
  4. package/src/canvas/canvas-diff.js +184 -0
  5. package/src/canvas/canvas-helpers.js +10 -14
  6. package/src/canvas/canvas-live-render.js +28 -2
  7. package/src/canvas/canvas-render.js +170 -20
  8. package/src/canvas/canvas-utils.js +22 -25
  9. package/src/editor/component-inline-edit.js +55 -44
  10. package/src/editor/content-inline-edit.js +47 -50
  11. package/src/editor/context-menu.js +78 -53
  12. package/src/editor/convert-to-component.js +11 -14
  13. package/src/editor/insertion-helper.js +8 -11
  14. package/src/editor/shortcuts.js +88 -64
  15. package/src/files/components.js +15 -4
  16. package/src/files/file-ops.js +57 -108
  17. package/src/files/files.js +81 -28
  18. package/src/panels/activity-bar.js +31 -7
  19. package/src/panels/block-action-bar.js +104 -80
  20. package/src/panels/canvas-dnd.js +4 -10
  21. package/src/panels/dnd.js +77 -35
  22. package/src/panels/editors.js +17 -26
  23. package/src/panels/elements-panel.js +17 -11
  24. package/src/panels/events-panel.js +44 -39
  25. package/src/panels/git-panel.js +47 -4
  26. package/src/panels/layers-panel.js +25 -21
  27. package/src/panels/left-panel.js +109 -44
  28. package/src/panels/overlays.js +91 -41
  29. package/src/panels/panel-events.js +28 -37
  30. package/src/panels/preview-render.js +7 -3
  31. package/src/panels/properties-panel.js +179 -104
  32. package/src/panels/pseudo-preview.js +9 -8
  33. package/src/panels/right-panel.js +85 -37
  34. package/src/panels/shared.js +0 -22
  35. package/src/panels/signals-panel.js +125 -54
  36. package/src/panels/statusbar.js +26 -19
  37. package/src/panels/style-inputs.js +5 -4
  38. package/src/panels/style-panel.js +128 -105
  39. package/src/panels/style-utils.js +8 -6
  40. package/src/panels/stylebook-layers-panel.js +5 -5
  41. package/src/panels/stylebook-panel.js +27 -31
  42. package/src/panels/tab-strip.js +124 -0
  43. package/src/panels/toolbar.js +40 -10
  44. package/src/reactivity.js +28 -0
  45. package/src/settings/content-types-editor.js +56 -7
  46. package/src/settings/defs-editor.js +56 -7
  47. package/src/settings/schema-field-ui.js +60 -25
  48. package/src/state.js +2 -459
  49. package/src/store.js +61 -219
  50. package/src/studio.js +163 -300
  51. package/src/tabs/tab.js +168 -0
  52. package/src/tabs/transact.js +406 -0
  53. package/src/ui/color-selector.js +4 -5
  54. package/src/view.js +3 -0
  55. package/src/workspace/workspace.js +90 -0
@@ -8,8 +8,8 @@ import { ref } from "lit-html/directives/ref.js";
8
8
  import { styleMap } from "lit-html/directives/style-map.js";
9
9
  import { ifDefined } from "lit-html/directives/if-defined.js";
10
10
 
11
- import { getState, renderOnly, updateUi, canvasWrap, canvasPanels } from "../store.js";
12
- import { ensureLitState } from "../panels/shared.js";
11
+ import { renderOnly, updateUi, canvasWrap, canvasPanels } from "../store.js";
12
+ import { activeTab } from "../workspace/workspace.js";
13
13
  import { view } from "../view.js";
14
14
 
15
15
  /** @type {any} */
@@ -44,14 +44,14 @@ export function initCanvasUtils(ctx) {
44
44
  export function canvasPanelTemplate(mediaName, label, fullWidth, width = null) {
45
45
  /**
46
46
  * @type {{
47
- * mediaName: any;
48
- * element: Element | null;
49
- * canvas: Element | null;
50
- * overlay: Element | null;
51
- * overlayClk: Element | null;
52
- * viewport: Element | null;
53
- * dropLine: Element | null;
54
- * _width: any;
47
+ * mediaName: string;
48
+ * element: HTMLElement | null;
49
+ * canvas: HTMLElement | null;
50
+ * overlay: HTMLElement | null;
51
+ * overlayClk: HTMLElement | null;
52
+ * viewport: HTMLElement | null;
53
+ * dropLine: HTMLElement | null;
54
+ * _width: number | null;
55
55
  * }}
56
56
  */
57
57
  const panel = {
@@ -69,7 +69,7 @@ export function canvasPanelTemplate(mediaName, label, fullWidth, width = null) {
69
69
  class=${`canvas-panel${fullWidth ? " full-width" : ""}`}
70
70
  data-media=${ifDefined(mediaName !== null ? mediaName : undefined)}
71
71
  ${ref((el) => {
72
- if (el) panel.element = el;
72
+ if (el) panel.element = /** @type {HTMLElement} */ (el);
73
73
  })}
74
74
  >
75
75
  ${label
@@ -88,34 +88,34 @@ export function canvasPanelTemplate(mediaName, label, fullWidth, width = null) {
88
88
  class="canvas-panel-viewport"
89
89
  style=${styleMap({ width: width && !fullWidth ? `${width}px` : "" })}
90
90
  ${ref((el) => {
91
- if (el) panel.viewport = el;
91
+ if (el) panel.viewport = /** @type {HTMLElement} */ (el);
92
92
  })}
93
93
  >
94
94
  <div
95
95
  class="canvas-panel-canvas"
96
96
  style=${styleMap({ width: width ? `${width}px` : "" })}
97
97
  ${ref((el) => {
98
- if (el) panel.canvas = el;
98
+ if (el) panel.canvas = /** @type {HTMLElement} */ (el);
99
99
  })}
100
100
  ></div>
101
101
  <div
102
102
  class="canvas-panel-overlay"
103
103
  ${ref((el) => {
104
- if (el) panel.overlay = el;
104
+ if (el) panel.overlay = /** @type {HTMLElement} */ (el);
105
105
  })}
106
106
  >
107
107
  <div
108
108
  class="canvas-drop-indicator"
109
109
  style="display:none"
110
110
  ${ref((el) => {
111
- if (el) panel.dropLine = el;
111
+ if (el) panel.dropLine = /** @type {HTMLElement} */ (el);
112
112
  })}
113
113
  ></div>
114
114
  </div>
115
115
  <div
116
116
  class="canvas-panel-click"
117
117
  ${ref((el) => {
118
- if (el) panel.overlayClk = el;
118
+ if (el) panel.overlayClk = /** @type {HTMLElement} */ (el);
119
119
  })}
120
120
  ></div>
121
121
  </div>
@@ -168,8 +168,7 @@ export function applyTransform() {
168
168
  if (!view.panzoomWrap) return;
169
169
  const zoom = _ctx.getZoom();
170
170
  view.panzoomWrap.style.transform = `translate(${view.panX}px, ${view.panY}px) scale(${zoom})`;
171
- const label = document.querySelector(".zoom-indicator-label");
172
- if (label) label.textContent = `${Math.round(zoom * 100)}%`;
171
+ renderZoomIndicator();
173
172
  renderOnly("overlays");
174
173
  if (_ctx.getCanvasMode() === "settings") _ctx.renderStylebookOverlays();
175
174
  }
@@ -207,7 +206,6 @@ export function fitToScreen() {
207
206
 
208
207
  /** Reset the zoom indicator (clear its content). Called when switching to non-panzoom modes. */
209
208
  export function resetZoomIndicator() {
210
- ensureLitState(zoomIndicatorHost);
211
209
  litRender(nothing, zoomIndicatorHost);
212
210
  }
213
211
 
@@ -220,7 +218,6 @@ export function renderZoomIndicator() {
220
218
  if (!zoomIndicatorHost.isConnected) {
221
219
  document.body.appendChild(zoomIndicatorHost);
222
220
  }
223
- ensureLitState(zoomIndicatorHost);
224
221
  litRender(
225
222
  html`
226
223
  <div class="zoom-indicator">
@@ -263,14 +260,14 @@ export function positionZoomIndicator() {
263
260
 
264
261
  /** Toggle "active" class on canvas panel headers based on activeMedia. */
265
262
  export function updateActivePanelHeaders() {
266
- const S = getState();
263
+ const activeMedia = activeTab.value?.session.ui.activeMedia ?? null;
267
264
  for (const p of canvasPanels) {
268
- const header = p.element.querySelector(".canvas-panel-header");
265
+ const header = p.element?.querySelector(".canvas-panel-header");
269
266
  if (header) {
270
267
  const isActive =
271
- (S.ui.activeMedia === null && p.mediaName === "base") ||
272
- (S.ui.activeMedia === null && p.mediaName === null) ||
273
- S.ui.activeMedia === p.mediaName;
268
+ (activeMedia === null && p.mediaName === "base") ||
269
+ (activeMedia === null && p.mediaName === null) ||
270
+ activeMedia === p.mediaName;
274
271
  header.classList.toggle("active", isActive);
275
272
  }
276
273
  }
@@ -4,20 +4,21 @@
4
4
  */
5
5
 
6
6
  import {
7
- getState,
8
- update,
9
7
  updateUi,
10
8
  renderOnly,
11
9
  getNodeAtPath,
12
- selectNode,
13
- removeNode,
14
- insertNode,
15
- updateProperty,
16
10
  parentElementPath,
17
11
  childIndex,
18
12
  canvasPanels,
19
13
  elToPath,
20
14
  } from "../store.js";
15
+ import { activeTab } from "../workspace/workspace.js";
16
+ import {
17
+ transactDoc,
18
+ mutateRemoveNode,
19
+ mutateInsertNode,
20
+ mutateUpdateProperty,
21
+ } from "../tabs/transact.js";
21
22
  import { view } from "../view.js";
22
23
  import { isSlashMenuOpen, showSlashMenu, dismissSlashMenu } from "./slash-menu.js";
23
24
  import { renderBlockActionBar } from "../panels/block-action-bar.js";
@@ -46,8 +47,7 @@ export function enterComponentInlineEdit(el, path) {
46
47
  return;
47
48
  }
48
49
 
49
- const S = getState();
50
- const node = getNodeAtPath(S.document, path);
50
+ const node = getNodeAtPath(activeTab.value?.doc.document, path);
51
51
  if (!node) return;
52
52
 
53
53
  const tc = node.textContent;
@@ -59,7 +59,9 @@ export function enterComponentInlineEdit(el, path) {
59
59
  if (voids.has(node.tagName)) return;
60
60
 
61
61
  for (const p of canvasPanels) {
62
- const boxes = p.overlay.querySelectorAll(".overlay-box");
62
+ const boxes = /** @type {NodeListOf<HTMLElement>} */ (
63
+ p.overlay.querySelectorAll(".overlay-box")
64
+ );
63
65
  for (const box of boxes) {
64
66
  box.style.border = "none";
65
67
  }
@@ -104,10 +106,11 @@ export function enterComponentInlineEdit(el, path) {
104
106
  if (view.blockActionBarEl && view.blockActionBarEl.contains(evt.target)) return;
105
107
  document.removeEventListener("mousedown", outsideHandler, true);
106
108
 
109
+ /** @type {(string | number)[] | null} */
107
110
  let hitPath = null,
108
111
  hitMedia = null;
109
112
  for (const p of canvasPanels) {
110
- const els = p.canvas.querySelectorAll("*");
113
+ const els = /** @type {NodeListOf<HTMLElement>} */ (p.canvas.querySelectorAll("*"));
111
114
  for (const el of els) el.style.pointerEvents = "auto";
112
115
  p.overlayClk.style.display = "none";
113
116
  const found = document.elementsFromPoint(evt.clientX, evt.clientY);
@@ -132,37 +135,44 @@ export function enterComponentInlineEdit(el, path) {
132
135
 
133
136
  const isEmpty = !newText;
134
137
  const pPath = parentElementPath(editPath);
135
- const S = getState();
136
138
 
137
139
  if (hitPath) {
140
+ let hp = hitPath;
138
141
  const media = hitMedia === "base" ? null : (hitMedia ?? null);
139
- updateUi("pendingInlineEdit", { path: hitPath, mediaName: hitMedia });
140
- const withMedia = { ...S, ui: { ...S.ui, activeMedia: media } };
142
+ updateUi("pendingInlineEdit", { path: hp, mediaName: hitMedia });
143
+ activeTab.value.session.ui.activeMedia = media;
141
144
  if (isEmpty && pPath) {
142
- let s = removeNode(withMedia, editPath);
143
- const removedIdx = /** @type {number} */ (childIndex(editPath));
144
- const hitIdx = /** @type {number} */ (childIndex(hitPath));
145
- const hitParent = parentElementPath(hitPath);
146
- if (hitParent && pPath && hitParent.join("/") === pPath.join("/") && hitIdx > removedIdx) {
147
- hitPath = [...pPath, "children", hitIdx - 1];
148
- updateUi("pendingInlineEdit", { path: hitPath, mediaName: hitMedia });
149
- }
150
- update(selectNode(s, hitPath));
145
+ transactDoc(activeTab.value, (t) => {
146
+ mutateRemoveNode(t, editPath);
147
+ const removedIdx = /** @type {number} */ (childIndex(editPath));
148
+ const hitIdx = /** @type {number} */ (childIndex(hp));
149
+ const hitParent = parentElementPath(hp);
150
+ if (
151
+ hitParent &&
152
+ pPath &&
153
+ hitParent.join("/") === pPath.join("/") &&
154
+ hitIdx > removedIdx
155
+ ) {
156
+ hp = [...pPath, "children", hitIdx - 1];
157
+ updateUi("pendingInlineEdit", { path: hp, mediaName: hitMedia });
158
+ }
159
+ t.session.selection = hp;
160
+ });
151
161
  } else if (newText !== originalText) {
152
- update(
153
- selectNode(
154
- updateProperty(withMedia, editPath, "textContent", newText || undefined),
155
- hitPath,
156
- ),
157
- );
162
+ transactDoc(activeTab.value, (t) => {
163
+ mutateUpdateProperty(t, editPath, "textContent", newText || undefined);
164
+ t.session.selection = hp;
165
+ });
158
166
  } else {
159
- update(selectNode(withMedia, hitPath));
167
+ activeTab.value.session.selection = hp;
160
168
  }
161
169
  } else {
162
170
  if (isEmpty && pPath) {
163
- update(removeNode(S, editPath));
171
+ transactDoc(activeTab.value, (t) => mutateRemoveNode(t, editPath));
164
172
  } else if (newText !== originalText) {
165
- update(updateProperty(S, editPath, "textContent", newText || undefined));
173
+ transactDoc(activeTab.value, (t) =>
174
+ mutateUpdateProperty(t, editPath, "textContent", newText || undefined),
175
+ );
166
176
  } else {
167
177
  renderOnly("canvas");
168
178
  renderOnly("overlays");
@@ -219,13 +229,13 @@ function splitParagraph() {
219
229
 
220
230
  cleanupComponentInlineEdit(el);
221
231
 
222
- const S = getState();
223
- let s = updateProperty(S, path, "textContent", textBefore || undefined);
224
- s = insertNode(s, pPath, idx + 1, newDef);
225
- s = selectNode(s, newPath);
232
+ transactDoc(activeTab.value, (t) => {
233
+ mutateUpdateProperty(t, path, "textContent", textBefore || undefined);
234
+ mutateInsertNode(t, pPath, idx + 1, newDef);
235
+ t.session.selection = newPath;
236
+ });
226
237
 
227
238
  updateUi("pendingInlineEdit", { path: newPath, mediaName });
228
- update(s);
229
239
  }
230
240
 
231
241
  function _commitComponentInlineEdit() {
@@ -235,12 +245,13 @@ function _commitComponentInlineEdit() {
235
245
 
236
246
  cleanupComponentInlineEdit(el);
237
247
 
238
- const S = getState();
239
248
  const pPath = parentElementPath(path);
240
249
  if (!newText && pPath) {
241
- update(removeNode(S, path));
250
+ transactDoc(activeTab.value, (t) => mutateRemoveNode(t, path));
242
251
  } else if (newText !== originalText) {
243
- update(updateProperty(S, path, "textContent", newText || undefined));
252
+ transactDoc(activeTab.value, (t) =>
253
+ mutateUpdateProperty(t, path, "textContent", newText || undefined),
254
+ );
244
255
  } else {
245
256
  renderOnly("canvas");
246
257
  renderOnly("overlays");
@@ -303,15 +314,15 @@ function handleComponentSlashSelect(cmd) {
303
314
 
304
315
  cleanupComponentInlineEdit(el);
305
316
 
306
- const S = getState();
307
317
  const newDef = defaultDef(cmd.tag);
308
318
  const newPath = [...pPath, "children", idx];
309
319
 
310
- let s = removeNode(S, path);
311
- s = insertNode(s, pPath, idx, newDef);
312
- s = selectNode(s, newPath);
320
+ transactDoc(activeTab.value, (t) => {
321
+ mutateRemoveNode(t, path);
322
+ mutateInsertNode(t, pPath, idx, newDef);
323
+ t.session.selection = newPath;
324
+ });
313
325
 
314
326
  const hasText = newDef.textContent != null;
315
327
  if (hasText) updateUi("pendingInlineEdit", { path: newPath, mediaName });
316
- update(s);
317
328
  }
@@ -4,17 +4,14 @@
4
4
  */
5
5
 
6
6
  import {
7
- getState,
8
- update,
9
7
  renderOnly,
10
- selectNode,
11
- insertNode,
12
- updateProperty,
13
8
  getNodeAtPath,
14
9
  parentElementPath,
15
10
  childIndex,
16
11
  canvasPanels,
17
12
  } from "../store.js";
13
+ import { activeTab } from "../workspace/workspace.js";
14
+ import { transactDoc, mutateInsertNode, mutateUpdateProperty } from "../tabs/transact.js";
18
15
  import { view } from "../view.js";
19
16
  import { startEditing, isEditableBlock } from "./inline-edit.js";
20
17
  import { restoreTemplateExpressions } from "../utils/edit-display.js";
@@ -46,32 +43,24 @@ export function enterInlineEdit(el, path) {
46
43
  /** @type {any} */ children,
47
44
  /** @type {any} */ textContent,
48
45
  ) {
49
- const S = getState();
50
- const node = getNodeAtPath(S.document, commitPath);
46
+ const node = getNodeAtPath(activeTab.value?.doc.document, commitPath);
51
47
  if (children) {
52
48
  if (node && JSON.stringify(node.children) === JSON.stringify(children)) return;
53
- let s = updateProperty(S, commitPath, "textContent", undefined);
54
- s = updateProperty(s, commitPath, "children", children);
55
- update(s);
49
+ transactDoc(activeTab.value, (t) => {
50
+ mutateUpdateProperty(t, commitPath, "textContent", undefined);
51
+ mutateUpdateProperty(t, commitPath, "children", children);
52
+ });
56
53
  } else if (textContent != null) {
57
54
  if (node && node.textContent === textContent && !node.children) return;
58
- let s = updateProperty(S, commitPath, "children", undefined);
59
- s = updateProperty(s, commitPath, "textContent", textContent);
60
- update(s);
55
+ transactDoc(activeTab.value, (t) => {
56
+ mutateUpdateProperty(t, commitPath, "children", undefined);
57
+ mutateUpdateProperty(t, commitPath, "textContent", textContent);
58
+ });
61
59
  }
62
60
  },
63
61
 
64
62
  onSplit(/** @type {any} */ splitPath, /** @type {any} */ before, /** @type {any} */ after) {
65
63
  const tag = "p";
66
- let s = getState();
67
-
68
- if (before.textContent != null) {
69
- s = updateProperty(s, splitPath, "children", undefined);
70
- s = updateProperty(s, splitPath, "textContent", before.textContent);
71
- } else if (before.children) {
72
- s = updateProperty(s, splitPath, "textContent", undefined);
73
- s = updateProperty(s, splitPath, "children", before.children);
74
- }
75
64
 
76
65
  // Insert new element after with "after" content
77
66
  const parentPath = /** @type {any} */ (parentElementPath(splitPath));
@@ -86,10 +75,19 @@ export function enterInlineEdit(el, path) {
86
75
  newNode.textContent = "";
87
76
  }
88
77
 
89
- s = insertNode(s, parentPath, idx + 1, newNode);
90
78
  const newPath = [...parentPath, "children", idx + 1];
91
- s = selectNode(s, newPath);
92
- update(s);
79
+
80
+ transactDoc(activeTab.value, (t) => {
81
+ if (before.textContent != null) {
82
+ mutateUpdateProperty(t, splitPath, "children", undefined);
83
+ mutateUpdateProperty(t, splitPath, "textContent", before.textContent);
84
+ } else if (before.children) {
85
+ mutateUpdateProperty(t, splitPath, "textContent", undefined);
86
+ mutateUpdateProperty(t, splitPath, "children", before.children);
87
+ }
88
+ mutateInsertNode(t, parentPath, idx + 1, newNode);
89
+ t.session.selection = newPath;
90
+ });
93
91
 
94
92
  // Re-enter editing on the new element after render
95
93
  requestAnimationFrame(() => {
@@ -125,17 +123,17 @@ export function enterInlineEdit(el, path) {
125
123
 
126
124
  // If the element is empty, swap its tagName instead of inserting after
127
125
  if (isEmpty) {
128
- let s = getState();
129
- s = updateProperty(s, afterPath, "tagName", cmd.tag);
130
- s = updateProperty(s, afterPath, "children", undefined);
131
- const def = defaultDef(cmd.tag);
132
- if (def.textContent && def.textContent !== "Paragraph text") {
133
- s = updateProperty(s, afterPath, "textContent", def.textContent);
134
- } else {
135
- s = updateProperty(s, afterPath, "textContent", undefined);
136
- }
137
- s = selectNode(s, afterPath);
138
- update(s);
126
+ transactDoc(activeTab.value, (t) => {
127
+ mutateUpdateProperty(t, afterPath, "tagName", cmd.tag);
128
+ mutateUpdateProperty(t, afterPath, "children", undefined);
129
+ const def = defaultDef(cmd.tag);
130
+ if (def.textContent && def.textContent !== "Paragraph text") {
131
+ mutateUpdateProperty(t, afterPath, "textContent", def.textContent);
132
+ } else {
133
+ mutateUpdateProperty(t, afterPath, "textContent", undefined);
134
+ }
135
+ t.session.selection = afterPath;
136
+ });
139
137
 
140
138
  requestAnimationFrame(() => {
141
139
  const activePanel = getActivePanel();
@@ -152,23 +150,22 @@ export function enterInlineEdit(el, path) {
152
150
  const elementDef = defaultDef(cmd.tag);
153
151
  const parentPath = /** @type {any} */ (parentElementPath(afterPath));
154
152
  const idx = /** @type {number} */ (childIndex(afterPath));
153
+ const newPath = [...parentPath, "children", idx + 1];
155
154
 
156
155
  // Apply pending commit from inline edit first (batched to avoid double render)
157
- let s = getState();
158
- if (commitData) {
159
- if (commitData.children) {
160
- s = updateProperty(s, afterPath, "textContent", undefined);
161
- s = updateProperty(s, afterPath, "children", commitData.children);
162
- } else if (commitData.textContent != null) {
163
- s = updateProperty(s, afterPath, "children", undefined);
164
- s = updateProperty(s, afterPath, "textContent", commitData.textContent);
156
+ transactDoc(activeTab.value, (t) => {
157
+ if (commitData) {
158
+ if (commitData.children) {
159
+ mutateUpdateProperty(t, afterPath, "textContent", undefined);
160
+ mutateUpdateProperty(t, afterPath, "children", commitData.children);
161
+ } else if (commitData.textContent != null) {
162
+ mutateUpdateProperty(t, afterPath, "children", undefined);
163
+ mutateUpdateProperty(t, afterPath, "textContent", commitData.textContent);
164
+ }
165
165
  }
166
- }
167
-
168
- s = insertNode(s, parentPath, idx + 1, structuredClone(elementDef));
169
- const newPath = [...parentPath, "children", idx + 1];
170
- s = selectNode(s, newPath);
171
- update(s);
166
+ mutateInsertNode(t, parentPath, idx + 1, structuredClone(elementDef));
167
+ t.session.selection = newPath;
168
+ });
172
169
 
173
170
  // If the inserted element is editable, enter editing
174
171
  requestAnimationFrame(() => {