@3sln/deck 0.0.5
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/LICENSE +21 -0
- package/README.md +50 -0
- package/bin/build.js +123 -0
- package/package.json +51 -0
- package/src/config.js +96 -0
- package/src/db.js +302 -0
- package/src/deck-demo.js +859 -0
- package/src/fetcher.js +35 -0
- package/src/highlight.js +45 -0
- package/src/main.js +326 -0
- package/src/state.js +227 -0
- package/src/sw.js +55 -0
- package/vite-plugin.js +147 -0
package/src/deck-demo.js
ADDED
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
import * as dodo from '@3sln/dodo';
|
|
2
|
+
import styleFactory, {css} from '@3sln/bones/style';
|
|
3
|
+
import reactiveFactory from '@3sln/bones/reactive';
|
|
4
|
+
import resizeFactory from '@3sln/bones/resize';
|
|
5
|
+
|
|
6
|
+
import {Engine, Provider, Query, Action} from '@3sln/ngin';
|
|
7
|
+
import {stylesheet as highlightStylesheet, highlight} from './highlight.js';
|
|
8
|
+
|
|
9
|
+
const {reconcile, h, div, button, pre, code, span, label, input, p} = dodo;
|
|
10
|
+
|
|
11
|
+
const {ObservableSubject, watch, zip, map, dedup} = reactiveFactory({dodo});
|
|
12
|
+
const {withContainerSize} = resizeFactory({dodo});
|
|
13
|
+
|
|
14
|
+
const rootNodeCaches = new WeakMap();
|
|
15
|
+
const DISPOSE_DELAY = 3000; // 3 seconds
|
|
16
|
+
const HOT = import.meta.hot ? true : false;
|
|
17
|
+
|
|
18
|
+
const commonStyle = css`
|
|
19
|
+
* {
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const propertiesStyle = css`
|
|
25
|
+
.properties {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
flex-wrap: wrap;
|
|
29
|
+
gap: 0 2em;
|
|
30
|
+
overflow-x: auto;
|
|
31
|
+
}
|
|
32
|
+
.property-item {
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
gap: 1em;
|
|
36
|
+
margin-bottom: 0.75em;
|
|
37
|
+
width: 250px;
|
|
38
|
+
}
|
|
39
|
+
.property-label {
|
|
40
|
+
flex: 1;
|
|
41
|
+
text-align: right;
|
|
42
|
+
font-size: 0.9em;
|
|
43
|
+
color: var(--text-color);
|
|
44
|
+
opacity: 0.8;
|
|
45
|
+
}
|
|
46
|
+
.property-item input {
|
|
47
|
+
flex: 2;
|
|
48
|
+
flex-grow: 0;
|
|
49
|
+
}
|
|
50
|
+
input[type='text'] {
|
|
51
|
+
background: rgba(0, 0, 0, 0.1);
|
|
52
|
+
border: 1px solid var(--input-border);
|
|
53
|
+
border-radius: 4px;
|
|
54
|
+
padding: 0.5em;
|
|
55
|
+
color: var(--text-color);
|
|
56
|
+
}
|
|
57
|
+
input[type='range'] {
|
|
58
|
+
accent-color: var(--link-color);
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
function getEngine(rootNode, key, src) {
|
|
63
|
+
if (!rootNodeCaches.has(rootNode)) {
|
|
64
|
+
rootNodeCaches.set(rootNode, new Map());
|
|
65
|
+
}
|
|
66
|
+
const cache = rootNodeCaches.get(rootNode);
|
|
67
|
+
|
|
68
|
+
if (cache.has(key)) {
|
|
69
|
+
const entry = cache.get(key);
|
|
70
|
+
clearTimeout(entry.disposeTimeout);
|
|
71
|
+
entry.refCount++;
|
|
72
|
+
return entry.engine;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const {engine, abortController} = createEngine(src);
|
|
76
|
+
const entry = {
|
|
77
|
+
engine,
|
|
78
|
+
refCount: 1,
|
|
79
|
+
disposeTimeout: null,
|
|
80
|
+
abortController: abortController,
|
|
81
|
+
};
|
|
82
|
+
cache.set(key, entry);
|
|
83
|
+
return engine;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function releaseEngine(rootNode, key) {
|
|
87
|
+
const cache = rootNodeCaches.get(rootNode);
|
|
88
|
+
if (!cache || !cache.has(key)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const entry = cache.get(key);
|
|
93
|
+
entry.refCount--;
|
|
94
|
+
|
|
95
|
+
if (entry.refCount === 0) {
|
|
96
|
+
entry.disposeTimeout = setTimeout(() => {
|
|
97
|
+
entry.disposeTimeout = null;
|
|
98
|
+
if (entry.refCount > 0) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
entry.engine.dispose();
|
|
103
|
+
entry.abortController.abort();
|
|
104
|
+
cache.delete(key);
|
|
105
|
+
}, DISPOSE_DELAY);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function propertyControl(engine, name) {
|
|
110
|
+
const spec$ = engine.query(new PropertySpec(name));
|
|
111
|
+
return watch(spec$, spec => {
|
|
112
|
+
if (!spec) return null;
|
|
113
|
+
const {name, options} = spec;
|
|
114
|
+
const control = watch(engine.query(new PropertyValue(name)), value => {
|
|
115
|
+
switch (options?.type ?? 'text') {
|
|
116
|
+
case 'range':
|
|
117
|
+
return input({type: 'range', min: options.min, max: options.max, value}).on({
|
|
118
|
+
input: e => engine.dispatch(new UpdatePropertyValue(name, e.target.valueAsNumber)),
|
|
119
|
+
});
|
|
120
|
+
case 'text':
|
|
121
|
+
return input({type: 'text', value}).on({
|
|
122
|
+
input: e => engine.dispatch(new UpdatePropertyValue(name, e.target.value)),
|
|
123
|
+
});
|
|
124
|
+
default:
|
|
125
|
+
return span('Unknown property type');
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
return div({className: 'property-item'}, label({className: 'property-label'}, name), control);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function createEngine(src) {
|
|
133
|
+
const abortController = new AbortController();
|
|
134
|
+
const sourceCode$ = new ObservableSubject('Loading...');
|
|
135
|
+
|
|
136
|
+
const demoState = new DemoState({
|
|
137
|
+
activePanelIds: {},
|
|
138
|
+
propertySpecs: {},
|
|
139
|
+
propertyValues: {},
|
|
140
|
+
panels: new Map(),
|
|
141
|
+
paneVisibility: {left: true, right: true},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const engine = new Engine({
|
|
145
|
+
providers: {state: Provider.fromSingleton(demoState)},
|
|
146
|
+
interceptors: [panelSanitizerInterceptor, actionLoggerInterceptor],
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const sourcePanelName = 'Source';
|
|
150
|
+
const propsPanelName = 'Properties';
|
|
151
|
+
|
|
152
|
+
engine.dispatch(
|
|
153
|
+
new CreateOrUpdatePanel({
|
|
154
|
+
name: sourcePanelName,
|
|
155
|
+
pane: 'right',
|
|
156
|
+
order: 1,
|
|
157
|
+
render: container => {
|
|
158
|
+
container.adoptedStyleSheets = [commonStyle, highlightStylesheet];
|
|
159
|
+
|
|
160
|
+
reconcile(container, [
|
|
161
|
+
watch(sourceCode$, text =>
|
|
162
|
+
pre(
|
|
163
|
+
code({className: 'language-javascript'}, text).on({
|
|
164
|
+
$update: el => {
|
|
165
|
+
delete el.dataset.highlighted;
|
|
166
|
+
highlight(el);
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
),
|
|
170
|
+
),
|
|
171
|
+
]);
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
let propertyPanelCreated = false;
|
|
177
|
+
const ensurePropertyPanel = () => {
|
|
178
|
+
if (propertyPanelCreated) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
engine.dispatch(
|
|
183
|
+
new CreateOrUpdatePanel({
|
|
184
|
+
name: propsPanelName,
|
|
185
|
+
pane: 'right',
|
|
186
|
+
order: 2,
|
|
187
|
+
render: container => {
|
|
188
|
+
container.adoptedStyleSheets = [commonStyle, propertiesStyle];
|
|
189
|
+
const propIds$ = engine.query(new AllPropertyNames());
|
|
190
|
+
reconcile(container, [
|
|
191
|
+
watch(propIds$, names => names?.map(name => propertyControl(engine, name).key(name))),
|
|
192
|
+
]);
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const driver = {
|
|
199
|
+
panel: (name, render, {pane = 'left', order = undefined} = {}) => {
|
|
200
|
+
const panel = {
|
|
201
|
+
name,
|
|
202
|
+
pane,
|
|
203
|
+
render,
|
|
204
|
+
order,
|
|
205
|
+
};
|
|
206
|
+
engine.dispatch(new CreateOrUpdatePanel(panel));
|
|
207
|
+
},
|
|
208
|
+
property: (name, options) => {
|
|
209
|
+
ensurePropertyPanel();
|
|
210
|
+
engine.dispatch(new UpsertProperty(name, options));
|
|
211
|
+
return engine.query(new PropertyValue(name));
|
|
212
|
+
},
|
|
213
|
+
setActivePanel: name => {
|
|
214
|
+
engine.dispatch(new ActivatePanel(name));
|
|
215
|
+
},
|
|
216
|
+
get signal() {
|
|
217
|
+
return abortController.signal;
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
(async () => {
|
|
222
|
+
if (!src) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
if (HOT) {
|
|
228
|
+
const m = await import(/* @vite-ignore */ `/@deck-dev-hmr/${encodeURIComponent(src)}`);
|
|
229
|
+
const sub = m.moduleText$.subscribe(text => {
|
|
230
|
+
sourceCode$.next(text);
|
|
231
|
+
});
|
|
232
|
+
abortController.signal.addEventListener('abort', () => {
|
|
233
|
+
sub.unsubscribe();
|
|
234
|
+
});
|
|
235
|
+
m.default(driver);
|
|
236
|
+
} else {
|
|
237
|
+
const url = new URL(src, location.href);
|
|
238
|
+
const m = await import(/* @vite-ignore */ url.href);
|
|
239
|
+
m.default(driver);
|
|
240
|
+
|
|
241
|
+
const text = await fetch(url).then(r => r.text());
|
|
242
|
+
sourceCode$.next(text);
|
|
243
|
+
}
|
|
244
|
+
} catch (err) {
|
|
245
|
+
console.error(`Failed to load demo module ${src}:`, err);
|
|
246
|
+
driver.panel('Error', container => {
|
|
247
|
+
reconcile(container, [
|
|
248
|
+
h('div', {$styling: {color: 'red'}}, `Error: Could not load demo module.`),
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
})();
|
|
253
|
+
|
|
254
|
+
return {engine, abortController};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function shallowCompare(objA, objB) {
|
|
258
|
+
if (objA === objB) return true;
|
|
259
|
+
if (!objA || !objB) return false;
|
|
260
|
+
const keysA = Object.keys(objA);
|
|
261
|
+
const keysB = Object.keys(objB);
|
|
262
|
+
if (keysA.length !== keysB.length) return false;
|
|
263
|
+
for (const key of keysA) {
|
|
264
|
+
if (objA[key] !== objB[key]) return false;
|
|
265
|
+
}
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// --- Reactive Store for Demo State ---
|
|
270
|
+
class DemoState {
|
|
271
|
+
#subject;
|
|
272
|
+
|
|
273
|
+
constructor(initialState) {
|
|
274
|
+
this.#subject = new ObservableSubject(initialState);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
get state$() {
|
|
278
|
+
return this.#subject;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
update(updater, ...args) {
|
|
282
|
+
const currentState = this.#subject.value;
|
|
283
|
+
this.#subject.next(updater(currentState, ...args));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// --- Ngin State Definitions ---
|
|
288
|
+
|
|
289
|
+
class SetPaneVisibility extends Action {
|
|
290
|
+
static deps = ['state'];
|
|
291
|
+
|
|
292
|
+
constructor(visibility) {
|
|
293
|
+
super();
|
|
294
|
+
this.visibility = visibility;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
execute({state}) {
|
|
298
|
+
state.update(s => ({
|
|
299
|
+
...s,
|
|
300
|
+
paneVisibility: this.visibility,
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
class PaneVisibility extends Query {
|
|
306
|
+
static deps = ['state'];
|
|
307
|
+
#sub;
|
|
308
|
+
|
|
309
|
+
boot({state}, {notify}) {
|
|
310
|
+
let lastVisibility = null;
|
|
311
|
+
this.#sub = state.state$.subscribe(s => {
|
|
312
|
+
if (s.paneVisibility !== lastVisibility) {
|
|
313
|
+
lastVisibility = s.paneVisibility;
|
|
314
|
+
notify(lastVisibility);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
kill() {
|
|
320
|
+
this.#sub?.unsubscribe();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
class ActivePanelForPane extends Query {
|
|
325
|
+
static deps = ['state'];
|
|
326
|
+
#sub;
|
|
327
|
+
|
|
328
|
+
constructor(pane) {
|
|
329
|
+
super();
|
|
330
|
+
this.pane = pane;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
boot({state}, {notify}) {
|
|
334
|
+
let lastId = null;
|
|
335
|
+
this.#sub = state.state$.subscribe(s => {
|
|
336
|
+
const newId = s.activePanelIds[this.pane];
|
|
337
|
+
if (newId !== lastId) {
|
|
338
|
+
lastId = newId;
|
|
339
|
+
notify(newId);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
kill() {
|
|
345
|
+
this.#sub?.unsubscribe();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
class SetActivePanel extends Action {
|
|
350
|
+
static deps = ['state'];
|
|
351
|
+
|
|
352
|
+
constructor(pane, id) {
|
|
353
|
+
super();
|
|
354
|
+
this.pane = pane;
|
|
355
|
+
this.id = id;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
execute({state}) {
|
|
359
|
+
state.update(
|
|
360
|
+
(s, pane, id) => ({
|
|
361
|
+
...s,
|
|
362
|
+
activePanelIds: {...s.activePanelIds, [pane]: id},
|
|
363
|
+
}),
|
|
364
|
+
this.pane,
|
|
365
|
+
this.id,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
class ActivatePanel extends Action {
|
|
371
|
+
static deps = ['state'];
|
|
372
|
+
|
|
373
|
+
constructor(name) {
|
|
374
|
+
super();
|
|
375
|
+
this.name = name;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
execute({state}) {
|
|
379
|
+
const currentState = state.state$.value;
|
|
380
|
+
const {panels, paneVisibility} = currentState;
|
|
381
|
+
const panel = panels.get(this.name);
|
|
382
|
+
if (!panel) return;
|
|
383
|
+
|
|
384
|
+
let targetPane = panel.pane;
|
|
385
|
+
if (!paneVisibility[targetPane]) {
|
|
386
|
+
targetPane = targetPane === 'left' ? 'right' : 'left';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (paneVisibility[targetPane]) {
|
|
390
|
+
state.update(s => ({
|
|
391
|
+
...s,
|
|
392
|
+
activePanelIds: {...s.activePanelIds, [targetPane]: this.name},
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
class AllPropertyNames extends Query {
|
|
399
|
+
static deps = ['state'];
|
|
400
|
+
#sub;
|
|
401
|
+
|
|
402
|
+
boot({state}, {notify}) {
|
|
403
|
+
let lastNames = [];
|
|
404
|
+
this.#sub = state.state$.subscribe(s => {
|
|
405
|
+
const newNames = Object.keys(s.propertySpecs);
|
|
406
|
+
if (
|
|
407
|
+
newNames.length !== lastNames.length ||
|
|
408
|
+
newNames.some((name, i) => name !== lastNames[i])
|
|
409
|
+
) {
|
|
410
|
+
lastNames = newNames;
|
|
411
|
+
notify(lastNames);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
kill() {
|
|
416
|
+
this.#sub?.unsubscribe();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
class PropertySpec extends Query {
|
|
421
|
+
static deps = ['state'];
|
|
422
|
+
#sub;
|
|
423
|
+
|
|
424
|
+
constructor(name) {
|
|
425
|
+
super();
|
|
426
|
+
this.name = name;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
boot({state}, {notify}) {
|
|
430
|
+
let lastSpec = null;
|
|
431
|
+
this.#sub = state.state$.subscribe(s => {
|
|
432
|
+
const newSpec = s.propertySpecs[this.name];
|
|
433
|
+
if (!shallowCompare(newSpec, lastSpec)) {
|
|
434
|
+
lastSpec = newSpec;
|
|
435
|
+
notify(newSpec);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
kill() {
|
|
440
|
+
this.#sub?.unsubscribe();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
class PropertyValue extends Query {
|
|
445
|
+
static deps = ['state'];
|
|
446
|
+
#sub;
|
|
447
|
+
|
|
448
|
+
constructor(name) {
|
|
449
|
+
super();
|
|
450
|
+
this.name = name;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
boot({state}, {notify}) {
|
|
454
|
+
let lastValue = undefined;
|
|
455
|
+
this.#sub = state.state$.subscribe(s => {
|
|
456
|
+
const newValue = s.propertyValues[this.name];
|
|
457
|
+
if (newValue !== lastValue) {
|
|
458
|
+
lastValue = newValue;
|
|
459
|
+
notify(newValue);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
kill() {
|
|
464
|
+
this.#sub?.unsubscribe();
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
class IsPropertiesPanelVisible extends Query {
|
|
469
|
+
static deps = ['state'];
|
|
470
|
+
#sub;
|
|
471
|
+
|
|
472
|
+
boot({state}, {notify}) {
|
|
473
|
+
let lastVisible = null;
|
|
474
|
+
this.#sub = state.state$.subscribe(s => {
|
|
475
|
+
const isVisible = Object.keys(s.propertySpecs).length > 0;
|
|
476
|
+
if (isVisible !== lastVisible) {
|
|
477
|
+
lastVisible = isVisible;
|
|
478
|
+
notify(isVisible);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
kill() {
|
|
483
|
+
this.#sub?.unsubscribe();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
class UpsertProperty extends Action {
|
|
488
|
+
static deps = ['state'];
|
|
489
|
+
|
|
490
|
+
constructor(name, options) {
|
|
491
|
+
super();
|
|
492
|
+
this.name = name;
|
|
493
|
+
this.options = options;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
execute({state}) {
|
|
497
|
+
state.update(
|
|
498
|
+
(s, {name, options}) => {
|
|
499
|
+
const existingSpec = s.propertySpecs[name];
|
|
500
|
+
const newSpecs = {...s.propertySpecs};
|
|
501
|
+
let newValues = {...s.propertyValues};
|
|
502
|
+
|
|
503
|
+
if (existingSpec && shallowCompare(existingSpec.options, options)) {
|
|
504
|
+
return s; // No change
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
newSpecs[name] = {name, options};
|
|
508
|
+
|
|
509
|
+
if (!existingSpec) {
|
|
510
|
+
newValues[name] = options?.defaultValue;
|
|
511
|
+
}
|
|
512
|
+
return {...s, propertySpecs: newSpecs, propertyValues: newValues};
|
|
513
|
+
},
|
|
514
|
+
{name: this.name, options: this.options},
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
class UpdatePropertyValue extends Action {
|
|
520
|
+
static deps = ['state'];
|
|
521
|
+
|
|
522
|
+
constructor(name, value) {
|
|
523
|
+
super();
|
|
524
|
+
this.name = name;
|
|
525
|
+
this.value = value;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
execute({state}) {
|
|
529
|
+
state.update(
|
|
530
|
+
(s, {name, value}) => ({
|
|
531
|
+
...s,
|
|
532
|
+
propertyValues: {...s.propertyValues, [name]: value},
|
|
533
|
+
}),
|
|
534
|
+
{name: this.name, value: this.value},
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
class Panels extends Query {
|
|
540
|
+
static deps = ['state'];
|
|
541
|
+
#sub;
|
|
542
|
+
|
|
543
|
+
boot({state}, {notify}) {
|
|
544
|
+
let lastPanels = null;
|
|
545
|
+
this.#sub = state.state$.subscribe(s => {
|
|
546
|
+
if (s.panels !== lastPanels) {
|
|
547
|
+
lastPanels = s.panels;
|
|
548
|
+
notify(Array.from(lastPanels.values()));
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
kill() {
|
|
554
|
+
this.#sub?.unsubscribe();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
class CreateOrUpdatePanel extends Action {
|
|
559
|
+
static deps = ['state'];
|
|
560
|
+
|
|
561
|
+
constructor(panel) {
|
|
562
|
+
super();
|
|
563
|
+
this.panel = panel;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
execute({state}) {
|
|
567
|
+
state.update((s, panel) => {
|
|
568
|
+
const newPanels = new Map(s.panels);
|
|
569
|
+
const existingPanel = newPanels.get(panel.name);
|
|
570
|
+
|
|
571
|
+
if (existingPanel) {
|
|
572
|
+
const updatedPanel = {...existingPanel, ...panel};
|
|
573
|
+
if (panel.order === undefined) {
|
|
574
|
+
updatedPanel.order = existingPanel.order;
|
|
575
|
+
}
|
|
576
|
+
newPanels.set(panel.name, updatedPanel);
|
|
577
|
+
return {...s, panels: newPanels};
|
|
578
|
+
} else {
|
|
579
|
+
const newPanel = {...panel};
|
|
580
|
+
if (newPanel.order === undefined) {
|
|
581
|
+
const maxOrder = Array.from(newPanels.values()).reduce(
|
|
582
|
+
(max, p) => Math.max(max, p.order || 0),
|
|
583
|
+
0,
|
|
584
|
+
);
|
|
585
|
+
newPanel.order = maxOrder + 1;
|
|
586
|
+
}
|
|
587
|
+
newPanels.set(panel.name, newPanel);
|
|
588
|
+
|
|
589
|
+
const newActivePanelIds = s.activePanelIds;
|
|
590
|
+
if (s.paneVisibility[newPanel.pane]) {
|
|
591
|
+
newActivePanelIds[newPanel.pane] = newPanel.name;
|
|
592
|
+
} else {
|
|
593
|
+
const pane = Object.entries(s.paneVisibility).find(([pane, visible]) => visible)?.[0];
|
|
594
|
+
if (pane) {
|
|
595
|
+
newActivePanelIds[pane] = newPanel.name;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return {
|
|
600
|
+
...s,
|
|
601
|
+
panels: newPanels,
|
|
602
|
+
activePanelIds: newActivePanelIds,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
}, this.panel);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const panelSanitizerInterceptor = {
|
|
610
|
+
deps: ['state'],
|
|
611
|
+
leave: ({state}, {action}) => {
|
|
612
|
+
const currentState = state.state$.value;
|
|
613
|
+
const {panels, activePanelIds, paneVisibility} = currentState;
|
|
614
|
+
const newActivePanelIds = {...activePanelIds};
|
|
615
|
+
let changed = false;
|
|
616
|
+
|
|
617
|
+
const getEffectivePane = panel => {
|
|
618
|
+
if (paneVisibility.left && !paneVisibility.right) return 'left';
|
|
619
|
+
if (!paneVisibility.left && paneVisibility.right) return 'right';
|
|
620
|
+
return panel.pane;
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const panelsArray = Array.from(panels.values());
|
|
624
|
+
|
|
625
|
+
for (const pane of ['left', 'right']) {
|
|
626
|
+
if (!paneVisibility[pane]) continue;
|
|
627
|
+
|
|
628
|
+
const panelsInPane = panelsArray.filter(p => getEffectivePane(p) === pane);
|
|
629
|
+
const activeId = newActivePanelIds[pane];
|
|
630
|
+
const activePanelIsInPane = panelsInPane.some(p => p.name === activeId);
|
|
631
|
+
|
|
632
|
+
if (!activeId || !activePanelIsInPane) {
|
|
633
|
+
newActivePanelIds[pane] = panelsInPane[panelsInPane.length - 1]?.name;
|
|
634
|
+
changed = true;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (changed) {
|
|
639
|
+
state.update(s => ({...s, activePanelIds: newActivePanelIds}));
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const actionLoggerInterceptor = {
|
|
645
|
+
error: (_, {action, error}) => {
|
|
646
|
+
console.error(action, error);
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const demoStyle = css`
|
|
651
|
+
:host {
|
|
652
|
+
display: flex;
|
|
653
|
+
border: 1px solid var(--border-color);
|
|
654
|
+
border-radius: 4px;
|
|
655
|
+
margin-bottom: 1em;
|
|
656
|
+
max-height: 50rem;
|
|
657
|
+
background-color: var(--card-bg);
|
|
658
|
+
}
|
|
659
|
+
.pane {
|
|
660
|
+
display: flex;
|
|
661
|
+
flex-direction: column;
|
|
662
|
+
border-right: 1px solid var(--border-color);
|
|
663
|
+
min-width: 0;
|
|
664
|
+
}
|
|
665
|
+
.pane:last-child {
|
|
666
|
+
border-right: none;
|
|
667
|
+
}
|
|
668
|
+
.tabs {
|
|
669
|
+
display: flex;
|
|
670
|
+
border-bottom: 1px solid var(--border-color);
|
|
671
|
+
flex-shrink: 0;
|
|
672
|
+
}
|
|
673
|
+
.tab label {
|
|
674
|
+
padding: 10px 16px;
|
|
675
|
+
cursor: pointer;
|
|
676
|
+
border-right: 1px solid var(--border-color);
|
|
677
|
+
background: var(--bg-color);
|
|
678
|
+
color: var(--text-color);
|
|
679
|
+
opacity: 0.7;
|
|
680
|
+
transition:
|
|
681
|
+
background 0.2s,
|
|
682
|
+
color 0.2s,
|
|
683
|
+
opacity 0.2s;
|
|
684
|
+
display: block;
|
|
685
|
+
}
|
|
686
|
+
.tab input[type='radio'] {
|
|
687
|
+
display: none;
|
|
688
|
+
}
|
|
689
|
+
.tab input[type='radio']:checked + label {
|
|
690
|
+
background: var(--card-bg);
|
|
691
|
+
color: var(--text-color);
|
|
692
|
+
opacity: 1;
|
|
693
|
+
border-bottom: 1px solid var(--card-bg);
|
|
694
|
+
margin-bottom: -1px;
|
|
695
|
+
}
|
|
696
|
+
.tab label:hover {
|
|
697
|
+
background: var(--card-hover-bg);
|
|
698
|
+
opacity: 1;
|
|
699
|
+
}
|
|
700
|
+
.content-wrapper {
|
|
701
|
+
display: flex;
|
|
702
|
+
flex-grow: 1;
|
|
703
|
+
overflow: hidden;
|
|
704
|
+
padding: 1rem;
|
|
705
|
+
}
|
|
706
|
+
.panel-content {
|
|
707
|
+
overflow: hidden;
|
|
708
|
+
width: 0px;
|
|
709
|
+
pointer-events: none;
|
|
710
|
+
}
|
|
711
|
+
.panel-content.active {
|
|
712
|
+
pointer-events: auto;
|
|
713
|
+
overflow: auto;
|
|
714
|
+
width: initial;
|
|
715
|
+
}
|
|
716
|
+
pre > code {
|
|
717
|
+
padding: 1em;
|
|
718
|
+
margin: 0;
|
|
719
|
+
border-radius: 0;
|
|
720
|
+
}
|
|
721
|
+
`;
|
|
722
|
+
|
|
723
|
+
class DeckDemo extends HTMLElement {
|
|
724
|
+
#engine;
|
|
725
|
+
#id;
|
|
726
|
+
|
|
727
|
+
constructor() {
|
|
728
|
+
super();
|
|
729
|
+
this.attachShadow({mode: 'open'});
|
|
730
|
+
this.shadowRoot.adoptedStyleSheets = [commonStyle, demoStyle];
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async connectedCallback() {
|
|
734
|
+
if (!this.id) {
|
|
735
|
+
throw new Error('An id is required for deck-demo');
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
this.#id = this.id;
|
|
739
|
+
this.#engine = getEngine(this.getRootNode(), this.id, this.getAttribute('src'));
|
|
740
|
+
this.#render();
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
disconnectedCallback() {
|
|
744
|
+
if (!this.#id) {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
reconcile(this.shadowRoot, null);
|
|
749
|
+
releaseEngine(this.getRootNode(), this.id);
|
|
750
|
+
this.#id = undefined;
|
|
751
|
+
this.#engine = undefined;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
#render() {
|
|
755
|
+
const renderPane = (pane, panels, activeId) => {
|
|
756
|
+
const sortedPanels = [...panels].sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
757
|
+
|
|
758
|
+
return div(
|
|
759
|
+
{className: 'pane', $styling: {flex: 1}},
|
|
760
|
+
div(
|
|
761
|
+
{className: 'tabs'},
|
|
762
|
+
...sortedPanels.map(p =>
|
|
763
|
+
div(
|
|
764
|
+
{className: 'tab'},
|
|
765
|
+
input({
|
|
766
|
+
type: 'radio',
|
|
767
|
+
name: `tabs-${pane}`,
|
|
768
|
+
id: `tab-${p.name}`,
|
|
769
|
+
checked: activeId === p.name,
|
|
770
|
+
}),
|
|
771
|
+
label(
|
|
772
|
+
{
|
|
773
|
+
for: `tab-${p.name}`,
|
|
774
|
+
},
|
|
775
|
+
p.name,
|
|
776
|
+
).on({
|
|
777
|
+
click: () => this.#engine.dispatch(new SetActivePanel(pane, p.name)),
|
|
778
|
+
}),
|
|
779
|
+
),
|
|
780
|
+
),
|
|
781
|
+
),
|
|
782
|
+
div(
|
|
783
|
+
{className: 'content-wrapper'},
|
|
784
|
+
...sortedPanels.map(p =>
|
|
785
|
+
div({
|
|
786
|
+
$classes: ['panel-content', activeId === p.name && 'active'],
|
|
787
|
+
})
|
|
788
|
+
.key(p.name)
|
|
789
|
+
.opaque()
|
|
790
|
+
.on({
|
|
791
|
+
$attach: el => {
|
|
792
|
+
const div = document.createElement('div');
|
|
793
|
+
const shadow = div.attachShadow({mode: 'open'});
|
|
794
|
+
const aborter = new AbortController();
|
|
795
|
+
|
|
796
|
+
el.appendChild(div);
|
|
797
|
+
el._aborter = aborter;
|
|
798
|
+
},
|
|
799
|
+
$update: el => {
|
|
800
|
+
p.render(el.firstChild.shadowRoot, el._aborter.signal);
|
|
801
|
+
},
|
|
802
|
+
$detach: el => {
|
|
803
|
+
el._aborter?.abort();
|
|
804
|
+
},
|
|
805
|
+
}),
|
|
806
|
+
),
|
|
807
|
+
),
|
|
808
|
+
);
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
const app = withContainerSize(size$ => {
|
|
812
|
+
dedup()(map(s => s && s.width > 768)(size$)).subscribe(isWide => {
|
|
813
|
+
this.#engine.dispatch(new SetPaneVisibility({left: true, right: isWide}));
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
const state$ = zip(
|
|
817
|
+
(panels, leftId, rightId, visibility) => ({
|
|
818
|
+
panels,
|
|
819
|
+
leftId,
|
|
820
|
+
rightId,
|
|
821
|
+
visibility,
|
|
822
|
+
}),
|
|
823
|
+
this.#engine.query(new Panels()),
|
|
824
|
+
this.#engine.query(new ActivePanelForPane('left')),
|
|
825
|
+
this.#engine.query(new ActivePanelForPane('right')),
|
|
826
|
+
this.#engine.query(new PaneVisibility()),
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
return watch(
|
|
830
|
+
state$,
|
|
831
|
+
({panels, leftId, rightId, visibility}) => {
|
|
832
|
+
if (!visibility) return null;
|
|
833
|
+
|
|
834
|
+
const leftPanels = visibility.left ? panels.filter(p => p.pane === 'left') : [];
|
|
835
|
+
const rightPanels = visibility.right ? panels.filter(p => p.pane === 'right') : [];
|
|
836
|
+
|
|
837
|
+
if (leftPanels.length > 0 && rightPanels.length > 0) {
|
|
838
|
+
return [
|
|
839
|
+
renderPane('left', leftPanels, leftId),
|
|
840
|
+
renderPane('right', rightPanels, rightId),
|
|
841
|
+
];
|
|
842
|
+
} else if (visibility.left) {
|
|
843
|
+
return renderPane('left', panels, leftId);
|
|
844
|
+
} else if (visibility.right) {
|
|
845
|
+
return renderPane('right', panels, rightId);
|
|
846
|
+
}
|
|
847
|
+
return null;
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
placeholder: () => p('Loading...'),
|
|
851
|
+
},
|
|
852
|
+
);
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
reconcile(this.shadowRoot, [app]);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
customElements.define('deck-demo', DeckDemo);
|