@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.
- package/dist/studio.js +4248 -3412
- package/dist/studio.js.map +49 -43
- package/package.json +5 -3
- package/src/canvas/canvas-diff.js +184 -0
- package/src/canvas/canvas-helpers.js +10 -14
- package/src/canvas/canvas-live-render.js +3 -2
- package/src/canvas/canvas-render.js +152 -20
- package/src/canvas/canvas-utils.js +21 -23
- package/src/editor/component-inline-edit.js +54 -41
- package/src/editor/content-inline-edit.js +46 -47
- package/src/editor/context-menu.js +63 -39
- package/src/editor/convert-to-component.js +11 -14
- package/src/editor/insertion-helper.js +8 -10
- package/src/editor/shortcuts.js +69 -39
- package/src/files/components.js +15 -4
- package/src/files/files.js +72 -24
- package/src/panels/activity-bar.js +29 -7
- package/src/panels/block-action-bar.js +104 -80
- package/src/panels/dnd.js +32 -28
- package/src/panels/editors.js +7 -14
- package/src/panels/elements-panel.js +9 -3
- package/src/panels/events-panel.js +44 -39
- package/src/panels/git-panel.js +45 -3
- package/src/panels/layers-panel.js +16 -11
- package/src/panels/left-panel.js +108 -43
- package/src/panels/overlays.js +91 -41
- package/src/panels/panel-events.js +9 -12
- package/src/panels/properties-panel.js +179 -104
- package/src/panels/right-panel.js +85 -37
- package/src/panels/shared.js +0 -22
- package/src/panels/signals-panel.js +125 -54
- package/src/panels/statusbar.js +23 -8
- package/src/panels/style-inputs.js +4 -2
- package/src/panels/style-panel.js +128 -105
- package/src/panels/stylebook-panel.js +11 -15
- package/src/panels/tab-strip.js +124 -0
- package/src/panels/toolbar.js +39 -9
- package/src/reactivity.js +28 -0
- package/src/settings/content-types-editor.js +56 -7
- package/src/settings/defs-editor.js +56 -7
- package/src/settings/schema-field-ui.js +60 -25
- package/src/state.js +0 -456
- package/src/store.js +97 -124
- package/src/studio.js +112 -121
- package/src/tabs/tab.js +153 -0
- package/src/tabs/transact.js +406 -0
- 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 {
|
|
61
|
-
export const activityBar = /** @type {
|
|
62
|
-
export const leftPanel = /** @type {
|
|
63
|
-
export const rightPanel = /** @type {
|
|
64
|
-
export const toolbarEl = /** @type {
|
|
65
|
-
export const statusbarEl = /** @type {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
124
|
-
* @param {
|
|
125
|
-
* @param {
|
|
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 {
|
|
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 {
|
|
150
|
-
* @returns {
|
|
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,
|
|
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,
|
|
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,
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
249
|
+
* @returns {import("./state.js").StudioState}
|
|
261
250
|
*/
|
|
262
251
|
export function getState() {
|
|
263
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
/** @
|
|
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).
|
|
297
|
+
* Dispatch a session-only state update (selection, hover, ui). Writes directly to reactive tab.
|
|
317
298
|
*
|
|
318
|
-
* @param {
|
|
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
|
|
326
|
-
* history).
|
|
323
|
+
* Update a single UI field. Routes through session dispatch.
|
|
327
324
|
*
|
|
328
325
|
* @param {string} field
|
|
329
|
-
* @param {
|
|
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 {
|
|
339
|
+
* @param {object} patch
|
|
339
340
|
*/
|
|
340
341
|
export function updateCanvas(patch) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
416
|
-
* @param {
|
|
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);
|