flexdesk 0.17.14 → 0.17.16
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/CHANGELOG.md +39 -0
- package/css/base.css +237 -8
- package/css/flexdesk.css +344 -17
- package/css/overrides.css +6 -0
- package/dist/charts.js +104 -34
- package/dist/charts.js.map +3 -3
- package/dist/{chunk-2PM6ZMNS.js → chunk-2Z2ENHR3.js} +2 -2
- package/dist/{chunk-IG74S3ZH.js → chunk-7JI7HHSY.js} +2 -2
- package/dist/{chunk-TLIV6QSU.js → chunk-7OL6F2AM.js} +3 -3
- package/dist/{chunk-MADMGRX2.js → chunk-PGZ4EHSE.js} +156 -9
- package/dist/chunk-PGZ4EHSE.js.map +7 -0
- package/dist/{chunk-ZMH3OC7H.js → chunk-XJNADRGR.js} +4 -2
- package/dist/chunk-XJNADRGR.js.map +7 -0
- package/dist/flexdesk.css +344 -17
- package/dist/flow.js +3 -3
- package/dist/tiles.js +84 -9
- package/dist/tiles.js.map +2 -2
- package/dist/widgets.js +448 -5
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +110 -19
- package/dist/wm.js.map +2 -2
- package/docs/README.md +1 -1
- package/docs/components/charts.md +70 -3
- package/docs/components/forms-and-dialogs.md +80 -2
- package/docs/components/menus.md +85 -1
- package/docs/components/shell.md +52 -0
- package/docs/components/slide-out-panel.md +15 -0
- package/docs/components/tiles.md +37 -0
- package/package.json +1 -1
- package/src/charts/plot_config_editor.js +125 -29
- package/src/tiles/tile_base.js +48 -2
- package/src/tiles/tile_grid.js +42 -1
- package/src/tiling/kind_taxonomy.js +45 -0
- package/src/tiling/tile_breadcrumb.js +12 -4
- package/src/tiling/wm.js +86 -11
- package/src/ui/components/category_picker.js +556 -0
- package/src/ui/components/computing_status_window.js +3 -0
- package/src/ui/components/confirm_dialog.js +3 -0
- package/src/ui/components/managed_window.js +204 -9
- package/src/ui/components/modal.js +11 -2
- package/src/ui/components/wizard.js +4 -1
- package/widgets.js +2 -1
- package/dist/chunk-MADMGRX2.js.map +0 -7
- package/dist/chunk-ZMH3OC7H.js.map +0 -7
- /package/dist/{chunk-2PM6ZMNS.js.map → chunk-2Z2ENHR3.js.map} +0 -0
- /package/dist/{chunk-IG74S3ZH.js.map → chunk-7JI7HHSY.js.map} +0 -0
- /package/dist/{chunk-TLIV6QSU.js.map → chunk-7OL6F2AM.js.map} +0 -0
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* It draws a layout picker, a tab per subplot slot (when the layout has more
|
|
12
12
|
* than one) and, for the subplot in front: its title; its chart type, legend,
|
|
13
13
|
* interpolation, NaN handling and data points; its x axis (time, or a series);
|
|
14
|
-
* its y axes (label, scale, position, min, max, step) and their series (a
|
|
15
|
-
* picker, label
|
|
14
|
+
* its y axes (label, scale, position, min, max, step) and their series (each a
|
|
15
|
+
* block: a header line of picker, label and remove over one label column of
|
|
16
|
+
* colour, width, dash, stack group, the embedder's own fields
|
|
16
17
|
* and, with `overlaySources`, an overlay source); its z axis while the chart
|
|
17
18
|
* type is 3D; its reference lines and shaded bands; and the toggles of any
|
|
18
19
|
* injected `seriesTransforms` (series_transforms.js).
|
|
@@ -101,15 +102,37 @@ function removeButton(title, onClick) {
|
|
|
101
102
|
return b;
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
let hintCount = 0;
|
|
106
|
+
|
|
104
107
|
/**
|
|
105
|
-
* A labelled control. A single input or select
|
|
106
|
-
* compound one (a picker, a colour field) is not,
|
|
107
|
-
* not land on whichever of its inputs comes
|
|
108
|
+
* A labelled control on the editor's label column. A single input or select
|
|
109
|
+
* is wrapped in a `<label>`; a compound one (a picker, a colour field) is not,
|
|
110
|
+
* so a click on its text does not land on whichever of its inputs comes
|
|
111
|
+
* first. A `hint` is drawn under the control, in the control's column, and
|
|
112
|
+
* outside any `<label>`: the row is then a `<div>` whose label text is the
|
|
113
|
+
* `<label for>` of a single control, and the hint describes the control
|
|
114
|
+
* (`aria-describedby`) without being part of its name.
|
|
108
115
|
*/
|
|
109
|
-
function field(label, control) {
|
|
116
|
+
function field(label, control, { hint = null } = {}) {
|
|
110
117
|
const single = control.matches('input, select, textarea');
|
|
111
|
-
const
|
|
112
|
-
|
|
118
|
+
const hinted = hint != null && hint !== '';
|
|
119
|
+
if (!hinted) {
|
|
120
|
+
const row = el(single ? 'label' : 'div', 'twm-plot-config__field');
|
|
121
|
+
row.append(el('span', 'twm-plot-config__label', label), control);
|
|
122
|
+
return row;
|
|
123
|
+
}
|
|
124
|
+
const row = el('div', 'twm-plot-config__field twm-plot-config__field--hinted');
|
|
125
|
+
const text = el(single ? 'label' : 'span', 'twm-plot-config__label', label);
|
|
126
|
+
const note = el('span', 'twm-plot-config__hint', hint);
|
|
127
|
+
note.id = `twm-plot-config-hint-${++hintCount}`;
|
|
128
|
+
if (single) {
|
|
129
|
+
if (!control.id) control.id = `${note.id}-control`;
|
|
130
|
+
text.htmlFor = control.id;
|
|
131
|
+
}
|
|
132
|
+
const target = single ? control : (control.querySelector('input, select, textarea') ?? control);
|
|
133
|
+
const describedBy = target.getAttribute('aria-describedby');
|
|
134
|
+
target.setAttribute('aria-describedby', describedBy ? `${describedBy} ${note.id}` : note.id);
|
|
135
|
+
row.append(text, control, note);
|
|
113
136
|
return row;
|
|
114
137
|
}
|
|
115
138
|
|
|
@@ -170,8 +193,13 @@ let editorCount = 0;
|
|
|
170
193
|
* source, written to `series.overlay.source`. Absent: no overlay controls.
|
|
171
194
|
* @param {Object} [options.seriesTransforms] - `{ [id]: transform }` (series_transforms.js);
|
|
172
195
|
* their toggles are drawn per subplot.
|
|
173
|
-
* @param {Function} [options.extraSeriesFields] - `(el, series, { subplot, axis, onChange })
|
|
174
|
-
* => { dispose? } | void`: fields of the embedder's own in each series
|
|
196
|
+
* @param {Function} [options.extraSeriesFields] - `(el, series, { subplot, axis, onChange, field })
|
|
197
|
+
* => { dispose? } | specs[] | void`: fields of the embedder's own in each series
|
|
198
|
+
* block, on the series' label column. It may draw into `el` (with
|
|
199
|
+
* `field(label, control, { hint })` for rows that line up) and return a handle,
|
|
200
|
+
* or return an ARRAY of field specs `{ label: string, control: HTMLElement,
|
|
201
|
+
* hint? }` / `{ label: string, render(slot) => { dispose? } | void, hint? }`,
|
|
202
|
+
* laid out after anything drawn into `el`. A bad spec is skipped with a warning.
|
|
175
203
|
* @param {Object} [options.logger] - Given to every AutocompleteField (provider errors);
|
|
176
204
|
* its `warn` also hears a part (an `extraSeriesFields` handle, say) that throws on dispose.
|
|
177
205
|
* @returns {{ dispose: Function, refresh: Function, selectSubplot: Function,
|
|
@@ -309,13 +337,14 @@ function renderPlotConfigEditor(container, config, options = {}) {
|
|
|
309
337
|
return node;
|
|
310
338
|
}
|
|
311
339
|
|
|
312
|
-
|
|
340
|
+
/** `keep` takes its teardown: the body's (track), or a narrower redraw's. */
|
|
341
|
+
function colorField(value, onSet, label, keep = track) {
|
|
313
342
|
const f = new ColorField({
|
|
314
343
|
value: value ?? '',
|
|
315
344
|
label,
|
|
316
345
|
onChange: (v) => { if (v !== null) { onSet(v || null); notify(); } },
|
|
317
346
|
});
|
|
318
|
-
|
|
347
|
+
keep(() => f.dispose());
|
|
319
348
|
return f.render();
|
|
320
349
|
}
|
|
321
350
|
|
|
@@ -489,18 +518,17 @@ function renderPlotConfigEditor(container, config, options = {}) {
|
|
|
489
518
|
return group;
|
|
490
519
|
}
|
|
491
520
|
|
|
521
|
+
/**
|
|
522
|
+
* One series as a block: a header line (series picker, legend label,
|
|
523
|
+
* remove) over its fields on the editor's label column (colour, width,
|
|
524
|
+
* dash, stack group, the embedder's fields, overlay).
|
|
525
|
+
*/
|
|
492
526
|
function seriesRow(sp, axis, s, overlaySources) {
|
|
493
527
|
const row = el('div', 'twm-plot-config__series');
|
|
494
|
-
const
|
|
495
|
-
|
|
528
|
+
const head = el('div', 'twm-plot-config__series-head');
|
|
529
|
+
head.append(
|
|
496
530
|
seriesPicker(s, 'variable', 'series', sp, 'Series'),
|
|
497
531
|
textInput(s.label, (v) => { s.label = v; notify(); }, { placeholder: 'Legend label', label: 'Legend label' }),
|
|
498
|
-
colorField(s.color, (v) => { if (v) s.color = v; else delete s.color; }, 'Colour'),
|
|
499
|
-
numberInput(s.lineWidth ?? 2, (v) => { s.lineWidth = v ?? 2; notify(); },
|
|
500
|
-
{ placeholder: '2', label: 'Line width', min: 1, max: 10, step: 1 }),
|
|
501
|
-
select(DASHES, s.lineStyle ?? 'solid', (v) => { s.lineStyle = v; notify(); }, { label: 'Dash' }),
|
|
502
|
-
textInput(s.stackGroup, (v) => { s.stackGroup = v || null; notify(); },
|
|
503
|
-
{ placeholder: 'Stack group', label: 'Stack group' }),
|
|
504
532
|
removeButton('Remove series', () => {
|
|
505
533
|
const i = axis.series.indexOf(s);
|
|
506
534
|
if (i >= 0) axis.series.splice(i, 1);
|
|
@@ -508,32 +536,100 @@ function renderPlotConfigEditor(container, config, options = {}) {
|
|
|
508
536
|
notify();
|
|
509
537
|
}),
|
|
510
538
|
);
|
|
511
|
-
row.appendChild(main);
|
|
512
539
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
540
|
+
const fields = el('div', 'twm-plot-config__series-fields');
|
|
541
|
+
fields.append(
|
|
542
|
+
field('Colour', colorField(s.color, (v) => { if (v) s.color = v; else delete s.color; }, 'Colour')),
|
|
543
|
+
field('Width', numberInput(s.lineWidth ?? 2, (v) => { s.lineWidth = v ?? 2; notify(); },
|
|
544
|
+
{ placeholder: '2', label: 'Line width', min: 1, max: 10, step: 1 })),
|
|
545
|
+
field('Dash', select(DASHES, s.lineStyle ?? 'solid', (v) => { s.lineStyle = v; notify(); }, { label: 'Dash' })),
|
|
546
|
+
field('Stack group', textInput(s.stackGroup, (v) => { s.stackGroup = v || null; notify(); },
|
|
547
|
+
{ placeholder: 'None', label: 'Stack group' })),
|
|
548
|
+
);
|
|
549
|
+
if (opts.extraSeriesFields) fields.appendChild(extraFields(sp, axis, s));
|
|
550
|
+
if (opts.overlaySources) fields.appendChild(overlayEditor(s, overlaySources));
|
|
551
|
+
row.append(head, fields);
|
|
520
552
|
return row;
|
|
521
553
|
}
|
|
522
554
|
|
|
555
|
+
const warn = (...args) => (opts.logger?.warn ? opts.logger.warn(...args) : console.warn(...args));
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* The embedder's fields of a series: whatever `extraSeriesFields` draws
|
|
559
|
+
* into the box (which sits in the series' field column), then, when it
|
|
560
|
+
* returns an ARRAY, those field specs laid out by `field()`. Anything
|
|
561
|
+
* else it returns is a handle, disposed when the row goes (`{ dispose }`).
|
|
562
|
+
* A spec that is not `{ label, control: Element }` or
|
|
563
|
+
* `{ label, render(slot) }` is skipped with a warning, as is a `render`
|
|
564
|
+
* that throws: one bad field never leaves half a body.
|
|
565
|
+
*/
|
|
566
|
+
function extraFields(sp, axis, s) {
|
|
567
|
+
const extra = el('div', 'twm-plot-config__series-extra');
|
|
568
|
+
const result = opts.extraSeriesFields(extra, s, { subplot: sp, axis, onChange: notify, field });
|
|
569
|
+
if (!Array.isArray(result)) {
|
|
570
|
+
if (typeof result?.dispose === 'function') track(() => result.dispose());
|
|
571
|
+
return extra;
|
|
572
|
+
}
|
|
573
|
+
for (const spec of result) {
|
|
574
|
+
const label = spec?.label;
|
|
575
|
+
let control = null;
|
|
576
|
+
if (typeof label !== 'string') {
|
|
577
|
+
warn('[plot config editor] an extraSeriesFields spec has no label; skipped', spec);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (spec.control != null) {
|
|
581
|
+
if (!(spec.control instanceof Element)) {
|
|
582
|
+
warn('[plot config editor] an extraSeriesFields spec control is not an Element; skipped', spec);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
control = spec.control;
|
|
586
|
+
} else if (typeof spec.render === 'function') {
|
|
587
|
+
control = el('div', 'twm-plot-config__slot');
|
|
588
|
+
try {
|
|
589
|
+
const handle = spec.render(control);
|
|
590
|
+
if (typeof handle?.dispose === 'function') track(() => handle.dispose());
|
|
591
|
+
} catch (error) {
|
|
592
|
+
warn('[plot config editor] an extraSeriesFields spec failed to render; skipped', error);
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
} else {
|
|
596
|
+
warn('[plot config editor] an extraSeriesFields spec has neither control nor render; skipped', spec);
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
extra.appendChild(field(label, control, { hint: spec.hint }));
|
|
600
|
+
}
|
|
601
|
+
return extra;
|
|
602
|
+
}
|
|
603
|
+
|
|
523
604
|
function overlayEditor(s, overlaySources) {
|
|
524
605
|
const wrap = el('div', 'twm-plot-config__overlay');
|
|
525
606
|
const head = el('div', 'twm-plot-config__overlay-source', 'Loading overlay sources…');
|
|
526
607
|
const details = el('div', 'twm-plot-config__overlay-fields');
|
|
527
608
|
wrap.append(head, details);
|
|
528
609
|
|
|
610
|
+
// The details' colour field is torn down on each redraw of the
|
|
611
|
+
// details, not left for the body's next redraw.
|
|
612
|
+
let detailDisposers = [];
|
|
613
|
+
const disposeDetails = () => {
|
|
614
|
+
const fns = detailDisposers;
|
|
615
|
+
detailDisposers = [];
|
|
616
|
+
for (const fn of fns) {
|
|
617
|
+
try { fn(); } catch (error) {
|
|
618
|
+
opts.logger?.warn?.('[plot config editor] a field failed to dispose', error);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
track(disposeDetails);
|
|
623
|
+
const keep = (fn) => { detailDisposers.push(fn); };
|
|
529
624
|
const drawDetails = () => {
|
|
625
|
+
disposeDetails();
|
|
530
626
|
details.replaceChildren();
|
|
531
627
|
const ov = s.overlay;
|
|
532
628
|
if (!ov) return;
|
|
533
629
|
details.append(
|
|
534
630
|
field('Label', textInput(ov.label, (v) => { ov.label = v; notify(); },
|
|
535
631
|
{ placeholder: `${s.label || s.variable || 'Series'} (actual)` })),
|
|
536
|
-
field('Colour', colorField(ov.color, (v) => { if (v) ov.color = v; else delete ov.color; }, 'Overlay colour')),
|
|
632
|
+
field('Colour', colorField(ov.color, (v) => { if (v) ov.color = v; else delete ov.color; }, 'Overlay colour', keep)),
|
|
537
633
|
field('Dash', select(DASHES, ov.lineStyle ?? 'dashed', (v) => { ov.lineStyle = v; notify(); })),
|
|
538
634
|
field('Width', numberInput(ov.lineWidth ?? 2, (v) => { ov.lineWidth = v ?? 2; notify(); },
|
|
539
635
|
{ placeholder: '2', min: 1, max: 10, step: 1 })),
|
package/src/tiles/tile_base.js
CHANGED
|
@@ -47,10 +47,13 @@ export class TileBase {
|
|
|
47
47
|
* @param {{resolve: Function}} [options.dataSource] - Where this tile asks for its
|
|
48
48
|
* own rows. Handed down by `TileGrid`; absent for a grid whose data is
|
|
49
49
|
* broadcast with `setData()`, and then nothing ever calls `loadData()`.
|
|
50
|
+
* @param {boolean} [options.configOnDoubleClick=true] - A double-click on the
|
|
51
|
+
* title strip (not on its buttons or an editable field in it) does what
|
|
52
|
+
* the cogwheel does. False leaves the cogwheel as the only way in.
|
|
50
53
|
*/
|
|
51
54
|
constructor({ id, grid, eventBus = null, config = {}, readonly = false,
|
|
52
55
|
headless = false, host = null, stateGuard = null,
|
|
53
|
-
dataSource = null }) {
|
|
56
|
+
dataSource = null, configOnDoubleClick = true }) {
|
|
54
57
|
this.host = host;
|
|
55
58
|
// THE TILE PULLS; THE GRID DOES NOT PUSH.
|
|
56
59
|
//
|
|
@@ -74,6 +77,9 @@ export class TileBase {
|
|
|
74
77
|
this.config = { ...this.getDefaultConfig(), ...config };
|
|
75
78
|
this.readonly = readonly;
|
|
76
79
|
this.headless = headless;
|
|
80
|
+
this.configOnDoubleClick = configOnDoubleClick;
|
|
81
|
+
/** STORY-0128. See `setConfiguring`. */
|
|
82
|
+
this._configuring = false;
|
|
77
83
|
this.element = null;
|
|
78
84
|
this.contentElement = null;
|
|
79
85
|
this.chartInstance = null;
|
|
@@ -288,7 +294,8 @@ export class TileBase {
|
|
|
288
294
|
if (this._disposed) return;
|
|
289
295
|
|
|
290
296
|
this.element = document.createElement('div');
|
|
291
|
-
this.element.className = `tile${this.readonly ? ' tile--readonly' : ''}
|
|
297
|
+
this.element.className = `tile${this.readonly ? ' tile--readonly' : ''}`
|
|
298
|
+
+ `${this._configuring ? ' tile--configuring' : ''}`;
|
|
292
299
|
|
|
293
300
|
// Use StateGuard bypass if available to set dataset properties
|
|
294
301
|
const stateGuard = this.stateGuard;
|
|
@@ -595,6 +602,17 @@ export class TileBase {
|
|
|
595
602
|
this._onConfigClick();
|
|
596
603
|
});
|
|
597
604
|
|
|
605
|
+
// STORY-0128. The title strip's double-click is a second door to
|
|
606
|
+
// the cogwheel, through the same `_onConfigClick`. Not on a button
|
|
607
|
+
// in the strip (each has its own verb) and not on an editable
|
|
608
|
+
// field, where a double-click selects a word.
|
|
609
|
+
header.addEventListener('dblclick', (e) => {
|
|
610
|
+
if (!this.configOnDoubleClick || !configBtn) return;
|
|
611
|
+
if (e.target.closest?.('button, input, textarea, select, [contenteditable=""], [contenteditable="true"], .tile-menu-dropdown')) return;
|
|
612
|
+
e.stopPropagation();
|
|
613
|
+
this._onConfigClick();
|
|
614
|
+
});
|
|
615
|
+
|
|
598
616
|
// "..." menu button + dropdown
|
|
599
617
|
const menuBtn = header.querySelector('.tile-menu-btn');
|
|
600
618
|
const menuDropdown = header.querySelector('.tile-menu-dropdown');
|
|
@@ -705,6 +723,34 @@ export class TileBase {
|
|
|
705
723
|
if (this._menuHome && menu.parentNode !== this._menuHome) this._menuHome.appendChild(menu);
|
|
706
724
|
}
|
|
707
725
|
|
|
726
|
+
/**
|
|
727
|
+
* Mark this tile as being configured, or clear the mark (STORY-0128).
|
|
728
|
+
* A convenience for `grid.setConfiguring(tile.id, on)`, which is the API:
|
|
729
|
+
* the grid holds the mark by tile id, so it outlives this tile instance
|
|
730
|
+
* (`setLayout` disposes and rebuilds every tile). A tile with no grid
|
|
731
|
+
* holds the mark itself.
|
|
732
|
+
*
|
|
733
|
+
* @param {boolean} [on=true]
|
|
734
|
+
*/
|
|
735
|
+
setConfiguring(on = true) {
|
|
736
|
+
if (typeof this.grid?.setConfiguring === 'function') {
|
|
737
|
+
this.grid.setConfiguring(this.id, on);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
this._applyConfiguring(on);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** Draw (or clear) the configuring mark. Called by the grid.
|
|
744
|
+
* @param {boolean} on
|
|
745
|
+
* @private */
|
|
746
|
+
_applyConfiguring(on) {
|
|
747
|
+
this._configuring = !!on;
|
|
748
|
+
this.element?.classList.toggle('tile--configuring', this._configuring);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** True while this tile is marked as being configured. */
|
|
752
|
+
get isConfiguring() { return this._configuring; }
|
|
753
|
+
|
|
708
754
|
/**
|
|
709
755
|
* Handle config button click — highlight tile and emit config request.
|
|
710
756
|
* @private
|
package/src/tiles/tile_grid.js
CHANGED
|
@@ -27,6 +27,8 @@ export class TileGrid {
|
|
|
27
27
|
* @param {*} [options.scope=null] - The board's scope, a filter every tile
|
|
28
28
|
* follows (0.16.1); see `setScope`.
|
|
29
29
|
* @param {Function} [options.onScopeChange] - `(scope)` after `setScope` changed it.
|
|
30
|
+
* @param {boolean} [options.configOnDoubleClick=true] - Whether a double-click
|
|
31
|
+
* on a tile's title strip opens its config, as its cogwheel does (0.17.16).
|
|
30
32
|
*/
|
|
31
33
|
constructor({ container, columns = 12, rowHeight = 80, gap = 16, eventBus = null,
|
|
32
34
|
showToolbar = true, readonly = false, onLayoutChange, onBeforeLayoutChange,
|
|
@@ -38,12 +40,19 @@ export class TileGrid {
|
|
|
38
40
|
host = null, stateGuard = null,
|
|
39
41
|
// Injected, never constructed here: the grid does not know what
|
|
40
42
|
// a binding is, only that a tile may have somewhere to ask.
|
|
41
|
-
dataSource = null, scope = null, onScopeChange = null
|
|
43
|
+
dataSource = null, scope = null, onScopeChange = null,
|
|
44
|
+
// STORY-0128. Handed to every tile: false turns off the
|
|
45
|
+
// title strip's double-click to configure.
|
|
46
|
+
configOnDoubleClick = true }) {
|
|
42
47
|
this.host = host;
|
|
43
48
|
this.scope = scope;
|
|
44
49
|
this._onScopeChangeCallback = onScopeChange;
|
|
45
50
|
this.stateGuard = stateGuard;
|
|
46
51
|
this.dataSource = dataSource;
|
|
52
|
+
this.configOnDoubleClick = configOnDoubleClick;
|
|
53
|
+
/** STORY-0128. Ids of the tiles marked as being configured; see
|
|
54
|
+
* `setConfiguring`. Kept by id so a rebuilt tile is marked again. */
|
|
55
|
+
this._configuringIds = new Set();
|
|
47
56
|
this.container = container;
|
|
48
57
|
this.columns = columns;
|
|
49
58
|
this.rowHeight = rowHeight;
|
|
@@ -194,11 +203,14 @@ export class TileGrid {
|
|
|
194
203
|
host: this.host,
|
|
195
204
|
stateGuard: this.stateGuard,
|
|
196
205
|
dataSource: this.dataSource,
|
|
206
|
+
configOnDoubleClick: this.configOnDoubleClick,
|
|
197
207
|
});
|
|
198
208
|
|
|
199
209
|
if (!tile) return null;
|
|
200
210
|
// Before mount, so a tile's first render and first load already follow it.
|
|
201
211
|
tile.scope = this.scope;
|
|
212
|
+
// STORY-0128. A tile rebuilt under a marked id (setLayout) is marked.
|
|
213
|
+
if (this._configuringIds.has(tileId)) tile._applyConfiguring(true);
|
|
202
214
|
|
|
203
215
|
// Add to layout
|
|
204
216
|
const layoutEntry = {
|
|
@@ -340,6 +352,35 @@ export class TileGrid {
|
|
|
340
352
|
}));
|
|
341
353
|
}
|
|
342
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Mark a tile as being configured, or clear the mark (STORY-0128).
|
|
357
|
+
*
|
|
358
|
+
* A config request (cogwheel or title double-click) highlights the
|
|
359
|
+
* requesting tile with `tile--selected` and takes that highlight off every
|
|
360
|
+
* other tile: one selection per grid. An embedder that keeps a config
|
|
361
|
+
* window open per tile marks each such tile here instead. The mark
|
|
362
|
+
* (`tile--configuring`, drawn like the selection) is the embedder's alone:
|
|
363
|
+
* no config request clears it, and it is held by id, so a tile that
|
|
364
|
+
* `setLayout` rebuilds (or an id added later) is marked again. Clear it
|
|
365
|
+
* when that tile's config window closes.
|
|
366
|
+
*
|
|
367
|
+
* @param {string} tileId
|
|
368
|
+
* @param {boolean} [on=true]
|
|
369
|
+
*/
|
|
370
|
+
setConfiguring(tileId, on = true) {
|
|
371
|
+
if (on) this._configuringIds.add(tileId);
|
|
372
|
+
else this._configuringIds.delete(tileId);
|
|
373
|
+
this.tiles.get(tileId)?._applyConfiguring(!!on);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* @param {string} tileId
|
|
378
|
+
* @returns {boolean} Whether the tile is marked as being configured.
|
|
379
|
+
*/
|
|
380
|
+
isConfiguring(tileId) {
|
|
381
|
+
return this._configuringIds.has(tileId);
|
|
382
|
+
}
|
|
383
|
+
|
|
343
384
|
/**
|
|
344
385
|
* Set layout and rebuild tiles.
|
|
345
386
|
* @param {Array|Object} layout - Layout array or object with tiles property
|
|
@@ -62,6 +62,20 @@
|
|
|
62
62
|
* a reason, and keeps the first of any duplicates a restored
|
|
63
63
|
* layout holds. The root kind cannot be single: every new
|
|
64
64
|
* pane, tab and desktop is seeded with it.
|
|
65
|
+
*
|
|
66
|
+
* window? `{ width?, height? }` — the initial size, in px, of a managed
|
|
67
|
+
* window this kind opens in (`navigate(…, { dest: 'window' })`,
|
|
68
|
+
* or always for `windowOnly`); clamped to the screen and to the
|
|
69
|
+
* window's minimum (400 × 300) as any window is. Absent: the
|
|
70
|
+
* window's default size (STORY-0127).
|
|
71
|
+
* windowOnly? `true`: the kind lives in a managed window, never in a tile
|
|
72
|
+
* — a tool, not a page. Every open path (navigate with any
|
|
73
|
+
* `dest`, openInPrimary, openInLeaf, the tab and split verbs)
|
|
74
|
+
* opens it in a window; its window never docks into a tile
|
|
75
|
+
* (no Back to tile, maximise is geometric, no drag-to-dock); a
|
|
76
|
+
* restored layout drops a tile tab of it, with one warning, and
|
|
77
|
+
* a tab's history entry of it opens a window. Neither the root
|
|
78
|
+
* nor a top-nav kind can be window-only (STORY-0127).
|
|
65
79
|
*/
|
|
66
80
|
|
|
67
81
|
import { PANEL_OFFERED, PANEL_SIDES, readPanelOffer } from './panel_availability.js';
|
|
@@ -104,6 +118,26 @@ export function createTaxonomy({ kinds, root } = {}) {
|
|
|
104
118
|
throw new TypeError(
|
|
105
119
|
`createTaxonomy: '${kind}'.single must be true or a prop name`);
|
|
106
120
|
}
|
|
121
|
+
if (def.window != null) {
|
|
122
|
+
const w = def.window;
|
|
123
|
+
const bad = (v) => v != null && !(typeof v === 'number' && Number.isFinite(v) && v > 0);
|
|
124
|
+
if (typeof w !== 'object' || Array.isArray(w) || bad(w.width) || bad(w.height)) {
|
|
125
|
+
throw new TypeError(
|
|
126
|
+
`createTaxonomy: '${kind}'.window must be { width?, height? } in positive px`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (def.windowOnly != null && typeof def.windowOnly !== 'boolean') {
|
|
130
|
+
throw new TypeError(`createTaxonomy: '${kind}'.windowOnly must be true or false`);
|
|
131
|
+
}
|
|
132
|
+
if (def.windowOnly && def.isTopNav) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`createTaxonomy: '${kind}' is a top-nav page and cannot be windowOnly — a page lives in tiles`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (kinds[root].windowOnly) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`createTaxonomy: the root kind '${root}' cannot be windowOnly — `
|
|
140
|
+
+ 'every new pane, tab and desktop is seeded with it');
|
|
107
141
|
}
|
|
108
142
|
if (kinds[root].single) {
|
|
109
143
|
throw new Error(
|
|
@@ -227,6 +261,15 @@ export function createTaxonomy({ kinds, root } = {}) {
|
|
|
227
261
|
return `${kind}\u0000${v == null ? '' : String(v)}`;
|
|
228
262
|
};
|
|
229
263
|
|
|
264
|
+
/** Whether `kind` lives in a managed window only (KindDef `windowOnly`). */
|
|
265
|
+
const isWindowOnly = (kind) => _kinds[kind]?.windowOnly === true;
|
|
266
|
+
|
|
267
|
+
/** The initial size of a window `kind` opens in (KindDef `window`), or null. */
|
|
268
|
+
const windowSize = (kind) => {
|
|
269
|
+
const w = _kinds[kind]?.window;
|
|
270
|
+
return w ? { width: w.width ?? null, height: w.height ?? null } : null;
|
|
271
|
+
};
|
|
272
|
+
|
|
230
273
|
/** The sentence a refused copy verb shows for a single kind. */
|
|
231
274
|
const singleReason = (kind) =>
|
|
232
275
|
`${_kinds[kind]?.label || kind} is open once: use the one that is open`;
|
|
@@ -246,5 +289,7 @@ export function createTaxonomy({ kinds, root } = {}) {
|
|
|
246
289
|
isSingle,
|
|
247
290
|
instanceKey,
|
|
248
291
|
singleReason,
|
|
292
|
+
isWindowOnly,
|
|
293
|
+
windowSize,
|
|
249
294
|
});
|
|
250
295
|
}
|
|
@@ -24,14 +24,19 @@
|
|
|
24
24
|
/** Build a breadcrumb strip element + render initial state. Returns
|
|
25
25
|
* `{ el, destroy }`. The strip is wired to the WM (clicks open content
|
|
26
26
|
* in the primary tile) and, when the embedder supplies a `rootCrumb`
|
|
27
|
-
* with a `subscribe` hook, to whatever keeps the root label fresh.
|
|
27
|
+
* with a `subscribe` hook, to whatever keeps the root label fresh.
|
|
28
|
+
* While the path is a single segment (the tile itself) the strip is
|
|
29
|
+
* `hidden` and takes no room; `refresh()` shows it once there are more. */
|
|
28
30
|
export function mountTileBreadcrumb(kind, props, ctx) {
|
|
29
31
|
const eventBus = ctx?.eventBus;
|
|
30
32
|
const taxonomy = ctx?.taxonomy;
|
|
31
33
|
const rootCrumb = ctx?.rootCrumb || null;
|
|
32
34
|
if (!taxonomy) {
|
|
33
35
|
console.error('[breadcrumb] no taxonomy in ctx — cannot render');
|
|
34
|
-
|
|
36
|
+
const el = document.createElement('nav');
|
|
37
|
+
el.className = 'twm-topbar-breadcrumb twm-tile-breadcrumb';
|
|
38
|
+
el.hidden = true; // nothing to draw: no strip, as for a one-segment path
|
|
39
|
+
return { el, refresh: () => {}, destroy: () => {} };
|
|
35
40
|
}
|
|
36
41
|
const root = document.createElement('nav');
|
|
37
42
|
root.className = 'twm-topbar-breadcrumb twm-tile-breadcrumb';
|
|
@@ -82,9 +87,12 @@ export function mountTileBreadcrumb(kind, props, ctx) {
|
|
|
82
87
|
try { return ctx.trailSegments() || []; }
|
|
83
88
|
catch (err) { console.warn('[breadcrumb] trailSegments threw', err); return []; }
|
|
84
89
|
};
|
|
90
|
+
// One segment is the tile itself, which its title already names: the strip
|
|
91
|
+
// is hidden (and its page-shell slot with it) until there is a level above.
|
|
85
92
|
const render = () => {
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
const segments = _segments(kind, props, taxonomy, rootCrumb, rootLabel, navigate, trailOf());
|
|
94
|
+
_renderInto(root, segments);
|
|
95
|
+
root.hidden = segments.length < 2;
|
|
88
96
|
};
|
|
89
97
|
render();
|
|
90
98
|
|