@jxsuite/studio 0.16.1 → 0.18.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 +2072 -1588
- package/dist/studio.js.map +30 -21
- package/package.json +2 -2
- package/src/browse/browse.js +24 -3
- package/src/canvas/canvas-render.js +2 -2
- package/src/canvas/canvas-utils.js +1 -1
- package/src/editor/shortcuts.js +7 -0
- package/src/files/files.js +17 -2
- package/src/panels/activity-bar.js +12 -0
- package/src/panels/left-panel.js +2 -2
- package/src/panels/overlays.js +2 -2
- package/src/panels/quick-search.js +170 -0
- package/src/panels/style-panel.js +1 -1
- package/src/panels/stylebook-panel.js +30 -529
- package/src/panels/toolbar.js +69 -15
- package/src/platforms/devserver.js +12 -0
- package/src/recent-projects.js +57 -0
- package/src/settings/css-vars-editor.js +307 -0
- package/src/settings/general-settings.js +94 -0
- package/src/settings/head-editor.js +184 -0
- package/src/settings/settings-modal.js +117 -0
- package/src/studio.js +58 -9
- package/src/ui/spectrum.js +2 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Head editor — structured form for managing $head entries (link, meta, script, style tags) with
|
|
3
|
+
* Monaco editor for script/style bodies.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { html, render as litRender, nothing } from "lit-html";
|
|
7
|
+
import { projectState } from "../store.js";
|
|
8
|
+
import { updateSiteConfig } from "../site-context.js";
|
|
9
|
+
|
|
10
|
+
/** @param {HTMLElement} container */
|
|
11
|
+
export function renderHeadEditor(container) {
|
|
12
|
+
const config = projectState.projectConfig || {};
|
|
13
|
+
const headEntries = config.$head || [];
|
|
14
|
+
|
|
15
|
+
const save = () => {
|
|
16
|
+
updateSiteConfig({ $head: headEntries });
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const addEntry = (/** @type {string} */ tag) => {
|
|
20
|
+
/** @type {any} */
|
|
21
|
+
const entry = { tag };
|
|
22
|
+
if (tag === "link") {
|
|
23
|
+
entry.rel = "stylesheet";
|
|
24
|
+
entry.href = "";
|
|
25
|
+
} else if (tag === "meta") {
|
|
26
|
+
entry.name = "";
|
|
27
|
+
entry.content = "";
|
|
28
|
+
} else if (tag === "script") {
|
|
29
|
+
entry.src = "";
|
|
30
|
+
entry.body = "";
|
|
31
|
+
} else if (tag === "style") {
|
|
32
|
+
entry.body = "";
|
|
33
|
+
}
|
|
34
|
+
headEntries.push(entry);
|
|
35
|
+
save();
|
|
36
|
+
renderHeadEditor(container);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const removeEntry = (/** @type {number} */ idx) => {
|
|
40
|
+
headEntries.splice(idx, 1);
|
|
41
|
+
save();
|
|
42
|
+
renderHeadEditor(container);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const updateEntry = (
|
|
46
|
+
/** @type {number} */ idx,
|
|
47
|
+
/** @type {string} */ key,
|
|
48
|
+
/** @type {string} */ val,
|
|
49
|
+
) => {
|
|
50
|
+
headEntries[idx][key] = val;
|
|
51
|
+
save();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const tpl = html`
|
|
55
|
+
<div class="settings-section">
|
|
56
|
+
<h3 class="settings-section-title">Head</h3>
|
|
57
|
+
<p class="settings-field-desc">
|
|
58
|
+
Manage global <head> tags — stylesheets, meta tags, scripts, and inline styles.
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
<div class="head-entries">
|
|
62
|
+
${headEntries.map(
|
|
63
|
+
(/** @type {any} */ entry, /** @type {number} */ idx) => html`
|
|
64
|
+
<div class="head-entry">
|
|
65
|
+
<div class="head-entry-header">
|
|
66
|
+
<span class="head-entry-tag"><${entry.tag}></span>
|
|
67
|
+
<sp-action-button quiet size="s" @click=${() => removeEntry(idx)}>
|
|
68
|
+
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
69
|
+
</sp-action-button>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="head-entry-fields">${renderEntryFields(entry, idx, updateEntry)}</div>
|
|
72
|
+
</div>
|
|
73
|
+
`,
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="head-add-actions" style="margin-top:12px;display:flex;gap:8px">
|
|
78
|
+
<sp-action-button size="s" @click=${() => addEntry("link")}> + Link </sp-action-button>
|
|
79
|
+
<sp-action-button size="s" @click=${() => addEntry("meta")}> + Meta </sp-action-button>
|
|
80
|
+
<sp-action-button size="s" @click=${() => addEntry("script")}> + Script </sp-action-button>
|
|
81
|
+
<sp-action-button size="s" @click=${() => addEntry("style")}> + Style </sp-action-button>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
litRender(tpl, container);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {any} entry
|
|
91
|
+
* @param {number} idx
|
|
92
|
+
* @param {(idx: number, key: string, val: string) => void} updateEntry
|
|
93
|
+
*/
|
|
94
|
+
function renderEntryFields(entry, idx, updateEntry) {
|
|
95
|
+
/** @type {any} */
|
|
96
|
+
let debounce;
|
|
97
|
+
const onFieldChange = (/** @type {string} */ key) => (/** @type {any} */ e) => {
|
|
98
|
+
clearTimeout(debounce);
|
|
99
|
+
debounce = setTimeout(() => {
|
|
100
|
+
updateEntry(idx, key, e.target.value);
|
|
101
|
+
}, 300);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
switch (entry.tag) {
|
|
105
|
+
case "link":
|
|
106
|
+
return html`
|
|
107
|
+
<div class="settings-field-row">
|
|
108
|
+
<sp-textfield
|
|
109
|
+
size="s"
|
|
110
|
+
label="rel"
|
|
111
|
+
.value=${entry.rel || ""}
|
|
112
|
+
@change=${onFieldChange("rel")}
|
|
113
|
+
></sp-textfield>
|
|
114
|
+
<sp-textfield
|
|
115
|
+
size="s"
|
|
116
|
+
label="href"
|
|
117
|
+
.value=${entry.href || ""}
|
|
118
|
+
@change=${onFieldChange("href")}
|
|
119
|
+
style="flex:1"
|
|
120
|
+
></sp-textfield>
|
|
121
|
+
</div>
|
|
122
|
+
`;
|
|
123
|
+
case "meta":
|
|
124
|
+
return html`
|
|
125
|
+
<div class="settings-field-row">
|
|
126
|
+
<sp-textfield
|
|
127
|
+
size="s"
|
|
128
|
+
label="name"
|
|
129
|
+
.value=${entry.name || ""}
|
|
130
|
+
@change=${onFieldChange("name")}
|
|
131
|
+
></sp-textfield>
|
|
132
|
+
<sp-textfield
|
|
133
|
+
size="s"
|
|
134
|
+
label="content"
|
|
135
|
+
.value=${entry.content || ""}
|
|
136
|
+
@change=${onFieldChange("content")}
|
|
137
|
+
style="flex:1"
|
|
138
|
+
></sp-textfield>
|
|
139
|
+
</div>
|
|
140
|
+
`;
|
|
141
|
+
case "script":
|
|
142
|
+
return html`
|
|
143
|
+
<div class="settings-field-row">
|
|
144
|
+
<sp-textfield
|
|
145
|
+
size="s"
|
|
146
|
+
label="src"
|
|
147
|
+
.value=${entry.src || ""}
|
|
148
|
+
@change=${onFieldChange("src")}
|
|
149
|
+
placeholder="URL or leave empty for inline"
|
|
150
|
+
style="flex:1"
|
|
151
|
+
></sp-textfield>
|
|
152
|
+
</div>
|
|
153
|
+
${!entry.src
|
|
154
|
+
? html`
|
|
155
|
+
<div class="head-entry-body">
|
|
156
|
+
<label class="settings-field-label">Script body</label>
|
|
157
|
+
<textarea
|
|
158
|
+
class="head-code-editor"
|
|
159
|
+
.value=${entry.body || ""}
|
|
160
|
+
@input=${onFieldChange("body")}
|
|
161
|
+
rows="6"
|
|
162
|
+
spellcheck="false"
|
|
163
|
+
></textarea>
|
|
164
|
+
</div>
|
|
165
|
+
`
|
|
166
|
+
: nothing}
|
|
167
|
+
`;
|
|
168
|
+
case "style":
|
|
169
|
+
return html`
|
|
170
|
+
<div class="head-entry-body">
|
|
171
|
+
<label class="settings-field-label">Style body</label>
|
|
172
|
+
<textarea
|
|
173
|
+
class="head-code-editor"
|
|
174
|
+
.value=${entry.body || ""}
|
|
175
|
+
@input=${onFieldChange("body")}
|
|
176
|
+
rows="8"
|
|
177
|
+
spellcheck="false"
|
|
178
|
+
></textarea>
|
|
179
|
+
</div>
|
|
180
|
+
`;
|
|
181
|
+
default:
|
|
182
|
+
return nothing;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings modal — site-wide project settings (CSS variables, definitions, content types, head,
|
|
3
|
+
* general). Modeled after VS Code / Obsidian settings panels: left sidebar nav + right content
|
|
4
|
+
* area.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { html, render as litRender } from "lit-html";
|
|
8
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
9
|
+
import { renderDefsEditor } from "./defs-editor.js";
|
|
10
|
+
import { renderContentTypesEditor } from "./content-types-editor.js";
|
|
11
|
+
import { renderCssVarsEditor } from "./css-vars-editor.js";
|
|
12
|
+
import { renderHeadEditor } from "./head-editor.js";
|
|
13
|
+
import { renderGeneralSettings } from "./general-settings.js";
|
|
14
|
+
|
|
15
|
+
/** @type {HTMLElement | null} */
|
|
16
|
+
let _host = null;
|
|
17
|
+
|
|
18
|
+
/** @type {string} */
|
|
19
|
+
let _activeSection = "general";
|
|
20
|
+
|
|
21
|
+
const sections = [
|
|
22
|
+
{ key: "general", label: "General", icon: "sp-icon-properties" },
|
|
23
|
+
{ key: "head", label: "Head", icon: "sp-icon-file-single-web-page" },
|
|
24
|
+
{ key: "cssVars", label: "CSS Variables", icon: "sp-icon-brush" },
|
|
25
|
+
{ key: "definitions", label: "Definitions", icon: "sp-icon-data" },
|
|
26
|
+
{ key: "contentTypes", label: "Content Types", icon: "sp-icon-view-grid" },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export function openSettingsModal() {
|
|
30
|
+
if (_host) return;
|
|
31
|
+
|
|
32
|
+
_host = document.createElement("div");
|
|
33
|
+
_host.style.display = "contents";
|
|
34
|
+
const themeRoot = document.querySelector("sp-theme") || document.body;
|
|
35
|
+
themeRoot.appendChild(_host);
|
|
36
|
+
_activeSection = "general";
|
|
37
|
+
renderModal();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function closeSettingsModal() {
|
|
41
|
+
if (!_host) return;
|
|
42
|
+
_host.remove();
|
|
43
|
+
_host = null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function renderModal() {
|
|
47
|
+
if (!_host) return;
|
|
48
|
+
|
|
49
|
+
const onNavClick = (/** @type {string} */ key) => {
|
|
50
|
+
_activeSection = key;
|
|
51
|
+
renderModal();
|
|
52
|
+
renderActiveSection();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const tpl = html`
|
|
56
|
+
<sp-underlay open @close=${closeSettingsModal}></sp-underlay>
|
|
57
|
+
<div
|
|
58
|
+
class="settings-modal"
|
|
59
|
+
@keydown=${(/** @type {KeyboardEvent} */ e) => {
|
|
60
|
+
if (e.key === "Escape") closeSettingsModal();
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<div class="settings-modal-header">
|
|
64
|
+
<h2 class="settings-modal-title">Settings</h2>
|
|
65
|
+
<sp-action-button quiet size="s" @click=${closeSettingsModal} title="Close">
|
|
66
|
+
<sp-icon-close slot="icon"></sp-icon-close>
|
|
67
|
+
</sp-action-button>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="settings-modal-body">
|
|
70
|
+
<nav class="settings-modal-nav">
|
|
71
|
+
${sections.map(
|
|
72
|
+
(s) => html`
|
|
73
|
+
<button
|
|
74
|
+
class="settings-nav-item${_activeSection === s.key ? " active" : ""}"
|
|
75
|
+
@click=${() => onNavClick(s.key)}
|
|
76
|
+
>
|
|
77
|
+
${s.label}
|
|
78
|
+
</button>
|
|
79
|
+
`,
|
|
80
|
+
)}
|
|
81
|
+
</nav>
|
|
82
|
+
<div
|
|
83
|
+
class="settings-modal-content"
|
|
84
|
+
${ref((el) => {
|
|
85
|
+
if (el) requestAnimationFrame(() => renderActiveSection());
|
|
86
|
+
})}
|
|
87
|
+
></div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
`;
|
|
91
|
+
|
|
92
|
+
litRender(tpl, _host);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function renderActiveSection() {
|
|
96
|
+
if (!_host) return;
|
|
97
|
+
const container = _host.querySelector(".settings-modal-content");
|
|
98
|
+
if (!container) return;
|
|
99
|
+
|
|
100
|
+
switch (_activeSection) {
|
|
101
|
+
case "general":
|
|
102
|
+
renderGeneralSettings(/** @type {HTMLElement} */ (container));
|
|
103
|
+
break;
|
|
104
|
+
case "head":
|
|
105
|
+
renderHeadEditor(/** @type {HTMLElement} */ (container));
|
|
106
|
+
break;
|
|
107
|
+
case "cssVars":
|
|
108
|
+
renderCssVarsEditor(/** @type {HTMLElement} */ (container));
|
|
109
|
+
break;
|
|
110
|
+
case "definitions":
|
|
111
|
+
renderDefsEditor(/** @type {HTMLElement} */ (container));
|
|
112
|
+
break;
|
|
113
|
+
case "contentTypes":
|
|
114
|
+
renderContentTypesEditor(/** @type {HTMLElement} */ (container));
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
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],
|