@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
package/src/store.js CHANGED
@@ -8,35 +8,10 @@
8
8
 
9
9
  // ─── Re-exports from state.js ────────────────────────────────────────────────
10
10
 
11
+ import { activeTab } from "./workspace/workspace.js";
12
+
11
13
  export {
12
14
  createState,
13
- selectNode,
14
- hoverNode,
15
- undo,
16
- redo,
17
- insertNode,
18
- removeNode,
19
- duplicateNode,
20
- wrapNode,
21
- moveNode,
22
- updateProperty,
23
- updateStyle,
24
- updateAttribute,
25
- addDef,
26
- removeDef,
27
- updateDef,
28
- renameDef,
29
- updateMediaStyle,
30
- updateMedia,
31
- updateNestedStyle,
32
- updateMediaNestedStyle,
33
- pushDocument,
34
- popDocument,
35
- updateProp,
36
- addSwitchCase,
37
- removeSwitchCase,
38
- renameSwitchCase,
39
- applyMutation,
40
15
  getNodeAtPath,
41
16
  flattenTree,
42
17
  nodeLabel,
@@ -48,8 +23,6 @@ export {
48
23
  projectState,
49
24
  setProjectState,
50
25
  updateFrontmatter,
51
- toFlat,
52
- fromFlat,
53
26
  } from "./state.js";
54
27
 
55
28
  // ─── DOM shortcuts & element refs ────────────────────────────────────────────
@@ -57,12 +30,12 @@ export {
57
30
  export const $ = (/** @type {string} */ sel) => document.querySelector(sel);
58
31
  export const _$$ = (/** @type {string} */ sel) => document.querySelectorAll(sel);
59
32
 
60
- export const canvasWrap = /** @type {any} */ (document.querySelector("#canvas-wrap"));
61
- export const activityBar = /** @type {any} */ (document.querySelector("#activity-bar"));
62
- export const leftPanel = /** @type {any} */ (document.querySelector("#left-panel"));
63
- export const rightPanel = /** @type {any} */ (document.querySelector("#right-panel"));
64
- export const toolbarEl = /** @type {any} */ (document.querySelector("#toolbar"));
65
- export const statusbarEl = /** @type {any} */ (document.querySelector("#statusbar"));
33
+ export const canvasWrap = /** @type {HTMLElement} */ (document.querySelector("#canvas-wrap"));
34
+ export const activityBar = /** @type {HTMLElement} */ (document.querySelector("#activity-bar"));
35
+ export const leftPanel = /** @type {HTMLElement} */ (document.querySelector("#left-panel"));
36
+ export const rightPanel = /** @type {HTMLElement} */ (document.querySelector("#right-panel"));
37
+ export const toolbarEl = /** @type {HTMLElement} */ (document.querySelector("#toolbar"));
38
+ export const statusbarEl = /** @type {HTMLElement} */ (document.querySelector("#statusbar"));
66
39
 
67
40
  // ─── Shared containers (mutated in place by owner modules) ───────────────────
68
41
 
@@ -70,9 +43,19 @@ export const statusbarEl = /** @type {any} */ (document.querySelector("#statusba
70
43
  export const elToPath = new WeakMap();
71
44
 
72
45
  /**
73
- * Canvas panels: Array<{ mediaName, canvas, overlay, overlayClk, viewport, dropLine }>
46
+ * Canvas panels: Array<{ mediaName, canvas, overlay, overlayClk, viewport, dropLine, element,
47
+ * _width }>
74
48
  *
75
- * @type {any[]}
49
+ * @type {{
50
+ * mediaName: string;
51
+ * canvas: HTMLElement;
52
+ * overlay: HTMLElement;
53
+ * overlayClk: HTMLElement;
54
+ * viewport: HTMLElement;
55
+ * dropLine: HTMLElement;
56
+ * element?: HTMLElement | null;
57
+ * _width?: number | null;
58
+ * }[]}
76
59
  */
77
60
  export const canvasPanels = [];
78
61
 
@@ -110,7 +93,7 @@ export const COMMON_SELECTORS = [
110
93
  "::placeholder",
111
94
  ];
112
95
 
113
- /** @param {any} k */
96
+ /** @param {string} k */
114
97
  export function isNestedSelector(k) {
115
98
  return k.startsWith(":") || k.startsWith(".") || k.startsWith("&") || k.startsWith("[");
116
99
  }
@@ -120,12 +103,12 @@ export function isNestedSelector(k) {
120
103
  const _styleDebounceTimers = new Map();
121
104
 
122
105
  /**
123
- * @param {any} prop
124
- * @param {any} ms
125
- * @param {any} fn
106
+ * @param {string} prop
107
+ * @param {number} ms
108
+ * @param {Function} fn
126
109
  */
127
110
  export function debouncedStyleCommit(prop, ms, fn) {
128
- return (/** @type {any[]} */ ...args) => {
111
+ return (/** @type {unknown[]} */ ...args) => {
129
112
  clearTimeout(_styleDebounceTimers.get(prop));
130
113
  _styleDebounceTimers.set(
131
114
  prop,
@@ -146,13 +129,13 @@ export function cancelStyleDebounce(/** @type {string} */ prop) {
146
129
  /**
147
130
  * Strip all on* event handler properties from a Jx document tree (deep clone).
148
131
  *
149
- * @param {any} node
150
- * @returns {any}
132
+ * @param {import("./state.js").JxNode | unknown} node
133
+ * @returns {import("./state.js").JxNode | unknown}
151
134
  */
152
135
  export function stripEventHandlers(node) {
153
136
  if (!node || typeof node !== "object") return node;
154
137
  if (Array.isArray(node)) return node.map(stripEventHandlers);
155
- /** @type {Record<string, any>} */
138
+ /** @type {Record<string, unknown>} */
156
139
  const out = {};
157
140
  for (const [k, v] of Object.entries(node)) {
158
141
  if (k.startsWith("on") && typeof v === "object" && (v?.$ref || v?.$prototype === "Function"))
@@ -160,12 +143,12 @@ export function stripEventHandlers(node) {
160
143
  if (k === "children") {
161
144
  out.children = Array.isArray(v) ? v.map(stripEventHandlers) : stripEventHandlers(v);
162
145
  } else if (k === "cases" && typeof v === "object") {
163
- /** @type {Record<string, any>} */
146
+ /** @type {Record<string, unknown>} */
164
147
  const cases = {};
165
148
  for (const [ck, cv] of Object.entries(v)) cases[ck] = stripEventHandlers(cv);
166
149
  out.cases = cases;
167
150
  } else if (k === "state" && typeof v === "object" && v !== null) {
168
- /** @type {Record<string, any>} */
151
+ /** @type {Record<string, unknown>} */
169
152
  const state = {};
170
153
  for (const [sk, sv] of Object.entries(v)) {
171
154
  if (sv && typeof sv === "object" && sv.timing === "server") continue;
@@ -224,197 +207,56 @@ export function renderOnly(...names) {
224
207
  }
225
208
  }
226
209
 
227
- // ─── Update dispatch (late-bound) ────────────────────────────────────────────
228
- // studio.js registers the real update implementation via setUpdateFn() during bootstrap.
229
- // This allows extracted modules to import `update` from store.js without circular deps.
230
-
231
- /** @type {Function} */
232
- let _updateFn = () => {
233
- throw new Error("update() called before setUpdateFn() — bootstrap not complete");
234
- };
235
-
236
- /** @type {Function} */
237
- let _getStateFn = () => null;
210
+ // ─── Session dispatch ──────────────────────────────────────────────────────
238
211
 
239
212
  /**
240
- * Register the update implementation. Called by studio.js at module load time.
213
+ * Dispatch a session-only state update (selection, hover, ui). Writes directly to reactive tab.
241
214
  *
242
- * @param {Function} fn
243
- */
244
- export function setUpdateFn(fn) {
245
- _updateFn = fn;
246
- }
247
-
248
- /**
249
- * Register the state getter. Called by studio.js at module load time.
250
- *
251
- * @param {Function} fn — returns current S
252
- */
253
- export function setGetStateFn(fn) {
254
- _getStateFn = fn;
255
- }
256
-
257
- /**
258
- * Get the current state (live, not stale).
259
- *
260
- * @returns {any}
261
- */
262
- export function getState() {
263
- return _getStateFn();
264
- }
265
-
266
- /**
267
- * Dispatch a state update + selective re-render.
268
- *
269
- * @param {any} newState
270
- */
271
- export function update(newState) {
272
- _updateFn(newState);
273
- }
274
-
275
- // ─── Session dispatch (late-bound) ──────────────────────────────────────────
276
- // Lightweight dispatcher for session-only changes (selection, hover, ui).
277
- // Does NOT trigger autosave middleware or push history.
278
-
279
- /** @type {Function} */
280
- let _updateSessionFn = () => {
281
- throw new Error("updateSession() called before setUpdateSessionFn() — bootstrap not complete");
282
- };
283
-
284
- /** @type {Function} */
285
- let _getDocFn = () => null;
286
-
287
- /** @type {Function} */
288
- let _getSessionFn = () => null;
289
-
290
- /** @param {Function} fn */
291
- export function setUpdateSessionFn(fn) {
292
- _updateSessionFn = fn;
293
- }
294
-
295
- /** @param {Function} fn */
296
- export function setGetDocFn(fn) {
297
- _getDocFn = fn;
298
- }
299
-
300
- /** @param {Function} fn */
301
- export function setGetSessionFn(fn) {
302
- _getSessionFn = fn;
303
- }
304
-
305
- /** @returns {any} */
306
- export function getDoc() {
307
- return _getDocFn();
308
- }
309
-
310
- /** @returns {any} */
311
- export function getSession() {
312
- return _getSessionFn();
313
- }
314
-
315
- /**
316
- * Dispatch a session-only state update (selection, hover, ui). Does not trigger autosave.
317
- *
318
- * @param {any} patch — partial session object, e.g. { ui: { zoom: 2 } }
215
+ * @param {object} patch — partial session object, e.g. { ui: { zoom: 2 } }
319
216
  */
320
217
  export function updateSession(patch) {
321
- _updateSessionFn(patch);
218
+ const tab = activeTab.value;
219
+ if (tab) {
220
+ const p = /** @type {any} */ (patch);
221
+ if (p.selection !== undefined) tab.session.selection = p.selection;
222
+ if (p.hover !== undefined) tab.session.hover = p.hover;
223
+ if (p.clipboard !== undefined) tab.session.clipboard = p.clipboard;
224
+ if (p.ui) {
225
+ for (const [k, v] of Object.entries(p.ui)) {
226
+ /** @type {any} */ (tab.session.ui)[k] = v;
227
+ }
228
+ }
229
+ if (p.canvas) {
230
+ for (const [k, v] of Object.entries(p.canvas)) {
231
+ /** @type {any} */ (tab.session.canvas)[k] = v;
232
+ }
233
+ }
234
+ }
322
235
  }
323
236
 
324
237
  /**
325
- * Update a single UI field and re-render. Routes through session dispatch (no autosave, no
326
- * history).
238
+ * Update a single UI field.
327
239
  *
328
240
  * @param {string} field
329
- * @param {any} value
241
+ * @param {unknown} value
330
242
  */
331
243
  export function updateUi(field, value) {
332
- _updateSessionFn({ ui: { [field]: value } });
244
+ const tab = activeTab.value;
245
+ if (tab) {
246
+ /** @type {any} */ (tab.session.ui)[field] = value;
247
+ }
333
248
  }
334
249
 
335
250
  /**
336
251
  * Update the canvas async state (status, scope, error).
337
252
  *
338
- * @param {any} patch
253
+ * @param {object} patch
339
254
  */
340
255
  export function updateCanvas(patch) {
341
- _updateSessionFn({ canvas: patch });
342
- }
343
-
344
- // ─── Subscription system ────────────────────────────────────────────────────
345
- // Panels subscribe to state changes and decide when to re-render, rather than
346
- // being called unconditionally from _update/_updateSession.
347
-
348
- /** @typedef {{ doc: boolean; selection: boolean; hover: boolean; ui: boolean; mode: boolean }} Change */
349
-
350
- /** @type {Set<(change: Change) => void>} */
351
- const _subscribers = new Set();
352
-
353
- /**
354
- * Subscribe to state changes. Returns an unsubscribe function.
355
- *
356
- * @param {(change: Change) => void} fn
357
- * @returns {() => void}
358
- */
359
- export function subscribe(fn) {
360
- _subscribers.add(fn);
361
- return () => _subscribers.delete(fn);
362
- }
363
-
364
- /**
365
- * Notify all subscribers of a state change.
366
- *
367
- * @param {Change} change
368
- */
369
- export function notify(change) {
370
- for (const fn of _subscribers) {
371
- try {
372
- fn(change);
373
- } catch (e) {
374
- console.error("Subscriber failed:", e);
256
+ const tab = activeTab.value;
257
+ if (tab) {
258
+ for (const [k, v] of Object.entries(patch)) {
259
+ /** @type {any} */ (tab.session.canvas)[k] = v;
375
260
  }
376
261
  }
377
262
  }
378
-
379
- /** @type {Function[]} */
380
- const _updateMiddleware = [];
381
-
382
- /**
383
- * Register middleware that runs after every update().
384
- *
385
- * @param {Function} fn — receives (state) after core update
386
- */
387
- export function addUpdateMiddleware(fn) {
388
- _updateMiddleware.push(fn);
389
- }
390
-
391
- /**
392
- * Run all registered update middleware.
393
- *
394
- * @param {any} state
395
- */
396
- export function runUpdateMiddleware(state) {
397
- for (const mw of _updateMiddleware) mw(state);
398
- }
399
-
400
- /** @type {Function[]} */
401
- const _postRenderHooks = [];
402
-
403
- /**
404
- * Register a hook that runs after renders in update().
405
- *
406
- * @param {Function} fn — receives (prevDoc, prevSel)
407
- */
408
- export function addPostRenderHook(fn) {
409
- _postRenderHooks.push(fn);
410
- }
411
-
412
- /**
413
- * Run all registered post-render hooks.
414
- *
415
- * @param {any} prevDoc
416
- * @param {any} prevSel
417
- */
418
- export function runPostRenderHooks(prevDoc, prevSel) {
419
- for (const hook of _postRenderHooks) hook(prevDoc, prevSel);
420
- }