@jxsuite/studio 0.1.0 → 0.5.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.
Files changed (38) hide show
  1. package/dist/studio.js +47638 -33445
  2. package/dist/studio.js.map +449 -344
  3. package/package.json +45 -34
  4. package/src/browse/browse.js +414 -0
  5. package/src/editor/context-menu.js +48 -1
  6. package/src/editor/convert-to-component.js +208 -0
  7. package/src/editor/inline-edit.js +33 -6
  8. package/src/editor/shortcuts.js +6 -1
  9. package/src/files/components.js +4 -2
  10. package/src/files/file-ops.js +102 -54
  11. package/src/files/files.js +22 -8
  12. package/src/markdown/md-convert.js +309 -11
  13. package/src/panels/activity-bar.js +3 -0
  14. package/src/panels/head-panel.js +576 -0
  15. package/src/panels/overlays.js +125 -0
  16. package/src/panels/right-panel.js +104 -0
  17. package/src/panels/shared.js +41 -0
  18. package/src/panels/signals-panel.js +95 -94
  19. package/src/panels/toolbar.js +217 -0
  20. package/src/platforms/devserver.js +58 -16
  21. package/src/settings/collections-editor.js +428 -0
  22. package/src/settings/defs-editor.js +418 -0
  23. package/src/settings/schema-field-ui.js +329 -0
  24. package/src/state.js +99 -2
  25. package/src/store.js +77 -41
  26. package/src/studio.js +1523 -1375
  27. package/src/ui/button-group.js +91 -0
  28. package/src/ui/color-selector.js +299 -0
  29. package/src/ui/field-row.js +47 -0
  30. package/src/ui/media-picker.js +172 -0
  31. package/src/ui/panel-resize.js +96 -0
  32. package/src/ui/spectrum.js +36 -2
  33. package/src/ui/unit-selector.js +106 -0
  34. package/src/ui/{jx-styled-combobox.js → value-selector.js} +7 -7
  35. package/src/ui/widgets.js +106 -0
  36. package/src/utils/inherited-style.js +54 -0
  37. package/src/utils/studio-utils.js +32 -0
  38. package/src/view.js +45 -0
