@jxsuite/studio 0.11.0 → 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 (47) hide show
  1. package/dist/studio.js +4248 -3412
  2. package/dist/studio.js.map +49 -43
  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 +3 -2
  7. package/src/canvas/canvas-render.js +152 -20
  8. package/src/canvas/canvas-utils.js +21 -23
  9. package/src/editor/component-inline-edit.js +54 -41
  10. package/src/editor/content-inline-edit.js +46 -47
  11. package/src/editor/context-menu.js +63 -39
  12. package/src/editor/convert-to-component.js +11 -14
  13. package/src/editor/insertion-helper.js +8 -10
  14. package/src/editor/shortcuts.js +69 -39
  15. package/src/files/components.js +15 -4
  16. package/src/files/files.js +72 -24
  17. package/src/panels/activity-bar.js +29 -7
  18. package/src/panels/block-action-bar.js +104 -80
  19. package/src/panels/dnd.js +32 -28
  20. package/src/panels/editors.js +7 -14
  21. package/src/panels/elements-panel.js +9 -3
  22. package/src/panels/events-panel.js +44 -39
  23. package/src/panels/git-panel.js +45 -3
  24. package/src/panels/layers-panel.js +16 -11
  25. package/src/panels/left-panel.js +108 -43
  26. package/src/panels/overlays.js +91 -41
  27. package/src/panels/panel-events.js +9 -12
  28. package/src/panels/properties-panel.js +179 -104
  29. package/src/panels/right-panel.js +85 -37
  30. package/src/panels/shared.js +0 -22
  31. package/src/panels/signals-panel.js +125 -54
  32. package/src/panels/statusbar.js +23 -8
  33. package/src/panels/style-inputs.js +4 -2
  34. package/src/panels/style-panel.js +128 -105
  35. package/src/panels/stylebook-panel.js +11 -15
  36. package/src/panels/tab-strip.js +124 -0
  37. package/src/panels/toolbar.js +39 -9
  38. package/src/reactivity.js +28 -0
  39. package/src/settings/content-types-editor.js +56 -7
  40. package/src/settings/defs-editor.js +56 -7
  41. package/src/settings/schema-field-ui.js +60 -25
  42. package/src/state.js +0 -456
  43. package/src/store.js +97 -124
  44. package/src/studio.js +112 -121
  45. package/src/tabs/tab.js +153 -0
  46. package/src/tabs/transact.js +406 -0
  47. package/src/workspace/workspace.js +89 -0
package/src/store.js CHANGED
@@ -8,35 +8,14 @@
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
15
  selectNode,
14
16
  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
17
  pushDocument,
34
18
  popDocument,
35
- updateProp,
36
- addSwitchCase,
37
- removeSwitchCase,
38
- renameSwitchCase,
39
- applyMutation,
40
19
  getNodeAtPath,
41
20
  flattenTree,
42
21
  nodeLabel,
