@jxsuite/studio 0.10.2 → 0.13.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 (53) hide show
  1. package/README.md +82 -0
  2. package/dist/studio.js +5114 -3424
  3. package/dist/studio.js.map +65 -49
  4. package/package.json +6 -3
  5. package/src/browse/browse.js +27 -3
  6. package/src/canvas/canvas-diff.js +184 -0
  7. package/src/canvas/canvas-helpers.js +10 -14
  8. package/src/canvas/canvas-live-render.js +136 -17
  9. package/src/canvas/canvas-render.js +154 -21
  10. package/src/canvas/canvas-utils.js +21 -23
  11. package/src/editor/component-inline-edit.js +54 -41
  12. package/src/editor/content-inline-edit.js +46 -47
  13. package/src/editor/context-menu.js +63 -39
  14. package/src/editor/convert-to-component.js +11 -14
  15. package/src/editor/insertion-helper.js +8 -10
  16. package/src/editor/shortcuts.js +69 -39
  17. package/src/files/components.js +15 -4
  18. package/src/files/files.js +72 -24
  19. package/src/panels/activity-bar.js +29 -7
  20. package/src/panels/block-action-bar.js +104 -80
  21. package/src/panels/canvas-dnd.js +132 -50
  22. package/src/panels/dnd.js +32 -28
  23. package/src/panels/editors.js +7 -14
  24. package/src/panels/elements-panel.js +9 -3
  25. package/src/panels/events-panel.js +44 -39
  26. package/src/panels/git-panel.js +45 -3
  27. package/src/panels/layers-panel.js +16 -11
  28. package/src/panels/left-panel.js +108 -43
  29. package/src/panels/overlays.js +97 -35
  30. package/src/panels/panel-events.js +18 -11
  31. package/src/panels/properties-panel.js +467 -98
  32. package/src/panels/right-panel.js +85 -37
  33. package/src/panels/shared.js +0 -22
  34. package/src/panels/signals-panel.js +125 -54
  35. package/src/panels/statusbar.js +23 -8
  36. package/src/panels/style-inputs.js +4 -2
  37. package/src/panels/style-panel.js +128 -105
  38. package/src/panels/stylebook-panel.js +42 -17
  39. package/src/panels/tab-strip.js +124 -0
  40. package/src/panels/toolbar.js +39 -9
  41. package/src/reactivity.js +28 -0
  42. package/src/settings/content-types-editor.js +78 -8
  43. package/src/settings/defs-editor.js +56 -7
  44. package/src/settings/schema-field-ui.js +99 -11
  45. package/src/site-context.js +105 -0
  46. package/src/state.js +0 -456
  47. package/src/store.js +105 -124
  48. package/src/studio.js +112 -121
  49. package/src/tabs/tab.js +153 -0
  50. package/src/tabs/transact.js +406 -0
  51. package/src/ui/spectrum.js +2 -0
  52. package/src/view.js +3 -0
  53. package/src/workspace/workspace.js +89 -0
