@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.
- package/README.md +82 -0
- package/dist/studio.js +5114 -3424
- package/dist/studio.js.map +65 -49
- package/package.json +6 -3
- package/src/browse/browse.js +27 -3
- package/src/canvas/canvas-diff.js +184 -0
- package/src/canvas/canvas-helpers.js +10 -14
- package/src/canvas/canvas-live-render.js +136 -17
- package/src/canvas/canvas-render.js +154 -21
- package/src/canvas/canvas-utils.js +21 -23
- package/src/editor/component-inline-edit.js +54 -41
- package/src/editor/content-inline-edit.js +46 -47
- package/src/editor/context-menu.js +63 -39
- package/src/editor/convert-to-component.js +11 -14
- package/src/editor/insertion-helper.js +8 -10
- package/src/editor/shortcuts.js +69 -39
- package/src/files/components.js +15 -4
- package/src/files/files.js +72 -24
- package/src/panels/activity-bar.js +29 -7
- package/src/panels/block-action-bar.js +104 -80
- package/src/panels/canvas-dnd.js +132 -50
- package/src/panels/dnd.js +32 -28
- package/src/panels/editors.js +7 -14
- package/src/panels/elements-panel.js +9 -3
- package/src/panels/events-panel.js +44 -39
- package/src/panels/git-panel.js +45 -3
- package/src/panels/layers-panel.js +16 -11
- package/src/panels/left-panel.js +108 -43
- package/src/panels/overlays.js +97 -35
- package/src/panels/panel-events.js +18 -11
- package/src/panels/properties-panel.js +467 -98
- package/src/panels/right-panel.js +85 -37
- package/src/panels/shared.js +0 -22
- package/src/panels/signals-panel.js +125 -54
- package/src/panels/statusbar.js +23 -8
- package/src/panels/style-inputs.js +4 -2
- package/src/panels/style-panel.js +128 -105
- package/src/panels/stylebook-panel.js +42 -17
- package/src/panels/tab-strip.js +124 -0
- package/src/panels/toolbar.js +39 -9
- package/src/reactivity.js +28 -0
- package/src/settings/content-types-editor.js +78 -8
- package/src/settings/defs-editor.js +56 -7
- package/src/settings/schema-field-ui.js +99 -11
- package/src/site-context.js +105 -0
- package/src/state.js +0 -456
- package/src/store.js +105 -124
- package/src/studio.js +112 -121
- package/src/tabs/tab.js +153 -0
- package/src/tabs/transact.js +406 -0
- package/src/ui/spectrum.js +2 -0
- package/src/view.js +3 -0
- 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
|
+
}
|
package/src/ui/spectrum.js
CHANGED
|
@@ -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
|
@@ -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
|
+
}
|