@jxsuite/studio 0.16.0 → 0.17.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 +466 -126
- package/dist/studio.js.map +19 -14
- package/package.json +1 -1
- package/src/browse/browse.js +24 -3
- package/src/canvas/canvas-live-render.js +1 -27
- package/src/editor/shortcuts.js +7 -0
- package/src/files/files.js +7 -2
- package/src/panels/panel-events.js +24 -19
- package/src/panels/properties-panel.js +14 -14
- package/src/panels/quick-search.js +170 -0
- package/src/panels/toolbar.js +62 -13
- package/src/platforms/devserver.js +12 -0
- package/src/recent-projects.js +57 -0
- package/src/studio.js +58 -9
- package/src/ui/spectrum.js +2 -0
- package/src/workspace/workspace.js +44 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const STORAGE_KEY = "jx-studio-recent-projects";
|
|
2
|
+
const FILES_STORAGE_KEY = "jx-studio-recent-files";
|
|
3
|
+
const MAX_RECENT = 8;
|
|
4
|
+
const MAX_RECENT_FILES = 10;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {{ name: string; root: string; timestamp: number }} RecentProject
|
|
8
|
+
*
|
|
9
|
+
* @typedef {{ path: string; name: string; timestamp: number }} RecentFile
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** @returns {RecentProject[]} */
|
|
13
|
+
export function getRecentProjects() {
|
|
14
|
+
try {
|
|
15
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
16
|
+
if (!raw) return [];
|
|
17
|
+
return /** @type {RecentProject[]} */ (JSON.parse(raw)).sort(
|
|
18
|
+
(a, b) => b.timestamp - a.timestamp,
|
|
19
|
+
);
|
|
20
|
+
} catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} name
|
|
27
|
+
* @param {string} root
|
|
28
|
+
*/
|
|
29
|
+
export function addRecentProject(name, root) {
|
|
30
|
+
const projects = getRecentProjects().filter((p) => p.root !== root);
|
|
31
|
+
projects.unshift({ name, root, timestamp: Date.now() });
|
|
32
|
+
if (projects.length > MAX_RECENT) projects.length = MAX_RECENT;
|
|
33
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(projects));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function clearRecentProjects() {
|
|
37
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @returns {RecentFile[]} */
|
|
41
|
+
export function getRecentFiles() {
|
|
42
|
+
try {
|
|
43
|
+
const raw = localStorage.getItem(FILES_STORAGE_KEY);
|
|
44
|
+
if (!raw) return [];
|
|
45
|
+
return /** @type {RecentFile[]} */ (JSON.parse(raw)).sort((a, b) => b.timestamp - a.timestamp);
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @param {{ path: string; name: string }} file */
|
|
52
|
+
export function trackRecentFile(file) {
|
|
53
|
+
const recent = getRecentFiles().filter((f) => f.path !== file.path);
|
|
54
|
+
recent.unshift({ path: file.path, name: file.name, timestamp: Date.now() });
|
|
55
|
+
if (recent.length > MAX_RECENT_FILES) recent.length = MAX_RECENT_FILES;
|
|
56
|
+
localStorage.setItem(FILES_STORAGE_KEY, JSON.stringify(recent));
|
|
57
|
+
}
|
package/src/studio.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
updateUi,
|
|
17
17
|
} from "./store.js";
|
|
18
18
|
|
|
19
|
-
import { activeTab, openTab } from "./workspace/workspace.js";
|
|
19
|
+
import { activeTab, openTab, replaceAllTabs } from "./workspace/workspace.js";
|
|
20
20
|
import { transactDoc, mutateUpdateDef, mutateUpdateProperty } from "./tabs/transact.js";
|
|
21
21
|
import { effect } from "./reactivity.js";
|
|
22
22
|
|
|
@@ -35,19 +35,14 @@ import {
|
|
|
35
35
|
setStatusbarRenderer,
|
|
36
36
|
mountStatusbar,
|
|
37
37
|
} from "./panels/statusbar.js";
|
|
38
|
-
import {
|
|
39
|
-
openFile,
|
|
40
|
-
loadMarkdown,
|
|
41
|
-
saveFile,
|
|
42
|
-
exportFile,
|
|
43
|
-
serializeDocument,
|
|
44
|
-
} from "./files/file-ops.js";
|
|
38
|
+
import { loadMarkdown, saveFile, exportFile, serializeDocument } from "./files/file-ops.js";
|
|
45
39
|
import {
|
|
46
40
|
loadProject as _loadProject,
|
|
47
41
|
openProject as _openProject,
|
|
48
42
|
renderFilesTemplate as _renderFilesTemplate,
|
|
49
43
|
openFileInTab,
|
|
50
44
|
setupTreeKeyboard,
|
|
45
|
+
loadDirectory,
|
|
51
46
|
} from "./files/files.js";
|
|
52
47
|
import { renderImportsTemplate } from "./panels/imports-panel.js";
|
|
53
48
|
import { renderHeadTemplate } from "./panels/head-panel.js";
|
|
@@ -86,6 +81,8 @@ import { renderBlockActionBar, initBlockActionBar } from "./panels/block-action-
|
|
|
86
81
|
import { initCssData } from "./panels/style-utils.js";
|
|
87
82
|
import { updateForcedPseudoPreview } from "./panels/pseudo-preview.js";
|
|
88
83
|
import { initPanelEvents } from "./panels/panel-events.js";
|
|
84
|
+
import { initQuickSearch } from "./panels/quick-search.js";
|
|
85
|
+
import { addRecentProject } from "./recent-projects.js";
|
|
89
86
|
|
|
90
87
|
// ─── Globals ──────────────────────────────────────────────────────────────────
|
|
91
88
|
// These mutable variables are local to studio.js for now. As sections are extracted
|
|
@@ -260,7 +257,7 @@ toolbarPanel.mount(toolbarEl, {
|
|
|
260
257
|
navigateBack: () => navigateBack(),
|
|
261
258
|
closeFunctionEditor: () => closeFunctionEditor(),
|
|
262
259
|
openProject: () => openProject(),
|
|
263
|
-
|
|
260
|
+
openRecentProject: (/** @type {string} */ root) => openRecentProject(root),
|
|
264
261
|
saveFile: () => saveFile(),
|
|
265
262
|
parseMediaEntries,
|
|
266
263
|
getCanvasMode,
|
|
@@ -269,6 +266,8 @@ toolbarPanel.mount(toolbarEl, {
|
|
|
269
266
|
safeRenderRightPanel: () => safeRenderRightPanel(),
|
|
270
267
|
});
|
|
271
268
|
|
|
269
|
+
initQuickSearch();
|
|
270
|
+
|
|
272
271
|
tabStrip.mount(/** @type {HTMLElement} */ (document.querySelector("#tab-strip")));
|
|
273
272
|
|
|
274
273
|
overlaysPanel.mount({
|
|
@@ -543,6 +542,56 @@ function openProject() {
|
|
|
543
542
|
renderLeftPanel,
|
|
544
543
|
});
|
|
545
544
|
}
|
|
545
|
+
async function openRecentProject(/** @type {string} */ root) {
|
|
546
|
+
try {
|
|
547
|
+
const platform = getPlatform();
|
|
548
|
+
platform.projectRoot = root;
|
|
549
|
+
const content = await platform.readFile("project.json");
|
|
550
|
+
const config = JSON.parse(content);
|
|
551
|
+
|
|
552
|
+
replaceAllTabs({ id: "initial", document: { tagName: "div", children: [] } });
|
|
553
|
+
|
|
554
|
+
setProjectState({
|
|
555
|
+
...projectState,
|
|
556
|
+
projectRoot: root,
|
|
557
|
+
isSiteProject: true,
|
|
558
|
+
projectConfig: config,
|
|
559
|
+
name: config.name || root.split("/").pop(),
|
|
560
|
+
dirs: new Map(),
|
|
561
|
+
expanded: new Set(),
|
|
562
|
+
selectedPath: null,
|
|
563
|
+
searchQuery: "",
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
await loadDirectory(".");
|
|
567
|
+
await loadComponentRegistry();
|
|
568
|
+
|
|
569
|
+
const conventionalDirs = [
|
|
570
|
+
"pages",
|
|
571
|
+
"layouts",
|
|
572
|
+
"components",
|
|
573
|
+
"content",
|
|
574
|
+
"data",
|
|
575
|
+
"public",
|
|
576
|
+
"styles",
|
|
577
|
+
];
|
|
578
|
+
const entries = projectState.dirs.get(".") || [];
|
|
579
|
+
for (const e of entries) {
|
|
580
|
+
if (e.type === "directory" && conventionalDirs.includes(e.name)) {
|
|
581
|
+
projectState.expanded.add(e.path || e.name);
|
|
582
|
+
await loadDirectory(e.path || e.name);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
addRecentProject(projectState.name, root);
|
|
587
|
+
view.leftTab = "files";
|
|
588
|
+
renderActivityBar();
|
|
589
|
+
renderLeftPanel();
|
|
590
|
+
statusMessage(`Opened project: ${projectState.name}`);
|
|
591
|
+
} catch (/** @type {any} */ e) {
|
|
592
|
+
statusMessage(`Error: ${e.message}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
546
595
|
function renderFilesTemplate() {
|
|
547
596
|
return _renderFilesTemplate({ openProject, openFileFromTree, renderLeftPanel });
|
|
548
597
|
}
|
package/src/ui/spectrum.js
CHANGED
|
@@ -114,6 +114,7 @@ import { IconDistributeHorizontalCenter } from "@spectrum-web-components/icons-w
|
|
|
114
114
|
import { IconTextBaselineShift } from "@spectrum-web-components/icons-workflow/src/elements/IconTextBaselineShift.js";
|
|
115
115
|
import { IconFlipVertical } from "@spectrum-web-components/icons-workflow/src/elements/IconFlipVertical.js";
|
|
116
116
|
import { IconRemove } from "@spectrum-web-components/icons-workflow/src/elements/IconRemove.js";
|
|
117
|
+
import { IconFullScreen } from "@spectrum-web-components/icons-workflow/src/elements/IconFullScreen.js";
|
|
117
118
|
import { IconViewColumn } from "@spectrum-web-components/icons-workflow/src/elements/IconViewColumn.js";
|
|
118
119
|
import { IconBox } from "@spectrum-web-components/icons-workflow/src/elements/IconBox.js";
|
|
119
120
|
import { IconVisibility } from "@spectrum-web-components/icons-workflow/src/elements/IconVisibility.js";
|
|
@@ -244,6 +245,7 @@ const components = [
|
|
|
244
245
|
["sp-icon-text-baseline-shift", IconTextBaselineShift],
|
|
245
246
|
["sp-icon-flip-vertical", IconFlipVertical],
|
|
246
247
|
["sp-icon-remove", IconRemove],
|
|
248
|
+
["sp-icon-full-screen", IconFullScreen],
|
|
247
249
|
["sp-icon-download", IconDownload],
|
|
248
250
|
["sp-icon-checkmark", IconCheckmark],
|
|
249
251
|
["sp-icon-view-column", IconViewColumn],
|
|
@@ -84,6 +84,50 @@ export function closeTab(tabId) {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/** Close all open tabs, disposing each. Defers reactivity until fully cleared. */
|
|
88
|
+
export function closeAllTabs() {
|
|
89
|
+
const tabs = [...workspace.tabs.values()];
|
|
90
|
+
workspace.tabs.clear();
|
|
91
|
+
workspace.tabOrder = [];
|
|
92
|
+
workspace.activeTabId = null;
|
|
93
|
+
for (const tab of tabs) {
|
|
94
|
+
disposeTab(tab);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Replace all existing tabs with a new one atomically. Ensures activeTab is never null during the
|
|
100
|
+
* transition.
|
|
101
|
+
*
|
|
102
|
+
* @param {{
|
|
103
|
+
* id: string;
|
|
104
|
+
* documentPath?: string | null;
|
|
105
|
+
* document: Record<string, any>;
|
|
106
|
+
* frontmatter?: Record<string, unknown>;
|
|
107
|
+
* sourceFormat?: string | null;
|
|
108
|
+
* }} newTabOpts
|
|
109
|
+
* @returns {import("../tabs/tab.js").Tab}
|
|
110
|
+
*/
|
|
111
|
+
export function replaceAllTabs(newTabOpts) {
|
|
112
|
+
const oldIds = [...workspace.tabs.keys()];
|
|
113
|
+
const oldTabs = [...workspace.tabs.values()];
|
|
114
|
+
|
|
115
|
+
const newTab = createTab(newTabOpts);
|
|
116
|
+
workspace.tabs.set(newTab.id, newTab);
|
|
117
|
+
workspace.activeTabId = newTab.id;
|
|
118
|
+
workspace.tabOrder = [newTab.id];
|
|
119
|
+
|
|
120
|
+
for (const id of oldIds) {
|
|
121
|
+
if (id === newTab.id) continue;
|
|
122
|
+
workspace.tabs.delete(id);
|
|
123
|
+
}
|
|
124
|
+
for (const tab of oldTabs) {
|
|
125
|
+
disposeTab(tab);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return newTab;
|
|
129
|
+
}
|
|
130
|
+
|
|
87
131
|
/**
|
|
88
132
|
* Switch to an existing tab.
|
|
89
133
|
*
|