@@ -0,0 +1,217 @@
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 } 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
+ const toolbarIconMap = /** @type {Record<string, any>} */ ({
19
+ "sp-icon-folder-open": html`<sp-icon-folder-open slot="icon"></sp-icon-folder-open>`,
20
+ "sp-icon-save-floppy": html`<sp-icon-save-floppy slot="icon"></sp-icon-save-floppy>`,
21
+ "sp-icon-back": html`<sp-icon-back slot="icon"></sp-icon-back>`,
22
+ "sp-icon-undo": html`<sp-icon-undo slot="icon"></sp-icon-undo>`,
23
+ "sp-icon-redo": html`<sp-icon-redo slot="icon"></sp-icon-redo>`,
24
+ "sp-icon-duplicate": html`<sp-icon-duplicate slot="icon"></sp-icon-duplicate>`,
25
+ "sp-icon-delete": html`<sp-icon-delete slot="icon"></sp-icon-delete>`,
26
+ "sp-icon-edit": html`<sp-icon-edit slot="icon"></sp-icon-edit>`,
27
+ "sp-icon-artboard": html`<sp-icon-artboard slot="icon"></sp-icon-artboard>`,
28
+ "sp-icon-preview": html`<sp-icon-preview slot="icon"></sp-icon-preview>`,
29
+ "sp-icon-code": html`<sp-icon-code slot="icon"></sp-icon-code>`,
30
+ "sp-icon-brush": html`<sp-icon-brush slot="icon"></sp-icon-brush>`,
31
+ "sp-icon-view-list": html`<sp-icon-view-list slot="icon"></sp-icon-view-list>`,
32
+ "sp-icon-gears": html`<sp-icon-gears slot="icon"></sp-icon-gears>`,
33
+ "sp-icon-document": html`<sp-icon-document slot="icon"></sp-icon-document>`,
34
+ });
35
+
36
+ /**
37
+ * @param {any} label
38
+ * @param {any} onClick
39
+ * @param {any} iconTag
40
+ */
41
+ function tbBtnTpl(label, onClick, iconTag) {
42
+ return html`
43
+ <sp-action-button size="s" @click=${onClick}>
44
+ ${iconTag ? toolbarIconMap[iconTag] : nothing} ${label}
45
+ </sp-action-button>
46
+ `;
47
+ }
48
+
49
+ /**
50
+ * Mount the toolbar panel.
51
+ *
52
+ * @param {HTMLElement} rootEl
53
+ * @param {any} ctx — { navigateBack, closeFunctionEditor, openProject, openFile, saveFile,
54
+ * parseMediaEntries, getCanvasMode, setCanvasMode, renderCanvas, safeRenderRightPanel }
55
+ */
56
+ export function mount(rootEl, ctx) {
57
+ _rootEl = rootEl;
58
+ _ctx = ctx;
59
+ }
60
+
61
+ export function unmount() {
62
+ _rootEl = null;
63
+ _ctx = null;
64
+ }
65
+
66
+ export function render() {
67
+ if (!_rootEl || !_ctx) return;
68
+ try {
69
+ litRender(toolbarTemplate(), _rootEl);
70
+ } catch (e) {
71
+ console.error("toolbar render error:", e);
72
+ }
73
+ }
74
+
75
+ function toolbarTemplate() {
76
+ const S = getState();
77
+ const canvasMode = _ctx.getCanvasMode();
78
+ const hasStack = S.documentStack && S.documentStack.length > 0;
79
+ const hasFunc = !!S.ui.editingFunction;
80
+
81
+ const breadcrumbTpl =
82
+ hasStack || hasFunc
83
+ ? html`
84
+ <div class="breadcrumb">
85
+ <sp-action-button
86
+ size="s"
87
+ title=${hasFunc ? "Close function editor" : "Return to parent document"}
88
+ @click=${hasFunc ? _ctx.closeFunctionEditor : _ctx.navigateBack}
89
+ >
90
+ ${toolbarIconMap["sp-icon-back"]}Back
91
+ </sp-action-button>
92
+ ${hasStack
93
+ ? S.documentStack.map(
94
+ (/** @type {any} */ frame) => html`
95
+ <span class="breadcrumb-item"
96
+ >${frame.documentPath?.split("/").pop() || "untitled"}</span
97
+ >
98
+ <span class="breadcrumb-sep"> › </span>
99
+ `,
100
+ )
101
+ : nothing}
102
+ <span
103
+ class="breadcrumb-item${hasFunc ? " clickable" : " current"}"
104
+ @click=${hasFunc ? _ctx.closeFunctionEditor : nothing}
105
+ >
106
+ ${S.documentPath?.split("/").pop() || S.document.tagName || "document"}
107
+ </span>
108
+ ${hasFunc
109
+ ? html`
110
+ <span class="breadcrumb-sep"> › </span>
111
+ <span class="breadcrumb-item current"
112
+ >${S.ui.editingFunction.type === "def"
113
+ ? `ƒ ${S.ui.editingFunction.defName}`
114
+ : `ƒ ${S.ui.editingFunction.eventKey}`}</span
115
+ >
116
+ `
117
+ : nothing}
118
+ </div>
119
+ `
120
+ : nothing;
121
+
122
+ const { featureQueries } = _ctx.parseMediaEntries(getEffectiveMedia(S.document.$media));
123
+ const togglesTpl =
124
+ featureQueries.length > 0
125
+ ? html`
126
+ <sp-action-group compact size="s">
127
+ ${featureQueries.map(
128
+ (/** @type {any} */ { name, query }) => html`
129
+ <sp-action-button
130
+ toggles
131
+ size="s"
132
+ title=${query}
133
+ ?selected=${!!S.ui.featureToggles[name]}
134
+ @click=${() => {
135
+ const newToggles = {
136
+ ...S.ui.featureToggles,
137
+ [name]: !S.ui.featureToggles[name],
138
+ };
139
+ updateUi("featureToggles", newToggles);
140
+ }}
141
+ >
142
+ ${mediaDisplayName(name)}
143
+ </sp-action-button>
144
+ `,
145
+ )}
146
+ </sp-action-group>
147
+ `
148
+ : nothing;
149
+
150
+ const modes = [
151
+ { key: "manage", label: "Manage", iconTag: "sp-icon-view-list" },
152
+ { key: "edit", label: "Edit", iconTag: "sp-icon-edit" },
153
+ { key: "design", label: "Design", iconTag: "sp-icon-artboard" },
154
+ { key: "preview", label: "Preview", iconTag: "sp-icon-preview" },
155
+ { key: "source", label: "Code", iconTag: "sp-icon-code" },
156
+ { key: "settings", label: "Settings", iconTag: "sp-icon-gears" },
157
+ ];
158
+
159
+ const modeSwitcherTpl = html`
160
+ <sp-action-group selects="single" size="s" compact>
161
+ ${modes.map(
162
+ (m) => html`
163
+ <sp-action-button
164
+ size="s"
165
+ ?selected=${canvasMode === m.key}
166
+ @click=${() => {
167
+ if (canvasMode === m.key) return;
168
+ if (S.ui.editingFunction) {
169
+ if (view.functionEditor) {
170
+ view.functionEditor.dispose();
171
+ view.functionEditor = null;
172
+ }
173
+ }
174
+ _ctx.setCanvasMode(m.key);
175
+ view.panX = 0;
176
+ view.panY = 0;
177
+ /** @type {Record<string, any>} */
178
+ const uiPatch = { editingFunction: null };
179
+ if (m.key === "settings") uiPatch.rightTab = "style";
180
+ if (m.key === "manage") uiPatch.leftTab = "files";
181
+ updateSession({ ui: uiPatch });
182
+ _ctx.renderCanvas();
183
+ _ctx.safeRenderRightPanel();
184
+ }}
185
+ >
186
+ ${toolbarIconMap[m.iconTag]}${m.label}
187
+ </sp-action-button>
188
+ `,
189
+ )}
190
+ </sp-action-group>
191
+ `;
192
+
193
+ return html`
194
+ <sp-action-group compact size="s">
195
+ ${tbBtnTpl("Open Project", _ctx.openProject, "sp-icon-folder-open")}
196
+ ${tbBtnTpl("Open File", _ctx.openFile, "sp-icon-document")}
197
+ ${tbBtnTpl("Save", _ctx.saveFile, "sp-icon-save-floppy")}
198
+ </sp-action-group>
199
+ <sp-action-group compact size="s">
200
+ ${tbBtnTpl("Undo", () => update(undo(getState())), "sp-icon-undo")}
201
+ ${tbBtnTpl("Redo", () => update(redo(getState())), "sp-icon-redo")}
202
+ </sp-action-group>
203
+ <div class="tb-spacer"></div>
204
+ ${S.documentPath
205
+ ? html`<span class="tb-file-title" title=${S.documentPath}
206
+ >${S.documentPath}${S.dirty ? html`<span class="tb-dirty">●</span>` : nothing}</span
207
+ >`
208
+ : S.fileHandle
209
+ ? html`<span class="tb-file-title"
210
+ >${S.fileHandle.name}${S.dirty ? html`<span class="tb-dirty">●</span>` : nothing}</span
211
+ >`
212
+ : nothing}
213
+ ${breadcrumbTpl}
214
+ <div class="tb-spacer"></div>
215
+ ${togglesTpl} ${modeSwitcherTpl}
216
+ `;
217
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Devserver.js — Dev Server Platform Adapter
3
3
  *
4
- * Implements the StudioPlatform interface for the @jxplatform/server development workflow. All file
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
- if (!_projectRoot || _projectRoot === ".") return rel;
29
- return rel === "." ? _projectRoot : `${_projectRoot}/${rel}`;
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
- if (!_projectRoot || _projectRoot === ".") return path;
39
- return path.startsWith(_projectRoot + "/") ? path.slice(_projectRoot.length + 1) : path;
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 (server-relative path). */
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
- throw new Error("Selected project is not under the dev server root");
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
- _projectRoot = match.path;
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: match.path,
99
- name: config.name || match.path.split("/").pop(),
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
- const url =
189
- scanDir === "."
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();