flexdesk 0.17.15 → 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 +23 -0
- package/css/base.css +224 -4
- package/css/flexdesk.css +314 -12
- 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-AD7FIA3Z.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-OEG6NPAN.js → chunk-XJNADRGR.js} +2 -2
- package/dist/flexdesk.css +314 -12
- 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 +102 -14
- 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 +61 -0
- package/docs/components/menus.md +85 -1
- package/docs/components/shell.md +37 -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/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/wizard.js +4 -1
- package/widgets.js +2 -1
- package/dist/chunk-MADMGRX2.js.map +0 -7
- /package/dist/{chunk-2PM6ZMNS.js.map → chunk-2Z2ENHR3.js.map} +0 -0
- /package/dist/{chunk-AD7FIA3Z.js.map → chunk-7JI7HHSY.js.map} +0 -0
- /package/dist/{chunk-TLIV6QSU.js.map → chunk-7OL6F2AM.js.map} +0 -0
- /package/dist/{chunk-OEG6NPAN.js.map → chunk-XJNADRGR.js.map} +0 -0
package/docs/README.md
CHANGED
|
@@ -49,7 +49,7 @@ FlexDesk is nine entry points that share their code, so you load only what you u
|
|
|
49
49
|
- [Settings page](components/settings-view.md): a settings store's schema drawn as a page.
|
|
50
50
|
- [Wizard](components/wizard.md): a multi-step flow, inline or in a window.
|
|
51
51
|
- [Input fields: slider and tags](components/input-fields.md): `SliderField` and `createTagInput`.
|
|
52
|
-
- [Menus](components/menus.md): `createMenuBar` and `
|
|
52
|
+
- [Menus](components/menus.md): `createMenuBar`, `ActionDropdown` and `CategoryPicker`.
|
|
53
53
|
|
|
54
54
|
### Layout, content and feedback
|
|
55
55
|
|
|
@@ -4,6 +4,70 @@
|
|
|
4
4
|
|
|
5
5
|
Plotly-backed charts: the wrapper, the chart types, subplot layouts, downsampling, the plot grid, the plot-config editor and the two plot windows. Plotly itself is not in the bundle; call `setPlotlySource()` with the path to your copy. The plot window's `streamEvents` option is described with the [other application-neutral options](../concepts/domain-neutrality.md#no-application-globals-the-gallery-picker-and-plot-window-events-0170).
|
|
6
6
|
|
|
7
|
+
## A plot config series is one block, and an embedder's fields share it (0.17.16)
|
|
8
|
+
|
|
9
|
+
[`renderPlotConfigEditor`](#editing-a-plot-config-renderplotconfigeditor-0170)
|
|
10
|
+
drew each series as one wrapped line: the series picker, the legend label, the
|
|
11
|
+
colour swatch with a wide hex box, the width, the dash and the stack group,
|
|
12
|
+
then the embedder's `extraSeriesFields` and the overlay select on label
|
|
13
|
+
columns of their own. Each series is now one block:
|
|
14
|
+
|
|
15
|
+
- **A header line**: the series picker, the legend label and the remove
|
|
16
|
+
button. It wraps when the window is narrow.
|
|
17
|
+
- **Its fields, one per line, on the editor's label column** (the same 96px
|
|
18
|
+
column as the subplot's and the axes' fields): **Colour** (a `ColorField`:
|
|
19
|
+
the swatch and a hex box sized to `#rrggbb`), **Width**, **Dash**,
|
|
20
|
+
**Stack group**, then the embedder's fields, then **Overlay** (with
|
|
21
|
+
`overlaySources`). An overlay's own label, colour, dash, width and mode sit
|
|
22
|
+
under its source, set in by a rule, with their controls still on the
|
|
23
|
+
series' control column.
|
|
24
|
+
|
|
25
|
+
It reads at the usual ~600px config window and down to 360px without
|
|
26
|
+
overflowing. The classes are `.twm-plot-config__series-head` and
|
|
27
|
+
`.twm-plot-config__series-fields`; `.twm-plot-config__series-main` is gone.
|
|
28
|
+
|
|
29
|
+
`extraSeriesFields(el, series, ctx)` can now put its fields on that same
|
|
30
|
+
column, in either of two ways, and the 0.17.0 form keeps working:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
renderPlotConfigEditor(el, plotConfig, {
|
|
34
|
+
// 1. return field specs: the editor lays them out
|
|
35
|
+
extraSeriesFields: (el, series, { onChange }) => [
|
|
36
|
+
{ label: 'Scenario', control: scenarioSelect(series, onChange),
|
|
37
|
+
hint: 'The run this series is read from' },
|
|
38
|
+
{ label: 'Note', render: (slot) => mountMyWidget(slot, series, onChange) }, // may return { dispose }
|
|
39
|
+
],
|
|
40
|
+
// 2. or draw rows with the editor's own helper
|
|
41
|
+
// extraSeriesFields: (el, series, { onChange, field }) => {
|
|
42
|
+
// el.appendChild(field('Scenario', scenarioSelect(series, onChange), { hint: '…' }));
|
|
43
|
+
// },
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- `ctx.field(label, control, { hint })` returns a field line (a `<label>`
|
|
48
|
+
when the control is a single input, select or textarea). A hint is drawn
|
|
49
|
+
under the control, outside the label: the line is then a `<div>`, its label
|
|
50
|
+
text is the `<label for>` of a single control, and the hint is the control's
|
|
51
|
+
`aria-describedby`, so it describes the control without joining its name.
|
|
52
|
+
- Only an ARRAY return is field specs. A spec is
|
|
53
|
+
`{ label: string, control: HTMLElement, hint? }` or
|
|
54
|
+
`{ label: string, render(slot), hint? }`; `render` draws into an empty slot
|
|
55
|
+
on the field line and may return `{ dispose }`. Specs are laid out after
|
|
56
|
+
anything drawn into `el`. A spec without a string `label`, with a `control`
|
|
57
|
+
that is not an Element, with neither `control` nor `render`, or whose
|
|
58
|
+
`render` throws is skipped with a warning (`logger.warn`, else
|
|
59
|
+
`console.warn`); the rest of the row is drawn. Any other return is a handle
|
|
60
|
+
as in 0.17.0 (`{ dispose }`), even one that has a `fields` property.
|
|
61
|
+
- `el` sits in the field column (`display: contents`), so rows appended to it
|
|
62
|
+
line up with the editor's; content drawn there any other way shows as
|
|
63
|
+
before. A returned `{ dispose }` is still disposed on every redraw.
|
|
64
|
+
|
|
65
|
+
The config the editor reads and writes is unchanged: no key was added, renamed
|
|
66
|
+
or defaulted differently, opening a series writes nothing, and each control
|
|
67
|
+
writes its own key only.
|
|
68
|
+
|
|
69
|
+
`tests/plot_config_editor.test.mjs` §12 pins it.
|
|
70
|
+
|
|
7
71
|
## An empty cell can name what is missing: `emptyMessage` as a function (0.17.12)
|
|
8
72
|
|
|
9
73
|
[`renderPlotGrid`](#a-plot-config-in-any-container-renderplotgrid-0170),
|
|
@@ -303,7 +367,7 @@ const editor = renderPlotConfigEditor(el, plotConfig, {
|
|
|
303
367
|
seriesProvider: ({ value, role, subplot }) => names(value), // items, or a promise of them
|
|
304
368
|
seriesTransforms, // their toggles, per subplot
|
|
305
369
|
overlaySources: () => mySources(), // optional
|
|
306
|
-
extraSeriesFields: (el, series, { subplot, axis, onChange }) => { /* your fields */ },
|
|
370
|
+
extraSeriesFields: (el, series, { subplot, axis, onChange, field }) => { /* your fields */ },
|
|
307
371
|
});
|
|
308
372
|
editor.dispose();
|
|
309
373
|
```
|
|
@@ -341,8 +405,11 @@ config it was given and then calls `onChange(config)`.
|
|
|
341
405
|
dash, width and mode. `renderPlotGrid` draws it from the data you pass under
|
|
342
406
|
the series' id in `overlays`. **None** removes `series.overlay`. Without
|
|
343
407
|
`overlaySources` there are no overlay controls.
|
|
344
|
-
- `extraSeriesFields(el, series, { subplot, axis, onChange })` puts fields of
|
|
345
|
-
your own in each series row; call `onChange()` after you edit the series.
|
|
408
|
+
- `extraSeriesFields(el, series, { subplot, axis, onChange, field })` puts fields of
|
|
409
|
+
your own in each series row; call `onChange()` after you edit the series.
|
|
410
|
+
Since 0.17.16 they can share the series' label column (`field`, or returned
|
|
411
|
+
field specs; see
|
|
412
|
+
[above](#a-plot-config-series-is-one-block-and-an-embedders-fields-share-it-01716)). What
|
|
346
413
|
it returns is disposed (`dispose()`) when the row is redrawn or the editor
|
|
347
414
|
is disposed. A `dispose()` that throws is reported to `logger.warn` and does
|
|
348
415
|
not stop the rest of the editor being disposed.
|
|
@@ -309,3 +309,64 @@ the caller's own DOM.
|
|
|
309
309
|
|
|
310
310
|
`tests/confirm_escaping.test.mjs` opens each of them in jsdom with hostile
|
|
311
311
|
names and labels.
|
|
312
|
+
|
|
313
|
+
## Windows: closing, focus and Esc (0.17.16)
|
|
314
|
+
|
|
315
|
+
Every dialog above is a `ManagedWindow`, and so is any window you build
|
|
316
|
+
yourself with `new ManagedWindow({ id, title, content, ... })`. Three things
|
|
317
|
+
changed for all of them (BUG-0053):
|
|
318
|
+
|
|
319
|
+
- **A closed window is gone.** `close()` (once `beforeClose` allows it) runs
|
|
320
|
+
`onClose`, removes the window from the registry and releases its content,
|
|
321
|
+
its DOM and every callback it was given. `ManagedWindow.get(id)` then answers
|
|
322
|
+
`null`, `ManagedWindow.all()` no longer lists it and `win.isClosed` is
|
|
323
|
+
`true`. A closed window cannot be shown again: `show()` on it warns and does
|
|
324
|
+
nothing. To reopen, build a new `ManagedWindow` with the same id; it restores
|
|
325
|
+
the geometry the last one saved under that id. Until 0.17.15 a closed window
|
|
326
|
+
stayed registered, holding its content and whatever its `onClose` captured,
|
|
327
|
+
until the same id opened again.
|
|
328
|
+
- **Saved geometry is bounded.** Position and size are still saved per id
|
|
329
|
+
under `flexdesk.managedWindows.v1` (or your `storageKey`), but only for the
|
|
330
|
+
50 most recently saved ids; older entries are dropped.
|
|
331
|
+
`ManagedWindow.configure({ maxSavedWindows })` changes the cap
|
|
332
|
+
(`DEFAULT_MAX_SAVED_WINDOWS` is 50). Entries saved by an earlier version
|
|
333
|
+
carry no use stamp: those under the window manager's minted ids
|
|
334
|
+
(`twm-mw-…`, which never open again) are purged when the entry is read, and
|
|
335
|
+
the others are stamped in the order they were stored, as older than anything
|
|
336
|
+
saved since, so your stable ids keep their geometry. A window whose id is
|
|
337
|
+
minted per opening passes `persistGeometry: false` and neither saves nor
|
|
338
|
+
restores geometry; the window manager's own windows do. Modal windows never
|
|
339
|
+
saved geometry and still do not.
|
|
340
|
+
- **Focus moves in and back.** With `focusOnOpen` (default `true`), opening a
|
|
341
|
+
window focuses the first element of its content that takes focus (a link,
|
|
342
|
+
button, input, select, textarea, `contenteditable` or `tabindex` element
|
|
343
|
+
that is not disabled or hidden), or the window itself when there is none.
|
|
344
|
+
`focusOnOpen: 'window'` always focuses the window itself, for a dialog whose
|
|
345
|
+
first button must not catch a stray Enter (`showRunProgressDialog` uses it,
|
|
346
|
+
so Stop is never focused). Closing gives focus back to the element that had
|
|
347
|
+
it before the window opened, if that element is still in the document and
|
|
348
|
+
focus is still in the window or nowhere (on `body`); a window closed while
|
|
349
|
+
the user works elsewhere does not pull focus back. Restoring a minimised
|
|
350
|
+
window is not an open and moves nothing. A dialog that focuses a field of
|
|
351
|
+
its own a frame later, as `openForm` and `openConfirm` do, still wins, and
|
|
352
|
+
`Wizard.openInWindow` renders its first step before it shows the window, so
|
|
353
|
+
focus lands on the step's first field. Pass `focusOnOpen: false` for a
|
|
354
|
+
window that must not take focus when it appears; `showComputingWindow`
|
|
355
|
+
does.
|
|
356
|
+
- **The window itself holds focus.** Every window element has
|
|
357
|
+
`tabindex="-1"`: never a Tab stop, but a press on its title bar or on empty
|
|
358
|
+
content moves focus into the window (when it was not there already), so the
|
|
359
|
+
window's Esc still works after the user clicked it.
|
|
360
|
+
- **Esc closes a non-modal window only from inside it.** Which window an Esc
|
|
361
|
+
closes:
|
|
362
|
+
1. a modal window on top, wherever focus is (as before);
|
|
363
|
+
2. otherwise the window holding focus, modal or not — so a modal under a
|
|
364
|
+
non-modal window still closes on an Esc from inside it;
|
|
365
|
+
3. otherwise (focus in no window) the topmost modal.
|
|
366
|
+
A non-modal window that does not hold focus never closes on Esc, so an Esc
|
|
367
|
+
meant for an editor beside it is left alone. An Esc something inside the
|
|
368
|
+
window already handled with `preventDefault()` (an autocomplete closing its
|
|
369
|
+
list) or one ending an IME composition (`isComposing`) closes nothing. One
|
|
370
|
+
Esc closes one window, even when the close hands focus to another window.
|
|
371
|
+
|
|
372
|
+
`tests/managed_window_lifecycle.test.mjs` holds all of these.
|
package/docs/components/menus.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **Package:** `@flexdesk/widgets` · [All docs](../README.md)
|
|
4
4
|
|
|
5
|
-
`createMenuBar` draws File / Edit / View menus from one declarative model, with pointer and keyboard behaviour and the WAI-ARIA menubar roles, and plugs into the shell. `ActionDropdown` draws a dropdown of action buttons from a trigger.
|
|
5
|
+
`createMenuBar` draws File / Edit / View menus from one declarative model, with pointer and keyboard behaviour and the WAI-ARIA menubar roles, and plugs into the shell. `ActionDropdown` draws a dropdown of action buttons from a trigger. `CategoryPicker` draws a categorised picker (search, categories, items, a detail pane) from a trigger.
|
|
6
6
|
|
|
7
7
|
## Context menus stay inside the window (0.17.9)
|
|
8
8
|
|
|
@@ -188,3 +188,87 @@ its right edge. Either flips to the other edge when the menu would leave the
|
|
|
188
188
|
window. A tile's ⋯ menu uses `'end'`
|
|
189
189
|
([Dashboard tiles](tiles.md#the--menu-is-drawn-over-the-page-0179)).
|
|
190
190
|
|
|
191
|
+
## A categorised picker: `CategoryPicker` (0.17.16)
|
|
192
|
+
|
|
193
|
+
An `ActionDropdown` is a flat list. When the choices fall into categories and
|
|
194
|
+
each needs a sentence (and perhaps a sample) before it is chosen, use
|
|
195
|
+
`CategoryPicker`: a search field over three columns — the categories (a
|
|
196
|
+
colour dot, the label, the item count), the active category's items (an icon
|
|
197
|
+
and the label), and a detail pane for the highlighted item (its title,
|
|
198
|
+
description, an optional monospace preview and an Add button).
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
import { CategoryPicker } from '@flexdesk/widgets';
|
|
202
|
+
|
|
203
|
+
const picker = new CategoryPicker({
|
|
204
|
+
trigger: addCellButton,
|
|
205
|
+
categories: [
|
|
206
|
+
{ id: 'text', label: 'Text', color: 'var(--accent-light)', items: [
|
|
207
|
+
{ id: 'heading', label: 'Heading', badge: 'H',
|
|
208
|
+
description: 'A section heading; it shapes the outline.',
|
|
209
|
+
preview: '## Debt dynamics' },
|
|
210
|
+
{ id: 'doc', label: 'Documentation', icon: 'description',
|
|
211
|
+
description: 'Markdown with LaTeX math.', keywords: ['markdown', 'latex'] },
|
|
212
|
+
] },
|
|
213
|
+
{ id: 'functions', label: 'Functions', color: 'var(--color-warning)', items: [
|
|
214
|
+
{ id: 'lag', label: 'Delay / Lag', badge: 'Δt',
|
|
215
|
+
description: 'The value of an expression some time ago.',
|
|
216
|
+
preview: 'expected = lag(inflation, 2)', keywords: 'delay history' },
|
|
217
|
+
] },
|
|
218
|
+
],
|
|
219
|
+
onSelect: (item, category) => insertCell(category.id, item.id),
|
|
220
|
+
placeholder: 'Search cells…', // the search field's placeholder (default 'Search…')
|
|
221
|
+
addLabel: 'Add cell', // the detail pane's button (default 'Add')
|
|
222
|
+
previewLabel: 'Writes', // a caption over the preview (none by default)
|
|
223
|
+
align: 'start', // or 'end', as ActionDropdown.position takes it
|
|
224
|
+
label: 'Add a cell', // the popover's accessible name (default 'Pick')
|
|
225
|
+
});
|
|
226
|
+
// The trigger's click toggles it; or drive it:
|
|
227
|
+
picker.open(); picker.close(); picker.toggle(); picker.position();
|
|
228
|
+
picker.setCategories(next); // finds the active category and the highlight again by id
|
|
229
|
+
picker.destroy();
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
An item has `id` and `label`, and optionally `icon` (a Material Symbols
|
|
233
|
+
ligature) or `badge` (a short text drawn as a monospace badge in the
|
|
234
|
+
category's colour), `description`, `preview` (text, shown monospace and
|
|
235
|
+
pre-wrapped) and `keywords` (an array or a string; searched, never shown). A
|
|
236
|
+
category's `color` is any CSS colour; it reaches the rows as
|
|
237
|
+
`--twm-category-picker-color`, and without one the dot and badge are muted.
|
|
238
|
+
`onSelect(item, category)` gets your own objects back.
|
|
239
|
+
|
|
240
|
+
- **Anchored like ActionDropdown.** It is appended to `body`, placed by
|
|
241
|
+
`ActionDropdown.position` (`position: fixed`, under the trigger when it fits,
|
|
242
|
+
flipped above it when it does not, kept 8px inside the window, capped to the
|
|
243
|
+
room there), and drawn on `--z-over-windows`, over the managed window it
|
|
244
|
+
opened from. Its columns scroll inside; the popover itself does not. The
|
|
245
|
+
trigger gets `aria-haspopup="dialog"`, `aria-expanded` and `twm-is-open`.
|
|
246
|
+
- **Search.** Focus is in the search field on open (the query starts empty).
|
|
247
|
+
Typing filters every category's items by label, description and keywords:
|
|
248
|
+
each word must appear, case-insensitively. While a query is set the items
|
|
249
|
+
column is one hit list grouped under the category labels, the counts are hit
|
|
250
|
+
counts (a category with none is dimmed), and the first hit is highlighted.
|
|
251
|
+
Clicking a category then jumps to its first hit.
|
|
252
|
+
- **Keyboard.** With no query, Up/Down move within the current column (the
|
|
253
|
+
categories, or the items) and wrap; Left/Right move between the two. With a
|
|
254
|
+
query, Up/Down walk the hit list across categories and Left/Right stay the
|
|
255
|
+
caret's. Enter picks the highlighted item. A printable key typed while a row
|
|
256
|
+
has focus goes to the search field. Tab and Shift+Tab stay inside: they
|
|
257
|
+
cycle the search field and Add (when it is enabled), as a modal
|
|
258
|
+
ManagedWindow's focus trap does. Keys an IME is composing with are left to
|
|
259
|
+
it. Escape closes the picker and nothing
|
|
260
|
+
else (it is caught first and stopped, so a ManagedWindow around it stays
|
|
261
|
+
open). Escape and a pick give focus back to the trigger.
|
|
262
|
+
- **Pointer.** Hovering or clicking an item shows it in the detail pane;
|
|
263
|
+
double-clicking it, or Add, picks it. A click outside closes the picker.
|
|
264
|
+
Pressing anywhere inside but the search field keeps focus where it is, so
|
|
265
|
+
the keyboard keeps working (the preview's text cannot be drag-selected).
|
|
266
|
+
- **Accessibility.** The popover is a named `role="dialog"`. The search field
|
|
267
|
+
is a combobox whose `aria-activedescendant` is the active category while the
|
|
268
|
+
categories column is current, else the highlighted item. While a query is
|
|
269
|
+
set, each category's hits are a `role="group"` named by its header.
|
|
270
|
+
|
|
271
|
+
The Add button is FlexDesk's `twm-btn twm-btn--primary`; everything else is
|
|
272
|
+
styled from the tokens (`tests/category_picker.test.mjs` checks there is no
|
|
273
|
+
colour literal in its rules, and that base.css and flexdesk.css carry the same
|
|
274
|
+
block).
|
package/docs/components/shell.md
CHANGED
|
@@ -170,6 +170,43 @@ a top-level page showed its name twice, in the title bar and in a one-segment
|
|
|
170
170
|
breadcrumb under it (STORY-0124). `renderBreadcrumb`, which draws segments a
|
|
171
171
|
content routes itself, is unchanged.
|
|
172
172
|
|
|
173
|
+
## A kind that lives in a window: `windowOnly` and `window` (0.17.16)
|
|
174
|
+
|
|
175
|
+
A kind can be a tool rather than a page: it opens in a managed window, never in
|
|
176
|
+
a tile, and it can say how big that window is.
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
const taxonomy = createTaxonomy({
|
|
180
|
+
root: 'start',
|
|
181
|
+
kinds: {
|
|
182
|
+
start: { label: 'Start', icon: 'apps' },
|
|
183
|
+
estimation: { label: 'Parameter Estimation', icon: 'tune',
|
|
184
|
+
single: true, windowOnly: true, window: { width: 1100, height: 700 } },
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
wm.navigate('estimation'); // a window, whatever `dest` says
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- **`window: { width?, height? }`** is the initial size, in px, of a window the
|
|
191
|
+
kind opens in, by `navigate(…, { dest: 'window' })` or, for a window-only
|
|
192
|
+
kind, always. ManagedWindow clamps it to the screen and to its minimum
|
|
193
|
+
(400 × 300), so a smaller size opens at the minimum. Without it a window
|
|
194
|
+
opens at ManagedWindow's default (600 × 400).
|
|
195
|
+
- **`windowOnly: true`** sends every open path to a window: `navigate` with
|
|
196
|
+
any `dest`, `openInPrimary`, `openInLeaf`, the tab verbs and
|
|
197
|
+
`splitLeafWith`, `navigateActiveTab` (crumbs, Backspace) and
|
|
198
|
+
`openInWindow`. With `single`, opening it again reveals the window that is
|
|
199
|
+
open. Its window never goes into a tile: it has no *Back to tile*, its
|
|
200
|
+
maximise button maximises it, and it does not snap onto a tile when dragged.
|
|
201
|
+
A restored layout that still holds a tab of the kind in a tile (or archived
|
|
202
|
+
under a page) drops it and warns once, naming the kinds; the rest of the
|
|
203
|
+
layout stays, and the next open makes a window. An entry of it in a tab's
|
|
204
|
+
back history (a layout saved before the kind was window-only) opens its
|
|
205
|
+
window when Backspace reaches it. Neither the root kind nor a top-nav
|
|
206
|
+
page can be window-only (`createTaxonomy` refuses).
|
|
207
|
+
- `taxonomy.isWindowOnly(kind)` and `taxonomy.windowSize(kind)` answer both
|
|
208
|
+
(STORY-0127).
|
|
209
|
+
|
|
173
210
|
## Buttons that change with the pane: `ctx.setChromeActions` (0.17.0)
|
|
174
211
|
|
|
175
212
|
A factory may return `chromeActions` — `[{icon, title, onClick}]`, drawn in the
|
package/docs/components/tiles.md
CHANGED
|
@@ -233,3 +233,40 @@ Until 0.17.8 the menu was `position: absolute` inside the tile, so in a tile
|
|
|
233
233
|
narrower than about 220px its left part and its labels were cut off
|
|
234
234
|
(BUG-0043). `tests/tile_menu_unclipped.test.mjs` holds it.
|
|
235
235
|
|
|
236
|
+
|
|
237
|
+
## A double-click on the title strip configures the tile (0.17.16)
|
|
238
|
+
|
|
239
|
+
A double-click on a tile's header (`.tile-header`: its title, its drag grip or
|
|
240
|
+
the strip between them) does what the cogwheel (`.tile-config-btn`) does: it
|
|
241
|
+
calls the same `_onConfigClick`, so the tile is selected and
|
|
242
|
+
`tile:config-request` goes out on the event bus and as a DOM event exactly as
|
|
243
|
+
for a click on the cogwheel. A double-click on one of the strip's buttons
|
|
244
|
+
(cogwheel, ⋯, info, expand, remove) or in an editable field you put in the
|
|
245
|
+
strip (`input`, `textarea`, `select`, `contenteditable`) does not. A read-only
|
|
246
|
+
tile has no cogwheel and opens nothing.
|
|
247
|
+
|
|
248
|
+
`new TileGrid({ configOnDoubleClick: false })` turns it off for every tile of
|
|
249
|
+
the grid; a tile takes the same option (`tile.configOnDoubleClick`, read at the
|
|
250
|
+
double-click). `tests/tile_title_dblclick_config.test.mjs` holds it.
|
|
251
|
+
|
|
252
|
+
## Several tiles marked as being configured: `grid.setConfiguring` (0.17.16)
|
|
253
|
+
|
|
254
|
+
A config request (cogwheel or double-click) highlights the requesting tile with
|
|
255
|
+
`tile--selected` and takes that highlight off every other tile of the grid:
|
|
256
|
+
one selection per grid, which suits one config panel. An embedder that keeps a
|
|
257
|
+
config window open per tile marks each of those tiles on the grid:
|
|
258
|
+
|
|
259
|
+
```js
|
|
260
|
+
grid.setConfiguring(tileId, true); // when its config window opens
|
|
261
|
+
grid.setConfiguring(tileId, false); // when its config window closes
|
|
262
|
+
grid.isConfiguring(tileId); // true while marked
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The mark is the class `tile--configuring`, drawn exactly like `tile--selected`
|
|
266
|
+
(same rules in `flexdesk.css`). It is the embedder's alone: no config request,
|
|
267
|
+
on that tile or another, clears it. The grid holds it **by tile id**, so a tile
|
|
268
|
+
that `setLayout` disposes and rebuilds (or an id added later) is marked again,
|
|
269
|
+
and clearing it after a rebuild reaches the live tile. `tile.setConfiguring(on)`
|
|
270
|
+
and `tile.isConfiguring` remain as conveniences: on a tile in a grid they go
|
|
271
|
+
through the grid. Do not add or remove `tile--selected` from outside; it
|
|
272
|
+
belongs to the config request.
|
package/package.json
CHANGED
|
@@ -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 })),
|