flexdesk 0.17.3 → 0.17.5

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 (50) hide show
  1. package/CHANGELOG.md +104 -65
  2. package/README.md +27 -4544
  3. package/css/base.css +18 -1
  4. package/css/flexdesk.css +18 -1
  5. package/dist/charts.js +20 -3
  6. package/dist/charts.js.map +2 -2
  7. package/dist/{chunk-4A7SJC3T.js → chunk-RKNFBX5K.js} +15 -4
  8. package/dist/{chunk-4A7SJC3T.js.map → chunk-RKNFBX5K.js.map} +2 -2
  9. package/dist/flexdesk.css +18 -1
  10. package/dist/tiles.js +1 -1
  11. package/dist/widgets.js +14 -2
  12. package/dist/widgets.js.map +2 -2
  13. package/docs/README.md +97 -0
  14. package/docs/best-practices.md +45 -0
  15. package/docs/components/charts.md +235 -0
  16. package/docs/components/command-palette.md +102 -0
  17. package/docs/components/conversation.md +72 -0
  18. package/docs/components/data-table-grid.md +328 -0
  19. package/docs/components/data-table.md +596 -0
  20. package/docs/components/editor.md +22 -0
  21. package/docs/components/flow-canvas-editor.md +482 -0
  22. package/docs/components/flow-editors.md +176 -0
  23. package/docs/components/flow-lane-editor.md +165 -0
  24. package/docs/components/flow-outline-editor.md +176 -0
  25. package/docs/components/forms-and-dialogs.md +294 -0
  26. package/docs/components/input-fields.md +72 -0
  27. package/docs/components/keymap.md +143 -0
  28. package/docs/components/layout-widgets.md +163 -0
  29. package/docs/components/markdown-and-outline.md +106 -0
  30. package/docs/components/menus.md +86 -0
  31. package/docs/components/notifications.md +194 -0
  32. package/docs/components/panels-and-desktops.md +127 -0
  33. package/docs/components/record-list-and-fact-table.md +128 -0
  34. package/docs/components/settings-view.md +112 -0
  35. package/docs/components/shell.md +189 -0
  36. package/docs/components/slide-out-panel.md +68 -0
  37. package/docs/components/tiles.md +210 -0
  38. package/docs/components/wizard.md +54 -0
  39. package/docs/concepts/building-and-packaging.md +33 -0
  40. package/docs/concepts/domain-neutrality.md +110 -0
  41. package/docs/concepts/host-port.md +137 -0
  42. package/docs/concepts/packages-and-entry-points.md +32 -0
  43. package/docs/concepts/peer-dependencies.md +42 -0
  44. package/docs/concepts/state-guard.md +58 -0
  45. package/docs/concepts/styling.md +47 -0
  46. package/docs/quickstart.md +179 -0
  47. package/package.json +2 -1
  48. package/src/charts/data_series_plot_window.js +17 -3
  49. package/src/ui/components/slide_out_panel.js +14 -3
  50. package/src/ui/utils/auto_scrollbars.js +16 -2
