@jxsuite/studio 0.1.0 → 0.5.1
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 +50941 -34749
- package/dist/studio.js.map +461 -345
- package/package.json +46 -35
- package/src/browse/browse.js +414 -0
- package/src/editor/context-menu.js +48 -1
- package/src/editor/convert-to-component.js +208 -0
- package/src/editor/inline-edit.js +33 -6
- package/src/editor/shortcuts.js +6 -1
- package/src/files/components.js +4 -2
- package/src/files/file-ops.js +102 -54
- package/src/files/files.js +22 -8
- package/src/markdown/md-convert.js +309 -11
- package/src/panels/activity-bar.js +3 -0
- package/src/panels/head-panel.js +576 -0
- package/src/panels/overlays.js +133 -0
- package/src/panels/right-panel.js +130 -0
- package/src/panels/shared.js +41 -0
- package/src/panels/signals-panel.js +95 -94
- package/src/panels/statusbar.js +15 -1
- package/src/panels/toolbar.js +223 -0
- package/src/platforms/devserver.js +58 -16
- package/src/settings/collections-editor.js +428 -0
- package/src/settings/defs-editor.js +418 -0
- package/src/settings/schema-field-ui.js +329 -0
- package/src/state.js +99 -2
- package/src/store.js +112 -41
- package/src/studio.js +1551 -1565
- package/src/ui/button-group.js +91 -0
- package/src/ui/color-selector.js +299 -0
- package/src/ui/field-row.js +47 -0
- package/src/ui/media-picker.js +172 -0
- package/src/ui/panel-resize.js +96 -0
- package/src/ui/spectrum.js +36 -2
- package/src/ui/unit-selector.js +106 -0
- package/src/ui/{jx-styled-combobox.js → value-selector.js} +7 -7
- package/src/ui/widgets.js +106 -0
- package/src/utils/canvas-media.js +151 -0
- package/src/utils/inherited-style.js +54 -0
- package/src/utils/studio-utils.js +32 -0
- package/src/view.js +68 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toolbar panel — extracted from studio.js renderToolbar(). Owns rendering of breadcrumbs, file
|
|
3
|
+
* ops, feature toggles, and mode switcher.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { html, render as litRender, nothing } from "lit-html";
|
|
7
|
+
import { getState, update, updateSession, updateUi, undo, redo, subscribe } from "../store.js";
|
|
8
|
+
import { getEffectiveMedia } from "../site-context.js";
|
|
9
|
+
import { mediaDisplayName } from "./shared.js";
|
|
10
|
+
import { view } from "../view.js";
|
|
11
|
+
|
|
12
|
+
/** @type {HTMLElement | null} */
|
|
13
|
+
let _rootEl = null;
|
|
14
|
+
|
|
15
|
+
/** @type {any} */
|
|
16
|
+
let _ctx = null;
|
|
17
|
+
|
|
18
|
+
/** @type {(() => void) | null} */
|
|
19
|
+
let _unsub = null;
|
|
20
|
+
|
|
21
|
+
const toolbarIconMap = /** @type {Record<string, any>} */ ({
|
|
22
|
+
"sp-icon-folder-open": html`<sp-icon-folder-open slot="icon"></sp-icon-folder-open>`,
|
|
23
|
+
"sp-icon-save-floppy": html`<sp-icon-save-floppy slot="icon"></sp-icon-save-floppy>`,
|
|
24
|
+
"sp-icon-back": html`<sp-icon-back slot="icon"></sp-icon-back>`,
|
|
25
|
+
"sp-icon-undo": html`<sp-icon-undo slot="icon"></sp-icon-undo>`,
|
|
26
|
+
"sp-icon-redo": html`<sp-icon-redo slot="icon"></sp-icon-redo>`,
|
|
27
|
+
"sp-icon-duplicate": html`<sp-icon-duplicate slot="icon"></sp-icon-duplicate>`,
|
|
28
|
+
"sp-icon-delete": html`<sp-icon-delete slot="icon"></sp-icon-delete>`,
|
|
29
|
+
"sp-icon-edit": html`<sp-icon-edit slot="icon"></sp-icon-edit>`,
|
|
30
|
+
"sp-icon-artboard": html`<sp-icon-artboard slot="icon"></sp-icon-artboard>`,
|
|
31
|
+
"sp-icon-preview": html`<sp-icon-preview slot="icon"></sp-icon-preview>`,
|
|
32
|
+
"sp-icon-code": html`<sp-icon-code slot="icon"></sp-icon-code>`,
|
|
33
|
+
"sp-icon-brush": html`<sp-icon-brush slot="icon"></sp-icon-brush>`,
|
|
34
|
+
"sp-icon-view-list": html`<sp-icon-view-list slot="icon"></sp-icon-view-list>`,
|
|
35
|
+
"sp-icon-gears": html`<sp-icon-gears slot="icon"></sp-icon-gears>`,
|
|
36
|
+
"sp-icon-document": html`<sp-icon-document slot="icon"></sp-icon-document>`,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {any} label
|
|
41
|
+
* @param {any} onClick
|
|
42
|
+
* @param {any} iconTag
|
|
43
|
+
*/
|
|
44
|
+
function tbBtnTpl(label, onClick, iconTag) {
|
|
45
|
+
return html`
|
|
46
|
+
<sp-action-button size="s" @click=${onClick}>
|
|
47
|
+
${iconTag ? toolbarIconMap[iconTag] : nothing} ${label}
|
|
48
|
+
</sp-action-button>
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Mount the toolbar panel.
|
|
54
|
+
*
|
|
55
|
+
* @param {HTMLElement} rootEl
|
|
56
|
+
* @param {any} ctx — { navigateBack, closeFunctionEditor, openProject, openFile, saveFile,
|
|
57
|
+
* parseMediaEntries, getCanvasMode, setCanvasMode, renderCanvas, safeRenderRightPanel }
|
|
58
|
+
*/
|
|
59
|
+
export function mount(rootEl, ctx) {
|
|
60
|
+
_rootEl = rootEl;
|
|
61
|
+
_ctx = ctx;
|
|
62
|
+
_unsub = subscribe(() => render());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function unmount() {
|
|
66
|
+
_unsub?.();
|
|
67
|
+
_unsub = null;
|
|
68
|
+
_rootEl = null;
|
|
69
|
+
_ctx = null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function render() {
|
|
73
|
+
if (!_rootEl || !_ctx) return;
|
|
74
|
+
try {
|
|
75
|
+
litRender(toolbarTemplate(), _rootEl);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.error("toolbar render error:", e);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toolbarTemplate() {
|
|
82
|
+
const S = getState();
|
|
83
|
+
const canvasMode = _ctx.getCanvasMode();
|
|
84
|
+
const hasStack = S.documentStack && S.documentStack.length > 0;
|
|
85
|
+
const hasFunc = !!S.ui.editingFunction;
|
|
86
|
+
|
|
87
|
+
const breadcrumbTpl =
|
|
88
|
+
hasStack || hasFunc
|
|
89
|
+
? html`
|
|
90
|
+
<div class="breadcrumb">
|
|
91
|
+
<sp-action-button
|
|
92
|
+
size="s"
|
|
93
|
+
title=${hasFunc ? "Close function editor" : "Return to parent document"}
|
|
94
|
+
@click=${hasFunc ? _ctx.closeFunctionEditor : _ctx.navigateBack}
|
|
95
|
+
>
|
|
96
|
+
${toolbarIconMap["sp-icon-back"]}Back
|
|
97
|
+
</sp-action-button>
|
|
98
|
+
${hasStack
|
|
99
|
+
? S.documentStack.map(
|
|
100
|
+
(/** @type {any} */ frame) => html`
|
|
101
|
+
<span class="breadcrumb-item"
|
|
102
|
+
>${frame.documentPath?.split("/").pop() || "untitled"}</span
|
|
103
|
+
>
|
|
104
|
+
<span class="breadcrumb-sep"> › </span>
|
|
105
|
+
`,
|
|
106
|
+
)
|
|
107
|
+
: nothing}
|
|
108
|
+
<span
|
|
109
|
+
class="breadcrumb-item${hasFunc ? " clickable" : " current"}"
|
|
110
|
+
@click=${hasFunc ? _ctx.closeFunctionEditor : nothing}
|
|
111
|
+
>
|
|
112
|
+
${S.documentPath?.split("/").pop() || S.document.tagName || "document"}
|
|
113
|
+
</span>
|
|
114
|
+
${hasFunc
|
|
115
|
+
? html`
|
|
116
|
+
<span class="breadcrumb-sep"> › </span>
|
|
117
|
+
<span class="breadcrumb-item current"
|
|
118
|
+
>${S.ui.editingFunction.type === "def"
|
|
119
|
+
? `ƒ ${S.ui.editingFunction.defName}`
|
|
120
|
+
: `ƒ ${S.ui.editingFunction.eventKey}`}</span
|
|
121
|
+
>
|
|
122
|
+
`
|
|
123
|
+
: nothing}
|
|
124
|
+
</div>
|
|
125
|
+
`
|
|
126
|
+
: nothing;
|
|
127
|
+
|
|
128
|
+
const { featureQueries } = _ctx.parseMediaEntries(getEffectiveMedia(S.document.$media));
|
|
129
|
+
const togglesTpl =
|
|
130
|
+
featureQueries.length > 0
|
|
131
|
+
? html`
|
|
132
|
+
<sp-action-group compact size="s">
|
|
133
|
+
${featureQueries.map(
|
|
134
|
+
(/** @type {any} */ { name, query }) => html`
|
|
135
|
+
<sp-action-button
|
|
136
|
+
toggles
|
|
137
|
+
size="s"
|
|
138
|
+
title=${query}
|
|
139
|
+
?selected=${!!S.ui.featureToggles[name]}
|
|
140
|
+
@click=${() => {
|
|
141
|
+
const newToggles = {
|
|
142
|
+
...S.ui.featureToggles,
|
|
143
|
+
[name]: !S.ui.featureToggles[name],
|
|
144
|
+
};
|
|
145
|
+
updateUi("featureToggles", newToggles);
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
${mediaDisplayName(name)}
|
|
149
|
+
</sp-action-button>
|
|
150
|
+
`,
|
|
151
|
+
)}
|
|
152
|
+
</sp-action-group>
|
|
153
|
+
`
|
|
154
|
+
: nothing;
|
|
155
|
+
|
|
156
|
+
const modes = [
|
|
157
|
+
{ key: "manage", label: "Manage", iconTag: "sp-icon-view-list" },
|
|
158
|
+
{ key: "edit", label: "Edit", iconTag: "sp-icon-edit" },
|
|
159
|
+
{ key: "design", label: "Design", iconTag: "sp-icon-artboard" },
|
|
160
|
+
{ key: "preview", label: "Preview", iconTag: "sp-icon-preview" },
|
|
161
|
+
{ key: "source", label: "Code", iconTag: "sp-icon-code" },
|
|
162
|
+
{ key: "settings", label: "Settings", iconTag: "sp-icon-gears" },
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const modeSwitcherTpl = html`
|
|
166
|
+
<sp-action-group selects="single" size="s" compact>
|
|
167
|
+
${modes.map(
|
|
168
|
+
(m) => html`
|
|
169
|
+
<sp-action-button
|
|
170
|
+
size="s"
|
|
171
|
+
?selected=${canvasMode === m.key}
|
|
172
|
+
@click=${() => {
|
|
173
|
+
if (canvasMode === m.key) return;
|
|
174
|
+
if (S.ui.editingFunction) {
|
|
175
|
+
if (view.functionEditor) {
|
|
176
|
+
view.functionEditor.dispose();
|
|
177
|
+
view.functionEditor = null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
_ctx.setCanvasMode(m.key);
|
|
181
|
+
view.panX = 0;
|
|
182
|
+
view.panY = 0;
|
|
183
|
+
/** @type {Record<string, any>} */
|
|
184
|
+
const uiPatch = { editingFunction: null };
|
|
185
|
+
if (m.key === "settings") uiPatch.rightTab = "style";
|
|
186
|
+
if (m.key === "manage") uiPatch.leftTab = "files";
|
|
187
|
+
updateSession({ ui: uiPatch });
|
|
188
|
+
_ctx.renderCanvas();
|
|
189
|
+
_ctx.safeRenderRightPanel();
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
${toolbarIconMap[m.iconTag]}${m.label}
|
|
193
|
+
</sp-action-button>
|
|
194
|
+
`,
|
|
195
|
+
)}
|
|
196
|
+
</sp-action-group>
|
|
197
|
+
`;
|
|
198
|
+
|
|
199
|
+
return html`
|
|
200
|
+
<sp-action-group compact size="s">
|
|
201
|
+
${tbBtnTpl("Open Project", _ctx.openProject, "sp-icon-folder-open")}
|
|
202
|
+
${tbBtnTpl("Open File", _ctx.openFile, "sp-icon-document")}
|
|
203
|
+
${tbBtnTpl("Save", _ctx.saveFile, "sp-icon-save-floppy")}
|
|
204
|
+
</sp-action-group>
|
|
205
|
+
<sp-action-group compact size="s">
|
|
206
|
+
${tbBtnTpl("Undo", () => update(undo(getState())), "sp-icon-undo")}
|
|
207
|
+
${tbBtnTpl("Redo", () => update(redo(getState())), "sp-icon-redo")}
|
|
208
|
+
</sp-action-group>
|
|
209
|
+
<div class="tb-spacer"></div>
|
|
210
|
+
${S.documentPath
|
|
211
|
+
? html`<span class="tb-file-title" title=${S.documentPath}
|
|
212
|
+
>${S.documentPath}${S.dirty ? html`<span class="tb-dirty">●</span>` : nothing}</span
|
|
213
|
+
>`
|
|
214
|
+
: S.fileHandle
|
|
215
|
+
? html`<span class="tb-file-title"
|
|
216
|
+
>${S.fileHandle.name}${S.dirty ? html`<span class="tb-dirty">●</span>` : nothing}</span
|
|
217
|
+
>`
|
|
218
|
+
: nothing}
|
|
219
|
+
${breadcrumbTpl}
|
|
220
|
+
<div class="tb-spacer"></div>
|
|
221
|
+
${togglesTpl} ${modeSwitcherTpl}
|
|
222
|
+
`;
|
|
223
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Devserver.js — Dev Server Platform Adapter
|
|
3
3
|
*
|
|
4
|
-
* Implements the StudioPlatform interface for the @
|
|
4
|
+
* Implements the StudioPlatform interface for the @jxsuite/server development workflow. All file
|
|
5
5
|
* I/O goes through /__studio/* REST endpoints. Project opening uses the Chrome File System Access
|
|
6
6
|
* API (showDirectoryPicker).
|
|
7
7
|
*
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* from responses.
|
|
18
18
|
*/
|
|
19
19
|
export function createDevServerPlatform() {
|
|
20
|
-
let _projectRoot = "
|
|
20
|
+
let _projectRoot = "";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Prefix a project-relative path with the active project root for server API calls.
|
|
@@ -25,8 +25,10 @@ export function createDevServerPlatform() {
|
|
|
25
25
|
* @param {string} rel
|
|
26
26
|
*/
|
|
27
27
|
function serverPath(rel) {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const r = rel.replaceAll("\\", "/");
|
|
29
|
+
if (!_projectRoot) return r;
|
|
30
|
+
if (r === ".") return _projectRoot;
|
|
31
|
+
return `${_projectRoot}/${r}`;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -35,19 +37,36 @@ export function createDevServerPlatform() {
|
|
|
35
37
|
* @param {string} path
|
|
36
38
|
*/
|
|
37
39
|
function stripRoot(path) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
const p = path.replaceAll("\\", "/");
|
|
41
|
+
if (!_projectRoot) return p;
|
|
42
|
+
return p.startsWith(_projectRoot + "/") ? p.slice(_projectRoot.length + 1) : p;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
return {
|
|
43
46
|
id: "devserver",
|
|
44
47
|
|
|
45
|
-
/** Get or set the current project root (
|
|
48
|
+
/** Get or set the current project root (absolute path). */
|
|
46
49
|
get projectRoot() {
|
|
47
50
|
return _projectRoot;
|
|
48
51
|
},
|
|
49
52
|
set projectRoot(v) {
|
|
50
|
-
_projectRoot = v || "
|
|
53
|
+
_projectRoot = v || "";
|
|
54
|
+
if (_projectRoot) this.activate(_projectRoot);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Notify the server which project root to use for resolving static file paths. Returns a
|
|
59
|
+
* promise so callers can await activation before loading assets.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} [root]
|
|
62
|
+
*/
|
|
63
|
+
async activate(root) {
|
|
64
|
+
const r = root ?? _projectRoot;
|
|
65
|
+
await fetch("/__studio/activate", {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "Content-Type": "application/json" },
|
|
68
|
+
body: JSON.stringify({ root: r }),
|
|
69
|
+
});
|
|
51
70
|
},
|
|
52
71
|
|
|
53
72
|
// ─── Project opening ──────────────────────────────────────────────────
|
|
@@ -87,16 +106,26 @@ export function createDevServerPlatform() {
|
|
|
87
106
|
);
|
|
88
107
|
|
|
89
108
|
if (!match) {
|
|
90
|
-
|
|
109
|
+
// Project is outside dev server root — ask the server to find it by directory name
|
|
110
|
+
const findRes = await fetch(
|
|
111
|
+
`/__studio/find-project?name=${encodeURIComponent(dirHandle.name)}`,
|
|
112
|
+
);
|
|
113
|
+
if (!findRes.ok) throw new Error("Could not locate project on disk");
|
|
114
|
+
const found = await findRes.json();
|
|
115
|
+
if (!found.path) throw new Error(`Could not find project directory "${dirHandle.name}"`);
|
|
116
|
+
_projectRoot = found.path;
|
|
117
|
+
} else {
|
|
118
|
+
_projectRoot = match.path;
|
|
91
119
|
}
|
|
92
120
|
|
|
93
|
-
|
|
121
|
+
// Notify server of active project for static file resolution
|
|
122
|
+
await this.activate();
|
|
94
123
|
|
|
95
124
|
return {
|
|
96
125
|
config,
|
|
97
126
|
handle: {
|
|
98
|
-
root:
|
|
99
|
-
name: config.name ||
|
|
127
|
+
root: _projectRoot,
|
|
128
|
+
name: config.name || _projectRoot.split("/").pop(),
|
|
100
129
|
projectConfig: config,
|
|
101
130
|
},
|
|
102
131
|
};
|
|
@@ -151,6 +180,21 @@ export function createDevServerPlatform() {
|
|
|
151
180
|
if (!res.ok) throw new Error(`Failed to write file: ${path}`);
|
|
152
181
|
},
|
|
153
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Upload a binary file (image, video, font, etc.).
|
|
185
|
+
*
|
|
186
|
+
* @param {string} path — project-relative destination path
|
|
187
|
+
* @param {File | Blob | ArrayBuffer} data — file content
|
|
188
|
+
*/
|
|
189
|
+
async uploadFile(path, data) {
|
|
190
|
+
const res = await fetch(
|
|
191
|
+
`/__studio/file/upload?path=${encodeURIComponent(serverPath(path))}`,
|
|
192
|
+
{ method: "POST", body: data },
|
|
193
|
+
);
|
|
194
|
+
if (!res.ok) throw new Error(`Upload failed: ${path}`);
|
|
195
|
+
return await res.json();
|
|
196
|
+
},
|
|
197
|
+
|
|
154
198
|
/** @param {string} path */
|
|
155
199
|
async deleteFile(path) {
|
|
156
200
|
const res = await fetch(`/__studio/file?path=${encodeURIComponent(serverPath(path))}`, {
|
|
@@ -185,10 +229,8 @@ export function createDevServerPlatform() {
|
|
|
185
229
|
/** @param {string} dir */
|
|
186
230
|
async discoverComponents(dir) {
|
|
187
231
|
const scanDir = dir || _projectRoot;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
? "/__studio/components"
|
|
191
|
-
: `/__studio/components?dir=${encodeURIComponent(scanDir)}`;
|
|
232
|
+
if (!scanDir) return [];
|
|
233
|
+
const url = `/__studio/components?dir=${encodeURIComponent(scanDir)}`;
|
|
192
234
|
const res = await fetch(url);
|
|
193
235
|
if (!res.ok) return [];
|
|
194
236
|
return await res.json();
|