@@ -57,12 +36,12 @@ export {
57
36
  export const $ = (/** @type {string} */ sel) => document.querySelector(sel);
58
37
  export const _$$ = (/** @type {string} */ sel) => document.querySelectorAll(sel);
59
38
 
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"));
39
+ export const canvasWrap = /** @type {HTMLElement} */ (document.querySelector("#canvas-wrap"));
40
+ export const activityBar = /** @type {HTMLElement} */ (document.querySelector("#activity-bar"));
41
+ export const leftPanel = /** @type {HTMLElement} */ (document.querySelector("#left-panel"));
42
+ export const rightPanel = /** @type {HTMLElement} */ (document.querySelector("#right-panel"));
43
+ export const toolbarEl = /** @type {HTMLElement} */ (document.querySelector("#toolbar"));
44
+ export const statusbarEl = /** @type {HTMLElement} */ (document.querySelector("#statusbar"));
66
45
 
67
46
  // ─── Shared containers (mutated in place by owner modules) ───────────────────
68
47
 
@@ -70,9 +49,19 @@ export const statusbarEl = /** @type {any} */ (document.querySelector("#statusba
70
49
  export const elToPath = new WeakMap();
71
50
 
72
51
  /**
73
- * Canvas panels: Array<{ mediaName, canvas, overlay, overlayClk, viewport, dropLine }>
52
+ * Canvas panels: Array<{ mediaName, canvas, overlay, overlayClk, viewport, dropLine, element,
53
+ * _width }>
74
54
  *
75
- * @type {any[]}
55
+ * @type {{
56
+ * mediaName: string;
57
+ * canvas: HTMLElement;
58
+ * overlay: HTMLElement;
59
+ * overlayClk: HTMLElement;
60
+ * viewport: HTMLElement;
61
+ * dropLine: HTMLElement;
62
+ * element?: HTMLElement | null;
63
+ * _width?: number | null;
64
+ * }[]}
76
65
  */
77
66
  export const canvasPanels = [];
78
67
 
@@ -110,7 +99,7 @@ export const COMMON_SELECTORS = [
110
99
  "::placeholder",
111
100
  ];
112
101
 
113
- /** @param {any} k */
102
+ /** @param {string} k */
114
103
  export function isNestedSelector(k) {
115
104
  return k.startsWith(":") || k.startsWith(".") || k.startsWith("&") || k.startsWith("[");
116
105
  }
@@ -120,12 +109,12 @@ export function isNestedSelector(k) {
120
109
  const _styleDebounceTimers = new Map();
121
110
 
122
111
  /**
123
- * @param {any} prop
124
- * @param {any} ms
125
- * @param {any} fn
112
+ * @param {string} prop
113
+ * @param {number} ms
114
+ * @param {Function} fn
126
115
  */
127
116
  export function debouncedStyleCommit(prop, ms, fn) {
128
- return (/** @type {any[]} */ ...args) => {
117
+ return (/** @type {unknown[]} */ ...args) => {
129
118
  clearTimeout(_styleDebounceTimers.get(prop));
130
119
  _styleDebounceTimers.set(
131
120
  prop,
@@ -146,13 +135,13 @@ export function cancelStyleDebounce(/** @type {string} */ prop) {
146
135
  /**
147
136
  * Strip all on* event handler properties from a Jx document tree (deep clone).
148
137
  *
149
- * @param {any} node
150
- * @returns {any}
138
+ * @param {import("./state.js").JxNode | unknown} node
139
+ * @returns {import("./state.js").JxNode | unknown}
151
140
  */
152
141
  export function stripEventHandlers(node) {
153
142
  if (!node || typeof node !== "object") return node;
154
143
  if (Array.isArray(node)) return node.map(stripEventHandlers);
155
- /** @type {Record<string, any>} */
144
+ /** @type {Record<string, unknown>} */
156
145
  const out = {};
157
146
  for (const [k, v] of Object.entries(node)) {
158
147
  if (k.startsWith("on") && typeof v === "object" && (v?.$ref || v?.$prototype === "Function"))
@@ -160,12 +149,12 @@ export function stripEventHandlers(node) {
160
149
  if (k === "children") {
161
150
  out.children = Array.isArray(v) ? v.map(stripEventHandlers) : stripEventHandlers(v);
162
151
  } else if (k === "cases" && typeof v === "object") {
163
- /** @type {Record<string, any>} */
152
+ /** @type {Record<string, unknown>} */
164
153
  const cases = {};
165
154
  for (const [ck, cv] of Object.entries(v)) cases[ck] = stripEventHandlers(cv);
166
155
  out.cases = cases;
167
156
  } else if (k === "state" && typeof v === "object" && v !== null) {
168
- /** @type {Record<string, any>} */
157
+ /** @type {Record<string, unknown>} */
169
158
  const state = {};
170
159
  for (const [sk, sv] of Object.entries(v)) {
171
160
  if (sv && typeof sv === "object" && sv.timing === "server") continue;
@@ -228,18 +217,18 @@ export function renderOnly(...names) {
228
217
  // studio.js registers the real update implementation via setUpdateFn() during bootstrap.
229
218
  // This allows extracted modules to import `update` from store.js without circular deps.
230
219
 
231
- /** @type {Function} */
220
+ /** @type {(state: import("./state.js").StudioState) => void} */
232
221
  let _updateFn = () => {
233
222
  throw new Error("update() called before setUpdateFn() — bootstrap not complete");
234
223
  };
235
224
 
236
- /** @type {Function} */
225
+ /** @type {() => import("./state.js").StudioState | null} */
237
226
  let _getStateFn = () => null;
238
227
 
239
228
  /**
240
229
  * Register the update implementation. Called by studio.js at module load time.
241
230
  *
242
- * @param {Function} fn
231
+ * @param {(state: import("./state.js").StudioState) => void} fn
243
232
  */
244
233
  export function setUpdateFn(fn) {
245
234
  _updateFn = fn;
@@ -248,25 +237,43 @@ export function setUpdateFn(fn) {
248
237
  /**
249
238
  * Register the state getter. Called by studio.js at module load time.
250
239
  *
251
- * @param {Function} fn — returns current S
240
+ * @param {() => import("./state.js").StudioState} fn — returns current S
252
241
  */
253
242
  export function setGetStateFn(fn) {
254
243
  _getStateFn = fn;
255
244
  }
256
245
 
257
246
  /**
258
- * Get the current state (live, not stale).
247
+ * Get the current state (live, not stale). Synthesized from the active tab's reactive state.
259
248
  *
260
- * @returns {any}
249
+ * @returns {import("./state.js").StudioState}
261
250
  */
262
251
  export function getState() {
263
- return _getStateFn();
252
+ const tab = activeTab.value;
253
+ if (tab) {
254
+ return /** @type {any} */ ({
255
+ document: tab.doc.document,
256
+ mode: tab.doc.mode,
257
+ dirty: tab.doc.dirty,
258
+ handlersSource: tab.doc.handlersSource,
259
+ content: tab.doc.content,
260
+ documentPath: tab.documentPath,
261
+ fileHandle: tab.fileHandle,
262
+ selection: tab.session.selection,
263
+ hover: tab.session.hover,
264
+ clipboard: tab.session.clipboard,
265
+ ui: tab.session.ui,
266
+ canvas: tab.session.canvas,
267
+ documentStack: tab.session.documentStack,
268
+ });
269
+ }
270
+ return /** @type {any} */ (_getStateFn());
264
271
  }
265
272
 
266
273
  /**
267
274
  * Dispatch a state update + selective re-render.
268
275
  *
269
- * @param {any} newState
276
+ * @param {import("./state.js").StudioState} newState
270
277
  */
271
278
  export function update(newState) {
272
279
  _updateFn(newState);
@@ -276,113 +283,79 @@ export function update(newState) {
276
283
  // Lightweight dispatcher for session-only changes (selection, hover, ui).
277
284
  // Does NOT trigger autosave middleware or push history.
278
285
 
279
- /** @type {Function} */
286
+ /** @type {(patch: object) => void} */
280
287
  let _updateSessionFn = () => {
281
288
  throw new Error("updateSession() called before setUpdateSessionFn() — bootstrap not complete");
282
289
  };
283
290
 
284
- /** @type {Function} */
285
- let _getDocFn = () => null;
286
-
287
- /** @type {Function} */
288
- let _getSessionFn = () => null;
289
-
290
- /** @param {Function} fn */
291
+ /** @param {(patch: object) => void} fn */
291
292
  export function setUpdateSessionFn(fn) {
292
293
  _updateSessionFn = fn;
293
294
  }
294
295
 
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
296
  /**
316
- * Dispatch a session-only state update (selection, hover, ui). Does not trigger autosave.
297
+ * Dispatch a session-only state update (selection, hover, ui). Writes directly to reactive tab.
317
298
  *
318
- * @param {any} patch — partial session object, e.g. { ui: { zoom: 2 } }
299
+ * @param {object} patch — partial session object, e.g. { ui: { zoom: 2 } }
319
300
  */
320
301
  export function updateSession(patch) {
302
+ const tab = activeTab.value;
303
+ if (tab) {
304
+ const p = /** @type {any} */ (patch);
305
+ if (p.selection !== undefined) tab.session.selection = p.selection;
306
+ if (p.hover !== undefined) tab.session.hover = p.hover;
307
+ if (p.clipboard !== undefined) tab.session.clipboard = p.clipboard;
308
+ if (p.ui) {
309
+ for (const [k, v] of Object.entries(p.ui)) {
310
+ /** @type {any} */ (tab.session.ui)[k] = v;
311
+ }
312
+ }
313
+ if (p.canvas) {
314
+ for (const [k, v] of Object.entries(p.canvas)) {
315
+ /** @type {any} */ (tab.session.canvas)[k] = v;
316
+ }
317
+ }
318
+ }
321
319
  _updateSessionFn(patch);
322
320
  }
323
321
 
324
322
  /**
325
- * Update a single UI field and re-render. Routes through session dispatch (no autosave, no
326
- * history).
323
+ * Update a single UI field. Routes through session dispatch.
327
324
  *
328
325
  * @param {string} field
329
- * @param {any} value
326
+ * @param {unknown} value
330
327
  */
331
328
  export function updateUi(field, value) {
329
+ const tab = activeTab.value;
330
+ if (tab) {
331
+ /** @type {any} */ (tab.session.ui)[field] = value;
332
+ }
332
333
  _updateSessionFn({ ui: { [field]: value } });
333
334
  }
334
335
 
335
336
  /**
336
337
  * Update the canvas async state (status, scope, error).
337
338
  *
338
- * @param {any} patch
339
+ * @param {object} patch
339
340
  */
340
341
  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);
342
+ const tab = activeTab.value;
343
+ if (tab) {
344
+ for (const [k, v] of Object.entries(patch)) {
345
+ /** @type {any} */ (tab.session.canvas)[k] = v;
375
346
  }
376
347
  }
348
+ _updateSessionFn({ canvas: patch });
377
349
  }
378
350
 
379
- /** @type {Function[]} */
351
+ /** @type {((state: import("./state.js").StudioState) => void)[]} */
380
352
  const _updateMiddleware = [];
381
353
 
382
354
  /**
383
355
  * Register middleware that runs after every update().
384
356
  *
385
- * @param {Function} fn — receives (state) after core update
357
+ * @param {(state: import("./state.js").StudioState) => void} fn — receives (state) after core
358
+ * update
386
359
  */
387
360
  export function addUpdateMiddleware(fn) {
388
361
  _updateMiddleware.push(fn);
@@ -391,19 +364,19 @@ export function addUpdateMiddleware(fn) {
391
364
  /**
392
365
  * Run all registered update middleware.
393
366
  *
394
- * @param {any} state
367
+ * @param {import("./state.js").StudioState} state
395
368
  */
396
369
  export function runUpdateMiddleware(state) {
397
370
  for (const mw of _updateMiddleware) mw(state);
398
371
  }
399
372
 
400
- /** @type {Function[]} */
373
+ /** @type {((prevDoc: object, prevSel: import("./state.js").JxPath | null) => void)[]} */
401
374
  const _postRenderHooks = [];
402
375
 
403
376
  /**
404
377
  * Register a hook that runs after renders in update().
405
378
  *
406
- * @param {Function} fn — receives (prevDoc, prevSel)
379
+ * @param {(prevDoc: object, prevSel: import("./state.js").JxPath | null) => void} fn
407
380
  */
408
381
  export function addPostRenderHook(fn) {
409
382
  _postRenderHooks.push(fn);
@@ -412,8 +385,8 @@ export function addPostRenderHook(fn) {
412
385
  /**
413
386
  * Run all registered post-render hooks.
414
387
  *
415
- * @param {any} prevDoc
416
- * @param {any} prevSel
388
+ * @param {object} prevDoc
389
+ * @param {import("./state.js").JxPath | null} prevSel
417
390
  */
418
391
  export function runPostRenderHooks(prevDoc, prevSel) {
419
392
  for (const hook of _postRenderHooks) hook(prevDoc, prevSel);