@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
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,11 +149,19 @@ 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
- } else if (k === "state" || k === "style" || k === "attributes" || k === "$media") {
156
+ } else if (k === "state" && typeof v === "object" && v !== null) {
157
+ /** @type {Record<string, unknown>} */
158
+ const state = {};
159
+ for (const [sk, sv] of Object.entries(v)) {
160
+ if (sv && typeof sv === "object" && sv.timing === "server") continue;
161
+ state[sk] = sv;
162
+ }
163
+ out.state = state;
164
+ } else if (k === "style" || k === "attributes" || k === "$media") {
168
165
  out[k] = v;
169
166
  } else {
170
167
  out[k] = v;
@@ -220,18 +217,18 @@ export function renderOnly(...names) {
220
217
  // studio.js registers the real update implementation via setUpdateFn() during bootstrap.
221
218
  // This allows extracted modules to import `update` from store.js without circular deps.
222
219
 
223
- /** @type {Function} */
220
+ /** @type {(state: import("./state.js").StudioState) => void} */
224
221
  let _updateFn = () => {
225
222
  throw new Error("update() called before setUpdateFn() — bootstrap not complete");
226
223
  };
227
224
 
228
- /** @type {Function} */
225
+ /** @type {() => import("./state.js").StudioState | null} */
229
226
  let _getStateFn = () => null;
230
227
 
231
228
  /**
232
229
  * Register the update implementation. Called by studio.js at module load time.
233
230
  *
234
- * @param {Function} fn
231
+ * @param {(state: import("./state.js").StudioState) => void} fn
235
232
  */
236
233
  export function setUpdateFn(fn) {
237
234
  _updateFn = fn;
@@ -240,25 +237,43 @@ export function setUpdateFn(fn) {
240
237
  /**
241
238
  * Register the state getter. Called by studio.js at module load time.
242
239
  *
243
- * @param {Function} fn — returns current S
240
+ * @param {() => import("./state.js").StudioState} fn — returns current S
244
241
  */
245
242
  export function setGetStateFn(fn) {
246
243
  _getStateFn = fn;
247
244
  }
248
245
 
249
246
  /**
250
- * Get the current state (live, not stale).
247
+ * Get the current state (live, not stale). Synthesized from the active tab's reactive state.
251
248
  *
252
- * @returns {any}
249
+ * @returns {import("./state.js").StudioState}
253
250
  */
254
251
  export function getState() {
255
- 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());
256
271
  }
257
272
 
258
273
  /**
259
274
  * Dispatch a state update + selective re-render.
260
275
  *
261
- * @param {any} newState
276
+ * @param {import("./state.js").StudioState} newState
262
277
  */
263
278
  export function update(newState) {
264
279
  _updateFn(newState);
@@ -268,113 +283,79 @@ export function update(newState) {
268
283
  // Lightweight dispatcher for session-only changes (selection, hover, ui).
269
284
  // Does NOT trigger autosave middleware or push history.
270
285
 
271
- /** @type {Function} */
286
+ /** @type {(patch: object) => void} */
272
287
  let _updateSessionFn = () => {
273
288
  throw new Error("updateSession() called before setUpdateSessionFn() — bootstrap not complete");
274
289
  };
275
290
 
276
- /** @type {Function} */
277
- let _getDocFn = () => null;
278
-
279
- /** @type {Function} */
280
- let _getSessionFn = () => null;
281
-
282
- /** @param {Function} fn */
291
+ /** @param {(patch: object) => void} fn */
283
292
  export function setUpdateSessionFn(fn) {
284
293
  _updateSessionFn = fn;
285
294
  }
286
295
 
287
- /** @param {Function} fn */
288
- export function setGetDocFn(fn) {
289
- _getDocFn = fn;
290
- }
291
-
292
- /** @param {Function} fn */
293
- export function setGetSessionFn(fn) {
294
- _getSessionFn = fn;
295
- }
296
-
297
- /** @returns {any} */
298
- export function getDoc() {
299
- return _getDocFn();
300
- }
301
-
302
- /** @returns {any} */
303
- export function getSession() {
304
- return _getSessionFn();
305
- }
306
-
307
296
  /**
308
- * 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.
309
298
  *
310
- * @param {any} patch — partial session object, e.g. { ui: { zoom: 2 } }
299
+ * @param {object} patch — partial session object, e.g. { ui: { zoom: 2 } }
311
300
  */
312
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
+ }
313
319
  _updateSessionFn(patch);
314
320
  }
315
321
 
316
322
  /**
317
- * Update a single UI field and re-render. Routes through session dispatch (no autosave, no
318
- * history).
323
+ * Update a single UI field. Routes through session dispatch.
319
324
  *
320
325
  * @param {string} field
321
- * @param {any} value
326
+ * @param {unknown} value
322
327
  */
323
328
  export function updateUi(field, value) {
329
+ const tab = activeTab.value;
330
+ if (tab) {
331
+ /** @type {any} */ (tab.session.ui)[field] = value;
332
+ }
324
333
  _updateSessionFn({ ui: { [field]: value } });
325
334
  }
326
335
 
327
336
  /**
328
337
  * Update the canvas async state (status, scope, error).
329
338
  *
330
- * @param {any} patch
339
+ * @param {object} patch
331
340
  */
332
341
  export function updateCanvas(patch) {
333
- _updateSessionFn({ canvas: patch });
334
- }
335
-
336
- // ─── Subscription system ────────────────────────────────────────────────────
337
- // Panels subscribe to state changes and decide when to re-render, rather than
338
- // being called unconditionally from _update/_updateSession.
339
-
340
- /** @typedef {{ doc: boolean; selection: boolean; hover: boolean; ui: boolean; mode: boolean }} Change */
341
-
342
- /** @type {Set<(change: Change) => void>} */
343
- const _subscribers = new Set();
344
-
345
- /**
346
- * Subscribe to state changes. Returns an unsubscribe function.
347
- *
348
- * @param {(change: Change) => void} fn
349
- * @returns {() => void}
350
- */
351
- export function subscribe(fn) {
352
- _subscribers.add(fn);
353
- return () => _subscribers.delete(fn);
354
- }
355
-
356
- /**
357
- * Notify all subscribers of a state change.
358
- *
359
- * @param {Change} change
360
- */
361
- export function notify(change) {
362
- for (const fn of _subscribers) {
363
- try {
364
- fn(change);
365
- } catch (e) {
366
- 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;
367
346
  }
368
347
  }
348
+ _updateSessionFn({ canvas: patch });
369
349
  }
370
350
 
371
- /** @type {Function[]} */
351
+ /** @type {((state: import("./state.js").StudioState) => void)[]} */
372
352
  const _updateMiddleware = [];
373
353
 
374
354
  /**
375
355
  * Register middleware that runs after every update().
376
356
  *
377
- * @param {Function} fn — receives (state) after core update
357
+ * @param {(state: import("./state.js").StudioState) => void} fn — receives (state) after core
358
+ * update
378
359
  */
379
360
  export function addUpdateMiddleware(fn) {
380
361
  _updateMiddleware.push(fn);
@@ -383,19 +364,19 @@ export function addUpdateMiddleware(fn) {
383
364
  /**
384
365
  * Run all registered update middleware.
385
366
  *
386
- * @param {any} state
367
+ * @param {import("./state.js").StudioState} state
387
368
  */
388
369
  export function runUpdateMiddleware(state) {
389
370
  for (const mw of _updateMiddleware) mw(state);
390
371
  }
391
372
 
392
- /** @type {Function[]} */
373
+ /** @type {((prevDoc: object, prevSel: import("./state.js").JxPath | null) => void)[]} */
393
374
  const _postRenderHooks = [];
394
375
 
395
376
  /**
396
377
  * Register a hook that runs after renders in update().
397
378
  *
398
- * @param {Function} fn — receives (prevDoc, prevSel)
379
+ * @param {(prevDoc: object, prevSel: import("./state.js").JxPath | null) => void} fn
399
380
  */
400
381
  export function addPostRenderHook(fn) {
401
382
  _postRenderHooks.push(fn);
@@ -404,8 +385,8 @@ export function addPostRenderHook(fn) {
404
385
  /**
405
386
  * Run all registered post-render hooks.
406
387
  *
407
- * @param {any} prevDoc
408
- * @param {any} prevSel
388
+ * @param {object} prevDoc
389
+ * @param {import("./state.js").JxPath | null} prevSel
409
390
  */
410
391
  export function runPostRenderHooks(prevDoc, prevSel) {
411
392
  for (const hook of _postRenderHooks) hook(prevDoc, prevSel);