flexdesk 0.17.15 → 0.17.17
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 +42 -0
- package/css/base.css +236 -5
- package/css/flexdesk.css +326 -13
- package/css/tokens.css +3 -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-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 +326 -13
- package/dist/flow.js +3 -3
- package/dist/tiles.js +84 -9
- package/dist/tiles.js.map +2 -2
- package/dist/tokens.css +3 -0
- package/dist/widgets.js +4559 -4091
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +212 -40
- 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/notifications.md +23 -1
- package/docs/components/panels-and-desktops.md +22 -0
- package/docs/components/shell.md +91 -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 +66 -13
- package/src/tiling/shell.js +4 -1
- package/src/tiling/wm.js +205 -35
- 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/notification_history.js +11 -1
- package/src/ui/components/wizard.js +4 -1
- package/src/ui/notification_center.js +47 -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).
|
|
@@ -120,7 +120,8 @@ background, a left border in the severity's colour (`--color-success`,
|
|
|
120
120
|
`--color-warning`, `--color-danger`; info keeps the blue) and the entry
|
|
121
121
|
animation `twm-notification-in`, which the sheet now defines. The card sets
|
|
122
122
|
`pointer-events: auto` over the root's `none`, so its buttons and its
|
|
123
|
-
click-to-close work.
|
|
123
|
+
click-to-close work. Since [Copy message](#copy-message), click-to-close takes
|
|
124
|
+
only a click on the card outside its text.
|
|
124
125
|
|
|
125
126
|
Until 0.17.2 the card carried the bare `notification` / `notification--<severity>`
|
|
126
127
|
classes, which no FlexDesk sheet styled: the card had no background and no
|
|
@@ -156,6 +157,27 @@ Until 0.17.3 the bell counted only on bus events, so a toast raised through
|
|
|
156
157
|
was pushed as a second entry: a toast that timed out was in `history` twice
|
|
157
158
|
and counted twice. `tests/notification_history_follows_center.test.mjs` holds it.
|
|
158
159
|
|
|
160
|
+
### Copy message
|
|
161
|
+
|
|
162
|
+
Right-clicking a toast, or its entry in the `NotificationHistory` popover, opens
|
|
163
|
+
the context menu (`showContextMenu`) with **Copy message**. It copies the
|
|
164
|
+
notification's title, message and description, one per line and skipping any
|
|
165
|
+
that is empty, as plain text through `navigator.clipboard.writeText`. The menu
|
|
166
|
+
replaces the browser's native one there. The history popover stays open while
|
|
167
|
+
its menu is used. Every context menu draws on `--z-context-menu` (tokens.css),
|
|
168
|
+
over toasts (`--z-toast`) and managed windows, so the menu opens over the toast
|
|
169
|
+
that was right-clicked.
|
|
170
|
+
|
|
171
|
+
The text of a toast and of a history entry (title, message, description,
|
|
172
|
+
status) is selectable: the sheets give it `user-select: text` over the body's
|
|
173
|
+
`none`. A click or double-click on a toast's text (title, message,
|
|
174
|
+
description, status) selects it and does not close the toast, nor does a click
|
|
175
|
+
that ends a selection inside it. Only a click on the card outside the text (the
|
|
176
|
+
header's empty space, the padding, the timestamp) closes it, besides the ×
|
|
177
|
+
button. `notificationText(notification)` returns the copied
|
|
178
|
+
text, and `showNotificationMenu(event, notification, logger)` opens the menu.
|
|
179
|
+
Both are exported from `widgets`. `tests/notification_copy.test.mjs` covers it.
|
|
180
|
+
|
|
159
181
|
### Alerts with history: a log, a pane, a tab host and a bell (0.14.0)
|
|
160
182
|
|
|
161
183
|
A toast is the moment. The **alert log** keeps what came in until it is
|
|
@@ -10,6 +10,7 @@ The shell's left, right and bottom panels, and its virtual desktops. A kind says
|
|
|
10
10
|
- [Panels keep the size you dragged them to (0.17.0)](#panels-keep-the-size-you-dragged-them-to-0170)
|
|
11
11
|
- [A navigation panel you drive yourself: `mountNavPanel` (0.17.0)](#a-navigation-panel-you-drive-yourself-mountnavpanel-0170)
|
|
12
12
|
- [Your desktops beside the app's (0.10.0)](#your-desktops-beside-the-apps-0100)
|
|
13
|
+
- [A page that opens on the desktop you are on (0.17.17)](#a-page-that-opens-on-the-desktop-you-are-on-01717)
|
|
13
14
|
- [A saved layout that cannot be read (0.17.14)](#a-saved-layout-that-cannot-be-read-01714)
|
|
14
15
|
|
|
15
16
|
## Panels a page does not offer (0.17.0)
|
|
@@ -127,6 +128,27 @@ main pane; it used to replace that pane with the active tab alone. A target stil
|
|
|
127
128
|
holding only its seed is replaced, as before. `dispose()` removes the bar, so a
|
|
128
129
|
rebuilt shell paints a fresh one.
|
|
129
130
|
|
|
131
|
+
## A page that opens on the desktop you are on (0.17.17)
|
|
132
|
+
|
|
133
|
+
A desktop is the tiles and panels arranged on it. A kind marked `single: true`
|
|
134
|
+
exists once in the app, so opening it from another desktop switches to the
|
|
135
|
+
desktop that has it. A kind marked `single: 'desktop'` exists once per desktop
|
|
136
|
+
instead: the top-nav button, `navigate` and `openInPrimary` reveal the
|
|
137
|
+
instance on the active desktop, or open one there when that desktop has none,
|
|
138
|
+
and never switch desktop.
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
kinds: {
|
|
142
|
+
model: { label: 'Model', isTopNav: true, single: 'desktop' },
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
On desktop 2, clicking *Model* opens the model on desktop 2 even while
|
|
147
|
+
desktop 1 shows its own; clicking it again reveals desktop 2's. Each desktop's
|
|
148
|
+
instance is saved with its layout and restored there. The details, and what
|
|
149
|
+
counts as a window on a desktop, are in
|
|
150
|
+
[Shell and navigation](shell.md#a-kind-that-exists-once-per-desktop-single-desktop-01717).
|
|
151
|
+
|
|
130
152
|
## A saved layout that cannot be read (0.17.14)
|
|
131
153
|
|
|
132
154
|
The shell saves its layout through the host's `state` under the key `desktops`
|
package/docs/components/shell.md
CHANGED
|
@@ -10,6 +10,7 @@ A complete first boot is in the [quickstart](../quickstart.md#hello-shell). This
|
|
|
10
10
|
|
|
11
11
|
- [Opt-in shell features](#opt-in-shell-features)
|
|
12
12
|
- [A kind that exists once: `single` (0.17.0)](#a-kind-that-exists-once-single-0170)
|
|
13
|
+
- [A kind that exists once per desktop: `single: 'desktop'` (0.17.17)](#a-kind-that-exists-once-per-desktop-single-desktop-01717)
|
|
13
14
|
- [An open single instance takes new props: `navigate(props)` (0.17.2)](#an-open-single-instance-takes-new-props-navigateprops-0172)
|
|
14
15
|
- [A link to a kind nobody registered (0.17.2)](#a-link-to-a-kind-nobody-registered-0172)
|
|
15
16
|
- [A pane that names what it shows: `ctx.setTitle` (0.12.1)](#a-pane-that-names-what-it-shows-ctxsettitle-0121)
|
|
@@ -74,6 +75,59 @@ with it. The taxonomy answers `isSingle(kind)`, `instanceKey(kind, props)` and
|
|
|
74
75
|
`singleReason(kind)`; the window manager answers `isSingleKind(kind)`,
|
|
75
76
|
`findInstance(kind, props)` and `revealInstance(kind, props)`.
|
|
76
77
|
|
|
78
|
+
## A kind that exists once per desktop: `single: 'desktop'` (0.17.17)
|
|
79
|
+
|
|
80
|
+
A virtual desktop is the tiles and panels the user arranged on it, so a page
|
|
81
|
+
the user opens from the top nav belongs on the desktop they are looking at,
|
|
82
|
+
not on another desktop that already shows it. Mark such a kind
|
|
83
|
+
`single: 'desktop'`: at most one instance per desktop.
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
createTaxonomy({
|
|
87
|
+
root: 'cosmos',
|
|
88
|
+
kinds: {
|
|
89
|
+
cosmos: { label: 'Cosmos', isTopNav: true },
|
|
90
|
+
chart: { label: 'Star chart', isTopNav: true, single: 'desktop' }, // one per desktop
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- **Every open path looks only at the active desktop** — `navigate`, the top
|
|
96
|
+
nav, `openInPrimary`, `openInTab*`, `splitLeafWith`, Back. When the desktop
|
|
97
|
+
has the instance (in a tile, in a top-nav page a tile switched away from, or
|
|
98
|
+
in a window on it) it is revealed there, as for `single: true`; when it has
|
|
99
|
+
none, one is opened there. The open never switches desktop, and the
|
|
100
|
+
instances on other desktops are left as they are.
|
|
101
|
+
- **A window is on the desktop it was opened on** or moved to. A window
|
|
102
|
+
contained to a pane (`promoteInPlace`) is shown on that desktop only; a
|
|
103
|
+
free-floating one (`navigate(kind, props, { dest: 'window' })`) is drawn over
|
|
104
|
+
every desktop but is still the instance of the one it was opened on, never
|
|
105
|
+
of another: on desktop 1, *Model* reveals desktop 1's own model even while
|
|
106
|
+
desktop 2's floats in front of it.
|
|
107
|
+
- **The copy verbs refuse** within the desktop, as for `single: true`.
|
|
108
|
+
- **The move verbs refuse** to carry the kind onto a desktop that already holds
|
|
109
|
+
it: *Move to desktop N* (Alt+Shift+N and the tile and window menus, whose row
|
|
110
|
+
is greyed with the reason), and dropping a floating window onto another
|
|
111
|
+
desktop's tile. `wm.desktopMoveRefusal({ leafId } | { windowId }, idx)`
|
|
112
|
+
answers that reason, or null. Removing a desktop brings its windows across
|
|
113
|
+
to the active desktop and never closes one, so that desktop can then hold
|
|
114
|
+
the kind twice, until a reload keeps the first.
|
|
115
|
+
- **Navigating inside a window looks on the window's own desktop**
|
|
116
|
+
(`openInWindow`, and the in-window navigation that goes through it): a
|
|
117
|
+
floating window of desktop 1 navigated to the kind while desktop 2 is on
|
|
118
|
+
screen reveals desktop 1's instance, and shows the kind itself when desktop
|
|
119
|
+
1 has none. The window stays on its desktop.
|
|
120
|
+
- **A page restore and a restored layout dedupe per desktop**: a tile's archived
|
|
121
|
+
page brings its instance back unless that desktop has it live elsewhere, and
|
|
122
|
+
a reload keeps the first instance on each desktop. Each desktop's instance
|
|
123
|
+
survives, live or archived.
|
|
124
|
+
|
|
125
|
+
`single: true` keeps its meaning: one instance in the app, and opening it
|
|
126
|
+
switches to the desktop it is on. Because `'desktop'` names this mode, it is
|
|
127
|
+
not read as a prop name. `findInstance(kind)` answers the active desktop's
|
|
128
|
+
instance; the taxonomy answers `isSinglePerDesktop(kind)`, and
|
|
129
|
+
`instanceKey(kind, props)` is the same for every props.
|
|
130
|
+
|
|
77
131
|
## An open single instance takes new props: `navigate(props)` (0.17.2)
|
|
78
132
|
|
|
79
133
|
Revealing a `single` instance used to drop the props the open asked for: the
|
|
@@ -170,6 +224,43 @@ a top-level page showed its name twice, in the title bar and in a one-segment
|
|
|
170
224
|
breadcrumb under it (STORY-0124). `renderBreadcrumb`, which draws segments a
|
|
171
225
|
content routes itself, is unchanged.
|
|
172
226
|
|
|
227
|
+
## A kind that lives in a window: `windowOnly` and `window` (0.17.16)
|
|
228
|
+
|
|
229
|
+
A kind can be a tool rather than a page: it opens in a managed window, never in
|
|
230
|
+
a tile, and it can say how big that window is.
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
const taxonomy = createTaxonomy({
|
|
234
|
+
root: 'start',
|
|
235
|
+
kinds: {
|
|
236
|
+
start: { label: 'Start', icon: 'apps' },
|
|
237
|
+
estimation: { label: 'Parameter Estimation', icon: 'tune',
|
|
238
|
+
single: true, windowOnly: true, window: { width: 1100, height: 700 } },
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
wm.navigate('estimation'); // a window, whatever `dest` says
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
- **`window: { width?, height? }`** is the initial size, in px, of a window the
|
|
245
|
+
kind opens in, by `navigate(…, { dest: 'window' })` or, for a window-only
|
|
246
|
+
kind, always. ManagedWindow clamps it to the screen and to its minimum
|
|
247
|
+
(400 × 300), so a smaller size opens at the minimum. Without it a window
|
|
248
|
+
opens at ManagedWindow's default (600 × 400).
|
|
249
|
+
- **`windowOnly: true`** sends every open path to a window: `navigate` with
|
|
250
|
+
any `dest`, `openInPrimary`, `openInLeaf`, the tab verbs and
|
|
251
|
+
`splitLeafWith`, `navigateActiveTab` (crumbs, Backspace) and
|
|
252
|
+
`openInWindow`. With `single`, opening it again reveals the window that is
|
|
253
|
+
open. Its window never goes into a tile: it has no *Back to tile*, its
|
|
254
|
+
maximise button maximises it, and it does not snap onto a tile when dragged.
|
|
255
|
+
A restored layout that still holds a tab of the kind in a tile (or archived
|
|
256
|
+
under a page) drops it and warns once, naming the kinds; the rest of the
|
|
257
|
+
layout stays, and the next open makes a window. An entry of it in a tab's
|
|
258
|
+
back history (a layout saved before the kind was window-only) opens its
|
|
259
|
+
window when Backspace reaches it. Neither the root kind nor a top-nav
|
|
260
|
+
page can be window-only (`createTaxonomy` refuses).
|
|
261
|
+
- `taxonomy.isWindowOnly(kind)` and `taxonomy.windowSize(kind)` answer both
|
|
262
|
+
(STORY-0127).
|
|
263
|
+
|
|
173
264
|
## Buttons that change with the pane: `ctx.setChromeActions` (0.17.0)
|
|
174
265
|
|
|
175
266
|
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.
|