@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.
- package/README.md +82 -0
- package/dist/studio.js +5114 -3424
- package/dist/studio.js.map +65 -49
- package/package.json +6 -3
- package/src/browse/browse.js +27 -3
- package/src/canvas/canvas-diff.js +184 -0
- package/src/canvas/canvas-helpers.js +10 -14
- package/src/canvas/canvas-live-render.js +136 -17
- package/src/canvas/canvas-render.js +154 -21
- 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/canvas-dnd.js +132 -50
- 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 +97 -35
- package/src/panels/panel-events.js +18 -11
- package/src/panels/properties-panel.js +467 -98
- 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 +42 -17
- 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 +78 -8
- package/src/settings/defs-editor.js +56 -7
- package/src/settings/schema-field-ui.js +99 -11
- package/src/site-context.js +105 -0
- package/src/state.js +0 -456
- package/src/store.js +105 -124
- package/src/studio.js +112 -121
- package/src/tabs/tab.js +153 -0
- package/src/tabs/transact.js +406 -0
- package/src/ui/spectrum.js +2 -0
- package/src/view.js +3 -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,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,
|
|
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"
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
249
|
+
* @returns {import("./state.js").StudioState}
|
|
253
250
|
*/
|
|
254
251
|
export function getState() {
|
|
255
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
/** @
|
|
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).
|
|
297
|
+
* Dispatch a session-only state update (selection, hover, ui). Writes directly to reactive tab.
|
|
309
298
|
*
|
|
310
|
-
* @param {
|
|
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
|
|
318
|
-
* history).
|
|
323
|
+
* Update a single UI field. Routes through session dispatch.
|
|
319
324
|
*
|
|
320
325
|
* @param {string} field
|
|
321
|
-
* @param {
|
|
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 {
|
|
339
|
+
* @param {object} patch
|
|
331
340
|
*/
|
|
332
341
|
export function updateCanvas(patch) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
408
|
-
* @param {
|
|
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);
|