flexdesk 0.17.3 → 0.17.4

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 (47) hide show
  1. package/CHANGELOG.md +86 -65
  2. package/README.md +27 -4544
  3. package/css/base.css +18 -1
  4. package/css/flexdesk.css +18 -1
  5. package/dist/{chunk-4A7SJC3T.js → chunk-RKNFBX5K.js} +15 -4
  6. package/dist/{chunk-4A7SJC3T.js.map → chunk-RKNFBX5K.js.map} +2 -2
  7. package/dist/flexdesk.css +18 -1
  8. package/dist/tiles.js +1 -1
  9. package/dist/widgets.js +14 -2
  10. package/dist/widgets.js.map +2 -2
  11. package/docs/README.md +97 -0
  12. package/docs/best-practices.md +45 -0
  13. package/docs/components/charts.md +211 -0
  14. package/docs/components/command-palette.md +102 -0
  15. package/docs/components/conversation.md +72 -0
  16. package/docs/components/data-table-grid.md +328 -0
  17. package/docs/components/data-table.md +596 -0
  18. package/docs/components/editor.md +22 -0
  19. package/docs/components/flow-canvas-editor.md +482 -0
  20. package/docs/components/flow-editors.md +176 -0
  21. package/docs/components/flow-lane-editor.md +165 -0
  22. package/docs/components/flow-outline-editor.md +176 -0
  23. package/docs/components/forms-and-dialogs.md +294 -0
  24. package/docs/components/input-fields.md +72 -0
  25. package/docs/components/keymap.md +143 -0
  26. package/docs/components/layout-widgets.md +163 -0
  27. package/docs/components/markdown-and-outline.md +106 -0
  28. package/docs/components/menus.md +86 -0
  29. package/docs/components/notifications.md +194 -0
  30. package/docs/components/panels-and-desktops.md +127 -0
  31. package/docs/components/record-list-and-fact-table.md +128 -0
  32. package/docs/components/settings-view.md +112 -0
  33. package/docs/components/shell.md +189 -0
  34. package/docs/components/slide-out-panel.md +68 -0
  35. package/docs/components/tiles.md +210 -0
  36. package/docs/components/wizard.md +54 -0
  37. package/docs/concepts/building-and-packaging.md +33 -0
  38. package/docs/concepts/domain-neutrality.md +110 -0
  39. package/docs/concepts/host-port.md +137 -0
  40. package/docs/concepts/packages-and-entry-points.md +32 -0
  41. package/docs/concepts/peer-dependencies.md +42 -0
  42. package/docs/concepts/state-guard.md +58 -0
  43. package/docs/concepts/styling.md +47 -0
  44. package/docs/quickstart.md +179 -0
  45. package/package.json +2 -1
  46. package/src/ui/components/slide_out_panel.js +14 -3
  47. package/src/ui/utils/auto_scrollbars.js +16 -2
