@peter.prib/renderengine 0.2.11 → 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.
- package/README.md +22 -6
- package/REFERENCE.md +132 -2
- package/dist/renderengine-workspace.umd.js +3346 -1245
- package/dist/renderengine.umd.js +1509 -158
- package/package.json +1 -1
- package/src/charting/ModelCheckDialog.js +10 -3
- package/src/charting/ModelComparisonDialog.js +10 -3
- package/src/charting/ModelFileFormat.js +4 -1
- package/src/charting/archimate/ArchimateCanvasEditor.js +14 -2
- package/src/charting/archimate/ArchimateElementTemplate.js +23 -0
- package/src/charting/arrangement/layouts.js +9 -4
- package/src/charting/datamodel/DataModel.js +18 -71
- package/src/charting/datamodel/DataModelCompared.js +1 -31
- package/src/charting/diagramModel/ListCompared.js +35 -0
- package/src/charting/model/Benchmark.js +59 -0
- package/src/charting/model/EditableModel.js +104 -0
- package/src/charting/{datamodel/DataModelEditCommand.js → model/ModelEditCommand.js} +47 -47
- package/src/charting/requirements/AzureDevOpsApi.js +55 -0
- package/src/charting/requirements/GitLabApi.js +43 -0
- package/src/charting/requirements/JiraApi.js +69 -0
- package/src/charting/requirements/RequirementsChecker.js +76 -0
- package/src/charting/requirements/RequirementsCompared.js +33 -0
- package/src/charting/requirements/RequirementsFileFormat.js +32 -0
- package/src/charting/requirements/RequirementsImportFormats.js +48 -0
- package/src/charting/requirements/RequirementsModel.js +305 -0
- package/src/charting/requirements/Storyboard.js +94 -0
- package/src/charting/requirements/TrackerApi.js +142 -0
- package/src/charting/requirements/TrackerExport.js +274 -0
- package/src/charting/requirements/index.js +12 -0
- package/src/charting/requirements/trackerApis.js +21 -0
- package/src/index.js +7 -0
- package/src/workspace/Workspace.js +329 -321
- package/src/workspace/index.js +2 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +31 -1
- package/src/workspace/parts/DataModelPart.js +668 -735
- package/src/workspace/parts/EditorPart.js +1 -1
- package/src/workspace/parts/ModelFile.js +3 -1
- package/src/workspace/parts/ModelHosted.js +176 -0
- package/src/workspace/parts/ModelsPart.js +146 -7
- package/src/workspace/parts/RequirementsPart.js +465 -0
- package/src/workspace/storage/ModelLibrary.js +8 -2
|
@@ -1,321 +1,329 @@
|
|
|
1
|
-
import { ContextBus } from './ContextBus.js';
|
|
2
|
-
import { PartRegistry } from './PartRegistry.js';
|
|
3
|
-
import { createLayout } from './layout/index.js';
|
|
4
|
-
import { ensureStyles } from './styles.js';
|
|
5
|
-
import { BUILTIN_THEMES, SYSTEM_THEME } from './themes.js';
|
|
6
|
-
import { render } from './dom.js';
|
|
7
|
-
|
|
8
|
-
/** Bump when the serialized shape changes; `deserialize` migrates on the way in. */
|
|
9
|
-
export const WORKSPACE_SCHEMA_VERSION = 1;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Workspace — the `<ws-workspace>` custom element and the shell's public API.
|
|
13
|
-
*
|
|
14
|
-
* Renders into **light DOM**: parts routinely carry their own stylesheets, and a
|
|
15
|
-
* consumer stylesheet cannot cross a shadow boundary. All shell markup is
|
|
16
|
-
* `ws-`-prefixed and themed through CSS custom properties instead.
|
|
17
|
-
*
|
|
18
|
-
* Typical wiring:
|
|
19
|
-
*
|
|
20
|
-
* const ws = document.querySelector('ws-workspace');
|
|
21
|
-
* ws.registerParts([ListPart, DetailPart, ChartPart]);
|
|
22
|
-
* ws.addPart({ id: 'nav', type: 'list', title: 'Tables', items: [...] });
|
|
23
|
-
* ws.registerPerspective('default', { type: 'split', dir: 'row', children: [...] });
|
|
24
|
-
* ws.activatePerspective('default');
|
|
25
|
-
*/
|
|
26
|
-
export class Workspace extends HTMLElement {
|
|
27
|
-
constructor() {
|
|
28
|
-
super();
|
|
29
|
-
this.bus = new ContextBus();
|
|
30
|
-
this.registry = new PartRegistry();
|
|
31
|
-
/** @type {Map<string, object>} name → layout JSON */
|
|
32
|
-
this.perspectives = new Map();
|
|
33
|
-
this.activePerspective = null;
|
|
34
|
-
/** @type {import('./layout/LayoutNode.js').LayoutNode|null} */
|
|
35
|
-
this.tree = null;
|
|
36
|
-
/** @type {Map<string, object>} theme name → CSS custom-property map */
|
|
37
|
-
this.themes = new Map(Object.entries(BUILTIN_THEMES));
|
|
38
|
-
/** The *selection* — may be `system`, which resolves at apply time. */
|
|
39
|
-
this.theme = 'light';
|
|
40
|
-
/** Custom properties currently written onto the host, so they can be
|
|
41
|
-
* cleared before a different theme is applied. */
|
|
42
|
-
this._appliedVars = [];
|
|
43
|
-
this._built = false;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/* ── Element lifecycle ──────────────────────────────────────────────── */
|
|
47
|
-
|
|
48
|
-
connectedCallback() {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
this.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
this.
|
|
123
|
-
|
|
124
|
-
this.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
this.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
this
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
this.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (this.
|
|
243
|
-
this.
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
1
|
+
import { ContextBus } from './ContextBus.js';
|
|
2
|
+
import { PartRegistry } from './PartRegistry.js';
|
|
3
|
+
import { createLayout } from './layout/index.js';
|
|
4
|
+
import { ensureStyles } from './styles.js';
|
|
5
|
+
import { BUILTIN_THEMES, SYSTEM_THEME } from './themes.js';
|
|
6
|
+
import { render } from './dom.js';
|
|
7
|
+
|
|
8
|
+
/** Bump when the serialized shape changes; `deserialize` migrates on the way in. */
|
|
9
|
+
export const WORKSPACE_SCHEMA_VERSION = 1;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Workspace — the `<ws-workspace>` custom element and the shell's public API.
|
|
13
|
+
*
|
|
14
|
+
* Renders into **light DOM**: parts routinely carry their own stylesheets, and a
|
|
15
|
+
* consumer stylesheet cannot cross a shadow boundary. All shell markup is
|
|
16
|
+
* `ws-`-prefixed and themed through CSS custom properties instead.
|
|
17
|
+
*
|
|
18
|
+
* Typical wiring:
|
|
19
|
+
*
|
|
20
|
+
* const ws = document.querySelector('ws-workspace');
|
|
21
|
+
* ws.registerParts([ListPart, DetailPart, ChartPart]);
|
|
22
|
+
* ws.addPart({ id: 'nav', type: 'list', title: 'Tables', items: [...] });
|
|
23
|
+
* ws.registerPerspective('default', { type: 'split', dir: 'row', children: [...] });
|
|
24
|
+
* ws.activatePerspective('default');
|
|
25
|
+
*/
|
|
26
|
+
export class Workspace extends HTMLElement {
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
this.bus = new ContextBus();
|
|
30
|
+
this.registry = new PartRegistry();
|
|
31
|
+
/** @type {Map<string, object>} name → layout JSON */
|
|
32
|
+
this.perspectives = new Map();
|
|
33
|
+
this.activePerspective = null;
|
|
34
|
+
/** @type {import('./layout/LayoutNode.js').LayoutNode|null} */
|
|
35
|
+
this.tree = null;
|
|
36
|
+
/** @type {Map<string, object>} theme name → CSS custom-property map */
|
|
37
|
+
this.themes = new Map(Object.entries(BUILTIN_THEMES));
|
|
38
|
+
/** The *selection* — may be `system`, which resolves at apply time. */
|
|
39
|
+
this.theme = 'light';
|
|
40
|
+
/** Custom properties currently written onto the host, so they can be
|
|
41
|
+
* cleared before a different theme is applied. */
|
|
42
|
+
this._appliedVars = [];
|
|
43
|
+
this._built = false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* ── Element lifecycle ──────────────────────────────────────────────── */
|
|
47
|
+
|
|
48
|
+
connectedCallback() {
|
|
49
|
+
// Moved, not new (a host re-parents it: a tab dragged to another
|
|
50
|
+
// group): its layout was disposed when it left (disconnectedCallback),
|
|
51
|
+
// so it is drawn again — the parts are still in the registry, as they
|
|
52
|
+
// were. Without this a workspace moved in the page came back empty.
|
|
53
|
+
if (this._built) {
|
|
54
|
+
if (!this.tree && this.activePerspective) this._renderTree();
|
|
55
|
+
this._watchSystemTheme(this.theme === SYSTEM_THEME);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this._built = true;
|
|
59
|
+
ensureStyles(this.ownerDocument ?? document);
|
|
60
|
+
this.classList.add('ws-workspace');
|
|
61
|
+
|
|
62
|
+
// Chrome is built from element specs rather than an HTML string, and
|
|
63
|
+
// `ref` captures the nodes directly — no querySelector round-trip.
|
|
64
|
+
// `label`, not `title` — `title` is a global HTML attribute and would
|
|
65
|
+
// also render a browser tooltip over the whole workspace.
|
|
66
|
+
render(this, [
|
|
67
|
+
{ cls: 'ws-toolbar', children: [
|
|
68
|
+
{ tag: 'span', cls: 'ws-toolbar-title', text: this.getAttribute('label') ?? '' },
|
|
69
|
+
{ tag: 'span', cls: 'ws-toolbar-spacer' },
|
|
70
|
+
{ tag: 'span', cls: 'ws-theme-picker', ref: n => { this._themePickerEl = n; } },
|
|
71
|
+
{ tag: 'span', cls: 'ws-perspectives', ref: n => { this._perspectivesEl = n; } },
|
|
72
|
+
]},
|
|
73
|
+
{ cls: 'ws-root', ref: n => { this._rootEl = n; } },
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
this.applyTheme(this.getAttribute('theme') ?? 'light');
|
|
77
|
+
this._renderThemePicker();
|
|
78
|
+
|
|
79
|
+
// A perspective registered before the element was connected renders now.
|
|
80
|
+
if (this.activePerspective) this._renderTree();
|
|
81
|
+
else if (this.perspectives.size) this.activatePerspective([...this.perspectives.keys()][0]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
disconnectedCallback() {
|
|
85
|
+
this.tree?.dispose();
|
|
86
|
+
this.tree = null;
|
|
87
|
+
this._watchSystemTheme(false); // a live matchMedia listener would outlive the element
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* ── Parts ──────────────────────────────────────────────────────────── */
|
|
91
|
+
|
|
92
|
+
registerPart(PartClass) { this.registry.register(PartClass); return this; }
|
|
93
|
+
registerParts(classes) { this.registry.registerAll(classes); return this; }
|
|
94
|
+
|
|
95
|
+
/** Create a pooled part instance from a spec. */
|
|
96
|
+
addPart(spec) { return this.registry.add(spec); }
|
|
97
|
+
addParts(specs) { return specs.map(s => this.addPart(s)); }
|
|
98
|
+
getPart(id) { return this.registry.get(id); }
|
|
99
|
+
removePart(id) { return this.registry.remove(id); }
|
|
100
|
+
|
|
101
|
+
/* ── Perspectives ───────────────────────────────────────────────────── */
|
|
102
|
+
|
|
103
|
+
registerPerspective(name, layoutJson) {
|
|
104
|
+
this.perspectives.set(name, layoutJson);
|
|
105
|
+
this._renderPerspectiveButtons();
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Switch to a named perspective.
|
|
111
|
+
*
|
|
112
|
+
* Parts are pooled by id and live for the workspace's lifetime, so a part
|
|
113
|
+
* the incoming perspective also references is re-parented rather than
|
|
114
|
+
* rebuilt — it keeps its scroll position, unsaved input and subscriptions.
|
|
115
|
+
* Parts the incoming perspective doesn't reference are detached, not
|
|
116
|
+
* destroyed, so switching back restores them intact.
|
|
117
|
+
*
|
|
118
|
+
* There is deliberately no legal-transition table: a framework cannot know
|
|
119
|
+
* a consumer's perspectives. Pass `guard` to veto a switch if you need one.
|
|
120
|
+
*/
|
|
121
|
+
activatePerspective(name) {
|
|
122
|
+
const layout = this.perspectives.get(name);
|
|
123
|
+
if (!layout) throw new Error(`Unknown perspective "${name}"`);
|
|
124
|
+
const from = this.activePerspective;
|
|
125
|
+
if (from === name) return true;
|
|
126
|
+
if (this.guard && this.guard(from, name) === false) return false;
|
|
127
|
+
|
|
128
|
+
this.tree?.dispose();
|
|
129
|
+
this.activePerspective = name;
|
|
130
|
+
this._renderTree();
|
|
131
|
+
|
|
132
|
+
this.dispatchEvent(new CustomEvent('ws-perspective-change', {
|
|
133
|
+
detail: { from, to: name }, bubbles: true,
|
|
134
|
+
}));
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_renderTree() {
|
|
139
|
+
if (!this._rootEl) return;
|
|
140
|
+
const layout = this.perspectives.get(this.activePerspective);
|
|
141
|
+
this._rootEl.textContent = '';
|
|
142
|
+
this.tree = createLayout(layout);
|
|
143
|
+
this.tree.render(this._rootEl, this._ctx());
|
|
144
|
+
this.tree.activate?.();
|
|
145
|
+
// Parts the new tree doesn't reference are deliberately NOT disposed —
|
|
146
|
+
// they are merely detached (PartNode.dispose), keeping their DOM and
|
|
147
|
+
// state. Disposing them would delete them from the pool, so switching
|
|
148
|
+
// back to the perspective that used them would find them gone. Part
|
|
149
|
+
// lifetime ends only at removePart()/deserialize().
|
|
150
|
+
this._renderPerspectiveButtons();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_ctx() {
|
|
154
|
+
return {
|
|
155
|
+
bus: this.bus,
|
|
156
|
+
registry: this.registry,
|
|
157
|
+
workspace: this,
|
|
158
|
+
onLayoutChange: () => this._onLayoutChange(),
|
|
159
|
+
...(this.services ?? {}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_onLayoutChange() {
|
|
164
|
+
if (this.activePerspective) {
|
|
165
|
+
this.perspectives.set(this.activePerspective, this.tree.serialize());
|
|
166
|
+
}
|
|
167
|
+
this.dispatchEvent(new CustomEvent('ws-layout-change', {
|
|
168
|
+
detail: { perspective: this.activePerspective }, bubbles: true,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_renderPerspectiveButtons() {
|
|
173
|
+
if (!this._perspectivesEl) return;
|
|
174
|
+
render(this._perspectivesEl, [...this.perspectives.keys()].map(name => ({
|
|
175
|
+
tag: 'button',
|
|
176
|
+
cls: ['ws-perspective-btn', name === this.activePerspective && 'ws-active'],
|
|
177
|
+
text: name,
|
|
178
|
+
attrs: { type: 'button' },
|
|
179
|
+
on: { click: () => this.activatePerspective(name) },
|
|
180
|
+
})));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* ── Theming ────────────────────────────────────────────────────────── */
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Register a custom-property set under a name. It appears in the toolbar's
|
|
187
|
+
* theme picker immediately — the picker is built from the registry, so a
|
|
188
|
+
* consumer theme needs no CSS and no picker change.
|
|
189
|
+
*/
|
|
190
|
+
registerTheme(name, vars) {
|
|
191
|
+
this.themes.set(name, vars);
|
|
192
|
+
this._renderThemePicker();
|
|
193
|
+
return this;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Registered theme names, plus the reserved `system` option. */
|
|
197
|
+
themeNames() { return [SYSTEM_THEME, ...this.themes.keys()]; }
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Apply a theme by name, or `system` to follow the OS setting.
|
|
201
|
+
*
|
|
202
|
+
* The previous theme's properties are removed before the new ones are set:
|
|
203
|
+
* a custom theme may define only some of the vars, and leftovers from the
|
|
204
|
+
* outgoing theme would otherwise show through as a mismatched palette.
|
|
205
|
+
*/
|
|
206
|
+
applyTheme(name) {
|
|
207
|
+
const resolved = name === SYSTEM_THEME ? this._systemTheme() : name;
|
|
208
|
+
const vars = this.themes.get(resolved);
|
|
209
|
+
if (!vars && name !== SYSTEM_THEME) return this;
|
|
210
|
+
|
|
211
|
+
for (const key of this._appliedVars) this.style.removeProperty(key);
|
|
212
|
+
this._appliedVars = [];
|
|
213
|
+
for (const [k, v] of Object.entries(vars ?? {})) {
|
|
214
|
+
this.style.setProperty(k, v);
|
|
215
|
+
this._appliedVars.push(k);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const previous = this.theme;
|
|
219
|
+
this.theme = name; // the selection ('system' stays 'system')
|
|
220
|
+
this.setAttribute('data-theme', resolved); // the resolved name, for consumer CSS hooks
|
|
221
|
+
this._watchSystemTheme(name === SYSTEM_THEME);
|
|
222
|
+
if (this._themeSelect) this._themeSelect.value = name;
|
|
223
|
+
|
|
224
|
+
if (previous !== name) {
|
|
225
|
+
this.dispatchEvent(new CustomEvent('ws-theme-change', {
|
|
226
|
+
detail: { theme: name, resolved }, bubbles: true,
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_systemTheme() {
|
|
233
|
+
return globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Keep `system` live: re-apply when the OS flips between light and dark. */
|
|
237
|
+
_watchSystemTheme(enabled) {
|
|
238
|
+
if (enabled && !this._systemThemeQuery) {
|
|
239
|
+
this._systemThemeQuery = globalThis.matchMedia?.('(prefers-color-scheme: dark)');
|
|
240
|
+
this._onSystemThemeChange = () => { if (this.theme === SYSTEM_THEME) this.applyTheme(SYSTEM_THEME); };
|
|
241
|
+
this._systemThemeQuery?.addEventListener('change', this._onSystemThemeChange);
|
|
242
|
+
} else if (!enabled && this._systemThemeQuery) {
|
|
243
|
+
this._systemThemeQuery.removeEventListener('change', this._onSystemThemeChange);
|
|
244
|
+
this._systemThemeQuery = null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
_renderThemePicker() {
|
|
249
|
+
if (!this._themePickerEl) return;
|
|
250
|
+
if (this.getAttribute('theme-picker') === 'hidden') {
|
|
251
|
+
this._themePickerEl.textContent = '';
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
render(this._themePickerEl, [
|
|
255
|
+
{ tag: 'label', text: 'Theme', attrs: { for: 'ws-theme-select' } },
|
|
256
|
+
{
|
|
257
|
+
tag: 'select',
|
|
258
|
+
cls: 'ws-theme-select',
|
|
259
|
+
attrs: { id: 'ws-theme-select', title: 'Colour theme' },
|
|
260
|
+
on: { change: e => this.applyTheme(e.target.value) },
|
|
261
|
+
ref: n => { this._themeSelect = n; n.value = this.theme; },
|
|
262
|
+
children: this.themeNames().map(name => ({
|
|
263
|
+
tag: 'option',
|
|
264
|
+
text: name === SYSTEM_THEME ? 'System' : name[0].toUpperCase() + name.slice(1),
|
|
265
|
+
attrs: { value: name, selected: name === this.theme },
|
|
266
|
+
})),
|
|
267
|
+
},
|
|
268
|
+
]);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* ── Persistence ────────────────────────────────────────────────────── */
|
|
272
|
+
|
|
273
|
+
serialize() {
|
|
274
|
+
if (this.tree && this.activePerspective) {
|
|
275
|
+
this.perspectives.set(this.activePerspective, this.tree.serialize());
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
version: WORKSPACE_SCHEMA_VERSION,
|
|
279
|
+
theme: this.theme,
|
|
280
|
+
activePerspective: this.activePerspective,
|
|
281
|
+
perspectives: Object.fromEntries(this.perspectives),
|
|
282
|
+
parts: this.registry.serialize(),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Restore a serialized workspace. Part *types* must already be registered —
|
|
288
|
+
* a saved document carries specs, not code.
|
|
289
|
+
*
|
|
290
|
+
* `version` is honoured from day one on purpose: layout schemas change, and
|
|
291
|
+
* sqlengine's dashboard already had to retrofit exactly this migration once.
|
|
292
|
+
*/
|
|
293
|
+
deserialize(json, { migrate } = {}) {
|
|
294
|
+
const doc = migrate ? migrate(json) : json;
|
|
295
|
+
if (doc.version > WORKSPACE_SCHEMA_VERSION) {
|
|
296
|
+
throw new Error(`Workspace document version ${doc.version} is newer than this build (${WORKSPACE_SCHEMA_VERSION})`);
|
|
297
|
+
}
|
|
298
|
+
this.registry.disposeAll();
|
|
299
|
+
for (const spec of doc.parts ?? []) {
|
|
300
|
+
if (spec.adapter) continue; // AdapterPart's mount fn cannot round-trip — re-add it in code
|
|
301
|
+
this.addPart(spec);
|
|
302
|
+
}
|
|
303
|
+
// Only a theme this build actually knows — a document naming one the
|
|
304
|
+
// consumer no longer registers must not leave the shell unstyled.
|
|
305
|
+
if (doc.theme && (doc.theme === SYSTEM_THEME || this.themes.has(doc.theme))) {
|
|
306
|
+
this.applyTheme(doc.theme);
|
|
307
|
+
}
|
|
308
|
+
this.perspectives = new Map(Object.entries(doc.perspectives ?? {}));
|
|
309
|
+
this.activePerspective = null;
|
|
310
|
+
if (doc.activePerspective && this.perspectives.has(doc.activePerspective)) {
|
|
311
|
+
this.activatePerspective(doc.activePerspective);
|
|
312
|
+
}
|
|
313
|
+
return this;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export const WORKSPACE_TAG = 'ws-workspace';
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Register the element. Custom element names must contain a hyphen, so
|
|
321
|
+
* `workspace` is not a legal name — `define()` throws SyntaxError on it.
|
|
322
|
+
* Guarded so importing this module twice does not throw on re-definition.
|
|
323
|
+
*/
|
|
324
|
+
export function defineWorkspace(tag = WORKSPACE_TAG) {
|
|
325
|
+
if (typeof customElements === 'undefined') return false;
|
|
326
|
+
if (customElements.get(tag)) return false;
|
|
327
|
+
customElements.define(tag, Workspace);
|
|
328
|
+
return true;
|
|
329
|
+
}
|