flexdesk 0.17.2 → 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.
- package/CHANGELOG.md +100 -63
- package/README.md +27 -4517
- package/css/base.css +18 -1
- package/css/flexdesk.css +18 -1
- package/dist/{chunk-4A7SJC3T.js → chunk-RKNFBX5K.js} +15 -4
- package/dist/{chunk-4A7SJC3T.js.map → chunk-RKNFBX5K.js.map} +2 -2
- package/dist/flexdesk.css +18 -1
- package/dist/tiles.js +1 -1
- package/dist/widgets.js +59 -26
- package/dist/widgets.js.map +2 -2
- package/docs/README.md +97 -0
- package/docs/best-practices.md +45 -0
- package/docs/components/charts.md +211 -0
- package/docs/components/command-palette.md +102 -0
- package/docs/components/conversation.md +72 -0
- package/docs/components/data-table-grid.md +328 -0
- package/docs/components/data-table.md +596 -0
- package/docs/components/editor.md +22 -0
- package/docs/components/flow-canvas-editor.md +482 -0
- package/docs/components/flow-editors.md +176 -0
- package/docs/components/flow-lane-editor.md +165 -0
- package/docs/components/flow-outline-editor.md +176 -0
- package/docs/components/forms-and-dialogs.md +294 -0
- package/docs/components/input-fields.md +72 -0
- package/docs/components/keymap.md +143 -0
- package/docs/components/layout-widgets.md +163 -0
- package/docs/components/markdown-and-outline.md +106 -0
- package/docs/components/menus.md +86 -0
- package/docs/components/notifications.md +194 -0
- package/docs/components/panels-and-desktops.md +127 -0
- package/docs/components/record-list-and-fact-table.md +128 -0
- package/docs/components/settings-view.md +112 -0
- package/docs/components/shell.md +189 -0
- package/docs/components/slide-out-panel.md +68 -0
- package/docs/components/tiles.md +210 -0
- package/docs/components/wizard.md +54 -0
- package/docs/concepts/building-and-packaging.md +33 -0
- package/docs/concepts/domain-neutrality.md +110 -0
- package/docs/concepts/host-port.md +137 -0
- package/docs/concepts/packages-and-entry-points.md +32 -0
- package/docs/concepts/peer-dependencies.md +42 -0
- package/docs/concepts/state-guard.md +58 -0
- package/docs/concepts/styling.md +47 -0
- package/docs/quickstart.md +179 -0
- package/package.json +2 -1
- package/src/ui/components/notification_history.js +17 -24
- package/src/ui/components/slide_out_panel.js +14 -3
- package/src/ui/notification_center.js +41 -3
- 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.
|