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.
Files changed (49) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/css/base.css +236 -5
  3. package/css/flexdesk.css +326 -13
  4. package/css/tokens.css +3 -0
  5. package/dist/charts.js +104 -34
  6. package/dist/charts.js.map +3 -3
  7. package/dist/{chunk-2PM6ZMNS.js → chunk-2Z2ENHR3.js} +2 -2
  8. package/dist/{chunk-AD7FIA3Z.js → chunk-7JI7HHSY.js} +2 -2
  9. package/dist/{chunk-TLIV6QSU.js → chunk-7OL6F2AM.js} +3 -3
  10. package/dist/{chunk-MADMGRX2.js → chunk-PGZ4EHSE.js} +156 -9
  11. package/dist/chunk-PGZ4EHSE.js.map +7 -0
  12. package/dist/{chunk-OEG6NPAN.js → chunk-XJNADRGR.js} +2 -2
  13. package/dist/flexdesk.css +326 -13
  14. package/dist/flow.js +3 -3
  15. package/dist/tiles.js +84 -9
  16. package/dist/tiles.js.map +2 -2
  17. package/dist/tokens.css +3 -0
  18. package/dist/widgets.js +4559 -4091
  19. package/dist/widgets.js.map +4 -4
  20. package/dist/wm.js +212 -40
  21. package/dist/wm.js.map +2 -2
  22. package/docs/README.md +1 -1
  23. package/docs/components/charts.md +70 -3
  24. package/docs/components/forms-and-dialogs.md +61 -0
  25. package/docs/components/menus.md +85 -1
  26. package/docs/components/notifications.md +23 -1
  27. package/docs/components/panels-and-desktops.md +22 -0
  28. package/docs/components/shell.md +91 -0
  29. package/docs/components/tiles.md +37 -0
  30. package/package.json +1 -1
  31. package/src/charts/plot_config_editor.js +125 -29
  32. package/src/tiles/tile_base.js +48 -2
  33. package/src/tiles/tile_grid.js +42 -1
  34. package/src/tiling/kind_taxonomy.js +66 -13
  35. package/src/tiling/shell.js +4 -1
  36. package/src/tiling/wm.js +205 -35
  37. package/src/ui/components/category_picker.js +556 -0
  38. package/src/ui/components/computing_status_window.js +3 -0
  39. package/src/ui/components/confirm_dialog.js +3 -0
  40. package/src/ui/components/managed_window.js +204 -9
  41. package/src/ui/components/notification_history.js +11 -1
  42. package/src/ui/components/wizard.js +4 -1
  43. package/src/ui/notification_center.js +47 -1
  44. package/widgets.js +2 -1
  45. package/dist/chunk-MADMGRX2.js.map +0 -7
  46. /package/dist/{chunk-2PM6ZMNS.js.map → chunk-2Z2ENHR3.js.map} +0 -0
  47. /package/dist/{chunk-AD7FIA3Z.js.map → chunk-7JI7HHSY.js.map} +0 -0
  48. /package/dist/{chunk-TLIV6QSU.js.map → chunk-7OL6F2AM.js.map} +0 -0
  49. /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 `ActionDropdown`.
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. What
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.
@@ -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`
@@ -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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flexdesk",
3
- "version": "0.17.15",
3
+ "version": "0.17.17",
4
4
  "description": "A tiling window manager and widget set for desktop-class web applications. Zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {