@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
|
|
32
32
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
|
33
|
-
"@jxsuite/runtime": "^0.
|
|
33
|
+
"@jxsuite/runtime": "^0.18.0",
|
|
34
34
|
"@spectrum-web-components/accordion": "^1.12.1",
|
|
35
35
|
"@spectrum-web-components/action-bar": "1.12.1",
|
|
36
36
|
"@spectrum-web-components/action-button": "^1.12.1",
|
package/src/browse/browse.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* button with type-aware entity creation (including content types from project.json).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { html, render as litRender } from "lit-html";
|
|
9
|
+
import { html, render as litRender, nothing } from "lit-html";
|
|
10
10
|
import { getPlatform } from "../platform.js";
|
|
11
11
|
import { projectState } from "../store.js";
|
|
12
12
|
import { yamlDefault } from "../settings/schema-field-ui.js";
|
|
@@ -62,6 +62,22 @@ function extOf(name) {
|
|
|
62
62
|
return dot > 0 ? name.slice(dot).toLowerCase() : "";
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
const IMAGE_EXTENSIONS = new Set([
|
|
66
|
+
".jpg",
|
|
67
|
+
".jpeg",
|
|
68
|
+
".png",
|
|
69
|
+
".gif",
|
|
70
|
+
".svg",
|
|
71
|
+
".webp",
|
|
72
|
+
".avif",
|
|
73
|
+
".ico",
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
/** @param {string} ext */
|
|
77
|
+
function isImage(ext) {
|
|
78
|
+
return IMAGE_EXTENSIONS.has(ext);
|
|
79
|
+
}
|
|
80
|
+
|
|
65
81
|
/** Map a file path to a display category. Media files override by extension. */
|
|
66
82
|
function categoryFor(/** @type {string} */ dir, /** @type {string} */ ext) {
|
|
67
83
|
if (ext && MEDIA_EXTENSIONS.has(ext)) return "Media";
|
|
@@ -396,9 +412,14 @@ export async function renderBrowse(container, ctx) {
|
|
|
396
412
|
<sp-table-row
|
|
397
413
|
value=${f.path}
|
|
398
414
|
class="browse-row"
|
|
399
|
-
|
|
415
|
+
style=${isImage(f.ext) ? "cursor:default" : ""}
|
|
416
|
+
@click=${isImage(f.ext) ? nothing : () => ctx.openFile(f.path)}
|
|
400
417
|
>
|
|
401
|
-
<sp-table-cell class="browse-name-cell"
|
|
418
|
+
<sp-table-cell class="browse-name-cell"
|
|
419
|
+
>${isImage(f.ext)
|
|
420
|
+
? html`<img class="browse-thumb" src="/${f.path}" />`
|
|
421
|
+
: nothing}${f.name}</sp-table-cell
|
|
422
|
+
>
|
|
402
423
|
<sp-table-cell>${f.category}</sp-table-cell>
|
|
403
424
|
<sp-table-cell>${f.type}</sp-table-cell>
|
|
404
425
|
<sp-table-cell class="browse-path-cell">${f.path}</sp-table-cell>
|
|
@@ -173,8 +173,8 @@ export function renderCanvas() {
|
|
|
173
173
|
return;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
//
|
|
177
|
-
if (canvasMode === "
|
|
176
|
+
// Stylebook mode: render element catalog with panzoom surface
|
|
177
|
+
if (canvasMode === "stylebook") {
|
|
178
178
|
renderStylebookMode({
|
|
179
179
|
canvasPanelTemplate,
|
|
180
180
|
applyTransform,
|
|
@@ -170,7 +170,7 @@ export function applyTransform() {
|
|
|
170
170
|
view.panzoomWrap.style.transform = `translate(${view.panX}px, ${view.panY}px) scale(${zoom})`;
|
|
171
171
|
renderZoomIndicator();
|
|
172
172
|
renderOnly("overlays");
|
|
173
|
-
if (_ctx.getCanvasMode() === "
|
|
173
|
+
if (_ctx.getCanvasMode() === "stylebook") _ctx.renderStylebookOverlays();
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
/** Calculate zoom + pan to fit all panels within the viewport. */
|
package/src/editor/shortcuts.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "../tabs/transact.js";
|
|
18
18
|
import { isEditing } from "./inline-edit.js";
|
|
19
19
|
import { copyNode, cutNode, pasteNode } from "./context-menu.js";
|
|
20
|
+
import { openQuickSearch } from "../panels/quick-search.js";
|
|
20
21
|
|
|
21
22
|
/** @typedef {import("../state.js").JxPath} JxPath */
|
|
22
23
|
|
|
@@ -44,6 +45,8 @@ export function initShortcuts(getContext) {
|
|
|
44
45
|
const { canvasMode, panX, panY, setPan, applyTransform } = getContext();
|
|
45
46
|
// Edit (content) mode: let the scroll container handle scrolling natively
|
|
46
47
|
if (canvasMode === "edit") return;
|
|
48
|
+
// Manage mode: browse table handles its own scrolling
|
|
49
|
+
if (canvasMode === "manage") return;
|
|
47
50
|
e.preventDefault();
|
|
48
51
|
if (e.ctrlKey || e.metaKey) {
|
|
49
52
|
// Zoom towards cursor
|
|
@@ -164,6 +167,10 @@ export function initShortcuts(getContext) {
|
|
|
164
167
|
e.preventDefault();
|
|
165
168
|
openProject();
|
|
166
169
|
break;
|
|
170
|
+
case "p":
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
openQuickSearch();
|
|
173
|
+
break;
|
|
167
174
|
case "s":
|
|
168
175
|
e.preventDefault();
|
|
169
176
|
saveFile();
|
package/src/files/files.js
CHANGED
|
@@ -15,9 +15,16 @@ import { createState, projectState, setProjectState } from "../store.js";
|
|
|
15
15
|
import { getPlatform } from "../platform.js";
|
|
16
16
|
import { statusMessage } from "../panels/statusbar.js";
|
|
17
17
|
import { loadComponentRegistry } from "./components.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
workspace,
|
|
20
|
+
openTab,
|
|
21
|
+
activateTab,
|
|
22
|
+
replaceAllTabs,
|
|
23
|
+
activeTab,
|
|
24
|
+
} from "../workspace/workspace.js";
|
|
19
25
|
import { loadMarkdown } from "./file-ops.js";
|
|
20
26
|
import { view } from "../view.js";
|
|
27
|
+
import { addRecentProject, trackRecentFile } from "../recent-projects.js";
|
|
21
28
|
|
|
22
29
|
// ─── File icon map ────────────────────────────────────────────────────────────
|
|
23
30
|
|
|
@@ -32,7 +39,7 @@ const fileIconMap = /** @type {Record<string, import("lit-html").TemplateResult>
|
|
|
32
39
|
|
|
33
40
|
// ─── File management ──────────────────────────────────────────────────────────
|
|
34
41
|
|
|
35
|
-
async function loadDirectory(/** @type {string} */ dirPath) {
|
|
42
|
+
export async function loadDirectory(/** @type {string} */ dirPath) {
|
|
36
43
|
if (!projectState) return;
|
|
37
44
|
try {
|
|
38
45
|
const platform = getPlatform();
|
|
@@ -131,6 +138,7 @@ export async function openProject({ renderActivityBar, renderLeftPanel }) {
|
|
|
131
138
|
projectState.projectDirs = foundDirs;
|
|
132
139
|
|
|
133
140
|
view.leftTab = "files";
|
|
141
|
+
addRecentProject(projectState.name, projectState.projectRoot);
|
|
134
142
|
renderActivityBar();
|
|
135
143
|
renderLeftPanel();
|
|
136
144
|
statusMessage(`Opened project: ${projectState.name}`);
|
|
@@ -633,6 +641,13 @@ export async function openFileInTab(path) {
|
|
|
633
641
|
sourceFormat: path.endsWith(".md") ? "md" : null,
|
|
634
642
|
});
|
|
635
643
|
projectState.selectedPath = path;
|
|
644
|
+
trackRecentFile({ path, name: path.split("/").pop() || path });
|
|
645
|
+
|
|
646
|
+
if (path === "project.json") {
|
|
647
|
+
const tab = activeTab.value;
|
|
648
|
+
if (tab) tab.session.ui.canvasMode = "stylebook";
|
|
649
|
+
}
|
|
650
|
+
|
|
636
651
|
statusMessage(`Opened ${path.split("/").pop()}`);
|
|
637
652
|
} catch (/** @type {any} */ e) {
|
|
638
653
|
statusMessage(`Error: ${e.message}`);
|
|
@@ -5,6 +5,7 @@ import { activityBar, renderOnly } from "../store.js";
|
|
|
5
5
|
import { effect, effectScope } from "../reactivity.js";
|
|
6
6
|
import { activeTab } from "../workspace/workspace.js";
|
|
7
7
|
import { view } from "../view.js";
|
|
8
|
+
import { openSettingsModal } from "../settings/settings-modal.js";
|
|
8
9
|
|
|
9
10
|
/** @type {import("@vue/reactivity").EffectScope | null} */
|
|
10
11
|
let _scope = null;
|
|
@@ -113,6 +114,17 @@ export function renderActivityBar() {
|
|
|
113
114
|
`,
|
|
114
115
|
)}
|
|
115
116
|
</sp-tabs>
|
|
117
|
+
<div style="margin-top:auto;padding:8px 0;display:flex;justify-content:center">
|
|
118
|
+
<sp-action-button
|
|
119
|
+
quiet
|
|
120
|
+
size="m"
|
|
121
|
+
title="Settings"
|
|
122
|
+
aria-label="Settings"
|
|
123
|
+
@click=${() => openSettingsModal()}
|
|
124
|
+
>
|
|
125
|
+
<sp-icon-gears slot="icon"></sp-icon-gears>
|
|
126
|
+
</sp-action-button>
|
|
127
|
+
</div>
|
|
116
128
|
`;
|
|
117
129
|
litRender(tpl, /** @type {any} */ (activityBar));
|
|
118
130
|
}
|
package/src/panels/left-panel.js
CHANGED
|
@@ -131,7 +131,7 @@ function _render() {
|
|
|
131
131
|
let content;
|
|
132
132
|
if (tab === "layers")
|
|
133
133
|
content =
|
|
134
|
-
ctx.getCanvasMode() === "
|
|
134
|
+
ctx.getCanvasMode() === "stylebook"
|
|
135
135
|
? renderStylebookLayersTemplate({
|
|
136
136
|
selectStylebookTag,
|
|
137
137
|
stylebookMeta,
|
|
@@ -197,7 +197,7 @@ function _render() {
|
|
|
197
197
|
litRender(html`<div class="panel-body">${content}</div>`, leftPanel);
|
|
198
198
|
|
|
199
199
|
// Post-render side effects
|
|
200
|
-
if (tab === "layers" && ctx.getCanvasMode() !== "
|
|
200
|
+
if (tab === "layers" && ctx.getCanvasMode() !== "stylebook") ctx.registerLayersDnD();
|
|
201
201
|
else if (tab === "imports") {
|
|
202
202
|
/* no post-render DnD needed */
|
|
203
203
|
} else if (tab === "blocks") {
|
package/src/panels/overlays.js
CHANGED
|
@@ -83,7 +83,7 @@ function _flush() {
|
|
|
83
83
|
const { stylebookTab } = tab.session.ui;
|
|
84
84
|
const canvasMode = _ctx.getCanvasMode();
|
|
85
85
|
|
|
86
|
-
if (canvasMode !== "design" && canvasMode !== "edit" && canvasMode !== "
|
|
86
|
+
if (canvasMode !== "design" && canvasMode !== "edit" && canvasMode !== "stylebook") {
|
|
87
87
|
for (const p of canvasPanels) {
|
|
88
88
|
litRender(nothing, p.overlay);
|
|
89
89
|
p.overlayClk.style.pointerEvents = "none";
|
|
@@ -95,7 +95,7 @@ function _flush() {
|
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
if (canvasMode === "
|
|
98
|
+
if (canvasMode === "stylebook") {
|
|
99
99
|
const enable = stylebookTab === "elements";
|
|
100
100
|
for (const p of canvasPanels) {
|
|
101
101
|
p.overlayClk.style.pointerEvents = enable ? "" : "none";
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { html, render as litRender, nothing } from "lit-html";
|
|
2
|
+
import { getPlatform } from "../platform.js";
|
|
3
|
+
import { openFileInTab } from "../files/files.js";
|
|
4
|
+
import { getRecentFiles, trackRecentFile } from "../recent-projects.js";
|
|
5
|
+
|
|
6
|
+
let _open = false;
|
|
7
|
+
let _query = "";
|
|
8
|
+
/** @type {any[]} */
|
|
9
|
+
let _results = [];
|
|
10
|
+
let _selectedIndex = 0;
|
|
11
|
+
let _debounceTimer = 0;
|
|
12
|
+
|
|
13
|
+
/** @type {HTMLElement | null} */
|
|
14
|
+
let _container = null;
|
|
15
|
+
|
|
16
|
+
export function initQuickSearch() {
|
|
17
|
+
_container = document.createElement("div");
|
|
18
|
+
_container.style.display = "contents";
|
|
19
|
+
(document.querySelector("sp-theme") || document.body).appendChild(_container);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function openQuickSearch() {
|
|
23
|
+
_open = true;
|
|
24
|
+
_query = "";
|
|
25
|
+
_results = [];
|
|
26
|
+
_selectedIndex = 0;
|
|
27
|
+
renderOverlay();
|
|
28
|
+
requestAnimationFrame(() => {
|
|
29
|
+
const input = _container?.querySelector(".quick-search-input");
|
|
30
|
+
if (input) /** @type {HTMLInputElement} */ (input).focus();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function closeQuickSearch() {
|
|
35
|
+
_open = false;
|
|
36
|
+
renderOverlay();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function doSearch(/** @type {string} */ query) {
|
|
40
|
+
if (!query.trim()) {
|
|
41
|
+
_results = [];
|
|
42
|
+
_selectedIndex = 0;
|
|
43
|
+
renderOverlay();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const platform = getPlatform();
|
|
48
|
+
_results = await platform.searchFiles(query.trim().toLowerCase());
|
|
49
|
+
_selectedIndex = 0;
|
|
50
|
+
renderOverlay();
|
|
51
|
+
} catch {
|
|
52
|
+
_results = [];
|
|
53
|
+
renderOverlay();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function onInput(/** @type {Event} */ e) {
|
|
58
|
+
_query = /** @type {HTMLInputElement} */ (e.target).value;
|
|
59
|
+
clearTimeout(_debounceTimer);
|
|
60
|
+
_debounceTimer = /** @type {any} */ (setTimeout(() => doSearch(_query), 150));
|
|
61
|
+
renderOverlay();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function onKeydown(/** @type {KeyboardEvent} */ e) {
|
|
65
|
+
const items = _query.trim() ? _results : getRecentFiles();
|
|
66
|
+
switch (e.key) {
|
|
67
|
+
case "ArrowDown":
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
_selectedIndex = Math.min(_selectedIndex + 1, items.length - 1);
|
|
70
|
+
renderOverlay();
|
|
71
|
+
break;
|
|
72
|
+
case "ArrowUp":
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
_selectedIndex = Math.max(_selectedIndex - 1, 0);
|
|
75
|
+
renderOverlay();
|
|
76
|
+
break;
|
|
77
|
+
case "Enter":
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
if (items[_selectedIndex]) selectItem(items[_selectedIndex]);
|
|
80
|
+
break;
|
|
81
|
+
case "Escape":
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
closeQuickSearch();
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function selectItem(/** @type {any} */ item) {
|
|
89
|
+
closeQuickSearch();
|
|
90
|
+
const path = item.path;
|
|
91
|
+
trackRecentFile({ path, name: path.split("/").pop() });
|
|
92
|
+
openFileInTab(path);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function fileIcon(/** @type {string} */ name) {
|
|
96
|
+
const ext = name.split(".").pop()?.toLowerCase();
|
|
97
|
+
switch (ext) {
|
|
98
|
+
case "json":
|
|
99
|
+
return html`<sp-icon-file-code size="s"></sp-icon-file-code>`;
|
|
100
|
+
case "md":
|
|
101
|
+
return html`<sp-icon-file-txt size="s"></sp-icon-file-txt>`;
|
|
102
|
+
default:
|
|
103
|
+
return html`<sp-icon-document size="s"></sp-icon-document>`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function dirPart(/** @type {string} */ path) {
|
|
108
|
+
const parts = path.split("/");
|
|
109
|
+
parts.pop();
|
|
110
|
+
return parts.length ? parts.join("/") : "";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function renderOverlay() {
|
|
114
|
+
if (!_container) return;
|
|
115
|
+
if (!_open) {
|
|
116
|
+
litRender(nothing, _container);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const recentFiles = getRecentFiles();
|
|
121
|
+
const showRecent = !_query.trim();
|
|
122
|
+
const items = showRecent ? recentFiles : _results;
|
|
123
|
+
|
|
124
|
+
const tpl = html`
|
|
125
|
+
<div class="quick-search-overlay" @click=${closeQuickSearch}>
|
|
126
|
+
<div class="quick-search-panel" @click=${(/** @type {Event} */ e) => e.stopPropagation()}>
|
|
127
|
+
<input
|
|
128
|
+
class="quick-search-input"
|
|
129
|
+
type="text"
|
|
130
|
+
placeholder="Search project files…"
|
|
131
|
+
.value=${_query}
|
|
132
|
+
@input=${onInput}
|
|
133
|
+
@keydown=${onKeydown}
|
|
134
|
+
/>
|
|
135
|
+
<div class="quick-search-results">
|
|
136
|
+
${items.length === 0 && _query.trim()
|
|
137
|
+
? html`<div class="quick-search-empty">No results</div>`
|
|
138
|
+
: nothing}
|
|
139
|
+
${items.length === 0 && !_query.trim() && recentFiles.length === 0
|
|
140
|
+
? html`<div class="quick-search-empty">Type to search project files</div>`
|
|
141
|
+
: nothing}
|
|
142
|
+
${showRecent && recentFiles.length
|
|
143
|
+
? html`<div class="quick-search-section-label">Recently opened</div>`
|
|
144
|
+
: nothing}
|
|
145
|
+
${items.map(
|
|
146
|
+
(item, i) => html`
|
|
147
|
+
<div
|
|
148
|
+
class="quick-search-item ${i === _selectedIndex ? "selected" : ""}"
|
|
149
|
+
@click=${() => selectItem(item)}
|
|
150
|
+
@mouseenter=${() => {
|
|
151
|
+
_selectedIndex = i;
|
|
152
|
+
renderOverlay();
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
<span class="quick-search-icon"
|
|
156
|
+
>${fileIcon(item.name || item.path.split("/").pop())}</span
|
|
157
|
+
>
|
|
158
|
+
<span class="quick-search-name">${item.name || item.path.split("/").pop()}</span>
|
|
159
|
+
<span class="quick-search-path">${dirPart(item.path)}</span>
|
|
160
|
+
${showRecent ? html`<span class="quick-search-badge">recent</span>` : nothing}
|
|
161
|
+
</div>
|
|
162
|
+
`,
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
`;
|
|
168
|
+
|
|
169
|
+
litRender(tpl, _container);
|
|
170
|
+
}
|
|
@@ -620,7 +620,7 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
|
|
|
620
620
|
export function renderStylePanelTemplate(ctx) {
|
|
621
621
|
const tab = activeTab.value;
|
|
622
622
|
if (!tab) return html`<div class="empty-state">No document loaded</div>`;
|
|
623
|
-
if (ctx.getCanvasMode() === "
|
|
623
|
+
if (ctx.getCanvasMode() === "stylebook" && tab.session.ui.stylebookSelection) {
|
|
624
624
|
const node = tab.doc.document;
|
|
625
625
|
if (!node) return html`<div class="empty-state">No document loaded</div>`;
|
|
626
626
|
return html`
|