@@ -0,0 +1,210 @@
1
+ # Dashboard tiles
2
+
3
+ > **Package:** `@flexdesk/tiles` · [All docs](../README.md)
4
+
5
+ A draggable, resizable widget grid: the tile base class, the grid, a widget registry, layout persistence, chart and KPI tiles, list and people tiles, the figure tile every Plotly tile draws through, and a form for a tile's settings.
6
+
7
+ Two different things in FlexDesk are called "tiles". `@flexdesk/wm` tiles the *window* (see [Shell and navigation](shell.md)); `@flexdesk/tiles` is this dashboard *widget grid*.
8
+
9
+ **On this page**
10
+
11
+ - [Layouts are keyed by whatever you key them by](#layouts-are-keyed-by-whatever-you-key-them-by)
12
+ - [Chart and KPI tiles (0.13.0)](#chart-and-kpi-tiles-0130)
13
+ - [Figure tiles: one override, the draw is the base's (0.17.0)](#figure-tiles-one-override-the-draw-is-the-bases-0170)
14
+ - [List and people tiles, and one scope for the board (0.16.1)](#list-and-people-tiles-and-one-scope-for-the-board-0161)
15
+ - [A tile's settings as a form: `mountTileConfigForm` (0.17.0)](#a-tiles-settings-as-a-form-mounttileconfigform-0170)
16
+
17
+ ## Layouts are keyed by whatever you key them by
18
+
19
+ `@flexdesk/tiles` stores layouts under keys **you** choose and resolves a starting
20
+ layout through a callback **you** supply:
21
+
22
+ ```js
23
+ import { configureLayoutPersistence } from '@flexdesk/tiles';
24
+
25
+ configureLayoutPersistence({
26
+ layoutsKey: 'myapp.layouts', // default: 'twm.tiles.layouts'
27
+ resolveTemplate: (ctx) => pickTemplate(ctx) ?? null, // null => empty grid
28
+ });
29
+ ```
30
+
31
+ **Configure it before anything reads it.** The store is a singleton; built before
32
+ you supply your key, it is built with the default, and every layout your users have
33
+ saved becomes silently invisible. That is a data-loss bug that raises no error, so
34
+ the configuration step is explicit rather than lazy.
35
+
36
+ A layout is keyed by *something* — a document, a user, a run — and the library has
37
+ no opinion about which. It calls that a `contextId`.
38
+
39
+ ## Chart and KPI tiles (0.13.0)
40
+
41
+ `@flexdesk/tiles` ships two widgets: `flexdesk.chart` (line, bar, area, scatter,
42
+ pie, donut, funnel, histogram — Plotly, so `setPlotlySource` first) and
43
+ `flexdesk.kpi` (one number, a label and an optional delta; plain DOM, no Plotly).
44
+ Nothing registers at import — call `registerChartTiles()` (idempotent). You hand
45
+ a tile **already-computed** series; turning records into series is yours:
46
+
47
+ ```js
48
+ import { registerChartTiles } from '@flexdesk/tiles';
49
+ registerChartTiles();
50
+ const chart = grid.addTile('flexdesk.chart', { x: 0, y: 0, w: 6, h: 4 },
51
+ { chart_type: 'line', title: 'Burndown', unit: 'pts' });
52
+ chart.onPointClick = ({ customdata }) => openItem(customdata);
53
+ chart.setData({
54
+ series: [
55
+ { x: days, y: remaining, name: 'Remaining', interpolation: 'step', customdata: ids },
56
+ { x: days, y: ideal, name: 'Ideal', lineStyle: 'dashed' },
57
+ ],
58
+ markers: [{ x: today, label: 'Today' }], // vertical lines
59
+ note: 'From 41 stories', // a line under the figure
60
+ });
61
+ grid.addTile('flexdesk.kpi', { x: 6, y: 0, w: 3, h: 2 }, { title: 'Open', unit: 'bugs' })
62
+ .setData({ value: 42, reference: 37 }); // ▲ +5 bugs
63
+ ```
64
+
65
+ `setData(null)` or an empty series is the empty state, and it releases any figure
66
+ still drawing. A pie takes `{labels, values}`, a histogram `{values}`. The config
67
+ (`chart_type`, `title`, `unit`, …) round-trips through `getLayout()`/`setLayout()`.
68
+ `buildOverlayShapes` gained `verticalLines: [{x, label?, color?, lineStyle?}]` for
69
+ the same marker outside a tile.
70
+
71
+ A chart tile's **Download PNG** (0.14.1) goes through the grid's `host`
72
+ (`new TileGrid({ host })`): with `dialogs`, the native Save dialog — a save
73
+ toasts where the file went, a cancel says nothing — and with no host, or a
74
+ `saveFile` that answers null, the browser download it always was.
75
+
76
+ ## Figure tiles: one override, the draw is the base's (0.17.0)
77
+
78
+ `FigureTile` is the base every Plotly tile draws through — `ChartTile` is one.
79
+ A subclass overrides **one** method, `figure(data)`, answering
80
+ `{traces, layout?, config?, frames?}` or `null`, synchronously:
81
+
82
+ ```js
83
+ import { FigureTile, registerWidget } from '@flexdesk/tiles';
84
+ import { buildHistogramTrace } from '@flexdesk/charts';
85
+
86
+ class SpreadTile extends FigureTile {
87
+ static TYPE = 'myapp.spread';
88
+ static TITLE = 'Spread';
89
+ figure(data) {
90
+ if (!data?.values?.length) return null; // the empty state
91
+ return { traces: [buildHistogramTrace({ x: data.values })], layout: { margin: { t: 8 } } };
92
+ }
93
+ }
94
+ registerWidget(SpreadTile);
95
+ ```
96
+
97
+ The base awaits `ensurePlotly()` (so a page using `setPlotlySource` draws on
98
+ first view, and data that changes while Plotly loads draws once), draws through
99
+ `createChart` with `scrollZoom` and the mode bar off (`config` goes over that),
100
+ adds `frames`, and discards a draw that lands after a newer one. `null` is the
101
+ empty state with `static EMPTY_MESSAGE` — call `this.showEmpty(why)` yourself
102
+ first to say why. A `figure()` that throws, or a draw that fails, is the error
103
+ state plus `tile:render-failed {tileId, message}`. Loading, empty, error and
104
+ `dispose()` purge the figure; a resize that settles relayouts it once. The ⋯
105
+ menu is `TileBase`'s entries plus **Download PNG** (through `host.dialogs`, as
106
+ above); extend it with `[...super.menuItems(), …]` and route your actions
107
+ through `_onMenuAction`, calling `super` for the rest. **Expand** opens
108
+ `openRawTracesWindow` with the last figure drawn (`getExpandData()`), and
109
+ `onPointClick({customdata, point, tile})` — an option or a property — hears a
110
+ click on a mark. `setData(data)` feeds one tile; `null` clears it.
111
+
112
+ `ChartTile` is now a `FigureTile`: its menu stays Download PNG alone, and its
113
+ header gains the Expand button.
114
+
115
+ ## List and people tiles, and one scope for the board (0.16.1)
116
+
117
+ Two more widgets, registered by `registerListTiles()` (idempotent, nothing at
118
+ import). Like the chart tile they take **rows you have already worked out**;
119
+ what they own is what every list on a board needs: the count ("7 of 12"), the
120
+ line that says what empty means, a button in the bar, a click, the keyboard and
121
+ a right-click menu per row.
122
+
123
+ ```js
124
+ import { registerListTiles, createScopePicker } from '@flexdesk/tiles';
125
+ registerListTiles();
126
+ const late = grid.addTile('flexdesk.list', { x: 0, y: 0, w: 12, h: 4 },
127
+ { title: 'Overdue', empty_text: 'Nothing is late.', max_rows: 0 });
128
+ late.onRowActivate = ({ row, event }) => open(row.key, { modified: event.ctrlKey });
129
+ late.rowMenu = ({ row }) => [{ label: 'Open', icon: 'open_in_new', action: 'open' }];
130
+ late.onRowMenuAction = ({ action, row }) => { /* … */ };
131
+ late.onAction = () => openAsList();
132
+ late.setData({
133
+ rows: [{ key: '7', icon: 'task', lead: 'TASK-0007', title: 'Sign-off',
134
+ cells: [{ text: 'priya' }, { text: '9 days late', tone: 'danger' }] }],
135
+ action: { label: 'Open as a list', icon: 'list' },
136
+ });
137
+ grid.addTile('flexdesk.people', { x: 0, y: 4, w: 6, h: 4 }, { title: 'Who has what' })
138
+ .setData({ people: [{ key: '', name: 'Nobody', muted: true, count: 2,
139
+ pills: [{ text: '2 undated', tone: 'muted' }] }] });
140
+ ```
141
+
142
+ - A row is `role="button"`: click, Enter and Space call `onRowActivate` with the
143
+ event; Up/Down/Home/End move. Right-click, the context-menu key and Shift+F10
144
+ open the row's menu (`showContextMenu`'s items; none means no menu). A row
145
+ carries `data-row-key`; `rowsDraggable` makes rows draggable.
146
+ - `{rows: []}` keeps the bar and says `empty` (or `config.empty_text`) where the
147
+ rows would be; `setData(null)` is the empty state. Text is always text.
148
+ - `tone` (`danger`, `warning`, `ok`, `muted`) picks a colour only; `className`
149
+ on a row or a cell is yours, added as it is.
150
+ - `groupByPerson(items, personOf)` groups for a people tile, most first.
151
+
152
+ **One scope for the whole board.** `createScopePicker({options, value, onChange})`
153
+ is a row of buttons, one pressed. `grid.setScope(value)` (or
154
+ `new TileGrid({ scope, onScopeChange })`) hands the value to every tile as
155
+ `tile.scope` and `tile.onScopeChange(scope)`, a tile added later starts with
156
+ it, a grid with a data source reloads its tiles, and the grid's `onScopeChange`
157
+ is told. What a scope means is yours; it is not part of the layout.
158
+
159
+ ## A tile's settings as a form: `mountTileConfigForm` (0.17.0)
160
+
161
+ `getConfigSchema()` answers `{fields}`; `mountTileConfigForm` draws them, so a
162
+ config panel is no longer a control written by hand per field:
163
+
164
+ ```js
165
+ import { mountTileConfigForm, openTileConfig } from '@flexdesk/tiles';
166
+ const form = mountTileConfigForm(host, {
167
+ schema: tile.getConfigSchema(),
168
+ config: tile.config, // defaults filled by mergeWithDefaults
169
+ data, // what getFieldOptions reads for dynamic options
170
+ onChange: (key, value, config) => tile.update(undefined, config),
171
+ renderers: { variable: ({ field, value, data, readonly, commit }) => myPicker(...) },
172
+ });
173
+ form.getValues(); form.setValues(next); form.isValid(); form.focusFirstInvalid();
174
+ form.flush(); form.dispose();
175
+ ```
176
+
177
+ - A field is `{key, type, label?, description?, default?, required?, readonly?,
178
+ min?, max?, step?, options?, visibleWhen?}`. Without a `label` the field is
179
+ called by its `key`, on its label and in its error messages. Every `FieldTypes` entry has a
180
+ control (text, number, checkbox, select, multiselect, color, range); any other
181
+ `type` needs a renderer, which answers an element or
182
+ `{element, setValue?, dispose?}` and calls `commit(value)` on an edit.
183
+ - An edit is checked by `validateField`. An invalid one shows its error under the
184
+ field and is **not applied**; a valid one calls `onChange` with the field's key.
185
+ `isValid()` is false while a shown row has an error; `focusFirstInvalid()`
186
+ focuses the first such row's control (a renderer's element, or the first
187
+ focusable element inside it) and answers whether there was one.
188
+ - `commitOn: 'input'` (the default) checks text, number, range and colour
189
+ controls on every keystroke; `commitOn: 'change'` checks them once the edit is
190
+ finished (blur, Enter, a released slider), for an `onChange` that is expensive.
191
+ `flush()` commits an edit typed but not yet finished.
192
+ - `visibleWhen: {field, in: [...]}` (or `{field, equals}`) hides the row, never
193
+ rebuilds the form, so the control being typed into keeps its focus.
194
+ - A field with no `key` throws. **The built-in tiles' schemas now use `key` and
195
+ `description`** (they said `name` and `help`, which `validateConfig` and
196
+ `mergeWithDefaults` never read: BUG-0021). A consumer that read `field.name`
197
+ or `field.help` from them reads `field.key` and `field.description`.
198
+
199
+ The grid's cogwheel emits `grid:config-modal-show`, which now also carries the
200
+ grid's `data`. Nothing listens unless you ask:
201
+ `openTileConfig(eventBus, {mode: 'slide-out', host})` shows the form in a
202
+ `SlideOutPanel` over `host` and saves every finished, valid edit (it runs the
203
+ form with `commitOn: 'change'`, so typing a title is one save, not one per
204
+ keystroke; closing the panel saves an edit still being typed);
205
+ `{mode: 'modal'}` (the default) saves on Save, and refuses Save while a field
206
+ shows an error (the modal stays open and the first invalid field is focused). A request that arrives while a
207
+ modal is open is ignored. It answers a function that unsubscribes and closes the
208
+ slide-out; an open modal is left to the user, keeps its form and still saves on
209
+ Save. A renderer's `commit` on a `readonly` field applies nothing and answers
210
+ `{valid: false, error}`.
@@ -0,0 +1,54 @@
1
+ # Wizard
2
+
3
+ > **Package:** `@flexdesk/widgets` · [All docs](../README.md)
4
+
5
+ A multi-step flow: a step indicator, a body slot and a footer with Cancel, Back and Next (Finish on the last step). Mount it inline in your page, or open it in a modal `ManagedWindow`.
6
+
7
+ ## A step wizard: `Wizard` (0.17.0)
8
+
9
+ `new Wizard({ steps, data, onFinish, onCancel, labels })` (in `@flexdesk/widgets`)
10
+ draws a multi-step flow: a step indicator, a body slot and a footer with Cancel,
11
+ Back and Next (Finish on the last step). Mount it inline with `mount(el)`, or in a
12
+ modal `ManagedWindow` with `openInWindow({ id, title, ... })`.
13
+
14
+ ```js
15
+ const wizard = new Wizard({
16
+ data: { file: null },
17
+ steps: [
18
+ { id: 'source', label: 'Source', icon: 'upload_file',
19
+ render(bodyEl, ctx) { // once per visit; may return a cleanup function
20
+ const input = picker(bodyEl);
21
+ input.onchange = () => { ctx.data.file = input.files[0]; ctx.revalidate(); };
22
+ },
23
+ canAdvance: (ctx) => ctx.data.file
24
+ ? { ok: true } : { ok: false, hint: 'Choose a file first' } },
25
+ { id: 'review', label: 'Review', render: (bodyEl, ctx) => review(bodyEl, ctx.data) },
26
+ ],
27
+ onFinish: async (ctx) => api.import(ctx.data), // a rejection marks the step as error
28
+ onCancel: () => {},
29
+ });
30
+ wizard.openInWindow({ id: 'import', title: 'Import' });
31
+ ```
32
+
33
+ - **Gating.** While the current step's `canAdvance(ctx)` answers `{ok: false, hint}`,
34
+ Next is disabled and the hint is shown in the footer. `revalidate()` (also
35
+ `ctx.revalidate()`) asks again and redraws the indicator and the footer; it never
36
+ re-renders the body, so an input keeps its focus and its value.
37
+ - **Steps** are drawn done / current / upcoming / error, joined by a connector. A
38
+ done step is a button that jumps back to it; an upcoming step is inert, so the
39
+ only way forward is Next.
40
+ - **Moves.** `next()`, `back()` and `goTo(idOrIndex)` (back to any earlier step, or
41
+ forward one step through the gate) resolve `true` when they moved.
42
+ `setStepState(id, 'error' | null, message)` marks or clears a step.
43
+ - **Finish.** On the last step Next reads Finish and calls `onFinish(ctx)`. Until
44
+ it settles every button is disabled and moves, cancels and closes are refused,
45
+ not queued. Resolved, the wizard closes; rejected, the step is marked as error
46
+ and the rejection's message is shown, and Finish can be pressed again.
47
+ - **Cancel** (the button, `cancel()`, or the window's close button and Escape)
48
+ closes the wizard and calls `onCancel(ctx)` exactly once. `dispose()` tears it
49
+ down without calling either callback.
50
+
51
+ Every transition goes through a core `StateMachine` (`step`, `finishing`,
52
+ `finished`, `cancelled`; pass `eventBus` to see its `fsm:*` events). `ctx` is
53
+ `{data, step, index, revalidate, next, back, goTo, setStepState}`, with `data`
54
+ the object you passed.
@@ -0,0 +1,33 @@
1
+ # Build and packaging
2
+
3
+ > **Files:** `build.mjs`, `dist/`, the `exports` map in `package.json` · [All docs](../README.md)
4
+
5
+ ## Build
6
+
7
+ ```bash
8
+ npm install
9
+ npm run build # -> dist/
10
+ ```
11
+
12
+ `dist/` is committed. The packaging decision is *prebuilt dist*: this repo owns the
13
+ build so that a consumer with no bundler can load the library through a plain import
14
+ map. See `.gitignore` — it says so there too, because it is the kind of thing someone
15
+ "tidies up" later.
16
+
17
+ ### The `development` condition resolves in the installed package (0.17.1)
18
+
19
+ Every JavaScript entry in the `exports` map has two conditions: `import`, the
20
+ prebuilt bundle under `dist/`, and `development`, the source barrel at the
21
+ package root (`core.js`, `host.js`, `wm.js`, `widgets.js`, `charts.js`,
22
+ `tiles.js`, `editor.js`, `canvas.js`, `flow.js`), which re-exports `src/`.
23
+ Up to 0.17.0 those nine barrels were missing from `files`, so the published
24
+ package did not contain them and a resolver that took the `development` branch
25
+ (`require.resolve` under `--conditions=development`, for one) got a path to
26
+ nothing. From 0.17.1 they ship, beside the `src/` they import.
27
+
28
+ Nothing about which file an `import` resolves to has changed: `import` is
29
+ listed first, so an ES import still gets `dist/`, with or without the
30
+ `development` condition.
31
+
32
+ A suite (`tests/exports_packed.test.mjs`) runs `npm pack --dry-run` and fails
33
+ if any target of any condition in the `exports` map is not in the tarball.
@@ -0,0 +1,110 @@
1
+ # Domain neutrality: your names, keys and globals
2
+
3
+ > **Packages:** `@flexdesk/core`, `@flexdesk/widgets`, `@flexdesk/editor`, `@flexdesk/charts`, `@flexdesk/tiles` · [All docs](../README.md)
4
+
5
+ FlexDesk was extracted from [EcoAgent](https://github.com/nor-os/EcoAgent), a macroeconomic simulation platform, and it knows nothing about economics: no application vocabulary, and no import of anything application-specific, in any shipped file. For you that means everything with a name is yours to choose. Storage keys, theme names, drag MIME types and export file names are options with generic defaults, and nothing reads an application's globals. The two sections here list what you can set and where.
6
+
7
+ ## Storage keys and names are yours (0.17.0)
8
+
9
+ Every localStorage key FlexDesk writes is an option, and so is each name in the
10
+ table below that it registers where other code can see it, except the last row:
11
+ the tile tab MIME is fixed. The defaults are FlexDesk's own; pass yours at boot,
12
+ before the first read:
13
+
14
+ | What | Default | Set it with |
15
+ |---|---|---|
16
+ | Settings (the `getSetting`/`setSetting` binding) | `'flexdesk.settings.v1'` | `configureSettings({ storageKey })` from `@flexdesk/core` |
17
+ | ManagedWindow geometry | `'flexdesk.managedWindows.v1'` | `ManagedWindow.configure({ storageKey })` from `@flexdesk/widgets` |
18
+ | Saved tile layouts | `'twm.tiles.layouts'` | `configureLayoutPersistence({ layoutsKey })` from `@flexdesk/tiles` |
19
+ | Saved tile configurations | `'twm.tiles.configs'` | `configureLayoutPersistence({ configsKey })` from `@flexdesk/tiles` |
20
+ | Built-in Monaco themes | `'flexdesk-dark'`, `'flexdesk-light'` | `setEditorThemeNames({ dark, light })` from `@flexdesk/editor` |
21
+ | MIME type of a dragged editor tab | `'application/x-flexdesk-tab'` | `setTabDragMime(mime)` from `@flexdesk/editor` |
22
+ | Chart PNG export file name | `'chart'` (saved as `chart.png`) | `setChartExportFilename(name)` from `@flexdesk/charts` |
23
+ | `LoggingService` service name on each entry | `'flexdesk'` | `new LoggingService({ name })` from `@flexdesk/core` |
24
+ | MIME type of a dragged tile tab | `'application/x-twm-tile-tab'` | fixed: not an option; read it as `TILE_TAB_MIME` (or `TileRenderer.TAB_MIME`) from `@flexdesk/wm` |
25
+
26
+ ```js
27
+ import { configureSettings } from '@flexdesk/core';
28
+ import { ManagedWindow } from '@flexdesk/widgets';
29
+ import { setEditorThemeNames, setTabDragMime } from '@flexdesk/editor';
30
+ import { setChartExportFilename } from '@flexdesk/charts';
31
+
32
+ configureSettings({ storageKey: 'myapp.settings.v1' });
33
+ ManagedWindow.configure({ storageKey: 'myapp.windows.v1' });
34
+ setEditorThemeNames({ dark: 'myapp-dark', light: 'myapp-light' });
35
+ setTabDragMime('application/x-myapp-tab');
36
+ setChartExportFilename('myapp_chart');
37
+ ```
38
+
39
+ Each default is exported beside its setter (`DEFAULT_SETTINGS_STORAGE_KEY`,
40
+ `DEFAULT_WINDOW_STORAGE_KEY`, `DEFAULT_EDITOR_THEME_NAMES`,
41
+ `DEFAULT_TAB_DRAG_MIME`, `DEFAULT_CHART_EXPORT_FILENAME`), and each setter has a
42
+ getter (`getSettingsStorageKey()`, `ManagedWindow.storageKey`,
43
+ `getEditorThemeNames()`, `getTabDragMime()`, `getChartExportFilename()`). An
44
+ embedder that passes nothing gets the defaults silently: nothing is logged.
45
+
46
+ - **Settings** are read lazily: importing `@flexdesk/core` touches no storage,
47
+ and the binding reads its key on the first `getSetting`/`setSetting`/reset.
48
+ `registerSettings()` and `registerSettingsEventBus()` may come before or after
49
+ `configureSettings()`. Configured after the binding was in use, it moves: values
50
+ return to their defaults, are reloaded from the new key, and one
51
+ `settings:changed` (`path: null`) is emitted. The old key is not touched. A
52
+ private store from `createSettingsStore()` moves the same way with
53
+ `store.setStorageKey(key)` (and answers `store.getStorageKey()`).
54
+ - **ManagedWindow** reads geometry when a window is first shown; configure it
55
+ before the first `show()`.
56
+ - **Editor themes**: a factory starts on the dark name current when it is
57
+ created, and `init()` defines the built-in themes under the names current
58
+ then. An app with a theme of its own defines it with `monaco.editor.defineTheme`
59
+ and passes its name to `factory.setTheme(name)`.
60
+ - **The tab MIME** is read at drag time by every `NotebookTabBar` and
61
+ `EditorPane`, so the two always agree.
62
+
63
+ **Upgrading from 0.16.** The old defaults were one application's names:
64
+ `'ecosim.settings.v1'`, `'ecosim.managedWindows.v1'`, `'ecosim-dark'` /
65
+ `'ecosim-light'`, `'application/x-ecosim-tab'`, `'ecosim_chart'` and the log
66
+ service name `'ecosim'`. If your users have settings or window geometry saved
67
+ under the old keys, pass those keys explicitly and nothing is lost:
68
+
69
+ ```js
70
+ configureSettings({ storageKey: 'ecosim.settings.v1' });
71
+ ManagedWindow.configure({ storageKey: 'ecosim.managedWindows.v1' });
72
+ ```
73
+
74
+ Code that called `factory.setTheme('ecosim-light')` now calls
75
+ `factory.setTheme(getEditorThemeNames().light)` (or names the themes back with
76
+ `setEditorThemeNames`), and code that read `'application/x-ecosim-tab'` off a drag
77
+ reads `getTabDragMime()`.
78
+
79
+ ## No application globals: the gallery picker and plot-window events (0.17.0)
80
+
81
+ Nothing under `src/` outside `src/host` reads `window.pywebview` or an
82
+ application's global; `tests/no_app_globals.test.mjs` pins it, with the toast
83
+ helpers ([Notifications](../components/notifications.md#notifications-notificationcenter)) and these two:
84
+
85
+ **`mountGalleryPicker(el, { entries | host, kind, ... })`** takes its entries from
86
+ `entries`, or else from the host's optional `gallery` capability:
87
+ `host.gallery.list(kind)` answers `[{id, kind, label, description?, fields?}]`
88
+ (or a promise of them). Neither shipped adapter advertises it; add your own:
89
+
90
+ ```js
91
+ const host = createHost({ ...createPywebviewHost({ bridgeRef }),
92
+ gallery: { list: (kind) => myBackend.starters(kind) } });
93
+ await mountGalleryPicker(el, { host, kind: 'report', onChange });
94
+ ```
95
+
96
+ With neither `entries` nor the capability, the picker shows its `emptyLabel`
97
+ and logs one warning; a `list` that throws does the same. Until 0.17.0 it
98
+ called one application's bridge method.
99
+
100
+ **`PlotPopoutWindow({ streamEvents })`** names the bus events a live data
101
+ source emits, next to `refreshEvent`: `{ delta, complete }`. The window
102
+ redraws on each while live update is on. There are no default names: without
103
+ `streamEvents`, or for a name left out or given as `null`, nothing is listened
104
+ for, and the window updates through `refreshEvent` and explicit calls only.
105
+
106
+ ```js
107
+ openPlotPopoutWindow({ id, title, plotConfig, getResults, services,
108
+ refreshEvent: 'run:finished',
109
+ streamEvents: { delta: 'sim:delta', complete: 'sim:done' } });
110
+ ```
@@ -0,0 +1,137 @@
1
+ # Host port
2
+
3
+ > **Package:** `@flexdesk/host` · [All docs](../README.md)
4
+
5
+ ## The host port
6
+
7
+ FlexDesk never touches `pywebview`, `fetch`, `localStorage` or the filesystem. It
8
+ asks a **Host**, and you supply one. `createHost({...})` validates the capabilities
9
+ you pass and freezes the result; anything you leave out is simply absent, and the
10
+ library degrades rather than throwing.
11
+
12
+ ```js
13
+ createHost({
14
+ state: { read, write }, // layout persistence
15
+ dialogs: { saveFile, openFile? }, // optional — CSV export falls back to a Blob;
16
+ // openFile (itself optional) to pickFile()
17
+ window: { minimize, maximize, ... }, // optional — no native chrome buttons without it
18
+ gallery: { list }, // optional (0.17.0) — mountGalleryPicker's entries
19
+ })
20
+ ```
21
+
22
+ `NULL_HOST` is a valid host with nothing behind it.
23
+
24
+ Two adapters ship ready-made. `createPywebviewHost()` speaks to a pywebview
25
+ window, and `createPhotinoHost()` (0.14.0) to a Photino.NET window over
26
+ `window.external.sendMessage` / `receiveMessage`:
27
+
28
+ ```js
29
+ import { createPhotinoHost } from '@flexdesk/host';
30
+
31
+ // In a browser tab this is NULL_HOST. Keep a capability of your own by passing
32
+ // it in, or leave one out with null.
33
+ const host = createPhotinoHost({ state: myServerState, onEvent: (name, value) => {} });
34
+ ```
35
+
36
+ `dialogs.saveFile` falls back to a Blob download when the window refuses, and
37
+ the messages the C# side has to answer are in
38
+ [`src/host/photino_host.md`](../../src/host/photino_host.md).
39
+
40
+ **The dialogs a pywebview bridge answers.** `createPywebviewHost()` calls these
41
+ methods on `window.pywebview.api`, positionally. Each is optional: a missing
42
+ one makes its capability answer `null`, and FlexDesk degrades.
43
+
44
+ | Bridge method | Arguments | Reply | Behind |
45
+ |---|---|---|---|
46
+ | `save_file_dialog` | `(data, filename, kind)`: the file's text (`csv`) or base64 bytes (`png`), the suggested name, the type | `{ok: true, path}` \| `{ok: false, cancelled: true}` \| `{ok: false, error}` | `dialogs.saveFile` |
47
+ | `read_file_dialog` (0.17.0) | `(accept, kind, encoding)`: a list of extensions or MIME types to filter by (`['.json']`, may be empty), a label for the filter or `null`, and `'text'` or `'base64'` | `{ok: true, name, path, text}` (`'text'`, read as UTF-8) or `{ok: true, name, path, data}` (`'base64'`, the bytes) \| `{ok: false, cancelled: true}` \| `{ok: false, error}` | `dialogs.openFile` |
48
+
49
+ ### Opening a file: `pickFile` (0.17.0)
50
+
51
+ ```js
52
+ import { pickFile } from '@flexdesk/host';
53
+
54
+ const r = await pickFile(host, { accept: ['.json'], kind: 'json' }); // encoding: 'text' (default) or 'base64'
55
+ if (r.ok) load(JSON.parse(r.text));
56
+ else if (r.error) toast(r.error); // r.cancelled: say nothing
57
+ ```
58
+
59
+ `pickFile(host, {accept, kind, encoding})` takes `accept` as a list or as a
60
+ comma-separated string (`'.json,.csv'`); the host always receives the list
61
+ (`['.json', '.csv']`). It asks the host's
62
+ `dialogs.openFile` first, a new optional member of `dialogs`: a host whose
63
+ `dialogs` has only `saveFile` is still complete. Without it, or when it
64
+ answers `null` (no dialog behind it), a transient hidden
65
+ `<input type="file">` opens the browser's chooser, so the same call works in a
66
+ browser tab, under `NULL_HOST`. Either way it resolves one of
67
+
68
+ ```
69
+ {ok: true, name, path?, text} encoding 'text'
70
+ {ok: true, name, path?, data} encoding 'base64': the file's bytes
71
+ {ok: false, cancelled: true} the user closed the chooser
72
+ {ok: false, error} show it
73
+ ```
74
+
75
+ `path` comes only from a native host. The pywebview adapter maps `openFile` to
76
+ `read_file_dialog(accept, kind, encoding)` (the table above); the Photino
77
+ adapter sends `dialogs.openFile`, documented in
78
+ [`photino_host.md`](../../src/host/photino_host.md). The browser chooser reports a
79
+ cancel through the input's `cancel` event, which every current engine fires.
80
+
81
+ ### A window with its own chrome (0.15.0)
82
+
83
+ The same app runs three ways, and the page needs no switch for it:
84
+
85
+ | | title bar and borders | FlexDesk draws |
86
+ |---|---|---|
87
+ | a browser tab | the browser's | nothing |
88
+ | a Photino window, native frame | the OS's | nothing |
89
+ | a Photino window, custom frame | none: your top bar is the title bar | minimise / maximise / close, resize handles, drag |
90
+
91
+ ```js
92
+ import { WindowChromeController } from '@flexdesk/widgets';
93
+
94
+ new WindowChromeController({
95
+ host, // createPhotinoHost(), or any Host
96
+ topBar: '.my-top-bar', // drags the window (default .twm-global-top-bar)
97
+ controls: '.my-top-bar .right', // where the three buttons go (default: the bar's .twm-bar-right)
98
+ noDrag: '.my-search', // more that never starts a drag
99
+ }).initialize(); // resolves true only with custom chrome
100
+ ```
101
+
102
+ `host.window.chrome()` decides. The Photino host asks its native side once
103
+ (`host.describe`, its `frame` field) and answers `'custom'` only when the
104
+ window was made frameless, so a browser tab and a native-frame window get no
105
+ handles, no buttons and no CSS. With custom chrome, a drag on the top bar and
106
+ a press on a resize handle go to the host's `startNativeDrag()` /
107
+ `startNativeResize(edge)`. On Windows the native side hands them to the OS,
108
+ so the window snaps, takes Win+arrows and resizes like any other.
109
+ [`photino_host.md`](../../src/host/photino_host.md) has the native half: keep the
110
+ frame styles, return the whole window from `WM_NCCALCSIZE`, start gestures with
111
+ `WM_NCLBUTTONDOWN`. Don't use Photino's `SetChromeless`, which loses all of that.
112
+
113
+ ### Links out, and the browser's right-click menu (0.16.0)
114
+
115
+ In a native window a `target="_blank"` link or a `window.open()` opens nothing,
116
+ or a bare popup with no way back. Hand them to the system browser instead:
117
+
118
+ ```js
119
+ import { routeExternalLinks } from '@flexdesk/host';
120
+ import { suppressNativeContextMenu } from '@flexdesk/widgets';
121
+
122
+ routeExternalLinks({ host }); // a no-op without host.shell, so call it anywhere
123
+ suppressNativeContextMenu(); // only your own menus, plus the native one in text
124
+ ```
125
+
126
+ `routeExternalLinks` sends http(s) and mailto links that leave the app (a
127
+ `_blank` target, another origin, a middle click) and external `window.open`
128
+ calls to `host.shell.openExternal(url)`. That is a new optional host
129
+ capability, which `createPhotinoHost()` offers as `shell.openExternal`. If the
130
+ native side refuses, the original `window.open` runs. In a browser tab the
131
+ host has no `shell`, and nothing changes.
132
+
133
+ `suppressNativeContextMenu` prevents the browser's or webview's own menu (Back,
134
+ Reload, Print, Inspect) on any right-click that no handler claimed with
135
+ `preventDefault()`. It keeps the native menu in editable text and on selected
136
+ text, where Paste and Copy are the point, and wherever `allow(event)` says so.
137
+ Both calls return `{ dispose }`.
@@ -0,0 +1,32 @@
1
+ # Packages and entry points
2
+
3
+ > **Install:** see the [quickstart](../quickstart.md) · **Component pages:** [docs index](../README.md#components) · [All docs](../README.md)
4
+
5
+ ## Nine entry points
6
+
7
+ Because someone who wants a DataTable should not have to download a window
8
+ manager, and neither of them should drag in a plotting engine or a code editor.
9
+
10
+ | Entry | Size | What is in it |
11
+ |---|---|---|
12
+ | `@flexdesk/core` | 30 kB | Event bus, settings store, logging, state machine, state guard |
13
+ | `@flexdesk/host` | 5 kB | The [host port](host-port.md) |
14
+ | `@flexdesk/wm` | 166 kB | Tiling window manager: tabs, splits, desktops, command palette, taxonomy, `createShell()` |
15
+ | `@flexdesk/widgets` | 192 kB | DataTable, modals, managed windows, toasts, trees, panels, context menus, menu bars, a document outline, progress, a conversation view |
16
+ | `@flexdesk/charts` | 26 kB | Plotly wrapper, chart types, subplot layouts, downsampling, a plot grid, a plot-config editor, plot windows, injected series transforms |
17
+ | `@flexdesk/tiles` | 67 kB | Draggable/resizable widget grid, tile base class, widget registry, layout persistence, figure, chart and KPI tiles |
18
+ | `@flexdesk/editor` | 62 kB | Monaco loader + factory, multi-file tab bar, search, split panes, undo manager |
19
+ | `@flexdesk/canvas` | 14 kB | The canvas kernel: a pannable, zoomable surface, the edge router, a generic graph layer ([Flow editors](../components/flow-editors.md#flow-editors)) |
20
+ | `@flexdesk/flow` | 381 kB | The flow-editor kit and the canvas, outline and lane editors ([Flow editors](../components/flow-editors.md#flow-editors)) |
21
+
22
+ The sizes of `@flexdesk/canvas` and `@flexdesk/flow` are the entry files as
23
+ `node build.mjs` reported them for the flow editors' integration build; like
24
+ every entry they also import shared chunks. The other rows are older figures.
25
+
26
+ They share code through chunks, so importing two of them does **not** give you two
27
+ event buses — and therefore not two copies of every module-level singleton. A test
28
+ asserts each kernel symbol is defined exactly once across the whole `dist/`.
29
+
30
+ Two different things here are called "tiles", which is unfortunate but honest to
31
+ the code: `@flexdesk/wm` tiles the *window*; `@flexdesk/tiles` is a dashboard
32
+ *widget grid*.
@@ -0,0 +1,42 @@
1
+ # Peer dependencies: Plotly and Monaco
2
+
3
+ > **Used by:** [Charts](../components/charts.md), [Editor](../components/editor.md), the chart and figure [tiles](../components/tiles.md) · [All docs](../README.md)
4
+
5
+ ## Peer dependencies
6
+
7
+ `@flexdesk/charts` needs Plotly (4.4 MB). `@flexdesk/editor` needs Monaco (14 MB).
8
+ Neither is bundled, and **neither is guessed at**:
9
+
10
+ ```js
11
+ import { setPlotlySource } from '@flexdesk/charts';
12
+ import { setMonacoBasePath, setDefaultLanguage } from '@flexdesk/editor';
13
+
14
+ setPlotlySource('/vendor/plotly/plotly.min.js');
15
+ setMonacoBasePath('/vendor/monaco');
16
+ setDefaultLanguage('my-dsl'); // default: 'plaintext'
17
+ ```
18
+
19
+ There is deliberately **no default path**. A hardcoded `'vendor/monaco'` is one
20
+ application's directory layout, and for everyone else it is a 404 that surfaces as
21
+ an editor which silently never appears. `initMonaco()` throws instead.
22
+
23
+ `getEditorFactory()` and `initMonaco()` **reject** when no path was set, when
24
+ `vs/loader.js` fails to load, or when Monaco's AMD loader reports an error, so a
25
+ consumer can show a message instead of waiting forever. What happens next
26
+ depends on which of those it was:
27
+
28
+ - **`vs/loader.js` failed to load** (a 404, a wrong path): not sticky. Fix the
29
+ path (`setMonacoBasePath`, or pass it to `initMonaco`) and the next call
30
+ tries again.
31
+ - **Monaco's AMD loader reported an error** (`vs/editor/editor.main` or a
32
+ dependency failed): **final for the page**. Monaco's loader caches the failed
33
+ module and has no way to forget it, so every later call rejects with that
34
+ same first error, whatever path it is given. Fix the path and reload.
35
+
36
+ Loading is shared: if you call `initMonaco(path)` yourself, `getEditorFactory()`
37
+ uses that load and needs no `setMonacoBasePath()`. `monacoReady` only ever
38
+ resolves — it is the "Monaco has arrived" signal, not the place to learn that
39
+ an attempt failed.
40
+
41
+ If you already load Plotly yourself (a `<script>` tag that sets `window.Plotly`),
42
+ the chart loader short-circuits and you can skip `setPlotlySource`.