@jxsuite/studio 0.19.0 → 0.21.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.css +98 -98
- package/dist/studio.js +13476 -12330
- package/dist/studio.js.map +152 -148
- package/dist/workers/editor.worker.js +79 -79
- package/dist/workers/json.worker.js +109 -109
- package/dist/workers/ts.worker.js +82 -82
- package/package.json +19 -7
- package/src/browse/browse-modal.js +16 -13
- package/src/browse/browse.js +44 -35
- package/src/canvas/canvas-diff.js +24 -16
- package/src/canvas/canvas-helpers.js +24 -19
- package/src/canvas/canvas-live-render.js +113 -39
- package/src/canvas/canvas-render.js +119 -73
- package/src/canvas/canvas-utils.js +51 -21
- package/src/canvas/nested-site-style.js +50 -0
- package/src/editor/component-inline-edit.js +14 -13
- package/src/editor/content-inline-edit.js +22 -10
- package/src/editor/context-menu.js +28 -14
- package/src/editor/convert-targets.js +60 -0
- package/src/editor/convert-to-component.js +20 -19
- package/src/editor/convert-to-repeater.js +226 -0
- package/src/editor/inline-edit.js +69 -28
- package/src/editor/insertion-helper.js +16 -12
- package/src/editor/shortcuts.js +14 -4
- package/src/editor/slash-menu.js +40 -19
- package/src/files/components.js +1 -1
- package/src/files/file-ops.js +37 -16
- package/src/files/files.js +322 -68
- package/src/github/github-auth.js +122 -0
- package/src/github/github-publish.js +126 -0
- package/src/markdown/md-convert.js +172 -107
- package/src/new-project/new-project-modal.js +204 -0
- package/src/panels/activity-bar.js +27 -25
- package/src/panels/ai-panel.js +406 -0
- package/src/panels/block-action-bar.js +77 -28
- package/src/panels/canvas-dnd.js +8 -2
- package/src/panels/data-explorer.js +19 -13
- package/src/panels/dnd.js +144 -92
- package/src/panels/editors.js +55 -35
- package/src/panels/elements-panel.js +33 -19
- package/src/panels/events-panel.js +15 -10
- package/src/panels/git-panel.js +475 -99
- package/src/panels/head-panel.js +121 -81
- package/src/panels/imports-panel.js +94 -65
- package/src/panels/layers-panel.js +64 -43
- package/src/panels/left-panel.js +105 -48
- package/src/panels/overlays.js +1 -0
- package/src/panels/panel-events.js +45 -30
- package/src/panels/preview-render.js +23 -18
- package/src/panels/properties-panel.js +378 -257
- package/src/panels/pseudo-preview.js +2 -2
- package/src/panels/quick-search.js +11 -9
- package/src/panels/right-panel.js +42 -16
- package/src/panels/shared.js +4 -4
- package/src/panels/signals-panel.js +415 -166
- package/src/panels/statusbar.js +10 -6
- package/src/panels/style-inputs.js +67 -35
- package/src/panels/style-panel.js +239 -134
- package/src/panels/style-utils.js +34 -16
- package/src/panels/stylebook-layers-panel.js +12 -10
- package/src/panels/stylebook-panel.js +134 -66
- package/src/panels/tab-strip.js +5 -2
- package/src/panels/toolbar.js +333 -109
- package/src/panels/welcome-screen.js +121 -0
- package/src/platform.js +2 -4
- package/src/platforms/devserver.js +113 -7
- package/src/resize-edges.js +98 -0
- package/src/services/cem-export.js +12 -7
- package/src/services/code-services.js +30 -12
- package/src/settings/content-types-editor.js +8 -8
- package/src/settings/css-vars-editor.js +30 -24
- package/src/settings/defs-editor.js +12 -7
- package/src/settings/general-settings.js +85 -3
- package/src/settings/head-editor.js +30 -24
- package/src/settings/schema-field-ui.js +58 -39
- package/src/settings/settings-modal.js +12 -9
- package/src/site-context.js +41 -31
- package/src/state.js +70 -29
- package/src/store.js +36 -35
- package/src/studio.js +120 -90
- package/src/tabs/tab.js +67 -43
- package/src/tabs/transact.js +23 -13
- package/src/ui/button-group.js +24 -24
- package/src/ui/color-selector.js +28 -23
- package/src/ui/field-row.js +3 -3
- package/src/ui/icons.js +2 -2
- package/src/ui/layers.js +31 -1
- package/src/ui/media-picker.js +7 -7
- package/src/ui/spectrum.js +5 -1
- package/src/ui/unit-selector.js +18 -16
- package/src/ui/value-selector.js +21 -15
- package/src/ui/widgets.js +22 -19
- package/src/utils/canvas-media.js +5 -4
- package/src/utils/edit-display.js +32 -26
- package/src/utils/google-fonts.js +11 -11
- package/src/utils/inherited-style.js +9 -8
- package/src/utils/studio-utils.js +18 -18
- package/src/view.js +58 -1
- package/src/workspace/workspace.js +29 -7
package/src/studio.js
CHANGED
|
@@ -15,10 +15,12 @@ import {
|
|
|
15
15
|
render,
|
|
16
16
|
projectState,
|
|
17
17
|
setProjectState,
|
|
18
|
+
requireProjectState,
|
|
18
19
|
updateUi,
|
|
20
|
+
initShellRefs,
|
|
19
21
|
} from "./store.js";
|
|
20
22
|
|
|
21
|
-
import { activeTab, openTab,
|
|
23
|
+
import { activeTab, openTab, closeAllTabs } from "./workspace/workspace.js";
|
|
22
24
|
import { transactDoc, mutateUpdateDef, mutateUpdateProperty } from "./tabs/transact.js";
|
|
23
25
|
import { effect } from "./reactivity.js";
|
|
24
26
|
|
|
@@ -45,6 +47,7 @@ import {
|
|
|
45
47
|
openFileInTab,
|
|
46
48
|
openHomePage,
|
|
47
49
|
setupTreeKeyboard,
|
|
50
|
+
registerFileTreeDnD,
|
|
48
51
|
loadDirectory,
|
|
49
52
|
} from "./files/files.js";
|
|
50
53
|
import { renderImportsTemplate } from "./panels/imports-panel.js";
|
|
@@ -54,6 +57,7 @@ import { exportCemManifest as _exportCemManifest } from "./services/cem-export.j
|
|
|
54
57
|
import { registerPlatform, getPlatform, hasPlatform } from "./platform.js";
|
|
55
58
|
import { parseMediaEntries } from "./utils/canvas-media.js";
|
|
56
59
|
import { createDevServerPlatform } from "./platforms/devserver.js";
|
|
60
|
+
import { mountResizeEdges } from "./resize-edges.js";
|
|
57
61
|
import { codeService } from "./services/code-services.js";
|
|
58
62
|
import { defCategory, defBadgeLabel, renderSignalsTemplate } from "./panels/signals-panel.js";
|
|
59
63
|
import { loadComponentRegistry } from "./files/components.js";
|
|
@@ -87,6 +91,9 @@ import { updateForcedPseudoPreview } from "./panels/pseudo-preview.js";
|
|
|
87
91
|
import { initPanelEvents } from "./panels/panel-events.js";
|
|
88
92
|
import { initQuickSearch } from "./panels/quick-search.js";
|
|
89
93
|
import { addRecentProject } from "./recent-projects.js";
|
|
94
|
+
import { initWelcome } from "./panels/welcome-screen.js";
|
|
95
|
+
import { openNewProjectModal } from "./new-project/new-project-modal.js";
|
|
96
|
+
import { cloneRepository } from "./panels/git-panel.js";
|
|
90
97
|
|
|
91
98
|
// ─── Globals ──────────────────────────────────────────────────────────────────
|
|
92
99
|
// These mutable variables are local to studio.js for now. As sections are extracted
|
|
@@ -105,12 +112,12 @@ function setCanvasMode(mode) {
|
|
|
105
112
|
if (tab) tab.session.ui.canvasMode = mode;
|
|
106
113
|
}
|
|
107
114
|
|
|
108
|
-
/** @type {
|
|
115
|
+
/** @type {import("./canvas/canvas-render.js").GitDiffState | null} */
|
|
109
116
|
let gitDiffState = null;
|
|
110
117
|
|
|
111
118
|
// ─── Component registry ───────────────────────────────────────────────────────
|
|
112
119
|
|
|
113
|
-
/** @param {
|
|
120
|
+
/** @param {string} componentPath */
|
|
114
121
|
async function navigateToComponent(componentPath) {
|
|
115
122
|
try {
|
|
116
123
|
const platform = getPlatform();
|
|
@@ -135,7 +142,7 @@ async function navigateToComponent(componentPath) {
|
|
|
135
142
|
// Load the component
|
|
136
143
|
tab.doc.document = parsed;
|
|
137
144
|
tab.doc.dirty = false;
|
|
138
|
-
tab.doc.mode = /** @type {
|
|
145
|
+
tab.doc.mode = /** @type {string} */ (/** @type {unknown} */ (null));
|
|
139
146
|
tab.doc.sourceFormat = null;
|
|
140
147
|
tab.documentPath = componentPath;
|
|
141
148
|
tab.session.selection = null;
|
|
@@ -145,8 +152,8 @@ async function navigateToComponent(componentPath) {
|
|
|
145
152
|
|
|
146
153
|
render();
|
|
147
154
|
statusMessage(`Editing component: ${parsed.tagName || componentPath}`);
|
|
148
|
-
} catch (/** @type {
|
|
149
|
-
const err = /** @type {
|
|
155
|
+
} catch (/** @type {unknown} */ e) {
|
|
156
|
+
const err = /** @type {Error} */ (e);
|
|
150
157
|
statusMessage(`Error: ${err.message}`);
|
|
151
158
|
}
|
|
152
159
|
}
|
|
@@ -158,21 +165,23 @@ async function navigateBack() {
|
|
|
158
165
|
try {
|
|
159
166
|
const platform = getPlatform();
|
|
160
167
|
await platform.writeFile(tab.documentPath, serializeDocument(tab));
|
|
161
|
-
} catch (/** @type {
|
|
162
|
-
const err = /** @type {
|
|
168
|
+
} catch (/** @type {unknown} */ e) {
|
|
169
|
+
const err = /** @type {Error} */ (e);
|
|
163
170
|
statusMessage(`Save error: ${err.message}`);
|
|
164
171
|
}
|
|
165
172
|
}
|
|
166
173
|
|
|
167
174
|
// Pop the stack
|
|
168
|
-
const frame = /** @type {
|
|
175
|
+
const frame = /** @type {Record<string, unknown> | undefined} */ (
|
|
176
|
+
tab.session.documentStack.pop()
|
|
177
|
+
);
|
|
169
178
|
if (!frame) return;
|
|
170
|
-
tab.doc.document = frame.document;
|
|
171
|
-
tab.doc.dirty = frame.dirty;
|
|
172
|
-
tab.doc.mode = frame.mode;
|
|
173
|
-
tab.doc.sourceFormat = frame.sourceFormat;
|
|
174
|
-
tab.documentPath = frame.documentPath;
|
|
175
|
-
tab.session.selection = frame.selection;
|
|
179
|
+
tab.doc.document = /** @type {JxMutableNode} */ (frame.document);
|
|
180
|
+
tab.doc.dirty = /** @type {boolean} */ (frame.dirty);
|
|
181
|
+
tab.doc.mode = /** @type {string} */ (frame.mode);
|
|
182
|
+
tab.doc.sourceFormat = /** @type {string | null} */ (frame.sourceFormat);
|
|
183
|
+
tab.documentPath = /** @type {string | null} */ (frame.documentPath);
|
|
184
|
+
tab.session.selection = /** @type {JxPath | null} */ (frame.selection);
|
|
176
185
|
view.leftTab = "layers";
|
|
177
186
|
|
|
178
187
|
render();
|
|
@@ -188,20 +197,20 @@ async function navigateToLevel(targetIndex) {
|
|
|
188
197
|
try {
|
|
189
198
|
const platform = getPlatform();
|
|
190
199
|
await platform.writeFile(tab.documentPath, serializeDocument(tab));
|
|
191
|
-
} catch (/** @type {
|
|
192
|
-
const err = /** @type {
|
|
200
|
+
} catch (/** @type {unknown} */ e) {
|
|
201
|
+
const err = /** @type {Error} */ (e);
|
|
193
202
|
statusMessage(`Save error: ${err.message}`);
|
|
194
203
|
}
|
|
195
204
|
}
|
|
196
205
|
|
|
197
|
-
const frame = /** @type {
|
|
206
|
+
const frame = /** @type {DocumentStackEntry} */ (stack[targetIndex]);
|
|
198
207
|
tab.session.documentStack = stack.slice(0, targetIndex);
|
|
199
|
-
tab.doc.document = frame.document;
|
|
200
|
-
tab.doc.dirty = frame.dirty;
|
|
201
|
-
tab.doc.mode = frame.mode;
|
|
202
|
-
tab.doc.sourceFormat = frame.sourceFormat;
|
|
203
|
-
tab.documentPath = frame.documentPath;
|
|
204
|
-
tab.session.selection = frame.selection;
|
|
208
|
+
tab.doc.document = /** @type {JxMutableNode} */ (frame.document);
|
|
209
|
+
tab.doc.dirty = /** @type {boolean} */ (frame.dirty);
|
|
210
|
+
tab.doc.mode = /** @type {string} */ (frame.mode);
|
|
211
|
+
tab.doc.sourceFormat = /** @type {string | null} */ (frame.sourceFormat);
|
|
212
|
+
tab.documentPath = /** @type {string | null} */ (frame.documentPath);
|
|
213
|
+
tab.session.selection = /** @type {JxPath | null} */ (frame.selection);
|
|
205
214
|
view.leftTab = "layers";
|
|
206
215
|
|
|
207
216
|
render();
|
|
@@ -210,23 +219,33 @@ async function navigateToLevel(targetIndex) {
|
|
|
210
219
|
|
|
211
220
|
async function closeFunctionEditor() {
|
|
212
221
|
const tab = activeTab.value;
|
|
213
|
-
const editing =
|
|
214
|
-
|
|
222
|
+
const editing =
|
|
223
|
+
/** @type {{ type: string; defName?: string; path?: JxPath; eventKey?: string } | null} */ (
|
|
224
|
+
tab?.session.ui.editingFunction
|
|
225
|
+
);
|
|
226
|
+
if (!editing || !tab) return;
|
|
215
227
|
if (view.functionEditor) {
|
|
216
228
|
const currentCode = view.functionEditor.getValue();
|
|
217
229
|
const minResult = await codeService("minify", { code: currentCode });
|
|
218
230
|
const bodyToStore = minResult?.code ?? currentCode;
|
|
219
231
|
if (editing.type === "def") {
|
|
220
|
-
transactDoc(tab, (t) =>
|
|
232
|
+
transactDoc(tab, (t) =>
|
|
233
|
+
mutateUpdateDef(t, /** @type {string} */ (editing.defName), { body: bodyToStore }),
|
|
234
|
+
);
|
|
221
235
|
} else if (editing.type === "event") {
|
|
222
|
-
const node = getNodeAtPath(tab.doc.document, editing.path);
|
|
223
|
-
const current = node?.[editing.eventKey] || {};
|
|
236
|
+
const node = getNodeAtPath(tab.doc.document, /** @type {JxPath} */ (editing.path));
|
|
237
|
+
const current = node?.[/** @type {string} */ (editing.eventKey)] || {};
|
|
224
238
|
transactDoc(tab, (t) =>
|
|
225
|
-
mutateUpdateProperty(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
239
|
+
mutateUpdateProperty(
|
|
240
|
+
t,
|
|
241
|
+
/** @type {JxPath} */ (editing.path),
|
|
242
|
+
/** @type {string} */ (editing.eventKey),
|
|
243
|
+
{
|
|
244
|
+
.../** @type {object} */ (current),
|
|
245
|
+
$prototype: "Function",
|
|
246
|
+
body: bodyToStore,
|
|
247
|
+
},
|
|
248
|
+
),
|
|
230
249
|
);
|
|
231
250
|
}
|
|
232
251
|
view.functionEditor.dispose();
|
|
@@ -243,10 +262,12 @@ document.body.appendChild(datalistHost);
|
|
|
243
262
|
litRender(
|
|
244
263
|
html`
|
|
245
264
|
<datalist id="tag-names">
|
|
246
|
-
${webdata.allTags.map((/** @type {
|
|
265
|
+
${webdata.allTags.map((/** @type {string} */ tag) => html`<option value=${tag}></option>`)}
|
|
247
266
|
</datalist>
|
|
248
267
|
<datalist id="css-props">
|
|
249
|
-
${webdata.cssProps.map(
|
|
268
|
+
${webdata.cssProps.map(
|
|
269
|
+
(/** @type {string[]} */ [name]) => html`<option value=${name}></option>`,
|
|
270
|
+
)}
|
|
250
271
|
</datalist>
|
|
251
272
|
`,
|
|
252
273
|
datalistHost,
|
|
@@ -263,20 +284,12 @@ if (!hasPlatform()) {
|
|
|
263
284
|
registerPlatform(createDevServerPlatform());
|
|
264
285
|
}
|
|
265
286
|
|
|
266
|
-
|
|
267
|
-
tagName: "div",
|
|
268
|
-
style: { padding: "2rem", fontFamily: "system-ui, sans-serif" },
|
|
269
|
-
children: [
|
|
270
|
-
{ tagName: "h1", textContent: "New Component" },
|
|
271
|
-
{ tagName: "p", textContent: "Open a Jx file or start editing." },
|
|
272
|
-
],
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
// Create the initial reactive tab — the canonical state container.
|
|
276
|
-
openTab({ id: "initial", document: structuredClone(EMPTY_DOC) });
|
|
287
|
+
mountResizeEdges();
|
|
277
288
|
|
|
278
289
|
// ─── Render loop ──────────────────────────────────────────────────────────────
|
|
279
290
|
|
|
291
|
+
initShellRefs();
|
|
292
|
+
|
|
280
293
|
// Mount extracted panel modules
|
|
281
294
|
toolbarPanel.mount(toolbarEl, {
|
|
282
295
|
navigateBack: () => navigateBack(),
|
|
@@ -338,25 +351,38 @@ initCanvasRender({
|
|
|
338
351
|
get gitDiffState() {
|
|
339
352
|
return gitDiffState;
|
|
340
353
|
},
|
|
341
|
-
setGitDiffState: (
|
|
354
|
+
setGitDiffState: (
|
|
355
|
+
/** @type {import("./canvas/canvas-render.js").GitDiffState | null} */ state,
|
|
356
|
+
) => {
|
|
342
357
|
gitDiffState = state;
|
|
343
358
|
},
|
|
344
359
|
});
|
|
345
360
|
|
|
361
|
+
initWelcome({
|
|
362
|
+
openProject: () => openProject(),
|
|
363
|
+
openRecentProject: (/** @type {string} */ root) => openRecentProject(root),
|
|
364
|
+
openNewProject: async () => {
|
|
365
|
+
const result = await openNewProjectModal();
|
|
366
|
+
if (result) openRecentProject(result.root);
|
|
367
|
+
},
|
|
368
|
+
cloneRepository: () => cloneRepository({ openRecentProject }),
|
|
369
|
+
});
|
|
370
|
+
|
|
346
371
|
// Effect-driven canvas rendering: auto-triggers renderCanvas when reactive deps change
|
|
347
372
|
let _canvasRenderScheduled = false;
|
|
348
373
|
effect(() => {
|
|
349
374
|
const tab = activeTab.value;
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
375
|
+
if (tab) {
|
|
376
|
+
void tab.doc.document;
|
|
377
|
+
void tab.doc.mode;
|
|
378
|
+
void tab.session.ui.canvasMode;
|
|
379
|
+
void tab.session.ui.editingFunction;
|
|
380
|
+
void tab.session.ui.featureToggles;
|
|
381
|
+
void tab.session.ui.settingsTab;
|
|
382
|
+
void tab.session.ui.stylebookTab;
|
|
383
|
+
void tab.session.ui.stylebookFilter;
|
|
384
|
+
void tab.session.ui.stylebookCustomizedOnly;
|
|
385
|
+
}
|
|
360
386
|
if (!_canvasRenderScheduled) {
|
|
361
387
|
_canvasRenderScheduled = true;
|
|
362
388
|
queueMicrotask(() => {
|
|
@@ -396,7 +422,11 @@ leftPanelMod.mount({
|
|
|
396
422
|
registerElementsDnD,
|
|
397
423
|
registerComponentsDnD,
|
|
398
424
|
setupTreeKeyboard,
|
|
399
|
-
|
|
425
|
+
registerFileTreeDnD,
|
|
426
|
+
cloneRepository: () => cloneRepository({ openRecentProject }),
|
|
427
|
+
setGitDiffState: (
|
|
428
|
+
/** @type {import("./canvas/canvas-render.js").GitDiffState | null} */ state,
|
|
429
|
+
) => {
|
|
400
430
|
gitDiffState = state;
|
|
401
431
|
},
|
|
402
432
|
});
|
|
@@ -412,7 +442,7 @@ mountStatusbar();
|
|
|
412
442
|
mountActivityBar();
|
|
413
443
|
|
|
414
444
|
// Clicking on the canvas-wrap background (outside any canvas panel) deselects the current element
|
|
415
|
-
canvasWrap.addEventListener("click", (/** @type {
|
|
445
|
+
canvasWrap.addEventListener("click", (/** @type {MouseEvent} */ e) => {
|
|
416
446
|
if (e.target !== canvasWrap && e.target !== view.panzoomWrap) return;
|
|
417
447
|
if (!activeTab.value?.session.selection) return;
|
|
418
448
|
activeTab.value.session.selection = null;
|
|
@@ -425,14 +455,17 @@ function safeRenderRightPanel() {
|
|
|
425
455
|
// Now that renderers are registered, bootstrap
|
|
426
456
|
registerFunctionCompletions();
|
|
427
457
|
|
|
428
|
-
const
|
|
458
|
+
const _urlParams = new URLSearchParams(location.search);
|
|
459
|
+
const _projectParam = _urlParams.get("project") || _urlParams.get("open");
|
|
429
460
|
|
|
430
|
-
if (
|
|
431
|
-
// ?
|
|
461
|
+
if (_projectParam) {
|
|
462
|
+
// ?project= mode: skip normal loadProject, set up site context from the path
|
|
432
463
|
const isAbsPath =
|
|
433
|
-
|
|
464
|
+
_projectParam.startsWith("/") ||
|
|
465
|
+
_projectParam.startsWith("~") ||
|
|
466
|
+
/^[A-Za-z]:[/\\]/.test(_projectParam);
|
|
434
467
|
if (!isAbsPath) {
|
|
435
|
-
statusMessage(`Error: ?
|
|
468
|
+
statusMessage(`Error: ?project= requires an absolute path (got "${_projectParam}")`);
|
|
436
469
|
render();
|
|
437
470
|
} else {
|
|
438
471
|
render();
|
|
@@ -440,7 +473,7 @@ if (_openParam) {
|
|
|
440
473
|
(async () => {
|
|
441
474
|
try {
|
|
442
475
|
const siteCtx = platform.resolveSiteContext
|
|
443
|
-
? await platform.resolveSiteContext(
|
|
476
|
+
? await platform.resolveSiteContext(_projectParam)
|
|
444
477
|
: { sitePath: null };
|
|
445
478
|
|
|
446
479
|
if (siteCtx.sitePath) {
|
|
@@ -456,7 +489,7 @@ if (_openParam) {
|
|
|
456
489
|
name: siteCtx.projectConfig?.name || "Project",
|
|
457
490
|
projectRoot: siteCtx.sitePath,
|
|
458
491
|
isSiteProject: true,
|
|
459
|
-
projectConfig: siteCtx.projectConfig,
|
|
492
|
+
projectConfig: siteCtx.projectConfig || null,
|
|
460
493
|
projectDirs: [],
|
|
461
494
|
dirs: new Map(),
|
|
462
495
|
expanded: new Set(),
|
|
@@ -477,22 +510,22 @@ if (_openParam) {
|
|
|
477
510
|
"styles",
|
|
478
511
|
];
|
|
479
512
|
const dirEntries = await platform.listDirectory(".");
|
|
480
|
-
|
|
513
|
+
requireProjectState().dirs.set(".", dirEntries);
|
|
481
514
|
const foundDirs = [];
|
|
482
515
|
for (const e of dirEntries) {
|
|
483
516
|
if (e.type === "directory" && conventionalDirs.includes(e.name)) {
|
|
484
517
|
foundDirs.push(e.name);
|
|
485
|
-
|
|
518
|
+
requireProjectState().expanded.add(e.path || e.name);
|
|
486
519
|
const sub = await platform.listDirectory(e.path || e.name);
|
|
487
|
-
|
|
520
|
+
requireProjectState().dirs.set(e.path || e.name, sub);
|
|
488
521
|
}
|
|
489
522
|
}
|
|
490
|
-
|
|
523
|
+
requireProjectState().projectDirs = foundDirs;
|
|
491
524
|
}
|
|
492
525
|
|
|
493
526
|
// Read and open the file
|
|
494
|
-
const _fileParam =
|
|
495
|
-
let fileRelPath = _fileParam || siteCtx.fileRelPath ||
|
|
527
|
+
const _fileParam = _urlParams.get("file");
|
|
528
|
+
let fileRelPath = _fileParam || siteCtx.fileRelPath || _projectParam;
|
|
496
529
|
|
|
497
530
|
// When opening project.json, default to home page instead
|
|
498
531
|
if (fileRelPath === "project.json" || fileRelPath.endsWith("/project.json")) {
|
|
@@ -520,9 +553,7 @@ if (_openParam) {
|
|
|
520
553
|
parsedDoc = JSON.parse(content);
|
|
521
554
|
}
|
|
522
555
|
|
|
523
|
-
// Open in a tab
|
|
524
|
-
const { closeTab } = await import("./workspace/workspace.js");
|
|
525
|
-
closeTab("initial");
|
|
556
|
+
// Open in a tab
|
|
526
557
|
openTab({
|
|
527
558
|
id: fileRelPath,
|
|
528
559
|
documentPath: fileRelPath,
|
|
@@ -535,13 +566,12 @@ if (_openParam) {
|
|
|
535
566
|
if (fileRelPath === "project.json" && activeTab.value) {
|
|
536
567
|
activeTab.value.session.ui.canvasMode = "stylebook";
|
|
537
568
|
}
|
|
538
|
-
view.leftTab = "layers";
|
|
539
569
|
|
|
540
570
|
render();
|
|
541
|
-
statusMessage(`Opened ${
|
|
571
|
+
statusMessage(`Opened ${fileRelPath}`);
|
|
542
572
|
}
|
|
543
|
-
} catch (/** @type {
|
|
544
|
-
statusMessage(`Error: ${e.message}`);
|
|
573
|
+
} catch (/** @type {unknown} */ e) {
|
|
574
|
+
statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
|
|
545
575
|
}
|
|
546
576
|
})();
|
|
547
577
|
}
|
|
@@ -594,7 +624,7 @@ async function openRecentProject(/** @type {string} */ root) {
|
|
|
594
624
|
const content = await platform.readFile("project.json");
|
|
595
625
|
const config = JSON.parse(content);
|
|
596
626
|
|
|
597
|
-
|
|
627
|
+
closeAllTabs();
|
|
598
628
|
|
|
599
629
|
setProjectState({
|
|
600
630
|
...projectState,
|
|
@@ -620,29 +650,29 @@ async function openRecentProject(/** @type {string} */ root) {
|
|
|
620
650
|
"public",
|
|
621
651
|
"styles",
|
|
622
652
|
];
|
|
623
|
-
const entries =
|
|
653
|
+
const entries = requireProjectState().dirs.get(".") || [];
|
|
624
654
|
for (const e of entries) {
|
|
625
655
|
if (e.type === "directory" && conventionalDirs.includes(e.name)) {
|
|
626
|
-
|
|
656
|
+
requireProjectState().expanded.add(e.path || e.name);
|
|
627
657
|
await loadDirectory(e.path || e.name);
|
|
628
658
|
}
|
|
629
659
|
}
|
|
630
660
|
|
|
631
|
-
addRecentProject(
|
|
661
|
+
addRecentProject(requireProjectState().name, root);
|
|
632
662
|
view.leftTab = "files";
|
|
633
663
|
renderActivityBar();
|
|
634
664
|
renderLeftPanel();
|
|
635
|
-
statusMessage(`Opened project: ${
|
|
665
|
+
statusMessage(`Opened project: ${requireProjectState().name}`);
|
|
636
666
|
|
|
637
667
|
await openHomePage();
|
|
638
|
-
} catch (/** @type {
|
|
639
|
-
statusMessage(`Error: ${e.message}`);
|
|
668
|
+
} catch (/** @type {unknown} */ e) {
|
|
669
|
+
statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
|
|
640
670
|
}
|
|
641
671
|
}
|
|
642
672
|
function renderFilesTemplate() {
|
|
643
673
|
return _renderFilesTemplate({ openProject, openFileFromTree, renderLeftPanel });
|
|
644
674
|
}
|
|
645
|
-
function openFileFromTree(/** @type {
|
|
675
|
+
function openFileFromTree(/** @type {string} */ path) {
|
|
646
676
|
return openFileInTab(path);
|
|
647
677
|
}
|
|
648
678
|
|
|
@@ -676,13 +706,13 @@ initShortcuts(() => ({
|
|
|
676
706
|
|
|
677
707
|
// ─── Autosave (registered as update middleware) ──────────────────────────────
|
|
678
708
|
|
|
679
|
-
/** @type {
|
|
709
|
+
/** @type {number} */
|
|
680
710
|
const AUTO_SAVE_DELAY = 2000;
|
|
681
711
|
|
|
682
712
|
function scheduleAutosave() {
|
|
683
713
|
const tab = activeTab.value;
|
|
684
714
|
if (!tab?.fileHandle || !tab.doc.dirty) return;
|
|
685
|
-
clearTimeout(view.autosaveTimer);
|
|
715
|
+
if (view.autosaveTimer) clearTimeout(view.autosaveTimer);
|
|
686
716
|
view.autosaveTimer = setTimeout(async () => {
|
|
687
717
|
const t = activeTab.value;
|
|
688
718
|
if (t?.fileHandle && t.doc.dirty && "createWritable" in t.fileHandle) {
|
package/src/tabs/tab.js
CHANGED
|
@@ -7,7 +7,7 @@ import { reactive, effectScope } from "../reactivity.js";
|
|
|
7
7
|
* zoom: number;
|
|
8
8
|
* activeMedia: string | null;
|
|
9
9
|
* activeSelector: string | null;
|
|
10
|
-
* editingFunction:
|
|
10
|
+
* editingFunction: FunctionEditDef | null;
|
|
11
11
|
* featureToggles: Record<string, boolean>;
|
|
12
12
|
* styleSections: Record<string, boolean>;
|
|
13
13
|
* inspectorSections: Record<string, boolean>;
|
|
@@ -19,18 +19,19 @@ import { reactive, effectScope } from "../reactivity.js";
|
|
|
19
19
|
* stylebookFilter: string;
|
|
20
20
|
* stylebookCustomizedOnly: boolean;
|
|
21
21
|
* settingsTab: string;
|
|
22
|
-
* gitStatus:
|
|
23
|
-
* gitBranches:
|
|
22
|
+
* gitStatus: GitStatusResult | null;
|
|
23
|
+
* gitBranches: GitBranchesResult | null;
|
|
24
24
|
* gitCommitMessage: string;
|
|
25
25
|
* gitLoading: boolean;
|
|
26
26
|
* gitError: string | null;
|
|
27
|
-
*
|
|
27
|
+
* gitDiffState: GitDiffState | null;
|
|
28
|
+
* pendingInlineEdit: InlineEditDef | null;
|
|
28
29
|
* }} TabUi
|
|
29
30
|
*/
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* @typedef {{
|
|
33
|
-
* document: Record<string,
|
|
34
|
+
* document: Record<string, unknown>;
|
|
34
35
|
* selection: (string | number)[] | null;
|
|
35
36
|
* }} HistorySnapshot
|
|
36
37
|
*/
|
|
@@ -40,9 +41,10 @@ import { reactive, effectScope } from "../reactivity.js";
|
|
|
40
41
|
* id: string;
|
|
41
42
|
* documentPath: string | null;
|
|
42
43
|
* fileHandle: FileSystemFileHandle | null;
|
|
43
|
-
*
|
|
44
|
+
* capabilities: { modes: string[] };
|
|
45
|
+
* scope: { stop(): void; run<T>(fn: () => T): T | undefined; [k: string]: unknown };
|
|
44
46
|
* doc: {
|
|
45
|
-
* document:
|
|
47
|
+
* document: JxMutableNode;
|
|
46
48
|
* content: { frontmatter: Record<string, unknown> };
|
|
47
49
|
* mode: string;
|
|
48
50
|
* sourceFormat: string | null;
|
|
@@ -52,14 +54,14 @@ import { reactive, effectScope } from "../reactivity.js";
|
|
|
52
54
|
* session: {
|
|
53
55
|
* selection: (string | number)[] | null;
|
|
54
56
|
* hover: (string | number)[] | null;
|
|
55
|
-
* clipboard:
|
|
56
|
-
* documentStack:
|
|
57
|
+
* clipboard: JxMutableNode | null;
|
|
58
|
+
* documentStack: DocumentStackEntry[];
|
|
57
59
|
* ui: TabUi;
|
|
58
60
|
* canvas: {
|
|
59
61
|
* status: string;
|
|
60
|
-
* scope: { stop(): void; [k: string]:
|
|
62
|
+
* scope: { stop(): void; [k: string]: unknown } | null;
|
|
61
63
|
* error: string | null;
|
|
62
|
-
* pendingInlineEdit:
|
|
64
|
+
* pendingInlineEdit: InlineEditDef | null;
|
|
63
65
|
* };
|
|
64
66
|
* };
|
|
65
67
|
* history: {
|
|
@@ -73,7 +75,7 @@ import { reactive, effectScope } from "../reactivity.js";
|
|
|
73
75
|
function createDefaultUi() {
|
|
74
76
|
return {
|
|
75
77
|
rightTab: "properties",
|
|
76
|
-
canvasMode: "
|
|
78
|
+
canvasMode: "edit",
|
|
77
79
|
zoom: 1,
|
|
78
80
|
activeMedia: null,
|
|
79
81
|
activeSelector: null,
|
|
@@ -94,10 +96,13 @@ function createDefaultUi() {
|
|
|
94
96
|
gitCommitMessage: "",
|
|
95
97
|
gitLoading: false,
|
|
96
98
|
gitError: null,
|
|
99
|
+
gitDiffState: null,
|
|
97
100
|
pendingInlineEdit: null,
|
|
98
101
|
};
|
|
99
102
|
}
|
|
100
103
|
|
|
104
|
+
const ALL_MODES = ["edit", "design", "preview", "source", "stylebook"];
|
|
105
|
+
|
|
101
106
|
/**
|
|
102
107
|
* Create a new tab with reactive doc/session/history trees, owned by an effectScope.
|
|
103
108
|
*
|
|
@@ -105,9 +110,10 @@ function createDefaultUi() {
|
|
|
105
110
|
* id: string;
|
|
106
111
|
* documentPath?: string | null;
|
|
107
112
|
* fileHandle?: FileSystemFileHandle | null;
|
|
108
|
-
* document: Record<string,
|
|
113
|
+
* document: Record<string, unknown>;
|
|
109
114
|
* frontmatter?: Record<string, unknown>;
|
|
110
115
|
* sourceFormat?: string | null;
|
|
116
|
+
* capabilities?: { modes?: string[] };
|
|
111
117
|
* }} opts
|
|
112
118
|
* @returns {Tab}
|
|
113
119
|
*/
|
|
@@ -118,46 +124,64 @@ export function createTab({
|
|
|
118
124
|
document,
|
|
119
125
|
frontmatter,
|
|
120
126
|
sourceFormat = null,
|
|
127
|
+
capabilities,
|
|
121
128
|
}) {
|
|
122
129
|
const scope = effectScope();
|
|
123
130
|
|
|
131
|
+
const resolvedModes = capabilities?.modes ?? inferModes(documentPath, sourceFormat);
|
|
132
|
+
|
|
124
133
|
const tab = /** @type {Tab} */ (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
/** @type {unknown} */ (
|
|
135
|
+
scope.run(() => ({
|
|
136
|
+
id,
|
|
137
|
+
documentPath,
|
|
138
|
+
fileHandle,
|
|
139
|
+
capabilities: { modes: resolvedModes },
|
|
140
|
+
scope,
|
|
141
|
+
doc: reactive({
|
|
142
|
+
document,
|
|
143
|
+
sourceFormat,
|
|
144
|
+
content: { frontmatter: frontmatter || {} },
|
|
145
|
+
mode:
|
|
146
|
+
sourceFormat === "md"
|
|
138
147
|
? "content"
|
|
139
|
-
: "
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
148
|
+
: documentPath?.endsWith(".md")
|
|
149
|
+
? "content"
|
|
150
|
+
: "component",
|
|
151
|
+
handlersSource: null,
|
|
152
|
+
dirty: false,
|
|
153
|
+
}),
|
|
154
|
+
session: reactive({
|
|
155
|
+
selection: null,
|
|
156
|
+
hover: null,
|
|
157
|
+
clipboard: null,
|
|
158
|
+
documentStack: [],
|
|
159
|
+
ui: createDefaultUi(),
|
|
160
|
+
canvas: { status: "idle", scope: null, error: null, pendingInlineEdit: null },
|
|
161
|
+
}),
|
|
162
|
+
history: reactive({
|
|
163
|
+
snapshots: [{ document: structuredClone(document), selection: null }],
|
|
164
|
+
index: 0,
|
|
165
|
+
}),
|
|
166
|
+
}))
|
|
167
|
+
)
|
|
156
168
|
);
|
|
157
169
|
|
|
158
170
|
return tab;
|
|
159
171
|
}
|
|
160
172
|
|
|
173
|
+
/**
|
|
174
|
+
* @param {string | null | undefined} documentPath
|
|
175
|
+
* @param {string | null} sourceFormat
|
|
176
|
+
* @returns {string[]}
|
|
177
|
+
*/
|
|
178
|
+
function inferModes(documentPath, sourceFormat) {
|
|
179
|
+
if (documentPath === "project.json") return ["stylebook", "source"];
|
|
180
|
+
if (sourceFormat === "md" || documentPath?.endsWith(".md"))
|
|
181
|
+
return ["edit", "design", "preview", "source"];
|
|
182
|
+
return ALL_MODES;
|
|
183
|
+
}
|
|
184
|
+
|
|
161
185
|
/**
|
|
162
186
|
* Dispose a tab — stops its effectScope, killing all effects created within it.
|
|
163
187
|
*
|