package/docs/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # FlexDesk documentation
2
+
3
+ New here? Start with the [quickstart](quickstart.md). The [README](../README.md) is the one-page overview.
4
+
5
+ - **[Components](#components).** One page per component: what it is, how to use it, and every option.
6
+ - **[Concepts](#concepts).** The ideas the components share: the host port, packages, naming, theming.
7
+ - **[Best practices](best-practices.md).** The rules the reference states, gathered in one place.
8
+
9
+ ## Packages
10
+
11
+ FlexDesk is nine entry points that share their code, so you load only what you use. [Packages and entry points](concepts/packages-and-entry-points.md) has the sizes and the reasoning.
12
+
13
+ | Entry | What is in it | Read |
14
+ |---|---|---|
15
+ | `@flexdesk/core` | Event bus, settings store, logging, state machine, state guard | [State guard](concepts/state-guard.md), [Settings page](components/settings-view.md), [Storage keys and names](concepts/domain-neutrality.md#storage-keys-and-names-are-yours-0170) |
16
+ | `@flexdesk/host` | The host port | [Host port](concepts/host-port.md) |
17
+ | `@flexdesk/wm` | Tiling window manager: tabs, splits, desktops, command palette, `createShell()` | [Shell and navigation](components/shell.md) and the three pages after it |
18
+ | `@flexdesk/widgets` | DataTable, forms, menus, tabs, toasts, panels and more | [Tables and records](#tables-and-records), [Forms, inputs and menus](#forms-inputs-and-menus), [Layout, content and feedback](#layout-content-and-feedback) |
19
+ | `@flexdesk/charts` | Plotly wrapper, plot grid and config editor, plot windows | [Charts](components/charts.md) |
20
+ | `@flexdesk/tiles` | Draggable, resizable widget grid and its tiles | [Dashboard tiles](components/tiles.md) |
21
+ | `@flexdesk/editor` | Monaco loader, editor factory, tab bar, search, split panes | [Editor](components/editor.md) |
22
+ | `@flexdesk/canvas` | The canvas kernel: a pannable, zoomable surface, the edge router, a graph layer | [Flow canvas editor](components/flow-canvas-editor.md) |
23
+ | `@flexdesk/flow` | The flow-editor kit and the canvas, outline and lane editors | [Flow editors and the flow kit](components/flow-editors.md) |
24
+
25
+ ## Components
26
+
27
+ ### Window manager
28
+
29
+ `@flexdesk/wm`
30
+
31
+ - [Shell and navigation](components/shell.md): `createShell`, kinds, navigating between them, what a pane tells its chrome.
32
+ - [Panels and desktops](components/panels-and-desktops.md): which panels a page offers, panel sizes, `mountNavPanel`, the desktop bar.
33
+ - [Command palette](components/command-palette.md): grouped results, drill-down, disabled rows, prefix-only sources.
34
+ - [Keys, Back and tab layout](components/keymap.md): rebinding the window manager's keys, Back and the breadcrumb, zoom, tab layout.
35
+
36
+ ### Tables and records
37
+
38
+ `@flexdesk/widgets`
39
+
40
+ - [DataTable](components/data-table.md): sorting, filtering, paging, rows from a server, row details, CSV export.
41
+ - [DataTable as a grid](components/data-table-grid.md): editable cells, staged edits, fill handle, paste from Excel, column groups, crosstabs.
42
+ - [Record lists and fact sheets](components/record-list-and-fact-table.md): the parts of a page that are not a table of rows.
43
+
44
+ ### Forms, inputs and menus
45
+
46
+ `@flexdesk/widgets`
47
+
48
+ - [Forms and dialogs](components/forms-and-dialogs.md): `openForm`, `openConfirm`, and every field type.
49
+ - [Settings page](components/settings-view.md): a settings store's schema drawn as a page.
50
+ - [Wizard](components/wizard.md): a multi-step flow, inline or in a window.
51
+ - [Input fields: slider and tags](components/input-fields.md): `SliderField` and `createTagInput`.
52
+ - [Menus](components/menus.md): `createMenuBar` and `ActionDropdown`.
53
+
54
+ ### Layout, content and feedback
55
+
56
+ `@flexdesk/widgets`
57
+
58
+ - [Tabs, section stacks and scrollbars](components/layout-widgets.md): `createTabs`, `createSectionStack`, `installAutoScrollbars`.
59
+ - [Slide-out panel](components/slide-out-panel.md): `SlideOutPanel`, an inline panel that slides in over its host.
60
+ - [Markdown and document outline](components/markdown-and-outline.md): `renderMarkdown` and `createDocumentOutline`.
61
+ - [Conversation view](components/conversation.md): a transcript above a composer.
62
+ - [Notifications and progress](components/notifications.md): toasts, history, the alert log, progress bars, lists and phases.
63
+
64
+ ### Charts, editor and dashboards
65
+
66
+ - [Charts](components/charts.md): `@flexdesk/charts`, on Plotly.
67
+ - [Editor](components/editor.md): `@flexdesk/editor`, on Monaco.
68
+ - [Dashboard tiles](components/tiles.md): `@flexdesk/tiles`, a draggable widget grid.
69
+
70
+ ### Flow editors
71
+
72
+ `@flexdesk/flow`, `@flexdesk/canvas`
73
+
74
+ - [Flow editors and the flow kit](components/flow-editors.md): what the three editors share.
75
+ - [Flow canvas editor](components/flow-canvas-editor.md): nodes and lines, and how to build a modeler.
76
+ - [Flow outline editor](components/flow-outline-editor.md): the graph drawn as a list of blocks.
77
+ - [Flow lane editor](components/flow-lane-editor.md): data flows as lanes, left to right.
78
+
79
+ ### Not yet documented
80
+
81
+ These have no page of their own: `ManagedWindow`, modals, `TreeView`, context menus, tooltips, the help modal, the window taskbar, and `@flexdesk/core`'s event bus, logging service and state machine. Where another page mentions one, it is in passing. Their sources are under `src/ui/components/`, `src/help/` and `src/core/`.
82
+
83
+ ## Concepts
84
+
85
+ - [Packages and entry points](concepts/packages-and-entry-points.md): the nine entries, what they weigh, and why they are separate.
86
+ - [Host port](concepts/host-port.md): how FlexDesk reaches storage, dialogs and native windows without touching them.
87
+ - [Domain neutrality](concepts/domain-neutrality.md): storage keys, theme names and MIME types are yours; no application globals.
88
+ - [State guard](concepts/state-guard.md): who wrote that `data-*` attribute.
89
+ - [Peer dependencies](concepts/peer-dependencies.md): Plotly and Monaco, and why there is no default path.
90
+ - [Theming and CSS](concepts/styling.md): the three stylesheets and the class-name rules.
91
+ - [Build and packaging](concepts/building-and-packaging.md): why `dist/` is committed, and the `exports` map.
92
+
93
+ ## Writing these docs
94
+
95
+ - A feature is documented in a section of the page for its component. The section's heading carries the release that introduced it, as in `A menu bar: createMenuBar (0.17.0)`.
96
+ - The [changelog](../CHANGELOG.md) links to those sections by their heading, so **do not rename a heading that has shipped**. If one must change, update the changelog link in the same commit.
97
+ - Some test suites read these pages and assert on specific sections, headings and tables. After editing them, run `node tests/run.mjs`, or the suite for the page you touched. `grep -l "docs/" tests/*.mjs` lists them.
@@ -0,0 +1,45 @@
1
+ # Best practices
2
+
3
+ The rules the reference pages state, gathered in one place. Each item says what to do and why, and links to the page that explains it.
4
+
5
+ ## Wiring FlexDesk into an application
6
+
7
+ **1. Describe your domain in the taxonomy, and nowhere else.** Your kinds, their ancestors, which are single-instance and which panels each page offers are all declared once, in `createTaxonomy`. FlexDesk has no ontology of its own, and which panels a page offers is "said once, in the taxonomy". Likewise hand the shell elements, not selectors: the library never guesses at your markup. See [kinds that exist once](components/shell.md#a-kind-that-exists-once-single-0170), [panels a page does not offer](components/panels-and-desktops.md#panels-a-page-does-not-offer-0170) and the [quickstart](quickstart.md#hello-shell).
8
+
9
+ **2. Start with the smallest host.** Pass `createHost` only the capabilities you have. A capability you leave out is absent and the library degrades instead of throwing: no native window buttons without `window`, a Blob download for CSV without `dialogs`. An in-memory `Map` is a perfectly good `state`, and `NULL_HOST` is a valid host with nothing behind it. See [the host port](concepts/host-port.md#the-host-port).
10
+
11
+ **3. Load every entry from one copy.** The entries share code through chunks, so importing several does not give you several event buses or several copies of a module-level singleton. Keep it that way: map all entries to the same install or the same import map, rather than mixing a vendored `dist/` with a second bundled copy. See [packages and entry points](concepts/packages-and-entry-points.md#nine-entry-points).
12
+
13
+ **4. Name your storage before anything reads it.** Every key FlexDesk writes is an option, with a generic default. Set them at boot, before the first read. Tile layouts are the sharp edge: the store is a singleton, and one built before you supply your key uses the default, so every layout your users saved becomes silently invisible, with no error. If users already have data under older keys, pass those keys explicitly and nothing is lost. See [storage keys and names](concepts/domain-neutrality.md#storage-keys-and-names-are-yours-0170) and [layouts are keyed by whatever you key them by](components/tiles.md#layouts-are-keyed-by-whatever-you-key-them-by).
14
+
15
+ **5. Set the peer-dependency paths yourself, and show the failure.** Plotly and Monaco are not bundled and their locations are not guessed. `initMonaco()` and `getEditorFactory()` reject when no path is set or loading fails, so catch that and show a message instead of waiting for an editor that never appears. A failure in Monaco's own loader is final for the page: fix the path and reload. If your page already loads Plotly, you can skip `setPlotlySource`. See [peer dependencies](concepts/peer-dependencies.md#peer-dependencies).
16
+
17
+ **6. Adopt new options one at a time.** Options are opt-in across the library. Most sections of the reference say what a component that sets none of the options does, and the answer is that it behaves as it did in the previous release. Turn a feature on where you need it and leave the rest alone.
18
+
19
+ ## Building screens
20
+
21
+ **7. Give a table values, not formatted text.** Pass `rows` as values and declare `columnTypes` (`'date'`, `'datetime'`, `'duration'`) so sorting and filtering work on the value, then format for display with `dateFormat`, `durationFormat` and `nullDisplay`. See [DataTable options in 0.5.0](components/data-table.md#datatable-options-in-050-lists-that-need-no-help).
22
+
23
+ **8. Set house rules once.** `DataTable.withDefaults({ … })` returns a subclass whose defaults sit under every config it is given, so each list is one `mount` call.
24
+
25
+ **9. Say how long a table lives.** `autoDispose: true` disposes a table that has left the document, which suits lists you rebuild. Do not set it on a table you detach and attach again later, such as a cached tab: a disposed table comes back empty. For that case add `keepWhileDetached`. See [DataTable options in 0.5.0](components/data-table.md#datatable-options-in-050-lists-that-need-no-help) and [0.7.0](components/data-table.md#datatable-options-in-070-details-rows-from-a-server-and-filters-that-are-not-text).
26
+
27
+ **10. Let the server be the server.** For large or live data, use `dataSource` instead of fetching and slicing in your code, and give the table a `getRowKey`: without it a row's index is only its place in the page drawn, so an open detail survives a new answer only where the same row sits at the same place. Only the newest answer is drawn. When a filter of your own, outside the table, changes, call `reload()`. See [paging](components/data-table.md#a-servers-rows-in-pages-or-as-one-list-paging) and [dataSource](components/data-table.md#datatable-options-in-070-details-rows-from-a-server-and-filters-that-are-not-text).
28
+
29
+ **11. Pass text as text.** A confirm's message and labels, and a progress bar's names and labels, are set as text, so `<` shows as `<`. Do not build HTML strings for them. See [confirms](components/forms-and-dialogs.md#confirms-show-text-not-markup-0170) and [progress](components/notifications.md#progress-bars-lists-and-phases-0170).
30
+
31
+ **12. Leave a key to the component that handled it.** The keymap skips any keydown that is already `defaultPrevented`. A list that deletes a row on Backspace calls `preventDefault()`, and the window manager does not also navigate back. To take a chord away from the window manager altogether, set it to `false` in `keymap.bindings`. See [keys](components/keymap.md#your-keys-not-the-window-managers-keymap-0170).
32
+
33
+ **13. Keep I/O out of the editors.** The flow editors never fetch, never save and never start a timer that writes. Step types, reference grammar, findings and all I/O are yours, passed in as options and returned through callbacks. See [flow editors](components/flow-editors.md#flow-editors).
34
+
35
+ ## Looking right, staying right
36
+
37
+ **14. Restyle through the tokens.** Every rule reads its values from `tokens.css`, so swapping that one file restyles the shell. Library classes are `twm-` prefixed, and that prefix is stable; the editor components and the help modal keep the class names they were extracted with. Supply the icon font, or accept that icons show their text names. See [theming and CSS](concepts/styling.md#styling).
38
+
39
+ **15. Run the state guard strictly in your own code.** `StateGuardService` defaults to `mode: 'warn'`. FlexDesk's own widgets run clean under `'strict'`, so choose strict for your code and add your application's keys on top of `DEFAULT_STATE_GUARD_CONFIG`. See [state guard](concepts/state-guard.md#the-state-guard-who-wrote-that-attribute-0170) and [the default allow-list](concepts/state-guard.md#the-defaults-cover-flexdesks-own-keys-0170).
40
+
41
+ **16. Dispose what you create.** `shell.dispose()` removes the `?` shortcut and the desktop bar along with the rest of the shell. A `NotificationCenter`'s `dispose()` hands the toast helpers back. Menu bars, tables and lists have `dispose()` too.
42
+
43
+ ## One page, browser and native window
44
+
45
+ **17. Call the native helpers unconditionally.** The same app runs in a browser tab, a native window with the OS frame, and a native window with its own chrome, and the page needs no switch. `routeExternalLinks({ host })` is a no-op without `host.shell`. `WindowChromeController` asks `host.window.chrome()` and draws handles and buttons only for a custom frame. `suppressNativeContextMenu()` leaves the native menu in editable and selected text. See [links out](concepts/host-port.md#links-out-and-the-browsers-right-click-menu-0160) and [a window with its own chrome](concepts/host-port.md#a-window-with-its-own-chrome-0150).
@@ -0,0 +1,211 @@
1
+ # Charts
2
+
3
+ > **Package:** `@flexdesk/charts` · **Needs:** Plotly, as a [peer dependency](../concepts/peer-dependencies.md) · [All docs](../README.md)
4
+
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
+
7
+ ## Derived series: `seriesTransforms` (0.17.0)
8
+
9
+ A plot can draw series worked out from its own: a trend, a moving average, a
10
+ decomposition. `@flexdesk/charts` ships none; you pass them to
11
+ `PlotPopoutWindow` (or `openPlotPopoutWindow`) as `seriesTransforms`:
12
+
13
+ ```js
14
+ import { openPlotPopoutWindow, renderSeriesTransformControls } from '@flexdesk/charts';
15
+
16
+ const seriesTransforms = {
17
+ hp: {
18
+ label: 'HP filter',
19
+ params: { lambda: { label: 'Lambda', default: 1600, min: 1, step: 100 } },
20
+ outputs: {
21
+ trend: { label: 'Trend', suffix: ' (HP Trend)', lineStyle: 'dashed' },
22
+ cycle: { label: 'Cycle', suffix: ' (HP Cycle)', lineStyle: 'dotted', lineWidth: 1 },
23
+ },
24
+ run: (y, { lambda }) => myBackend.hpFilter(y, lambda), // -> { trend: [...], cycle: [...] }
25
+ },
26
+ };
27
+
28
+ openPlotPopoutWindow({ id, title, plotConfig, getResults, services, seriesTransforms,
29
+ configRenderer: (el) => renderSeriesTransformControls(el, plotConfig.subplots[0],
30
+ seriesTransforms, { onChange: redraw }) });
31
+ ```
32
+
33
+ What a subplot switches on is in the subplot:
34
+ `subplot.transforms = { hp: { outputs: { trend: true }, params: { lambda: 100 } } }`.
35
+ For every transform with an output switched on, `run(y, params)` is called
36
+ once per line or scatter trace of the subplot (params: the declared defaults
37
+ under the subplot's own), and each switched-on output it returns is drawn as
38
+ an extra trace in the source trace's colour, on its axis, named
39
+ `<source name><suffix>`, with the output's `lineStyle` and `lineWidth`
40
+ (default 2). A transform that throws or rejects is skipped for that trace,
41
+ reported to `services.logger.warn`, and the figure is drawn without it.
42
+ `deriveSeriesTraces(traces, subplot, seriesTransforms, { onError })` is that
43
+ step on its own, for a renderer of your own.
44
+
45
+ `renderSeriesTransformControls(container, subplot, seriesTransforms, { onChange })`
46
+ renders, per transform, a toggle for each output and a number field for each
47
+ parameter (disabled while no output is on); it edits `subplot.transforms` in
48
+ place and returns `{ dispose }`. With no transforms it renders nothing.
49
+
50
+ Until 0.17.0 the plot window called one application's backend for a
51
+ Hodrick-Prescott filter, and every default subplot carried `showHpTrend`,
52
+ `showHpCycle` and `hpLambda`. Those keys are gone and are ignored in saved
53
+ configs; without `seriesTransforms` no derived trace is drawn.
54
+ `tests/series_transforms.test.mjs` pins all of it.
55
+
56
+ ## A plot config in any container: `renderPlotGrid` (0.17.0)
57
+
58
+ `PlotPopoutWindow` used to keep its subplot renderer private, so anything else
59
+ that showed the same plot config (a notebook cell, a dashboard tile) had to
60
+ copy it. It is now exported, and the window is one of its callers:
61
+
62
+ ```js
63
+ import { renderPlotGrid, migrateData } from '@flexdesk/charts';
64
+
65
+ const grid = renderPlotGrid(el, migrateData(savedConfig), { time, series }, {
66
+ overlays, // { [series.id]: { years, values } } for series with an `overlay`
67
+ seriesTransforms, // as above
68
+ hiddenSeries: new Set(), // trace names not to draw
69
+ });
70
+ await grid.update({ results: next }); // any of plotConfig, results, overlays, hiddenSeries
71
+ grid.resize();
72
+ grid.dispose();
73
+ ```
74
+
75
+ It lays a `.twm-plot-grid` out from `PLOT_LAYOUTS` (a cell per slot, a
76
+ `.twm-plot-grid__cell` in that slot's grid area) and draws one figure per
77
+ subplot: its chart type, interpolation, data points and stack groups, every
78
+ y-axis (left or right, linear or log, min, max, step), reference lines and
79
+ shaded bands, the NaN strategy, the x-axis clipped to the results' time
80
+ range, or an x variable instead of time (`xAxis: { useTime: false, variable }`).
81
+ A subplot with nothing to draw shows `emptyMessage` (default `No data yet.`); a
82
+ slot with no subplot shows `noSubplotMessage` (`No subplot configured.`); null
83
+ results empty every subplot. A cell that is detached or has no size is not
84
+ drawn: the grid's own ResizeObserver draws it once it has one. `dispose()`
85
+ purges every figure and removes the grid; `grid.charts` is the cells, for
86
+ Plotly calls of your own (relayout, toImage), and `grid.plotted` is the cells
87
+ that show a figure right now, however it got there (an `update()`, or a
88
+ deferred draw on a resize). `update()` resolves whether it drew anything, but a
89
+ cell it deferred is drawn later, so ask `grid.plotted`, not that value, before
90
+ acting on a figure.
91
+
92
+ The other options:
93
+
94
+ | Option | What it does |
95
+ |---|---|
96
+ | `resolveSeries(series, results, subplot)` | Returns `{ y, x? }` or null. Default `resolveSeriesData`: `series[_resultKey ?? variable]`, then `variable`, then the name after its first `.`; `x` is `_resultTime`. Also resolves the x variable. |
97
+ | `derivedTraces(traces, subplot, { results, index })` | Extra Plotly traces (or a promise of them) appended to a subplot's figure. |
98
+ | `onTransformError`, `onError` | A transform that failed (`{ transformId, traceName, error }`); a subplot that failed to draw (`{ index, error }`), on an `update()` or on the resize that draws a deferred one, which then shows the empty message. |
99
+ | `theme` | Overrides for `PLOT_GRID_THEME`: `fontFamily`, `fontColor`, `fontSize`, `paperBackground`, `plotBackground`, `gridColor`, `lineColor`, `tickColor`, `zeroLineColor`, `hoverBackground`, `hoverBorder`. |
100
+ | `alwaysShowLegend` | Draws a subplot whose legend is `'hidden'` with one at the top. |
101
+ | `layout` | Plotly layout fields merged over every figure's (`hovermode`, `margin`, ...). |
102
+
103
+ `PlotPopoutWindow` takes `resolveSeries` too, for its chart and its data
104
+ table. Its chart cells were `div.plot-tile-chart` and are now
105
+ `div.twm-plot-grid__cell` inside a `.twm-plot-grid`. An overlay is drawn for
106
+ any series with an `overlay` and data under its id in `actuals`; it used to
107
+ need `overlay.etlKey` as well. More changes to what the window draws, for
108
+ configs outside the fixtures:
109
+
110
+ - On an `'area-stacked'` subplot, overlays are stacked too, in a group of their
111
+ own per y-axis (`stackgroup: 'overlay-<y ref>'`, `groupnorm: 'fraction'`,
112
+ `mode: 'lines'`, no fill), so they read on the same 0..1 scale as the stack.
113
+ They used to be drawn unstacked, at their raw values.
114
+ - A subplot with no traces draws an empty figure, not the empty message, only
115
+ when **all of its own** series are hidden (its overlays and derived traces do
116
+ not count). It used to do that for every subplot as soon as any series
117
+ anywhere in the window was hidden. Null results win over that rule: with no
118
+ results every configured subplot shows `emptyMessage`, hidden series or not.
119
+ - While `getResults()` answers null and nothing has been drawn yet, every cell
120
+ shows `emptyMessage`; the cells used to stay blank. Figures already drawn
121
+ stay while the results are briefly null, as before.
122
+ - A y axis's positive `step` sets the Plotly `dtick` of that axis. The window
123
+ used to ignore `step`, so Plotly chose the tick spacing.
124
+ - A subplot with `xAxis: { useTime: false, variable }` is drawn against that
125
+ variable, with an x-axis title (`xAxis.label`, else the variable's name) and
126
+ no clip of the x axis to the time range. The window used to ignore `xAxis`
127
+ and always drew against time, clipped to the first and last time point.
128
+
129
+ Its zoom and pan buttons and Download PNG act on `grid.plotted`, so a figure the
130
+ grid drew on a resize (the window opened while its plot area had no size) is
131
+ zoomable and exportable; Download PNG exports the first subplot that shows a
132
+ figure, where it used to try the first slot whatever it held.
133
+ `tests/plot_grid.test.mjs` pins all of it,
134
+ including that the window draws the same traces, names and axes for the
135
+ fixture configs (`tests/fixtures/plot_grid_fixtures.mjs`) as it did before.
136
+
137
+ ## Editing a plot config: `renderPlotConfigEditor` (0.17.0)
138
+
139
+ `renderPlotGrid` draws a plot config; `renderPlotConfigEditor` edits one, in
140
+ place, so an embedder no longer writes its own panel for it:
141
+
142
+ ```js
143
+ import { renderPlotConfigEditor, renderPlotGrid, migrateData } from '@flexdesk/charts';
144
+
145
+ const plotConfig = migrateData(savedConfig);
146
+ const editor = renderPlotConfigEditor(el, plotConfig, {
147
+ onChange: () => grid.update({ plotConfig }), // after every edit
148
+ seriesProvider: ({ value, role, subplot }) => names(value), // items, or a promise of them
149
+ seriesTransforms, // their toggles, per subplot
150
+ overlaySources: () => mySources(), // optional
151
+ extraSeriesFields: (el, series, { subplot, axis, onChange }) => { /* your fields */ },
152
+ });
153
+ editor.dispose();
154
+ ```
155
+
156
+ It draws a layout picker (every `PLOT_LAYOUTS` entry), a tab per subplot slot
157
+ when the layout has more than one, and for the subplot in front: its title;
158
+ its chart type, legend, interpolation, NaN handling and data points; its x
159
+ axis (time, or a series); its y axes (label, scale, position, min, max, step)
160
+ and, under each, its series (a picker, legend label, colour, width, dash and
161
+ stack group); its z axis while the chart type is 3D; its reference lines and
162
+ shaded bands; and the toggles of any `seriesTransforms`. Every edit writes the
163
+ config it was given and then calls `onChange(config)`.
164
+
165
+ - A layout with more slots than there are subplots adds default subplots
166
+ (`makeDefaultSubplot`); one with fewer keeps the extra subplots, so switching
167
+ back restores them.
168
+ - **Add Y axis** adds `makeDefaultYAxis(index)` (left for an even index, right
169
+ for an odd one) and **Add series** adds `makeDefaultSeries(index)` (coloured
170
+ by its place among the subplot's series). Both come from `@flexdesk/charts`,
171
+ and `makeDefaultSubplot` and `migrateData` build their axes and series from
172
+ them, so an axis added in the editor matches a default subplot's.
173
+ - A 3D chart type gives the subplot a z axis (label, scale, min, max, step and
174
+ a series) and uses its first y axis only: the other y axes are shown disabled
175
+ and **Add Y axis** is disabled. A 2D chart type sets `zAxis` back to `null`.
176
+ - Every series picker is an `AutocompleteField` fed by `seriesProvider`, asked
177
+ `{ value, role, subplot }`, where `role` is `'series'`, `'x'` or `'z'`. It
178
+ answers AutocompleteField items (`{ label, insertText?, type? }`) or a promise
179
+ of them. A provider that throws or rejects shows no suggestions and leaves
180
+ the typed text as it is (`logger.warn` hears it when you pass `logger`).
181
+ Without `seriesProvider` the pickers suggest nothing and take typed text.
182
+ - `overlaySources()` answers choice options (`{ value, label }`, or
183
+ `{ group, options }`), or a promise of them, asked once per draw. With it,
184
+ every series row gets an overlay source select; choosing one writes
185
+ `series.overlay = { source, ... }` and shows the overlay's label, colour,
186
+ dash, width and mode. `renderPlotGrid` draws it from the data you pass under
187
+ the series' id in `overlays`. **None** removes `series.overlay`. Without
188
+ `overlaySources` there are no overlay controls.
189
+ - `extraSeriesFields(el, series, { subplot, axis, onChange })` puts fields of
190
+ your own in each series row; call `onChange()` after you edit the series. What
191
+ it returns is disposed (`dispose()`) when the row is redrawn or the editor
192
+ is disposed. A `dispose()` that throws is reported to `logger.warn` and does
193
+ not stop the rest of the editor being disposed.
194
+
195
+ It returns `{ dispose, refresh, selectSubplot, activeSubplot }`: `dispose()`
196
+ disposes every field it created (every `AutocompleteField` among them) and
197
+ removes the editor; `refresh()` redraws it after the config was changed from
198
+ outside; `selectSubplot(index)` brings a subplot to the front. It fits
199
+ `PlotPopoutWindow`'s `configRenderer` slot, `(container) => { dispose() } | void`:
200
+ the window disposes what the renderer returns when it closes, so return the
201
+ editor and its dropdowns leave `document.body` with the window.
202
+ `openPlotPopoutWindow` is async; await it:
203
+
204
+ ```js
205
+ const win = await openPlotPopoutWindow({ id, title, plotConfig, getResults, services,
206
+ configRenderer: (el) => renderPlotConfigEditor(el, plotConfig,
207
+ { onChange: () => win.updateConfig(plotConfig) }) });
208
+ ```
209
+
210
+ Its classes are under `.twm-plot-config` in `flexdesk.css`.
211
+ `tests/plot_config_editor.test.mjs` pins all of it.
@@ -0,0 +1,102 @@
1
+ # Command palette
2
+
3
+ > **Package:** `@flexdesk/wm` · **See also:** [Keys, Back and tab layout](keymap.md) · [All docs](../README.md)
4
+
5
+ Ctrl+K opens the command palette. It searches the entity catalog you pass to `createShell` (built with `createEntityCatalog`) and the commands you give it, and a pick can open in place, in a split or in a window. Everything here is passed as `palette: { … }`, and the options are opt-in: a shell that passes none keeps the chip strip, the panel toggles and the flat list.
6
+
7
+ ## The palette that finds anything (0.9.0; matching tightened in 0.9.1)
8
+
9
+ Ctrl+K's grouped layout. Every option is off unless you pass it, so a shell that
10
+ passes none of them keeps the chip strip, the panel toggles and the flat list:
11
+
12
+ ```js
13
+ await createShell({
14
+ // …everything above…
15
+ palette: {
16
+ placeholder: 'Search everything…',
17
+ layout: 'grouped', // a header per kind, matches marked
18
+ chrome: { pages: false, panels: false }, // nothing between the input and the results
19
+ groupCap: 4, // then "Show all N", which scopes to the kind
20
+ cache: true, // a re-open draws the last catalogue at once
21
+ empty: async () => recentRows(), // shown before anything is typed, under "Recent"
22
+ commands: () => commandRows(), // `{label, action}`; `>` restricts to them
23
+ drill: (pick) => pick.kind === 'planet' // Tab on a row enters a scope
24
+ ? { label: `${pick.label} › moons`, search: (q, signal) => findMoons(pick.id, q, signal) }
25
+ : null,
26
+ onPick: (pick, { mode, scope }) => openSomewhere(pick, mode), // 'open' | 'split' | 'window'
27
+ },
28
+ });
29
+ ```
30
+
31
+ A row may carry `context` (drawn, not searched), `keywords` (searched, not
32
+ drawn), `modes` (which of `split`/`window` its keys offer), `drillLabel` (the Tab
33
+ hint), `boost`, `group`/`groupLabel`, `kindLabel`, `shortcut` and `verb`. A group's
34
+ header is the taxonomy's `plural` for the kind, else its `label`. Matching ranks
35
+ exact, prefix, word starts (`ord li` finds *Order lines*), substring, then an
36
+ abbreviation: it starts at a word, and the letters it skips outside word starts are
37
+ at most half the query (`cstmr` finds *customer*; `cour` does not find *Scroll up
38
+ to*). A drill's `search` is debounced, and a stale answer is aborted and
39
+ never drawn. `palette.invalidate()` drops the cache.
40
+
41
+ **The built-in *Keyboard shortcuts* command** opens the help topic
42
+ `keyboard-shortcuts`. If your `HelpProvider` defines that id, your topic is
43
+ shown. If it does not, or you register no help at all, the modal shows the window
44
+ manager's own bindings, `KEYMAP_SHORTCUTS` from `@flexdesk/wm`, as a table
45
+ (`keyboardShortcutsTopic()`). Before this the modal showed *Failed to load help
46
+ topic.* The fallback comes from `HelpModal.open(id, { fallback })`
47
+ (`@flexdesk/widgets`), and you can pass one yourself. A topic id that nobody
48
+ provides now shows *Help topic not found.* where it used to raise a load error.
49
+
50
+ ## A palette row drawn disabled, with a reason (0.17.2)
51
+
52
+ A palette row (a command from `commands`, or any other row) may carry
53
+ `enabled: () => boolean` and `disabledReason`. While `enabled()` returns false
54
+ the row is still listed, so it can say why it does nothing:
55
+
56
+ ```js
57
+ palette: {
58
+ commands: () => [
59
+ { id: 'save', label: 'Save', action: save,
60
+ enabled: () => dirty, disabledReason: 'Nothing to save' },
61
+ ],
62
+ },
63
+ ```
64
+
65
+ - The row gets the class `twm-cmdpal__item--disabled` and `aria-disabled="true"`,
66
+ and `disabledReason`, when given, is its `title` (the tooltip). The stylesheet
67
+ greys it.
68
+ - Picking it with a click, Enter, Ctrl+Enter or Shift+Enter runs nothing: not
69
+ its `action`, not `onPick`. The palette stays open on it.
70
+ - The arrow keys still move onto it and past it.
71
+ - `enabled()` is called again on every draw and every pick, so the row follows
72
+ your state while the palette is open. An `enabled()` that throws counts as
73
+ false.
74
+ - A row without `enabled` is drawn and picked as before. Both layouts (flat and
75
+ `grouped`) behave the same way.
76
+
77
+ `tests/command_palette_disabled.test.mjs` drives it in jsdom.
78
+
79
+ ## A catalog source only behind its prefix: `scopedOnly` (0.17.2)
80
+
81
+ A source can be too broad to compete with everything else in the palette: help
82
+ topics, say, which would crowd every query. Mark it `scopedOnly` and its rows are
83
+ listed only once its prefix has made the type chip:
84
+
85
+ ```js
86
+ createEntityCatalog({
87
+ sources: [
88
+ { navKind: 'model', list: listModels, shape: toRow },
89
+ { navKind: 'topic', list: listTopics, shape: toRow, scopedOnly: true },
90
+ ],
91
+ aliases: { help: ['topic'] },
92
+ });
93
+ ```
94
+
95
+ A plain typed query (`solver`) lists no `topic` row, in the flat and the grouped
96
+ layout alike: no row, no group header, no *Show all*. `help: solver` or
97
+ `topic: solver` lists its rows as any source's, and `help:` alone lists them all.
98
+ The mark is the palette's business: `loadAll`, `loadGrouped` and the tile menu
99
+ still return every source, a picked row reaches `onPick` as its `shape` made it,
100
+ and commands and `empty()` rows of the same kind are not held back. A source
101
+ without the option, or with `scopedOnly: false`, is searched exactly as in
102
+ 0.17.1. A value that is not a boolean is refused with a `TypeError`.
@@ -0,0 +1,72 @@
1
+ # Conversation view
2
+
3
+ > **Package:** `@flexdesk/widgets` · [All docs](../README.md)
4
+
5
+ A transcript of role-tagged messages above a composer. It has no Markdown and no model in it: you draw a message's body, and it tells you when the user submits or cancels.
6
+
7
+ ## A conversation: `mountConversationView` (0.17.0)
8
+
9
+ A transcript of role-tagged messages above a composer, in `@flexdesk/widgets`.
10
+ It has no markdown and no model in it: you draw a message's body, and it tells
11
+ you when the user submits or cancels.
12
+
13
+ ```js
14
+ const view = mountConversationView(el, {
15
+ render: (message, bodyEl, { streaming }) => { bodyEl.innerHTML = toHtml(message.text); },
16
+ onSubmit: (text) => { view.append({ role: 'user', text }); view.setBusy(true); ask(text); },
17
+ onCancel: () => abort(),
18
+ placeholder: 'Ask anything',
19
+ });
20
+ view.beginStream({ role: 'assistant' });
21
+ for await (const chunk of reply) view.appendChunk(chunk); // re-renders this message only
22
+ view.endStream();
23
+ view.setBusy(false);
24
+ ```
25
+
26
+ | Method | Does |
27
+ |---|---|
28
+ | `append({ id?, role, text, meta? })` → id | Adds a message; `role` becomes `twm-chat__msg--<role>` |
29
+ | `replaceAll(messages)`, `clear()` | Restores a transcript (and scrolls to its end), or empties it |
30
+ | `beginStream(message?)` → id, `appendChunk(text)`, `endStream()` | A message that grows; each chunk re-renders that message's body and nothing else |
31
+ | `slot(id)` | The element below a message's body, yours to fill (tool output, buttons); a re-render leaves it alone |
32
+ | `messages()`, `streamingId()` | Copies of the messages; the one streaming |
33
+ | `getScrollTop()`, `setScrollTop(px)`, `scrollToBottom()`, `isPinned()` | The transcript's scroll position, to keep across a remount |
34
+ | `setBusy(bool)`, `isBusy()` | `true` turns Send into Cancel |
35
+ | `getText()`, `setText(text)`, `focus()`, `destroy()` | The composer, and teardown |
36
+
37
+ - **`render(message, bodyEl, { streaming })`** fills an emptied body. One that
38
+ throws leaves that message as its plain text and the view carries on; the
39
+ error goes to `onRenderError(error, message)`. Without `render`, every body
40
+ is plain text (`textContent`, never markup).
41
+ - **Pinned to the bottom.** The transcript starts out following new content.
42
+ Any scroll up, even a few px from the end (the first frame of a smooth wheel
43
+ or arrow-key scroll), stops that and shows *Jump to latest*, and later chunks
44
+ leave the reader where they are. It follows again only after a scroll down
45
+ that ends within `pinThreshold` px (32) of the end, a click on *Jump to
46
+ latest*, `scrollToBottom()`, `replaceAll()` / `clear()`, or a
47
+ `setScrollTop(px)` that lands within the threshold. Content growing below
48
+ never unpins it, nor does content shrinking under it (the browser moves
49
+ `scrollTop` back to the new end, which is not the reader scrolling). A wheel
50
+ over a scroller nested in a body or a slot (a `<pre>`, a tool-output box)
51
+ scrolls that scroller (or, at its top with `overscroll-behavior: contain`,
52
+ keeps the wheel), and a wheel a handler in the transcript cancels, a
53
+ Ctrl+wheel (page zoom, or a trackpad pinch) and a wheel up over a transcript
54
+ already at its top move nothing, so in all these cases the transcript keeps
55
+ following.
56
+ - **The composer** grows with its text up to `composerMaxHeight` (200 px).
57
+ Enter submits and clears it (blank text is ignored, and so is Enter during
58
+ IME composition); Shift+Enter is left to the browser, which inserts the
59
+ newline (so undo and `input` listeners see it). While busy, Enter submits
60
+ nothing, and Esc (wherever focus is inside the view) or a click on Cancel
61
+ calls `onCancel` once per busy period. That Esc goes no further
62
+ (`stopPropagation`), so a `ManagedWindow` or panel holding the view that
63
+ closes on Escape stays open. An Esc a handler inside the view has already
64
+ consumed (`preventDefault`, e.g. an inline popup in the transcript closing
65
+ itself) is left alone: it neither cancels nor is stopped.
66
+ - **Ids are unique.** `append`, `beginStream` or `replaceAll` with an id already
67
+ in the transcript (or repeated within the `replaceAll` list) throws, and the
68
+ refused call changes nothing: no row, no open stream, the old transcript kept.
69
+ - `labels: { send, cancel, jump, input }` replaces the four texts.
70
+
71
+ `tests/conversation_view.test.mjs` drives all of it in jsdom with a fake
72
+ scroll geometry.