@@ -0,0 +1,406 @@
1
+ import { toRaw } from "../reactivity.js";
2
+ import { getNodeAtPath, parentElementPath, childIndex, pathsEqual, isAncestor } from "../state.js";
3
+
4
+ /**
5
+ * @typedef {import("../tabs/tab.js").Tab} Tab
6
+ *
7
+ * @typedef {import("../state.js").JxPath} JxPath
8
+ *
9
+ * @typedef {import("../state.js").JxNode} JxNode
10
+ *
11
+ * @typedef {string | number | boolean | object | null | undefined} JsonValue
12
+ */
13
+
14
+ const HISTORY_LIMIT = 100;
15
+
16
+ // ─── Transactional layer ─────────────────────────────────────────────────────
17
+
18
+ /**
19
+ * Apply a document mutation transactionally: push to history and mark dirty. The mutationFn
20
+ * receives the tab and should mutate tab.doc.document in place.
21
+ *
22
+ * @param {Tab} tab
23
+ * @param {(tab: Tab) => void} mutationFn
24
+ * @param {{ skipHistory?: boolean }} [opts]
25
+ */
26
+ export function transactDoc(tab, mutationFn, { skipHistory = false } = {}) {
27
+ mutationFn(tab);
28
+
29
+ if (!skipHistory) {
30
+ const raw = toRaw(tab.doc.document);
31
+ const snapshot = {
32
+ document: structuredClone(raw),
33
+ selection: tab.session.selection ? [...tab.session.selection] : null,
34
+ };
35
+ const truncated = tab.history.snapshots.slice(0, tab.history.index + 1);
36
+ truncated.push(snapshot);
37
+ if (truncated.length > HISTORY_LIMIT) truncated.shift();
38
+ tab.history.snapshots = truncated;
39
+ tab.history.index = truncated.length - 1;
40
+ }
41
+
42
+ tab.doc.dirty = true;
43
+ }
44
+
45
+ /**
46
+ * Convenience: transact with a mutation fn that receives the document directly.
47
+ *
48
+ * @param {Tab} tab
49
+ * @param {(doc: JxNode) => void} fn
50
+ * @param {{ skipHistory?: boolean }} [opts]
51
+ */
52
+ export function transact(tab, fn, opts) {
53
+ transactDoc(tab, (t) => fn(t.doc.document), opts);
54
+ }
55
+
56
+ // ─── Undo / Redo ─────────────────────────────────────────────────────────────
57
+
58
+ /** @param {Tab} tab */
59
+ export function undo(tab) {
60
+ if (tab.history.index <= 0) return;
61
+ tab.history.index--;
62
+ const snap = tab.history.snapshots[tab.history.index];
63
+ tab.doc.document = structuredClone(toRaw(snap.document));
64
+ tab.session.selection = snap.selection ? [...toRaw(snap.selection)] : null;
65
+ tab.doc.dirty = true;
66
+ }
67
+
68
+ /** @param {Tab} tab */
69
+ export function redo(tab) {
70
+ if (tab.history.index >= tab.history.snapshots.length - 1) return;
71
+ tab.history.index++;
72
+ const snap = tab.history.snapshots[tab.history.index];
73
+ tab.doc.document = structuredClone(toRaw(snap.document));
74
+ tab.session.selection = snap.selection ? [...toRaw(snap.selection)] : null;
75
+ tab.doc.dirty = true;
76
+ }
77
+
78
+ // ─── In-place document mutators ──────────────────────────────────────────────
79
+
80
+ /**
81
+ * @param {Tab} tab
82
+ * @param {JxPath} parentPath
83
+ * @param {number} index
84
+ * @param {JxNode} nodeDef
85
+ */
86
+ export function mutateInsertNode(tab, parentPath, index, nodeDef) {
87
+ const parent = getNodeAtPath(tab.doc.document, parentPath);
88
+ if (!parent.children) parent.children = [];
89
+ parent.children.splice(index, 0, structuredClone(nodeDef));
90
+ }
91
+
92
+ /**
93
+ * @param {Tab} tab
94
+ * @param {JxPath} path
95
+ */
96
+ export function mutateRemoveNode(tab, path) {
97
+ if (!path || path.length < 2) return;
98
+ const elemPath = /** @type {JxPath} */ (parentElementPath(path));
99
+ const idx = /** @type {number} */ (childIndex(path));
100
+ getNodeAtPath(tab.doc.document, elemPath).children.splice(idx, 1);
101
+ if (tab.session.selection && isAncestor(path, tab.session.selection)) {
102
+ tab.session.selection = null;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * @param {Tab} tab
108
+ * @param {JxPath} path
109
+ */
110
+ export function mutateDuplicateNode(tab, path) {
111
+ if (!path || path.length < 2) return;
112
+ const node = getNodeAtPath(tab.doc.document, path);
113
+ if (!node) return;
114
+ const elemPath = /** @type {JxPath} */ (parentElementPath(path));
115
+ const idx = /** @type {number} */ (childIndex(path));
116
+ const clone = structuredClone(toRaw(node));
117
+ getNodeAtPath(tab.doc.document, elemPath).children.splice(idx + 1, 0, clone);
118
+ tab.session.selection = [...elemPath, "children", idx + 1];
119
+ }
120
+
121
+ /**
122
+ * @param {Tab} tab
123
+ * @param {JxPath} path
124
+ * @param {string} wrapperTag
125
+ */
126
+ export function mutateWrapNode(tab, path, wrapperTag = "div") {
127
+ if (!path || path.length < 2) return;
128
+ const node = getNodeAtPath(tab.doc.document, path);
129
+ if (!node) return;
130
+ const elemPath = /** @type {JxPath} */ (parentElementPath(path));
131
+ const idx = /** @type {number} */ (childIndex(path));
132
+ const wrapper = { tagName: wrapperTag, children: [structuredClone(toRaw(node))] };
133
+ getNodeAtPath(tab.doc.document, elemPath).children.splice(idx, 1, wrapper);
134
+ tab.session.selection = [...elemPath, "children", idx];
135
+ }
136
+
137
+ /**
138
+ * @param {Tab} tab
139
+ * @param {JxPath} fromPath
140
+ * @param {JxPath} toParentPath
141
+ * @param {number} toIndex
142
+ */
143
+ export function mutateMoveNode(tab, fromPath, toParentPath, toIndex) {
144
+ const doc = tab.doc.document;
145
+ const fromParentPath = /** @type {JxPath} */ (parentElementPath(fromPath));
146
+ const fromIdx = /** @type {number} */ (childIndex(fromPath));
147
+ const fromParent = getNodeAtPath(doc, fromParentPath);
148
+ const [node] = fromParent.children.splice(fromIdx, 1);
149
+ const toParent = getNodeAtPath(doc, toParentPath);
150
+ if (!toParent.children) toParent.children = [];
151
+ let adjustedIndex = toIndex;
152
+ if (fromParent === toParent && fromIdx < toIndex) adjustedIndex--;
153
+ toParent.children.splice(adjustedIndex, 0, node);
154
+
155
+ if (pathsEqual(tab.session.selection, fromPath)) {
156
+ let idx = toIndex;
157
+ if (
158
+ fromParentPath.length === toParentPath.length &&
159
+ fromParentPath.every((v, i) => v === toParentPath[i]) &&
160
+ fromIdx < toIndex
161
+ ) {
162
+ idx = toIndex - 1;
163
+ }
164
+ tab.session.selection = [...toParentPath, "children", idx];
165
+ }
166
+ }
167
+
168
+ /**
169
+ * @param {Tab} tab
170
+ * @param {JxPath} path
171
+ * @param {string} key
172
+ * @param {JsonValue} value
173
+ */
174
+ export function mutateUpdateProperty(tab, path, key, value) {
175
+ const node = getNodeAtPath(tab.doc.document, path);
176
+ if (value === undefined || value === null || value === "") delete node[key];
177
+ else node[key] = value;
178
+ }
179
+
180
+ /**
181
+ * @param {Tab} tab
182
+ * @param {JxPath} path
183
+ * @param {string} prop
184
+ * @param {string | undefined} value
185
+ */
186
+ export function mutateUpdateStyle(tab, path, prop, value) {
187
+ const node = getNodeAtPath(tab.doc.document, path);
188
+ if (!node.style) node.style = {};
189
+ if (value === undefined || value === "") delete node.style[prop];
190
+ else node.style[prop] = value;
191
+ if (Object.keys(node.style).length === 0) delete node.style;
192
+ }
193
+
194
+ /**
195
+ * @param {Tab} tab
196
+ * @param {JxPath} path
197
+ * @param {string} attr
198
+ * @param {string | undefined} value
199
+ */
200
+ export function mutateUpdateAttribute(tab, path, attr, value) {
201
+ const node = getNodeAtPath(tab.doc.document, path);
202
+ if (!node.attributes) node.attributes = {};
203
+ if (value === undefined || value === "") delete node.attributes[attr];
204
+ else node.attributes[attr] = value;
205
+ if (Object.keys(node.attributes).length === 0) delete node.attributes;
206
+ }
207
+
208
+ /**
209
+ * @param {Tab} tab
210
+ * @param {JxPath} path
211
+ * @param {string} mediaName
212
+ * @param {string} prop
213
+ * @param {string | undefined} value
214
+ */
215
+ export function mutateUpdateMediaStyle(tab, path, mediaName, prop, value) {
216
+ const node = getNodeAtPath(tab.doc.document, path);
217
+ if (!node.style) node.style = {};
218
+ const key = `@${mediaName}`;
219
+ if (!node.style[key]) node.style[key] = {};
220
+ if (value === undefined || value === "") {
221
+ delete node.style[key][prop];
222
+ if (Object.keys(node.style[key]).length === 0) delete node.style[key];
223
+ } else {
224
+ node.style[key][prop] = value;
225
+ }
226
+ if (Object.keys(node.style).length === 0) delete node.style;
227
+ }
228
+
229
+ /**
230
+ * @param {Tab} tab
231
+ * @param {JxPath} path
232
+ * @param {string} selector
233
+ * @param {string} prop
234
+ * @param {string | undefined} value
235
+ */
236
+ export function mutateUpdateNestedStyle(tab, path, selector, prop, value) {
237
+ const node = getNodeAtPath(tab.doc.document, path);
238
+ if (!node.style) node.style = {};
239
+ if (!node.style[selector]) node.style[selector] = {};
240
+ if (value === undefined || value === "") {
241
+ delete node.style[selector][prop];
242
+ if (Object.keys(node.style[selector]).length === 0) delete node.style[selector];
243
+ } else {
244
+ node.style[selector][prop] = value;
245
+ }
246
+ if (Object.keys(node.style).length === 0) delete node.style;
247
+ }
248
+
249
+ /**
250
+ * @param {Tab} tab
251
+ * @param {JxPath} path
252
+ * @param {string} mediaName
253
+ * @param {string} selector
254
+ * @param {string} prop
255
+ * @param {string | undefined} value
256
+ */
257
+ export function mutateUpdateMediaNestedStyle(tab, path, mediaName, selector, prop, value) {
258
+ const node = getNodeAtPath(tab.doc.document, path);
259
+ if (!node.style) node.style = {};
260
+ const key = `@${mediaName}`;
261
+ if (!node.style[key]) node.style[key] = {};
262
+ if (!node.style[key][selector]) node.style[key][selector] = {};
263
+ if (value === undefined || value === "") {
264
+ delete node.style[key][selector][prop];
265
+ if (Object.keys(node.style[key][selector]).length === 0) delete node.style[key][selector];
266
+ if (Object.keys(node.style[key]).length === 0) delete node.style[key];
267
+ } else {
268
+ node.style[key][selector][prop] = value;
269
+ }
270
+ if (Object.keys(node.style).length === 0) delete node.style;
271
+ }
272
+
273
+ /**
274
+ * @param {Tab} tab
275
+ * @param {string} name
276
+ * @param {Record<string, JsonValue>} def
277
+ */
278
+ export function mutateAddDef(tab, name, def) {
279
+ const doc = tab.doc.document;
280
+ if (!doc.state) doc.state = {};
281
+ doc.state[name] = def;
282
+ }
283
+
284
+ /**
285
+ * @param {Tab} tab
286
+ * @param {string} name
287
+ */
288
+ export function mutateRemoveDef(tab, name) {
289
+ const doc = tab.doc.document;
290
+ if (doc.state) {
291
+ delete doc.state[name];
292
+ if (Object.keys(doc.state).length === 0) delete doc.state;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * @param {Tab} tab
298
+ * @param {string} name
299
+ * @param {Record<string, any>} updates
300
+ */
301
+ export function mutateUpdateDef(tab, name, updates) {
302
+ const doc = tab.doc.document;
303
+ if (!doc.state) doc.state = {};
304
+ if (!doc.state[name]) doc.state[name] = {};
305
+ Object.assign(doc.state[name], updates);
306
+ for (const k of Object.keys(doc.state[name])) {
307
+ if (doc.state[name][k] === undefined || doc.state[name][k] === null) {
308
+ delete doc.state[name][k];
309
+ }
310
+ }
311
+ }
312
+
313
+ /**
314
+ * @param {Tab} tab
315
+ * @param {string} oldName
316
+ * @param {string} newName
317
+ */
318
+ export function mutateRenameDef(tab, oldName, newName) {
319
+ const doc = tab.doc.document;
320
+ if (!doc.state || !doc.state[oldName]) return;
321
+ doc.state[newName] = doc.state[oldName];
322
+ delete doc.state[oldName];
323
+ }
324
+
325
+ /**
326
+ * @param {Tab} tab
327
+ * @param {string} name
328
+ * @param {string | undefined} query
329
+ */
330
+ export function mutateUpdateMedia(tab, name, query) {
331
+ const doc = tab.doc.document;
332
+ if (!doc.$media) doc.$media = {};
333
+ if (query === undefined || query === "") {
334
+ delete doc.$media[name];
335
+ if (Object.keys(doc.$media).length === 0) delete doc.$media;
336
+ } else {
337
+ doc.$media[name] = query;
338
+ }
339
+ }
340
+
341
+ /**
342
+ * @param {Tab} tab
343
+ * @param {JxPath} path
344
+ * @param {string} propName
345
+ * @param {JsonValue} value
346
+ */
347
+ export function mutateUpdateProp(tab, path, propName, value) {
348
+ const node = getNodeAtPath(tab.doc.document, path);
349
+ if (!node.$props) node.$props = {};
350
+ if (value === undefined || value === null || value === "") delete node.$props[propName];
351
+ else node.$props[propName] = value;
352
+ if (Object.keys(node.$props).length === 0) delete node.$props;
353
+ }
354
+
355
+ /**
356
+ * @param {Tab} tab
357
+ * @param {JxPath} path
358
+ * @param {string} caseName
359
+ * @param {JxNode} [caseDef]
360
+ */
361
+ export function mutateAddSwitchCase(tab, path, caseName, caseDef) {
362
+ const node = getNodeAtPath(tab.doc.document, path);
363
+ if (!node.cases) node.cases = {};
364
+ node.cases[caseName] = caseDef || { tagName: "div", textContent: caseName };
365
+ }
366
+
367
+ /**
368
+ * @param {Tab} tab
369
+ * @param {JxPath} path
370
+ * @param {string} caseName
371
+ */
372
+ export function mutateRemoveSwitchCase(tab, path, caseName) {
373
+ const node = getNodeAtPath(tab.doc.document, path);
374
+ if (node.cases) delete node.cases[caseName];
375
+ }
376
+
377
+ /**
378
+ * @param {Tab} tab
379
+ * @param {JxPath} path
380
+ * @param {string} oldName
381
+ * @param {string} newName
382
+ */
383
+ export function mutateRenameSwitchCase(tab, path, oldName, newName) {
384
+ const node = getNodeAtPath(tab.doc.document, path);
385
+ if (!node.cases || !node.cases[oldName]) return;
386
+ node.cases[newName] = node.cases[oldName];
387
+ delete node.cases[oldName];
388
+ }
389
+
390
+ // ─── Frontmatter ─────────────────────────────────────────────────────────────
391
+
392
+ /**
393
+ * Update a frontmatter field on a tab. Does not push document history.
394
+ *
395
+ * @param {Tab} tab
396
+ * @param {string} field
397
+ * @param {JsonValue} value
398
+ */
399
+ export function mutateUpdateFrontmatter(tab, field, value) {
400
+ if (value === undefined || value === null || value === "") {
401
+ delete tab.doc.content.frontmatter[field];
402
+ } else {
403
+ tab.doc.content.frontmatter[field] = value;
404
+ }
405
+ tab.doc.dirty = true;
406
+ }
@@ -47,6 +47,7 @@ import { PickerButton } from "@spectrum-web-components/picker-button/src/PickerB
47
47
  import { Accordion } from "@spectrum-web-components/accordion/src/Accordion.js";
48
48
  import { AccordionItem } from "@spectrum-web-components/accordion/src/AccordionItem.js";
49
49
  import { ActionBar } from "@spectrum-web-components/action-bar/src/ActionBar.js";
50
+ import { Toast } from "@spectrum-web-components/toast/src/Toast.js";
50
51
  import { Table } from "@spectrum-web-components/table/src/Table.js";
51
52
  import { TableHead } from "@spectrum-web-components/table/src/TableHead.js";
52
53
  import { TableHeadCell } from "@spectrum-web-components/table/src/TableHeadCell.js";
@@ -180,6 +181,7 @@ const components = [
180
181
  ["sp-accordion", Accordion],
181
182
  ["sp-accordion-item", AccordionItem],
182
183
  ["sp-action-bar", ActionBar],
184
+ ["sp-toast", Toast],
183
185
  ["sp-table", Table],
184
186
  ["sp-table-head", TableHead],
185
187
  ["sp-table-head-cell", TableHeadCell],
package/src/view.js CHANGED
@@ -61,6 +61,9 @@ export const view = {
61
61
  showAddBreakpointForm: false,
62
62
  addBreakpointPreview: "",
63
63
 
64
+ // Layout selection (when user clicks a layout element)
65
+ layoutSelection: null,
66
+
64
67
  // Autosave
65
68
  autosaveTimer: null,
66
69
  };
@@ -0,0 +1,89 @@
1
+ import { reactive, computed } from "../reactivity.js";
2
+ import { createTab, disposeTab } from "../tabs/tab.js";
3
+
4
+ /**
5
+ * @typedef {import("../tabs/tab.js").Tab} Tab
6
+ *
7
+ * @typedef {import("../files/components.js").ComponentEntry} ComponentEntry
8
+ *
9
+ * @typedef {{ name: string; path: string; type: string }} FileEntry
10
+ */
11
+
12
+ export const workspace = reactive({
13
+ /** @type {string | null} */
14
+ projectRoot: null,
15
+ /** @type {object | null} */
16
+ projectConfig: null,
17
+ /** @type {ComponentEntry[]} */
18
+ componentRegistry: [],
19
+ fileTree: {
20
+ /** @type {Map<string, FileEntry[]>} */
21
+ dirs: new Map(),
22
+ /** @type {Set<string>} */
23
+ expanded: new Set(),
24
+ /** @type {string | null} */
25
+ selectedPath: null,
26
+ searchQuery: "",
27
+ },
28
+ /** @type {Map<string, Tab>} */
29
+ tabs: new Map(),
30
+ /** @type {string[]} */
31
+ tabOrder: [],
32
+ /** @type {string | null} */
33
+ activeTabId: null,
34
+ ui: {
35
+ activityBar: "files",
36
+ },
37
+ });
38
+
39
+ /** @type {import("@vue/reactivity").ComputedRef<Tab>} */
40
+ export const activeTab = /** @type {any} */ (
41
+ computed(() =>
42
+ workspace.activeTabId ? (workspace.tabs.get(workspace.activeTabId) ?? null) : null,
43
+ )
44
+ );
45
+
46
+ /**
47
+ * Open a new tab and make it active.
48
+ *
49
+ * @param {{
50
+ * id: string;
51
+ * documentPath?: string | null;
52
+ * fileHandle?: FileSystemFileHandle | null;
53
+ * document: Record<string, any>;
54
+ * frontmatter?: Record<string, unknown>;
55
+ * }} opts
56
+ * @returns {Tab}
57
+ */
58
+ export function openTab(opts) {
59
+ const tab = createTab(opts);
60
+ workspace.tabs.set(tab.id, tab);
61
+ workspace.tabOrder.push(tab.id);
62
+ workspace.activeTabId = tab.id;
63
+ return tab;
64
+ }
65
+
66
+ /**
67
+ * Close a tab and dispose its scope. Activates the last remaining tab if the closed tab was active.
68
+ *
69
+ * @param {string} tabId
70
+ */
71
+ export function closeTab(tabId) {
72
+ const tab = workspace.tabs.get(tabId);
73
+ if (!tab) return;
74
+ disposeTab(tab);
75
+ workspace.tabs.delete(tabId);
76
+ workspace.tabOrder = workspace.tabOrder.filter((id) => id !== tabId);
77
+ if (workspace.activeTabId === tabId) {
78
+ workspace.activeTabId = workspace.tabOrder[workspace.tabOrder.length - 1] || null;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Switch to an existing tab.
84
+ *
85
+ * @param {string} tabId
86
+ */
87
+ export function activateTab(tabId) {
88
+ if (workspace.tabs.has(tabId)) workspace.activeTabId = tabId;
89
+ }