@jxsuite/studio 0.13.0 → 0.15.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 +467 -625
- package/dist/studio.js.map +37 -37
- package/package.json +1 -1
- package/src/canvas/canvas-live-render.js +25 -0
- package/src/canvas/canvas-render.js +20 -2
- package/src/canvas/canvas-utils.js +1 -2
- package/src/editor/component-inline-edit.js +1 -3
- package/src/editor/content-inline-edit.js +1 -3
- package/src/editor/context-menu.js +73 -35
- package/src/editor/insertion-helper.js +0 -1
- package/src/editor/shortcuts.js +39 -45
- package/src/files/file-ops.js +57 -108
- package/src/files/files.js +13 -8
- package/src/panels/activity-bar.js +6 -4
- package/src/panels/canvas-dnd.js +4 -10
- package/src/panels/dnd.js +45 -7
- package/src/panels/editors.js +14 -16
- package/src/panels/elements-panel.js +13 -13
- package/src/panels/git-panel.js +2 -1
- package/src/panels/layers-panel.js +11 -12
- package/src/panels/left-panel.js +2 -2
- package/src/panels/panel-events.js +19 -25
- package/src/panels/preview-render.js +7 -3
- package/src/panels/pseudo-preview.js +9 -8
- package/src/panels/statusbar.js +8 -16
- package/src/panels/style-inputs.js +2 -3
- package/src/panels/style-utils.js +8 -6
- package/src/panels/stylebook-layers-panel.js +5 -5
- package/src/panels/stylebook-panel.js +16 -16
- package/src/panels/toolbar.js +2 -2
- package/src/state.js +2 -3
- package/src/store.js +2 -133
- package/src/studio.js +111 -239
- package/src/tabs/tab.js +19 -4
- package/src/tabs/transact.js +19 -1
- package/src/ui/color-selector.js +4 -5
- package/src/view.js +3 -0
- package/src/workspace/workspace.js +5 -0
package/src/studio.js
CHANGED
|
@@ -6,27 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
-
createState,
|
|
10
|
-
pushDocument,
|
|
11
|
-
popDocument,
|
|
12
9
|
getNodeAtPath,
|
|
13
10
|
canvasWrap,
|
|
14
11
|
toolbarEl,
|
|
15
|
-
canvasPanels,
|
|
16
12
|
registerRenderer,
|
|
17
13
|
render,
|
|
18
|
-
setUpdateFn,
|
|
19
|
-
setGetStateFn,
|
|
20
|
-
addUpdateMiddleware,
|
|
21
|
-
runUpdateMiddleware,
|
|
22
|
-
addPostRenderHook,
|
|
23
|
-
runPostRenderHooks,
|
|
24
14
|
projectState,
|
|
25
15
|
setProjectState,
|
|
26
16
|
updateUi,
|
|
27
|
-
setUpdateSessionFn,
|
|
28
|
-
toFlat,
|
|
29
|
-
fromFlat,
|
|
30
17
|
} from "./store.js";
|
|
31
18
|
|
|
32
19
|
import { activeTab, openTab } from "./workspace/workspace.js";
|
|
@@ -36,17 +23,9 @@ import { effect } from "./reactivity.js";
|
|
|
36
23
|
import { view } from "./view.js";
|
|
37
24
|
|
|
38
25
|
import { isEditing, isEditableBlock } from "./editor/inline-edit.js";
|
|
39
|
-
import {
|
|
40
|
-
enterComponentInlineEdit,
|
|
41
|
-
initComponentInlineEdit,
|
|
42
|
-
} from "./editor/component-inline-edit.js";
|
|
26
|
+
import { initComponentInlineEdit } from "./editor/component-inline-edit.js";
|
|
43
27
|
import { enterInlineEdit } from "./editor/content-inline-edit.js";
|
|
44
|
-
import {
|
|
45
|
-
initCanvasUtils,
|
|
46
|
-
applyTransform,
|
|
47
|
-
positionZoomIndicator,
|
|
48
|
-
updateActivePanelHeaders,
|
|
49
|
-
} from "./canvas/canvas-utils.js";
|
|
28
|
+
import { initCanvasUtils, applyTransform, positionZoomIndicator } from "./canvas/canvas-utils.js";
|
|
50
29
|
import { initCanvasHelpers, getActivePanel, findCanvasElement } from "./canvas/canvas-helpers.js";
|
|
51
30
|
import { initCanvasRender, renderCanvas } from "./canvas/canvas-render.js";
|
|
52
31
|
import { initCanvasLiveRender } from "./canvas/canvas-live-render.js";
|
|
@@ -56,17 +35,11 @@ import {
|
|
|
56
35
|
setStatusbarRenderer,
|
|
57
36
|
mountStatusbar,
|
|
58
37
|
} from "./panels/statusbar.js";
|
|
59
|
-
import {
|
|
60
|
-
openFile as _openFile,
|
|
61
|
-
loadMarkdown as _loadMarkdown,
|
|
62
|
-
saveFile as _saveFile,
|
|
63
|
-
exportFile as _exportFile,
|
|
64
|
-
} from "./files/file-ops.js";
|
|
38
|
+
import { openFile, loadMarkdown, saveFile, exportFile } from "./files/file-ops.js";
|
|
65
39
|
import {
|
|
66
40
|
loadProject as _loadProject,
|
|
67
41
|
openProject as _openProject,
|
|
68
42
|
renderFilesTemplate as _renderFilesTemplate,
|
|
69
|
-
openFileFromTree as _openFileFromTree,
|
|
70
43
|
openFileInTab,
|
|
71
44
|
setupTreeKeyboard,
|
|
72
45
|
} from "./files/files.js";
|
|
@@ -112,13 +85,6 @@ import { initPanelEvents } from "./panels/panel-events.js";
|
|
|
112
85
|
// These mutable variables are local to studio.js for now. As sections are extracted
|
|
113
86
|
// into their own modules, they will migrate to ctx in store.js.
|
|
114
87
|
|
|
115
|
-
/** @type {any} */
|
|
116
|
-
let S; // current state (flat compatibility view)
|
|
117
|
-
/** @type {any} */
|
|
118
|
-
let doc = null; // doc slice (persisted, history, autosave)
|
|
119
|
-
/** @type {any} */
|
|
120
|
-
let session = null; // session slice (selection, hover, ui)
|
|
121
|
-
|
|
122
88
|
/** Creates a display:contents container appended to sp-theme or body, for floating popovers/menus. */
|
|
123
89
|
function createFloatingContainer() {
|
|
124
90
|
const el = document.createElement("div");
|
|
@@ -127,7 +93,18 @@ function createFloatingContainer() {
|
|
|
127
93
|
return el;
|
|
128
94
|
}
|
|
129
95
|
|
|
130
|
-
|
|
96
|
+
function getCanvasMode() {
|
|
97
|
+
return activeTab.value?.session.ui.canvasMode ?? "design";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @param {string} mode */
|
|
101
|
+
function setCanvasMode(mode) {
|
|
102
|
+
if (getCanvasMode() === "git-diff" && mode !== "git-diff") {
|
|
103
|
+
gitDiffState = null;
|
|
104
|
+
}
|
|
105
|
+
const tab = activeTab.value;
|
|
106
|
+
if (tab) tab.session.ui.canvasMode = mode;
|
|
107
|
+
}
|
|
131
108
|
|
|
132
109
|
/** @type {any} */
|
|
133
110
|
let gitDiffState = null;
|
|
@@ -141,9 +118,30 @@ async function navigateToComponent(componentPath) {
|
|
|
141
118
|
const content = await platform.readFile(componentPath);
|
|
142
119
|
if (!content) return;
|
|
143
120
|
const parsed = JSON.parse(content);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
121
|
+
const tab = activeTab.value;
|
|
122
|
+
if (!tab) return;
|
|
123
|
+
|
|
124
|
+
// Push current state onto the document stack
|
|
125
|
+
const frame = {
|
|
126
|
+
document: tab.doc.document,
|
|
127
|
+
selection: tab.session.selection,
|
|
128
|
+
documentPath: tab.documentPath,
|
|
129
|
+
dirty: tab.doc.dirty,
|
|
130
|
+
mode: tab.doc.mode,
|
|
131
|
+
};
|
|
132
|
+
if (!tab.session.documentStack) tab.session.documentStack = [];
|
|
133
|
+
tab.session.documentStack.push(frame);
|
|
134
|
+
|
|
135
|
+
// Load the component
|
|
136
|
+
tab.doc.document = parsed;
|
|
137
|
+
tab.doc.dirty = false;
|
|
138
|
+
tab.doc.mode = /** @type {any} */ (null);
|
|
139
|
+
tab.documentPath = componentPath;
|
|
140
|
+
tab.session.selection = null;
|
|
141
|
+
view.leftTab = "layers";
|
|
142
|
+
tab.session.ui.activeMedia = null;
|
|
143
|
+
tab.session.ui.activeSelector = null;
|
|
144
|
+
|
|
147
145
|
render();
|
|
148
146
|
statusMessage(`Editing component: ${parsed.tagName || componentPath}`);
|
|
149
147
|
} catch (/** @type {any} */ e) {
|
|
@@ -153,37 +151,46 @@ async function navigateToComponent(componentPath) {
|
|
|
153
151
|
}
|
|
154
152
|
|
|
155
153
|
async function navigateBack() {
|
|
156
|
-
|
|
157
|
-
if (
|
|
154
|
+
const tab = activeTab.value;
|
|
155
|
+
if (!tab?.session.documentStack || tab.session.documentStack.length === 0) return;
|
|
156
|
+
if (tab.doc.dirty && tab.documentPath) {
|
|
158
157
|
try {
|
|
159
158
|
const platform = getPlatform();
|
|
160
|
-
await platform.writeFile(
|
|
159
|
+
await platform.writeFile(tab.documentPath, JSON.stringify(tab.doc.document, null, 2));
|
|
161
160
|
} catch (/** @type {any} */ e) {
|
|
162
161
|
const err = /** @type {any} */ (e);
|
|
163
162
|
statusMessage(`Save error: ${err.message}`);
|
|
164
163
|
}
|
|
165
164
|
}
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
|
|
166
|
+
// Pop the stack
|
|
167
|
+
const frame = /** @type {any} */ (tab.session.documentStack.pop());
|
|
168
|
+
if (!frame) return;
|
|
169
|
+
tab.doc.document = frame.document;
|
|
170
|
+
tab.doc.dirty = frame.dirty;
|
|
171
|
+
tab.doc.mode = frame.mode;
|
|
172
|
+
tab.documentPath = frame.documentPath;
|
|
173
|
+
tab.session.selection = frame.selection;
|
|
174
|
+
view.leftTab = "layers";
|
|
175
|
+
|
|
168
176
|
render();
|
|
169
177
|
statusMessage("Returned to parent document");
|
|
170
178
|
}
|
|
171
179
|
|
|
172
180
|
async function closeFunctionEditor() {
|
|
173
|
-
const
|
|
181
|
+
const tab = activeTab.value;
|
|
182
|
+
const editing = /** @type {any} */ (tab?.session.ui.editingFunction);
|
|
174
183
|
if (!editing) return;
|
|
175
184
|
if (view.functionEditor) {
|
|
176
185
|
const currentCode = view.functionEditor.getValue();
|
|
177
186
|
const minResult = await codeService("minify", { code: currentCode });
|
|
178
187
|
const bodyToStore = minResult?.code ?? currentCode;
|
|
179
188
|
if (editing.type === "def") {
|
|
180
|
-
transactDoc(
|
|
181
|
-
mutateUpdateDef(t, editing.defName, { body: bodyToStore }),
|
|
182
|
-
);
|
|
189
|
+
transactDoc(tab, (t) => mutateUpdateDef(t, editing.defName, { body: bodyToStore }));
|
|
183
190
|
} else if (editing.type === "event") {
|
|
184
|
-
const node = getNodeAtPath(
|
|
191
|
+
const node = getNodeAtPath(tab.doc.document, editing.path);
|
|
185
192
|
const current = node?.[editing.eventKey] || {};
|
|
186
|
-
transactDoc(
|
|
193
|
+
transactDoc(tab, (t) =>
|
|
187
194
|
mutateUpdateProperty(t, editing.path, editing.eventKey, {
|
|
188
195
|
...current,
|
|
189
196
|
$prototype: "Function",
|
|
@@ -234,13 +241,7 @@ const EMPTY_DOC = {
|
|
|
234
241
|
],
|
|
235
242
|
};
|
|
236
243
|
|
|
237
|
-
|
|
238
|
-
({ doc, session } = fromFlat(S));
|
|
239
|
-
setGetStateFn(() => S);
|
|
240
|
-
|
|
241
|
-
// Create the initial reactive tab — this is the canonical state container.
|
|
242
|
-
// The flat `S` object and `_update`/`_updateSession` are kept as a compatibility layer
|
|
243
|
-
// while call sites are progressively migrated to direct reactive mutations.
|
|
244
|
+
// Create the initial reactive tab — the canonical state container.
|
|
244
245
|
openTab({ id: "initial", document: structuredClone(EMPTY_DOC) });
|
|
245
246
|
|
|
246
247
|
// ─── Render loop ──────────────────────────────────────────────────────────────
|
|
@@ -253,14 +254,8 @@ toolbarPanel.mount(toolbarEl, {
|
|
|
253
254
|
openFile: () => openFile(),
|
|
254
255
|
saveFile: () => saveFile(),
|
|
255
256
|
parseMediaEntries,
|
|
256
|
-
getCanvasMode
|
|
257
|
-
setCanvasMode
|
|
258
|
-
// Clear gitDiffState when exiting diff mode via toolbar
|
|
259
|
-
if (canvasMode === "git-diff" && m !== "git-diff") {
|
|
260
|
-
gitDiffState = null;
|
|
261
|
-
}
|
|
262
|
-
canvasMode = m;
|
|
263
|
-
},
|
|
257
|
+
getCanvasMode,
|
|
258
|
+
setCanvasMode,
|
|
264
259
|
renderCanvas: () => renderCanvas(),
|
|
265
260
|
safeRenderRightPanel: () => safeRenderRightPanel(),
|
|
266
261
|
});
|
|
@@ -268,51 +263,41 @@ toolbarPanel.mount(toolbarEl, {
|
|
|
268
263
|
tabStrip.mount(/** @type {HTMLElement} */ (document.querySelector("#tab-strip")));
|
|
269
264
|
|
|
270
265
|
overlaysPanel.mount({
|
|
271
|
-
getCanvasMode
|
|
266
|
+
getCanvasMode,
|
|
272
267
|
isEditing,
|
|
273
268
|
renderBlockActionBar,
|
|
274
269
|
});
|
|
275
270
|
|
|
276
271
|
initBlockActionBar({
|
|
277
|
-
getCanvasMode
|
|
272
|
+
getCanvasMode,
|
|
278
273
|
navigateToComponent,
|
|
279
274
|
createFloatingContainer,
|
|
280
275
|
});
|
|
281
276
|
|
|
282
277
|
initComponentInlineEdit({ findCanvasElement });
|
|
283
|
-
initCanvasHelpers({
|
|
278
|
+
initCanvasHelpers({
|
|
279
|
+
getCanvasMode,
|
|
280
|
+
getZoom: () => activeTab.value?.session.ui.zoom ?? 1,
|
|
281
|
+
});
|
|
284
282
|
initCanvasUtils({
|
|
285
|
-
getCanvasMode
|
|
286
|
-
getZoom: () =>
|
|
283
|
+
getCanvasMode,
|
|
284
|
+
getZoom: () => activeTab.value?.session.ui.zoom ?? 1,
|
|
287
285
|
setZoomDirect: (zoom) => {
|
|
288
|
-
session = { ...session, ui: { ...session.ui, zoom } };
|
|
289
|
-
S = toFlat(doc, session);
|
|
290
286
|
if (activeTab.value) activeTab.value.session.ui.zoom = zoom;
|
|
291
287
|
},
|
|
292
288
|
renderStylebookOverlays,
|
|
293
289
|
});
|
|
294
290
|
initPanelEvents({
|
|
295
|
-
|
|
296
|
-
setState: (s) => {
|
|
297
|
-
S = s;
|
|
298
|
-
({ doc, session } = fromFlat(S));
|
|
299
|
-
},
|
|
300
|
-
getCanvasMode: () => canvasMode,
|
|
291
|
+
getCanvasMode,
|
|
301
292
|
enterInlineEdit,
|
|
302
293
|
navigateToComponent,
|
|
303
294
|
});
|
|
304
295
|
initCanvasLiveRender({
|
|
305
|
-
getCanvasMode
|
|
296
|
+
getCanvasMode,
|
|
306
297
|
});
|
|
307
298
|
initCanvasRender({
|
|
308
|
-
getCanvasMode
|
|
309
|
-
setCanvasMode
|
|
310
|
-
// Clear gitDiffState when exiting diff mode
|
|
311
|
-
if (canvasMode === "git-diff" && mode !== "git-diff") {
|
|
312
|
-
gitDiffState = null;
|
|
313
|
-
}
|
|
314
|
-
canvasMode = mode;
|
|
315
|
-
},
|
|
299
|
+
getCanvasMode,
|
|
300
|
+
setCanvasMode,
|
|
316
301
|
openFileFromTree,
|
|
317
302
|
exportFile,
|
|
318
303
|
gitDiffState,
|
|
@@ -328,6 +313,7 @@ effect(() => {
|
|
|
328
313
|
if (!tab) return;
|
|
329
314
|
void tab.doc.document;
|
|
330
315
|
void tab.doc.mode;
|
|
316
|
+
void tab.session.ui.canvasMode;
|
|
331
317
|
void tab.session.ui.editingFunction;
|
|
332
318
|
void tab.session.ui.featureToggles;
|
|
333
319
|
void tab.session.ui.settingsTab;
|
|
@@ -349,16 +335,14 @@ effect(() => {
|
|
|
349
335
|
|
|
350
336
|
rightPanelMod.mount({
|
|
351
337
|
navigateToComponent,
|
|
352
|
-
getCanvasMode
|
|
338
|
+
getCanvasMode,
|
|
353
339
|
renderCanvas: () => renderCanvas(),
|
|
354
340
|
updateForcedPseudoPreview,
|
|
355
341
|
});
|
|
356
342
|
|
|
357
343
|
leftPanelMod.mount({
|
|
358
|
-
getCanvasMode
|
|
359
|
-
setCanvasMode
|
|
360
|
-
canvasMode = mode;
|
|
361
|
-
},
|
|
344
|
+
getCanvasMode,
|
|
345
|
+
setCanvasMode,
|
|
362
346
|
renderImportsTemplate,
|
|
363
347
|
renderFilesTemplate,
|
|
364
348
|
renderSignalsTemplate,
|
|
@@ -383,14 +367,14 @@ registerRenderer("leftPanel", () => leftPanelMod.render());
|
|
|
383
367
|
registerRenderer("canvas", () => renderCanvas());
|
|
384
368
|
registerRenderer("rightPanel", () => rightPanelMod.render());
|
|
385
369
|
registerRenderer("overlays", () => overlaysPanel.render());
|
|
386
|
-
setStatusbarRenderer(() => renderStatusbar(
|
|
370
|
+
setStatusbarRenderer(() => renderStatusbar());
|
|
387
371
|
mountStatusbar();
|
|
388
372
|
mountActivityBar();
|
|
389
373
|
|
|
390
374
|
// Clicking on the canvas-wrap background (outside any canvas panel) deselects the current element
|
|
391
375
|
canvasWrap.addEventListener("click", (/** @type {any} */ e) => {
|
|
392
376
|
if (e.target !== canvasWrap && e.target !== view.panzoomWrap) return;
|
|
393
|
-
if (!
|
|
377
|
+
if (!activeTab.value?.session.selection) return;
|
|
394
378
|
activeTab.value.session.selection = null;
|
|
395
379
|
});
|
|
396
380
|
|
|
@@ -398,80 +382,7 @@ function safeRenderRightPanel() {
|
|
|
398
382
|
rightPanelMod.render();
|
|
399
383
|
}
|
|
400
384
|
|
|
401
|
-
//
|
|
402
|
-
setGetStateFn(() => S);
|
|
403
|
-
setUpdateFn(function _update(/** @type {any} */ newState) {
|
|
404
|
-
const prevDoc = S.document;
|
|
405
|
-
const prevSel = S.selection;
|
|
406
|
-
const prevUi = S.ui;
|
|
407
|
-
S = newState;
|
|
408
|
-
|
|
409
|
-
// Keep doc/session slices in sync with flat S
|
|
410
|
-
({ doc, session } = fromFlat(S));
|
|
411
|
-
|
|
412
|
-
// Sync into reactive tab so effects fire
|
|
413
|
-
const tab = activeTab.value;
|
|
414
|
-
if (tab) {
|
|
415
|
-
tab.doc.document = S.document;
|
|
416
|
-
tab.doc.dirty = S.dirty;
|
|
417
|
-
tab.doc.mode = S.mode;
|
|
418
|
-
tab.doc.handlersSource = S.handlersSource;
|
|
419
|
-
tab.doc.content.frontmatter = S.content?.frontmatter ?? {};
|
|
420
|
-
tab.session.selection = S.selection;
|
|
421
|
-
tab.session.hover = S.hover;
|
|
422
|
-
tab.session.clipboard = S.clipboard ?? null;
|
|
423
|
-
for (const [k, v] of Object.entries(S.ui || {})) {
|
|
424
|
-
/** @type {any} */ (tab.session.ui)[k] = v;
|
|
425
|
-
}
|
|
426
|
-
tab.session.canvas.status = S.canvas?.status ?? "idle";
|
|
427
|
-
tab.session.canvas.scope = S.canvas?.scope ?? null;
|
|
428
|
-
tab.session.canvas.error = S.canvas?.error ?? null;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (prevUi?.activeMedia !== S.ui?.activeMedia) {
|
|
432
|
-
updateActivePanelHeaders();
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
runPostRenderHooks(prevDoc, prevSel);
|
|
436
|
-
runUpdateMiddleware(S);
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
// Register session dispatch — lightweight path for selection/hover/ui changes
|
|
440
|
-
setUpdateSessionFn(function _updateSession(/** @type {any} */ patch) {
|
|
441
|
-
const prev = session;
|
|
442
|
-
session = { ...session, ...patch };
|
|
443
|
-
if (patch.ui) {
|
|
444
|
-
session.ui = { ...prev.ui, ...patch.ui };
|
|
445
|
-
}
|
|
446
|
-
if (patch.canvas) {
|
|
447
|
-
session.canvas = { ...prev.canvas, ...patch.canvas };
|
|
448
|
-
}
|
|
449
|
-
S = toFlat(doc, session);
|
|
450
|
-
|
|
451
|
-
if (prev.ui?.activeMedia !== session.ui?.activeMedia) {
|
|
452
|
-
updateActivePanelHeaders();
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Process pending inline edit when canvas becomes ready
|
|
456
|
-
const canvasChanged = prev.canvas !== session.canvas;
|
|
457
|
-
if (canvasChanged && session.canvas.status === "ready" && session.ui?.pendingInlineEdit) {
|
|
458
|
-
const { path, mediaName: mn } = session.ui.pendingInlineEdit;
|
|
459
|
-
updateUi("pendingInlineEdit", null);
|
|
460
|
-
const targetPanel =
|
|
461
|
-
canvasPanels.find((/** @type {any} */ p) => p.mediaName === mn) || canvasPanels[0];
|
|
462
|
-
if (targetPanel) {
|
|
463
|
-
const el = findCanvasElement(path, targetPanel.canvas);
|
|
464
|
-
if (el) enterComponentInlineEdit(el, path);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
runPostRenderHooks(doc.document, prev.selection);
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
// Register post-render hook for pseudo-state preview
|
|
472
|
-
addPostRenderHook(() => updateForcedPseudoPreview());
|
|
473
|
-
|
|
474
|
-
// Now that renderers and update are registered, bootstrap
|
|
385
|
+
// Now that renderers are registered, bootstrap
|
|
475
386
|
registerFunctionCompletions();
|
|
476
387
|
|
|
477
388
|
const _openParam = new URLSearchParams(location.search).get("open");
|
|
@@ -544,32 +455,30 @@ if (_openParam) {
|
|
|
544
455
|
const fileRelPath = _fileParam || siteCtx.fileRelPath || _openParam;
|
|
545
456
|
const content = await platform.readFile(fileRelPath);
|
|
546
457
|
if (content) {
|
|
547
|
-
let parsedDoc;
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
458
|
+
let parsedDoc, frontmatter;
|
|
459
|
+
const isMd = fileRelPath.endsWith(".md");
|
|
460
|
+
if (isMd) {
|
|
461
|
+
const result = await loadMarkdown(content);
|
|
462
|
+
parsedDoc = result.document;
|
|
463
|
+
frontmatter = result.frontmatter;
|
|
553
464
|
} else {
|
|
554
465
|
parsedDoc = JSON.parse(content);
|
|
555
|
-
S = createState(parsedDoc);
|
|
556
|
-
S.documentPath = fileRelPath;
|
|
557
|
-
}
|
|
558
|
-
S.dirty = false;
|
|
559
|
-
S.ui = { ...S.ui, leftTab: "files" };
|
|
560
|
-
({ doc, session } = fromFlat(S));
|
|
561
|
-
|
|
562
|
-
// Sync into reactive tab
|
|
563
|
-
const tab = activeTab.value;
|
|
564
|
-
if (tab) {
|
|
565
|
-
tab.doc.document = S.document;
|
|
566
|
-
tab.doc.mode = S.mode;
|
|
567
|
-
tab.doc.dirty = false;
|
|
568
|
-
tab.doc.content = S.content || { frontmatter: {} };
|
|
569
|
-
/** @type {any} */ (tab).documentPath = fileRelPath;
|
|
570
|
-
tab.session.ui.leftTab = "files";
|
|
571
466
|
}
|
|
572
467
|
|
|
468
|
+
// Open in a tab (replaces initial tab)
|
|
469
|
+
const { closeTab } = await import("./workspace/workspace.js");
|
|
470
|
+
closeTab("initial");
|
|
471
|
+
openTab({
|
|
472
|
+
id: fileRelPath,
|
|
473
|
+
documentPath: fileRelPath,
|
|
474
|
+
document: parsedDoc,
|
|
475
|
+
frontmatter,
|
|
476
|
+
sourceFormat: isMd ? "md" : null,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
if (isMd && activeTab.value) activeTab.value.doc.mode = "content";
|
|
480
|
+
view.leftTab = "files";
|
|
481
|
+
|
|
573
482
|
render();
|
|
574
483
|
statusMessage(`Opened ${_openParam}`);
|
|
575
484
|
}
|
|
@@ -609,35 +518,6 @@ function renderLeftPanel() {
|
|
|
609
518
|
|
|
610
519
|
// ─── Source/Function editors: delegated to panels/editors.js ─────────────────
|
|
611
520
|
|
|
612
|
-
// ─── Toolbar (delegated to panels/toolbar.js) ────────────────────────────────
|
|
613
|
-
|
|
614
|
-
function renderToolbar() {
|
|
615
|
-
toolbarPanel.render();
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// ─── File Operations (delegated to file-ops.js) ─────────────────────────────
|
|
619
|
-
|
|
620
|
-
function fileOpsCtx() {
|
|
621
|
-
return {
|
|
622
|
-
S,
|
|
623
|
-
commit: (/** @type {any} */ ns) => {
|
|
624
|
-
S = ns;
|
|
625
|
-
({ doc, session } = fromFlat(S));
|
|
626
|
-
render();
|
|
627
|
-
},
|
|
628
|
-
renderToolbar,
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
function openFile() {
|
|
632
|
-
return _openFile(fileOpsCtx());
|
|
633
|
-
}
|
|
634
|
-
function saveFile() {
|
|
635
|
-
return _saveFile(fileOpsCtx());
|
|
636
|
-
}
|
|
637
|
-
function exportFile() {
|
|
638
|
-
return _exportFile(fileOpsCtx());
|
|
639
|
-
}
|
|
640
|
-
|
|
641
521
|
// ─── File tree (delegated to files.js) ───────────────────────────────────────
|
|
642
522
|
|
|
643
523
|
function loadProject() {
|
|
@@ -645,11 +525,6 @@ function loadProject() {
|
|
|
645
525
|
}
|
|
646
526
|
function openProject() {
|
|
647
527
|
return _openProject({
|
|
648
|
-
S,
|
|
649
|
-
commit: (/** @type {any} */ ns) => {
|
|
650
|
-
S = ns;
|
|
651
|
-
({ doc, session } = fromFlat(S));
|
|
652
|
-
},
|
|
653
528
|
renderActivityBar: () => renderActivityBar(),
|
|
654
529
|
renderLeftPanel,
|
|
655
530
|
});
|
|
@@ -663,12 +538,7 @@ function openFileFromTree(/** @type {any} */ path) {
|
|
|
663
538
|
|
|
664
539
|
// ─── Keyboard shortcuts ───────────────────────────────────────────────────────
|
|
665
540
|
initShortcuts(() => ({
|
|
666
|
-
|
|
667
|
-
setS: (ns) => {
|
|
668
|
-
S = ns;
|
|
669
|
-
({ doc, session } = fromFlat(S));
|
|
670
|
-
},
|
|
671
|
-
canvasMode,
|
|
541
|
+
canvasMode: getCanvasMode(),
|
|
672
542
|
panX: view.panX,
|
|
673
543
|
panY: view.panY,
|
|
674
544
|
setPan: (x, y) => {
|
|
@@ -700,21 +570,23 @@ initShortcuts(() => ({
|
|
|
700
570
|
const AUTO_SAVE_DELAY = 2000;
|
|
701
571
|
|
|
702
572
|
function scheduleAutosave() {
|
|
703
|
-
|
|
573
|
+
const tab = activeTab.value;
|
|
574
|
+
if (!tab?.fileHandle || !tab.doc.dirty) return;
|
|
704
575
|
clearTimeout(view.autosaveTimer);
|
|
705
576
|
view.autosaveTimer = setTimeout(async () => {
|
|
706
|
-
|
|
577
|
+
const t = activeTab.value;
|
|
578
|
+
if (t?.fileHandle && t.doc.dirty && "createWritable" in t.fileHandle) {
|
|
707
579
|
try {
|
|
708
|
-
const writable = await
|
|
709
|
-
await writable.write(JSON.stringify(
|
|
580
|
+
const writable = await t.fileHandle.createWritable();
|
|
581
|
+
await writable.write(JSON.stringify(t.doc.document, null, 2));
|
|
710
582
|
await writable.close();
|
|
711
|
-
|
|
583
|
+
t.doc.dirty = false;
|
|
712
584
|
statusMessage("Auto-saved");
|
|
713
585
|
} catch {}
|
|
714
586
|
}
|
|
715
587
|
}, AUTO_SAVE_DELAY);
|
|
716
588
|
}
|
|
717
589
|
|
|
718
|
-
|
|
719
|
-
if (
|
|
590
|
+
effect(() => {
|
|
591
|
+
if (activeTab.value?.doc.dirty) scheduleAutosave();
|
|
720
592
|
});
|
package/src/tabs/tab.js
CHANGED
|
@@ -2,8 +2,8 @@ import { reactive, effectScope } from "../reactivity.js";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @typedef {{
|
|
5
|
-
* leftTab: string;
|
|
6
5
|
* rightTab: string;
|
|
6
|
+
* canvasMode: string;
|
|
7
7
|
* zoom: number;
|
|
8
8
|
* activeMedia: string | null;
|
|
9
9
|
* activeSelector: string | null;
|
|
@@ -45,6 +45,7 @@ import { reactive, effectScope } from "../reactivity.js";
|
|
|
45
45
|
* document: Record<string, any>;
|
|
46
46
|
* content: { frontmatter: Record<string, unknown> };
|
|
47
47
|
* mode: string;
|
|
48
|
+
* sourceFormat: string | null;
|
|
48
49
|
* handlersSource: string | null;
|
|
49
50
|
* dirty: boolean;
|
|
50
51
|
* };
|
|
@@ -71,8 +72,8 @@ import { reactive, effectScope } from "../reactivity.js";
|
|
|
71
72
|
/** @returns {TabUi} */
|
|
72
73
|
function createDefaultUi() {
|
|
73
74
|
return {
|
|
74
|
-
leftTab: "layers",
|
|
75
75
|
rightTab: "properties",
|
|
76
|
+
canvasMode: "design",
|
|
76
77
|
zoom: 1,
|
|
77
78
|
activeMedia: null,
|
|
78
79
|
activeSelector: null,
|
|
@@ -106,10 +107,18 @@ function createDefaultUi() {
|
|
|
106
107
|
* fileHandle?: FileSystemFileHandle | null;
|
|
107
108
|
* document: Record<string, any>;
|
|
108
109
|
* frontmatter?: Record<string, unknown>;
|
|
110
|
+
* sourceFormat?: string | null;
|
|
109
111
|
* }} opts
|
|
110
112
|
* @returns {Tab}
|
|
111
113
|
*/
|
|
112
|
-
export function createTab({
|
|
114
|
+
export function createTab({
|
|
115
|
+
id,
|
|
116
|
+
documentPath = null,
|
|
117
|
+
fileHandle = null,
|
|
118
|
+
document,
|
|
119
|
+
frontmatter,
|
|
120
|
+
sourceFormat = null,
|
|
121
|
+
}) {
|
|
113
122
|
const scope = effectScope();
|
|
114
123
|
|
|
115
124
|
const tab = /** @type {Tab} */ (
|
|
@@ -120,8 +129,14 @@ export function createTab({ id, documentPath = null, fileHandle = null, document
|
|
|
120
129
|
scope,
|
|
121
130
|
doc: reactive({
|
|
122
131
|
document,
|
|
132
|
+
sourceFormat,
|
|
123
133
|
content: { frontmatter: frontmatter || {} },
|
|
124
|
-
mode:
|
|
134
|
+
mode:
|
|
135
|
+
sourceFormat === "md"
|
|
136
|
+
? "content"
|
|
137
|
+
: documentPath?.endsWith(".md")
|
|
138
|
+
? "content"
|
|
139
|
+
: "component",
|
|
125
140
|
handlersSource: null,
|
|
126
141
|
dirty: false,
|
|
127
142
|
}),
|
package/src/tabs/transact.js
CHANGED
|
@@ -26,8 +26,12 @@ const HISTORY_LIMIT = 100;
|
|
|
26
26
|
export function transactDoc(tab, mutationFn, { skipHistory = false } = {}) {
|
|
27
27
|
mutationFn(tab);
|
|
28
28
|
|
|
29
|
+
// Replace the document root reference so effects tracking tab.doc.document re-trigger.
|
|
30
|
+
// Nested objects are shared — the mutation already modified them in place.
|
|
31
|
+
const raw = toRaw(tab.doc.document);
|
|
32
|
+
tab.doc.document = { ...raw };
|
|
33
|
+
|
|
29
34
|
if (!skipHistory) {
|
|
30
|
-
const raw = toRaw(tab.doc.document);
|
|
31
35
|
const snapshot = {
|
|
32
36
|
document: structuredClone(raw),
|
|
33
37
|
selection: tab.session.selection ? [...tab.session.selection] : null,
|
|
@@ -270,6 +274,20 @@ export function mutateUpdateMediaNestedStyle(tab, path, mediaName, selector, pro
|
|
|
270
274
|
if (Object.keys(node.style).length === 0) delete node.style;
|
|
271
275
|
}
|
|
272
276
|
|
|
277
|
+
/**
|
|
278
|
+
* @param {Tab} tab
|
|
279
|
+
* @param {JxPath} path
|
|
280
|
+
* @param {Record<string, any> | undefined} style
|
|
281
|
+
*/
|
|
282
|
+
export function mutateReplaceStyle(tab, path, style) {
|
|
283
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
284
|
+
if (style && Object.keys(style).length > 0) {
|
|
285
|
+
node.style = style;
|
|
286
|
+
} else {
|
|
287
|
+
delete node.style;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
273
291
|
/**
|
|
274
292
|
* @param {Tab} tab
|
|
275
293
|
* @param {string} name
|
package/src/ui/color-selector.js
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
import { LitElement, html, nothing } from "lit";
|
|
14
14
|
import { html as litHtml } from "lit-html";
|
|
15
15
|
import { live } from "lit-html/directives/live.js";
|
|
16
|
-
import {
|
|
16
|
+
import { debouncedStyleCommit } from "../store.js";
|
|
17
|
+
import { activeTab } from "../workspace/workspace.js";
|
|
17
18
|
import { getEffectiveStyle } from "../site-context.js";
|
|
18
19
|
import { kebabToLabel } from "../utils/studio-utils.js";
|
|
19
20
|
|
|
@@ -21,8 +22,7 @@ import { kebabToLabel } from "../utils/studio-utils.js";
|
|
|
21
22
|
|
|
22
23
|
/** Extract --color-* CSS custom properties from the effective (site + document) style. */
|
|
23
24
|
function getColorVars() {
|
|
24
|
-
const
|
|
25
|
-
const style = getEffectiveStyle(S?.document?.style);
|
|
25
|
+
const style = getEffectiveStyle(activeTab.value?.doc.document?.style);
|
|
26
26
|
if (!style) return [];
|
|
27
27
|
const vars = [];
|
|
28
28
|
for (const [k, v] of Object.entries(style)) {
|
|
@@ -46,8 +46,7 @@ function resolveColorForDisplay(/** @type {any} */ val) {
|
|
|
46
46
|
if (!val) return "transparent";
|
|
47
47
|
const m = val.match(/^var\((--[^)]+)\)$/);
|
|
48
48
|
if (m) {
|
|
49
|
-
const
|
|
50
|
-
const style = getEffectiveStyle(S?.document?.style);
|
|
49
|
+
const style = getEffectiveStyle(activeTab.value?.doc.document?.style);
|
|
51
50
|
const resolved = style?.[m[1]];
|
|
52
51
|
if (typeof resolved === "string") return resolved;
|
|
53
52
|
return "transparent";
|
package/src/view.js
CHANGED