flexdesk 0.17.0 → 0.17.2
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 +41 -0
- package/README.md +399 -3
- package/canvas.js +18 -0
- package/charts.js +21 -0
- package/core.js +14 -0
- package/css/base.css +23 -1
- package/css/flexdesk.css +61 -1
- package/dist/charts.js +3 -5
- package/dist/charts.js.map +1 -1
- package/dist/{chunk-Q36EPT2R.js → chunk-LWV23YVE.js} +1614 -138
- package/dist/chunk-LWV23YVE.js.map +7 -0
- package/dist/{chunk-MCIXFNVF.js → chunk-RMTRWWEO.js} +2 -2
- package/dist/flexdesk.css +61 -1
- package/dist/flow.js +2 -2
- package/dist/flow.js.map +2 -2
- package/dist/tiles.js +2 -2
- package/dist/widgets.js +186 -55
- package/dist/widgets.js.map +2 -2
- package/dist/wm.js +90 -10
- package/dist/wm.js.map +2 -2
- package/editor.js +20 -0
- package/flow.js +21 -0
- package/host.js +14 -0
- package/package.json +10 -1
- package/src/flow/canvas_editor/editor.js +1 -1
- package/src/tiling/command_palette.js +46 -7
- package/src/tiling/content_registry.js +3 -0
- package/src/tiling/entity_sources.js +10 -0
- package/src/tiling/wm.js +65 -3
- package/src/ui/components/cell_editor_host.js +100 -18
- package/src/ui/components/cell_editors.js +480 -0
- package/src/ui/components/cell_grid.js +121 -8
- package/src/ui/components/cell_paste.js +243 -0
- package/src/ui/components/data_table.js +96 -10
- package/src/ui/components/settings_view.js +192 -51
- package/src/ui/components/staged_edits.js +19 -1
- package/src/ui/components/tsv.js +74 -0
- package/src/ui/notification_center.js +5 -5
- package/tiles.js +27 -0
- package/widgets.js +69 -0
- package/wm.js +28 -0
- package/dist/chunk-Q36EPT2R.js.map +0 -7
- package/dist/chunk-QWQJUDPD.js +0 -610
- package/dist/chunk-QWQJUDPD.js.map +0 -7
- /package/dist/{chunk-MCIXFNVF.js.map → chunk-RMTRWWEO.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.2 (2026-10-22)
|
|
4
|
+
|
|
5
|
+
A patch release: everything below is additive. Every entry is documented in a
|
|
6
|
+
README section headed "(0.17.2)"; each entry links to it.
|
|
7
|
+
|
|
8
|
+
### Added and changed
|
|
9
|
+
|
|
10
|
+
- [An open single instance takes new props: `navigate(props)`](README.md#an-open-single-instance-takes-new-props-navigateprops-0172): opening a `single` kind that is already open with new props reveals it and calls its content's `navigate(props)` hook once; the props are recorded on the tab. An open that names no props only reveals.
|
|
11
|
+
- [A link to a kind nobody registered](README.md#a-link-to-a-kind-nobody-registered-0172): `wm.navigate` to a kind that is not registered logs one warning, changes nothing and returns `false`.
|
|
12
|
+
- [A palette row drawn disabled, with a reason](README.md#a-palette-row-drawn-disabled-with-a-reason-0172): a command with `enabled: () => false` is listed greyed (`aria-disabled`), does nothing when picked, and shows its `disabledReason` as a tooltip.
|
|
13
|
+
- [A catalog source only behind its prefix: `scopedOnly`](README.md#a-catalog-source-only-behind-its-prefix-scopedonly-0172): a catalog source with `scopedOnly: true` is searched only when its prefix is typed, in the flat and the grouped palette.
|
|
14
|
+
- [The settings categories outside the page: `SettingsCategories`](README.md#the-settings-categories-outside-the-page-settingscategories-0172): `SettingsView` can leave out its own category list and filter, and `SettingsCategories` (new export from `@flexdesk/widgets`) shows them anywhere, kept in step with the view.
|
|
15
|
+
- [Toast cards are styled and clickable](README.md#toast-cards-are-styled-and-clickable-0172): toast cards use `twm-notification` and `twm-notification--<severity>`, styled in flexdesk.css and base.css, with their entry animation defined there; cards take pointer events, so their buttons work (BUG-0033). A sheet that targeted `.notification` must target `.twm-notification`.
|
|
16
|
+
|
|
17
|
+
### Removed or renamed exports
|
|
18
|
+
|
|
19
|
+
None. Compared with 0.17.1, across every entry point (`core`, `widgets`, `wm`,
|
|
20
|
+
`host`, `canvas`, `flow`, `charts`, `tiles`, `editor`) and the package
|
|
21
|
+
`exports` map, exports were only added: `SettingsCategories` in `@flexdesk/widgets`.
|
|
22
|
+
Not an export but worth knowing: toast cards changed class from `notification`
|
|
23
|
+
to `twm-notification` (see above).
|
|
24
|
+
|
|
25
|
+
## 0.17.1 (2026-10-21)
|
|
26
|
+
|
|
27
|
+
A patch release: everything below is additive. Every entry is documented in a
|
|
28
|
+
README section headed "(0.17.1)"; each entry links to it.
|
|
29
|
+
|
|
30
|
+
### Added and changed
|
|
31
|
+
|
|
32
|
+
- [Built-in cell editors](README.md#built-in-cell-editors-0171): an editable DataTable column opens the editor for its type (`text`, `multitext`, `number`, `bool`, `choice`, `datetime`), each with a parse that refuses what it cannot read; `editors` replaces one per type or per column, with `editTypes`, `locale` and `editorMessages`. The editors and their parsers (`CELL_EDITORS`, `createNumberEditor`, `parseNumber`, `parseDateTime` and the rest of `cell_editors.js`) are exported from `@flexdesk/widgets`.
|
|
33
|
+
- [Copying cells as tab-separated values](README.md#copying-cells-as-tab-separated-values-0171): in cell mode Ctrl+C copies the selected cell range as the tab-separated text Excel and Google Sheets paste into cells; `toTSV` and `tsvCell` (from `@flexdesk/widgets`) write it.
|
|
34
|
+
- [Pasting a block from Excel](README.md#pasting-a-block-from-excel-0171): `pasteable: true` beside `staged` pastes a tab-separated block at the cursor cell, each value read by its column's editor, staged as one undo entry, with column and row overrun handling; `fromTSV` (from `@flexdesk/widgets`) reads it.
|
|
35
|
+
- [Building a modeler](README.md#building-a-modeler-0171): the canvas editor's modeler options shown working together, with `demo/flow_modeler.html` (linked from `demo/index.html`).
|
|
36
|
+
- [The `development` condition resolves in the installed package](README.md#the-development-condition-resolves-in-the-installed-package-0171): the nine root entry barrels are in `files`, so the `exports` map's `development` targets ship (BUG-0032). An `import` still resolves to `dist/`.
|
|
37
|
+
|
|
38
|
+
### Removed or renamed exports
|
|
39
|
+
|
|
40
|
+
None. Compared with 0.17.0, across every entry point (`core`, `widgets`, `wm`,
|
|
41
|
+
`host`, `canvas`, `flow`, `charts`, `tiles`, `editor`) and the package
|
|
42
|
+
`exports` map, exports were only added.
|
|
43
|
+
|
|
3
44
|
## 0.17.0 (2026-10-21)
|
|
4
45
|
|
|
5
46
|
Every feature below is documented in a README section headed "(0.17.0)"; each
|
package/README.md
CHANGED
|
@@ -237,6 +237,65 @@ with it. The taxonomy answers `isSingle(kind)`, `instanceKey(kind, props)` and
|
|
|
237
237
|
`singleReason(kind)`; the window manager answers `isSingleKind(kind)`,
|
|
238
238
|
`findInstance(kind, props)` and `revealInstance(kind, props)`.
|
|
239
239
|
|
|
240
|
+
### An open single instance takes new props: `navigate(props)` (0.17.2)
|
|
241
|
+
|
|
242
|
+
Revealing a `single` instance used to drop the props the open asked for: the
|
|
243
|
+
observatory kept showing the planet it was opened with. A content factory that
|
|
244
|
+
returns a `navigate(props)` hook from its mount is now handed them:
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
content: {
|
|
248
|
+
observatory: (el, props) => {
|
|
249
|
+
const view = mountObservatory(el, props);
|
|
250
|
+
return {
|
|
251
|
+
destroy: () => view.destroy(),
|
|
252
|
+
navigate: (next) => view.focusPlanet(next.planet), // already open: go there
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
wm.navigate('observatory', { planet: 'mars' }); // open: revealed, then navigate({ planet: 'mars' })
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
- **Every open path that reveals the instance calls it once**, after the
|
|
261
|
+
reveal, with a copy of the new props — `navigate`, the top nav,
|
|
262
|
+
`openInPrimary`, `openInTab*`, `splitLeafWith`, Back. The same props twice
|
|
263
|
+
are two calls; nothing is deduplicated, and no second instance is mounted.
|
|
264
|
+
- **The props are recorded on the instance's tab** (or its window's), so a
|
|
265
|
+
top-nav page restore or a reload mounts it with the latest. The tile is not
|
|
266
|
+
rebuilt by the write.
|
|
267
|
+
- **An open that names no props only reveals** — the top-nav button,
|
|
268
|
+
`openInPrimary(kind)`, `navigate(kind)`: it asks to see the instance, not to
|
|
269
|
+
empty it, so the hook is not called and the tab keeps its props.
|
|
270
|
+
- **A content without the hook is revealed as before**: its tab keeps its props
|
|
271
|
+
and nothing is called. A hook that throws is a console warning.
|
|
272
|
+
|
|
273
|
+
### A link to a kind nobody registered (0.17.2)
|
|
274
|
+
|
|
275
|
+
`wm.navigate(kind, props, opts)` returns `true` once it has routed the
|
|
276
|
+
navigation. A `kind` the shell's `content` map has no factory for is refused
|
|
277
|
+
instead, on every destination (`main`, `origin`, a new tab, a window, a split,
|
|
278
|
+
the legacy `target`):
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
if (!wm.navigate(link.kind, link.props)) {
|
|
282
|
+
// nothing opened: tell the user the link is dead
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
- **One `console.warn` naming the kind**, per call, and `false` back.
|
|
287
|
+
- **Nothing changes**: no tile, tab, window or desktop, no per-tab history, no
|
|
288
|
+
`onChange` — and no "no factory registered yet" placeholder tile, which is
|
|
289
|
+
what such a link used to leave behind.
|
|
290
|
+
- **No exception reaches the caller**, whatever `kind` is (`undefined`, `null`
|
|
291
|
+
and `''` included).
|
|
292
|
+
|
|
293
|
+
A shell that only navigates to kinds it registered sees no change; the return
|
|
294
|
+
value is new, so code that ignored it is unaffected. A split destination used
|
|
295
|
+
to hand back the new leaf's id; it now returns `true` like the others — call
|
|
296
|
+
`wm.splitLeafWith(...)` for the id. A restored layout that names an
|
|
297
|
+
unregistered kind still draws its placeholder: only `navigate` refuses.
|
|
298
|
+
|
|
240
299
|
### A pane that names what it shows: `ctx.setTitle` (0.12.1)
|
|
241
300
|
|
|
242
301
|
A content factory receives `(hostEl, props, ctx)`, and `ctx` carries, per pane,
|
|
@@ -450,6 +509,60 @@ topic.* The fallback comes from `HelpModal.open(id, { fallback })`
|
|
|
450
509
|
(`@flexdesk/widgets`), and you can pass one yourself. A topic id that nobody
|
|
451
510
|
provides now shows *Help topic not found.* where it used to raise a load error.
|
|
452
511
|
|
|
512
|
+
### A palette row drawn disabled, with a reason (0.17.2)
|
|
513
|
+
|
|
514
|
+
A palette row (a command from `commands`, or any other row) may carry
|
|
515
|
+
`enabled: () => boolean` and `disabledReason`. While `enabled()` returns false
|
|
516
|
+
the row is still listed, so it can say why it does nothing:
|
|
517
|
+
|
|
518
|
+
```js
|
|
519
|
+
palette: {
|
|
520
|
+
commands: () => [
|
|
521
|
+
{ id: 'save', label: 'Save', action: save,
|
|
522
|
+
enabled: () => dirty, disabledReason: 'Nothing to save' },
|
|
523
|
+
],
|
|
524
|
+
},
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
- The row gets the class `twm-cmdpal__item--disabled` and `aria-disabled="true"`,
|
|
528
|
+
and `disabledReason`, when given, is its `title` (the tooltip). The stylesheet
|
|
529
|
+
greys it.
|
|
530
|
+
- Picking it with a click, Enter, Ctrl+Enter or Shift+Enter runs nothing: not
|
|
531
|
+
its `action`, not `onPick`. The palette stays open on it.
|
|
532
|
+
- The arrow keys still move onto it and past it.
|
|
533
|
+
- `enabled()` is called again on every draw and every pick, so the row follows
|
|
534
|
+
your state while the palette is open. An `enabled()` that throws counts as
|
|
535
|
+
false.
|
|
536
|
+
- A row without `enabled` is drawn and picked as before. Both layouts (flat and
|
|
537
|
+
`grouped`) behave the same way.
|
|
538
|
+
|
|
539
|
+
`tests/command_palette_disabled.test.mjs` drives it in jsdom.
|
|
540
|
+
|
|
541
|
+
### A catalog source only behind its prefix: `scopedOnly` (0.17.2)
|
|
542
|
+
|
|
543
|
+
A source can be too broad to compete with everything else in the palette: help
|
|
544
|
+
topics, say, which would crowd every query. Mark it `scopedOnly` and its rows are
|
|
545
|
+
listed only once its prefix has made the type chip:
|
|
546
|
+
|
|
547
|
+
```js
|
|
548
|
+
createEntityCatalog({
|
|
549
|
+
sources: [
|
|
550
|
+
{ navKind: 'model', list: listModels, shape: toRow },
|
|
551
|
+
{ navKind: 'topic', list: listTopics, shape: toRow, scopedOnly: true },
|
|
552
|
+
],
|
|
553
|
+
aliases: { help: ['topic'] },
|
|
554
|
+
});
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
A plain typed query (`solver`) lists no `topic` row, in the flat and the grouped
|
|
558
|
+
layout alike: no row, no group header, no *Show all*. `help: solver` or
|
|
559
|
+
`topic: solver` lists its rows as any source's, and `help:` alone lists them all.
|
|
560
|
+
The mark is the palette's business: `loadAll`, `loadGrouped` and the tile menu
|
|
561
|
+
still return every source, a picked row reaches `onPick` as its `shape` made it,
|
|
562
|
+
and commands and `empty()` rows of the same kind are not held back. A source
|
|
563
|
+
without the option, or with `scopedOnly: false`, is searched exactly as in
|
|
564
|
+
0.17.1. A value that is not a boolean is refused with a `TypeError`.
|
|
565
|
+
|
|
453
566
|
### Your keys, not the window manager's: `keymap` (0.17.0)
|
|
454
567
|
|
|
455
568
|
The window manager binds F1..F12, Backspace, Ctrl+K, Ctrl+Tab, Ctrl/Shift+Arrows,
|
|
@@ -1324,10 +1437,10 @@ grid.getCellSelection(); // { anchor, focus, range }
|
|
|
1324
1437
|
| The keys (focused table) | Arrows move the cursor (Shift extends the range). Ctrl+arrow jumps to the edge of the block of filled cells, as a spreadsheet does. Home/End go to the row's first and last cell (Ctrl: the page's first and last cell). Tab moves right and wraps to the next row (Shift+Tab back); on the last cell (Shift+Tab: the first) the table does not take the key, so focus moves on out of the table. ArrowDown on the last row stays put. Ctrl+A selects every cell of the page; Escape collapses the range onto the cursor. The cursor stays on the page drawn and is scrolled into view; under `virtualize` its row is drawn. |
|
|
1325
1438
|
| `getCellSelection()` | `{anchor, focus, range}`. A cell is `{row, col, rowIdx}`: `row` is the position in the order shown (as `getDisplayedRows()` lists them, after sort and filter), `col` the data column, `rowIdx` the original row index. `range` is `{top, bottom, left, right, rows, columns}`: positions, the data columns at its edges, and the original row indices and data columns it covers (a hidden column inside it is not one of them). Null outside cell mode. |
|
|
1326
1439
|
| `setCellSelection(focus, anchor?)` | Puts the cursor on `{row, col}`, or selects from `anchor` to it; `null` clears. The row is brought into view. |
|
|
1327
|
-
| `getSelection()`, `setSelection()
|
|
1440
|
+
| `getSelection()`, `setSelection()` | Still about rows: `getSelection()` returns the rows the range touches and `onSelectionChange` hears them. `setSelection(indices)` selects the cells of those rows, first to last as shown. Ctrl+C and the copy menu copy cells, not rows, from 0.17.1 (below). |
|
|
1328
1441
|
| `onCellSelectionChange(sel)` | Called with `getCellSelection()` after every change. |
|
|
1329
1442
|
| `editable` | Which cells open an editor: `true`, an array of data column indices, or `(colIdx, row, rowIdx) => boolean`. Needs cell mode. F2, Enter, a double-click or a printable key opens it on the cursor cell (a printable key replaces the value with that character). In the editor Enter commits and moves down, Tab commits and moves right (Shift: up, left), Escape restores the cell and calls nothing; the focus leaving it commits without moving. `editCell(row, col)` opens one from code; `isEditing()` says whether one is open. |
|
|
1330
|
-
| `editors: {[colIdx \| type]: factory}` | The editor for a column, looked up by column index and then by type (`columnTypes`, else the detected `num`/`text`). A factory gets `{value, row, rowIdx, colIdx, column: {index, header, type}, initialText, commit(value), cancel()}` and returns an element or `{el, getValue?, focus?, destroy?}`. An input, select or textarea commits its `value` on Enter and Tab; any other editor calls `commit(value)` itself.
|
|
1443
|
+
| `editors: {[colIdx \| type]: factory}` | The editor for a column, looked up by column index and then by type (`columnTypes`, else the detected `num`/`text`). A factory gets `{value, row, rowIdx, colIdx, column: {index, header, type}, initialText, commit(value), cancel()}` and returns an element or `{el, getValue?, focus?, destroy?}`. An input, select or textarea commits its `value` on Enter and Tab; any other editor calls `commit(value)` itself. Without one, a column gets the built-in editor for its type, else a text box (`createTextEditor`); see [Built-in cell editors](#built-in-cell-editors-0171) for the editors, the `editTypes`, `locale` and `editorMessages` options and what a factory is told since 0.17.1. |
|
|
1331
1444
|
| `onCellCommit({rowIdx, colIdx, value, previous})` | Called exactly once per commit; `previous` is the value the row holds, and a value left as the cell showed it is no commit. Return nothing or `{ok: true}` (or a Promise of it) and the value is written into the row the table holds for `rowIdx` when it settles (`rows[rowIdx][colIdx]`, so a row put in by `updateRow` meanwhile keeps its other cells) and drawn. `{ok: false, error}`, a rejection or a throw leaves the typed value in the cell, drawn `twm-dt-cell--error` with the message as its `title` and `data-error`, until a later commit of that cell succeeds, the value the row holds is committed back (no call: nothing to save), or new rows arrive. While the Promise is out the cell is `twm-dt-cell--pending`. The editor always opens with the value the cell shows, a refused or pending one included. |
|
|
1332
1445
|
| While an editor is open | `render()`, and so `setData`, a sort or a page, waits and runs when the editor closes, so what is being typed is never thrown away. A commit that settles while another editor is open in its row marks its cell at once and draws the row when that editor closes. `updateRow` on the row the editor is in stores the data at once (`config.rows`, the window's row list) and returns `true`, and the row is drawn with it when the editor closes; the editor's cell is left as it is. A row that scrolls out of the virtual window commits its editor first. An editor with no `getValue` (a `{el}` around an element that is not an input, select or textarea) has no value the table can read: Enter, Tab and the focus leaving do nothing, and its row scrolling out cancels it, so what it holds is lost unless it has called `commit(value)` itself. |
|
|
1333
1446
|
| `headerMenu(colIdx, ctx) => items` | Items for the header's right-click menu, asked for the data column right-clicked (`ctx` is `{colIdx, header, hiddenColumns, table}`). Items are `{label, action, icon?, disabled?, danger?, title?}` or `{separator: true}`. Choosing one calls `onHeaderMenuAction(action, colIdx)`. With `columnChooser` the chooser's items follow behind a separator. `[]` or `null` shows only what would show without the hook: the chooser, or the page's own menu. Works in either selection mode. |
|
|
@@ -1338,6 +1451,100 @@ DOM), `CellEditorHost` (one cell's editor, opened and closed exactly once) and
|
|
|
1338
1451
|
`createTextEditor` (the default text box). The table's own half of cell mode
|
|
1339
1452
|
is internal.
|
|
1340
1453
|
|
|
1454
|
+
#### Built-in cell editors (0.17.1)
|
|
1455
|
+
|
|
1456
|
+
An editable column opens the editor for its type: `text`, `multitext`,
|
|
1457
|
+
`number`, `bool`, `choice` or `datetime`. Each reads what is typed with a
|
|
1458
|
+
parse, and a value that does not parse is refused on the cell. `editors`
|
|
1459
|
+
replaces any of them, for its type or its column only.
|
|
1460
|
+
|
|
1461
|
+
```js
|
|
1462
|
+
const grid = DataTable.mount(host, {
|
|
1463
|
+
headers: ['Item', 'Qty', 'Colour', 'Note'],
|
|
1464
|
+
rows,
|
|
1465
|
+
selectionMode: 'cells',
|
|
1466
|
+
editable: true,
|
|
1467
|
+
editTypes: [null, 'number', { type: 'choice', options: ['red', 'green'] }, 'multitext'],
|
|
1468
|
+
locale: 'de-DE', // '1.234,56' is 1234.56
|
|
1469
|
+
editorMessages: { number: 'Keine Zahl' },
|
|
1470
|
+
editors: { 3: myNoteEditor }, // by column index or by type
|
|
1471
|
+
onCellCommit: ({ value }) => save(value), // Qty commits a Number
|
|
1472
|
+
});
|
|
1473
|
+
```
|
|
1474
|
+
|
|
1475
|
+
| Option | What it does |
|
|
1476
|
+
|---|---|
|
|
1477
|
+
| The built-in editors | `text` a text box; `multitext` a text area (Enter is a new line, Ctrl/Cmd+Enter commits); `number` a text box that commits a Number; `bool` a checkbox that commits `true`/`false` when ticked or unticked (a space that opens it ticks it over); `choice` an `AutocompleteField` over the column's `options` that commits the option's value; `datetime` a text box that commits a Date. A blank number, date or choice commits `null`. A value opened and left as it was commits nothing. |
|
|
1478
|
+
| `editTypes` | Each column's editor type, by position (an array) or by data column index (an object): a name (`'text'`, `'multitext'`, `'number'`, `'bool'`, `'choice'`, `'datetime'`, or any name `editors` is keyed by) or `{type, options?, allowOther?, rows?}`. Unset, it is the column's `columnTypes` type, else the detected one, so a numeric column opens the number editor and a `date` column the datetime editor. `choice` offers `options` (values, or `{value, label}`), filtered as you type; a pick commits its value, and typed text matching an option's label or value (any case) commits that value. Other text is refused unless `allowOther`, when it is the value. With the list open, Escape closes the list only. `rows` is a `multitext` area's height in lines (3). Only editing reads `editTypes`: drawing, sorting and filtering are as without it. |
|
|
1479
|
+
| `locale` | The locale the number and datetime editors read and show text in (Intl's; unset: the runtime's). `'1.234,56'` is 1234.56 under `de-DE` and `'1,234.56'` is under `en-US`, and `'1 234,5'` is 1234.5 under `fr-FR` (any space, a no-break one included, where the locale groups with a space); a group separator anywhere but between groups of three digits, or a space inside the number anywhere else, is refused, never misread. Spaces around the text are ignored. A number opens without groups, in the locale's decimal separator. A datetime reads ISO 8601 (`2026-12-31`, `2026-12-31T08:30`, with `Z` or an offset an exact instant) in any locale, and day, month and year in the locale's order with a four-digit year and an optional time (`31/12/2026 08:30` under `en-GB`); when the locale's order makes no date but the day-month swap does, the swap is read. A date with no zone is in `dateTimeZone`'s. It opens as ISO. |
|
|
1480
|
+
| A value that does not parse | `abc` in a number, `31/02/2026` in a date, a choice outside the options: Enter and Tab are refused, the editor stays open, and the cell carries the message (`twm-dt-cell--error`, `title`, `data-error`) until the next keystroke. Nothing is committed. If the focus leaves (or the row scrolls out of the window) with it, the editor closes and the cell keeps the text, marked as a refused commit is, and `onCellCommit` is not called. Under `staged` nothing is staged; anything staged into the cell afterwards (a typed value, a fill, the value the row holds) ends the refusal. The words are `EDITOR_MESSAGES`, overridden by `editorMessages: {number, datetime, bool, choice}`. |
|
|
1481
|
+
| `editors` (0.17.1) | The editor for a column, looked up by column index and then by type (`editTypes`, else `columnTypes`, else the detected `num`/`text`; as named, then as the built-in editors name it: `num` is `number`, `date` is `datetime`). A factory gets `{value, row, rowIdx, colIdx, column: {index, header, type, ...the editTypes entry}, locale, utc, messages, initialText, commit(value, move?), cancel()}` and returns an element or `{el, getValue?, parse?, multiline?, focus?, destroy?}`. An input, select or textarea commits its `value` on Enter and Tab; any other editor calls `commit(value)` itself. With `parse()` (`=> {ok: true, value} \| {ok: false, error}`) Enter, Tab and the focus leaving commit what it reads, and a refusal is handled as the built-in editors' is. `multiline` keeps Enter for a new line (Ctrl/Cmd+Enter commits). An injected editor replaces the built-in one for its type (or its column) only. Without one, a column gets the built-in editor for its type, else a text box (`createTextEditor`). |
|
|
1482
|
+
|
|
1483
|
+
Exported from cell_editors.js for a consumer that builds its own grid or
|
|
1484
|
+
reads pasted text: the six editors as `CELL_EDITORS` (`{text, multitext,
|
|
1485
|
+
number, bool, choice, datetime}`, each `{create, parse}`: `create` a factory,
|
|
1486
|
+
`parse(text, {locale, utc, options, allowOther, messages})` the same reading
|
|
1487
|
+
of plain text, for a paste) with their parts: `parseNumber`, `parseDateTime`,
|
|
1488
|
+
`parseBool`, `parseChoice`, `formatNumberForEdit`, `formatDateTimeForEdit`,
|
|
1489
|
+
`numberSeparators`, `dateOrder`, `normalizeOptions`, `editorTypeOf`,
|
|
1490
|
+
`EDITOR_MESSAGES` and `createNumberEditor`, `createMultiTextEditor`,
|
|
1491
|
+
`createBoolEditor`, `createChoiceEditor`, `createDateTimeEditor`.
|
|
1492
|
+
`CellEditorHost` takes an `onRefuse(session, error, text, reason)` beside
|
|
1493
|
+
`onCommit` and `onCancel`, called when an editor closes holding a value that
|
|
1494
|
+
does not parse (without it, such a close is a cancel).
|
|
1495
|
+
|
|
1496
|
+
#### Copying cells as tab-separated values (0.17.1)
|
|
1497
|
+
|
|
1498
|
+
In cell mode Ctrl+C copies the cells selected, as the tab-separated text Excel
|
|
1499
|
+
and Google Sheets paste into cells.
|
|
1500
|
+
|
|
1501
|
+
| What | What it does |
|
|
1502
|
+
|---|---|
|
|
1503
|
+
| Ctrl+C, the copy menu, `copyToClipboard()` | With `copyable` (the default), copy the CELLS selected, not their rows: a 3×4 range is 3 lines of 4 tab-separated values, in the order shown after sort and filter, its shown columns only (a hidden column inside it is left out), with no header line. With no range beyond the cursor, the cursor cell's value alone. A cell is its RAW value, not the text it shows: no `formatValue`, typed-column format, rounding or `nullDisplay` (a Price drawn `1,234.50`, value 1234.5, copies as `1234.5`); the value the cell holds now, a staged, pending or refused one included; `null` is empty. Dates copy as ISO 8601: a `Date` as `toISOString()`, in a column declared `'date'` as its day `YYYY-MM-DD` in the table's zone (`dateTimeZone`); a number in a declared `'date'`/`'datetime'` column is read as epoch ms the same way; text is copied as it is. A value holding a tab, a newline or a quote is quoted the way Excel quotes it (`"say ""hi"""`), so Excel and Google Sheets paste it back into one cell. Cell mode has no CSV copy: the right-click menu's copy item is "Copy Cells" alone (the same text as Ctrl+C), Ctrl+Shift+C is not bound (the key is left to the page), and `copyToClipboard('csv')` copies nothing and warns. Without cell mode, Ctrl+C, Ctrl+Shift+C and the copy menu ("Copy Row(s)", "Copy as CSV") copy the selected rows with a header line, as in 0.16. |
|
|
1504
|
+
|
|
1505
|
+
`toTSV(matrix)` and `tsvCell(value)` write tab-separated text with Excel's
|
|
1506
|
+
quoting (a cell holding a tab, a newline or a quote is quoted and its quotes
|
|
1507
|
+
doubled; `null` is empty; no newline after the last row): the writer cell mode
|
|
1508
|
+
copies with.
|
|
1509
|
+
|
|
1510
|
+
#### Pasting a block from Excel (0.17.1)
|
|
1511
|
+
|
|
1512
|
+
With `pasteable: true` beside `staged`, Ctrl+V on the focused table pastes the
|
|
1513
|
+
tab-separated block Excel and Google Sheets copy at the CURSOR cell: it fills
|
|
1514
|
+
right over the columns shown and down over the rows in the order shown, each
|
|
1515
|
+
value read by its column's editor, and every cell is STAGED as one undo entry.
|
|
1516
|
+
|
|
1517
|
+
```js
|
|
1518
|
+
const grid = DataTable.mount(host, {
|
|
1519
|
+
headers: ['Item', 'Qty', 'When'],
|
|
1520
|
+
rows,
|
|
1521
|
+
selectionMode: 'cells',
|
|
1522
|
+
editable: (colIdx, row) => colIdx !== 0 || !row.locked,
|
|
1523
|
+
editTypes: [null, 'number', 'datetime'],
|
|
1524
|
+
staged: true,
|
|
1525
|
+
pasteable: true,
|
|
1526
|
+
onPasteOverrun: ({ rows }) => confirm(`Add ${rows} row(s)?`),
|
|
1527
|
+
onRowsAppend: ({ rows, errors }) => store.addRows(rows),
|
|
1528
|
+
onCommit: ({ edits }) => api.saveBatch(edits),
|
|
1529
|
+
});
|
|
1530
|
+
await grid.paste('Bolt\t4\t2026-12-31\nNut\t9\t2027-01-02'); // what Ctrl+V does
|
|
1531
|
+
```
|
|
1532
|
+
|
|
1533
|
+
| Option or method | What it does |
|
|
1534
|
+
|---|---|
|
|
1535
|
+
| `pasteable: true` | Needs `staged` (ignored with a warning without it). Ctrl+V on the focused table takes the browser's `paste` event (its `text/plain`, read by `fromTSV`); a paste inside an open editor or another field is that field's. The block lands at the cursor cell; a hidden column is stepped over. Each value is read by the parse of its column's editor: the built-in editor's for its type (`editTypes`, else `columnTypes`, else the detected one; `locale`, the zone and `editorMessages` as an editor reads them), or an injected `editors` factory's own `parse` property (`factory.parse = (text, ctx) => ({ok: true, value} \| {ok: false, error})`), else the text as it is. Every cell that reads is staged as ONE undo entry: one Ctrl+Z takes the whole paste back. A value equal to what the row holds is no change. The range then covers the block, the cursor on the cell it began at. Nothing is pasted while an editor is open, a commit is out or another paste waits. |
|
|
1536
|
+
| Columns past the last | Dropped. A console warning and a `'warn'` toast (`toast:show` on `services.eventBus`) name how many: "3 column(s) past the last column were not pasted." |
|
|
1537
|
+
| Rows past the last loaded row | The last loaded row is the last row in the order shown (filtered and sorted; for a server's page, the last row it sent). `onPasteOverrun({rows: n})` is asked first, `n` the rows past it. Anything but `true` (or a Promise of `true`) stages NOTHING; without the hook, such a paste stages nothing and warns. `true` hands the extra rows to `onRowsAppend({rows, errors})` and, once that settles, stages the rows that fit: `rows` are data rows (as long as `headers`, every pasted column read by its parse, the others `null`), and `errors` the values that did not read (`{row, colIdx, text, error}`, `row` an index into `rows`; that cell is `null`). Adding them is the consumer's: the table holds no row it was not given. A consumer that adds them with `setData({rows})` (the old rows first, the new ones after) keeps the paste's staged cells, because they are staged after it. Without `onRowsAppend` the extra rows are dropped with a warning. |
|
|
1538
|
+
| A value that does not parse | `abc` in a number column: its cell is marked as an editor's refusal is (`twm-dt-cell--error`, the message as `title` and `data-error`, the pasted text kept in the cell) and nothing is staged in it; the rest of the block still stages. Anything staged into the cell afterwards ends the mark. |
|
|
1539
|
+
| A cell `editable` refuses | Skipped: nothing is staged in it and its row is untouched. It is drawn `twm-dt-cell--paste-skipped` until the selection moves, the next paste or fill, or new rows. |
|
|
1540
|
+
| `paste(text)` | What Ctrl+V does, from code. Resolves `{rows, cols, staged, skipped, refused, droppedColumns, appended}` (`staged` the NUMBER of cells that changed; `skipped` `[{row, col, rowIdx}]`; `refused` the same with `text` and `error`; `appended` the rows handed to `onRowsAppend`), or `null` when nothing was pasted: no `pasteable`, empty text, no cursor, an editor open, a commit or another paste out, an overrun not accepted, or new rows while `onPasteOverrun` was asked. |
|
|
1541
|
+
|
|
1542
|
+
`fromTSV(text)` reads tab-separated text as a matrix of strings, as Excel
|
|
1543
|
+
writes it: character by character, so a quoted cell holding a tab, a newline
|
|
1544
|
+
or doubled quotes is one cell; a quote opens a quoted cell only at its start;
|
|
1545
|
+
CRLF lines read as LF ones; the newline after the last row (and any empty
|
|
1546
|
+
lines after it) is no row; empty text is `[]`. Exported beside `toTSV`.
|
|
1547
|
+
|
|
1341
1548
|
#### Staged edits, undo and redo, one commit (0.17.0)
|
|
1342
1549
|
|
|
1343
1550
|
With `staged: true` beside `editable`, a commit from an editor is STAGED
|
|
@@ -2086,7 +2293,7 @@ nor is a step whose type the palette does not offer (a start step) or that is
|
|
|
2086
2293
|
`unavailable`. What is copied is the graph's own text, `{nodes, connections}`
|
|
2087
2294
|
as `serialise()` writes it, so a graph from elsewhere pastes too. Ctrl/⌘+V
|
|
2088
2295
|
gives every pasted step a fresh id (as a new step gets one), keeps the lines
|
|
2089
|
-
between them, sets the copy off two grid steps right and down (each further
|
|
2296
|
+
between them (data lines too, under `connections` with `dataPorts: 'connect'`, from 0.17.1), sets the copy off two grid steps right and down (each further
|
|
2090
2297
|
paste of the same text one more), picks it, and is one `flow:paste` entry. A
|
|
2091
2298
|
paste right after Ctrl/⌘+X puts the steps back where they were; a cut is one
|
|
2092
2299
|
remove entry. A step whose title the flow already has gets a derived label,
|
|
@@ -2168,6 +2375,112 @@ Edge:
|
|
|
2168
2375
|
the step between its ends. Above the node limit the strip said so.
|
|
2169
2376
|
- A real pointer over a step highlighted its lines, and moving off cleared them.
|
|
2170
2377
|
|
|
2378
|
+
#### Building a modeler (0.17.1)
|
|
2379
|
+
|
|
2380
|
+
A typed node modeler (a dataflow, a signal chain, a diagram of blocks) is the
|
|
2381
|
+
canvas editor with its modeler options turned on. Every one of them is a
|
|
2382
|
+
`createCanvasEditor` option or a key on a catalogue type, off by default: an
|
|
2383
|
+
editor that sets none of them draws, binds and serialises exactly as before.
|
|
2384
|
+
None of them stores anything: the graph, the viewport and the values shown are
|
|
2385
|
+
the host's, handed in and handed back. Each edit they add is on the closed list
|
|
2386
|
+
`CANVAS_ACTIONS`, so one gesture is one undo entry. `demo/flow_modeler.html`
|
|
2387
|
+
(linked from `demo/index.html`) uses all of them together on a signal-flow
|
|
2388
|
+
diagram of sources, gains, a sum and displays.
|
|
2389
|
+
|
|
2390
|
+
| Option | What it gives | Details |
|
|
2391
|
+
|---|---|---|
|
|
2392
|
+
| `render`, `update`, `size`, `accent` (type keys) | A node that draws its own controls and values, at its own size, marked in a tone; `setCatalogue` adds a type while the editor is open | [Node bodies](#node-bodies-0170) |
|
|
2393
|
+
| `connections` (`dataPorts`, `policy`) with `accepts` / `provides` (port keys) | Data ports that connect, typed, under the host's rules, by drag or click; an empty drop picks a step that arrives connected | `connections` above |
|
|
2394
|
+
| `multiSelect` | Marquee, bulk move and delete, nudge, align and distribute | *Many picked* above |
|
|
2395
|
+
| `clipboard`, `prepareCopy` | Copy, cut, paste and duplicate of the picked steps and the lines among them | *Copy and paste* above |
|
|
2396
|
+
| `groups`, `notes` | Flat named frames round steps, and free-text notes | *Groups and notes* above |
|
|
2397
|
+
| `layout`, `rank` (type key) | Arrange by the kernel's layered layout, sources first and sinks last, or your own | `layout` above |
|
|
2398
|
+
| `edgeStyle`, `routing`, `hoverHighlight` | Lines in a tone, dashed, wider, with an arrow or a crow's foot and a label; bent round steps; lit while pointed at | `edgeStyle` above |
|
|
2399
|
+
| `viewport`, `onViewportChange`, `zoomControls`, `minimap` | Where the reader looks, kept by you; zoom buttons; an overview | `viewport` above |
|
|
2400
|
+
| `renameInPlace`, `inline` (type key) | A step's name and chosen settings edited on the node | [Edited on the node](#edited-on-the-node-0170) |
|
|
2401
|
+
| `setNodeValues`, `collapsible` | Values painted on the steps as often as you like; steps folded to their header | [Live values and folded steps](#live-values-and-folded-steps-0170) |
|
|
2402
|
+
|
|
2403
|
+
A type draws its own body (`render` once, `update` on every repaint), with its
|
|
2404
|
+
own `size` and `accent`; `inline` draws chosen settings on the node, and
|
|
2405
|
+
`rank` places it when Arrange runs the layered layout:
|
|
2406
|
+
|
|
2407
|
+
```js
|
|
2408
|
+
const IN = { name: 'in', direction: 'input', port_type: 'DATA', accepts: ['number'], maxConnections: 1 };
|
|
2409
|
+
const OUT = { name: 'out', direction: 'output', port_type: 'DATA', provides: ['number'] };
|
|
2410
|
+
const types = [
|
|
2411
|
+
{ type_id: 'source', label: 'Source', rank: 'source', inline: ['amplitude'], ports: [OUT],
|
|
2412
|
+
config_schema: { type: 'object', properties: { amplitude: { type: 'number', title: 'Amplitude' } } } },
|
|
2413
|
+
{ type_id: 'gain', label: 'Gain', size: { w: 224, h: 132 }, accent: 'amber', ports: [IN, OUT],
|
|
2414
|
+
render(body, node, ctx) {
|
|
2415
|
+
const range = Object.assign(document.createElement('input'), { type: 'range', min: -2, max: 2, step: 0.1 });
|
|
2416
|
+
range.addEventListener('change', () => ctx.setConfig({ k: Number(range.value) })); // one undo entry
|
|
2417
|
+
body.append(range);
|
|
2418
|
+
},
|
|
2419
|
+
update(body, node, ctx) { body.querySelector('input').value = node.config.k ?? 1; } },
|
|
2420
|
+
{ type_id: 'display', label: 'Display', rank: 'sink', ports: [IN],
|
|
2421
|
+
render: (body) => body.append(document.createElement('output')),
|
|
2422
|
+
update: (body, node, ctx) => { body.querySelector('output').textContent = ctx.values?.badge ?? '—'; } },
|
|
2423
|
+
];
|
|
2424
|
+
editor.setCatalogue(createStepCatalogue([...types, laterType], { categories })); // a type added after load
|
|
2425
|
+
```
|
|
2426
|
+
|
|
2427
|
+
The editor's options, each one independent of the others:
|
|
2428
|
+
|
|
2429
|
+
```js
|
|
2430
|
+
const editor = createCanvasEditor(host, {
|
|
2431
|
+
catalogue: createStepCatalogue(types, { categories }),
|
|
2432
|
+
connections: { // data ports connect, typed by accepts/provides
|
|
2433
|
+
dataPorts: 'connect',
|
|
2434
|
+
policy: ({ connection, graph }) => (closesLoop(graph, connection)
|
|
2435
|
+
? { ok: false, reason: 'That line would feed a block its own output.' } : null),
|
|
2436
|
+
},
|
|
2437
|
+
multiSelect: true, // marquee, bulk move, align, distribute
|
|
2438
|
+
clipboard: true, // or {read(), write(text)} to hold it yourself
|
|
2439
|
+
prepareCopy: (copy, { from, via }) => { copy.config.phase = 0; },
|
|
2440
|
+
groups: true, notes: true, // Ctrl+G frames, free-text notes
|
|
2441
|
+
layout: 'layered', // Arrange: columns by the lines, `rank` hints
|
|
2442
|
+
edgeStyle: (c, { source, target }) => ({ tone: 'teal', marker: 'arrow', width: 2,
|
|
2443
|
+
dashed: c.targetPort === 'b', label: source.label }),
|
|
2444
|
+
routing: 'obstacles', hoverHighlight: true,
|
|
2445
|
+
viewport: savedView, // opens there instead of fitting
|
|
2446
|
+
onViewportChange: (view) => { savedView = view; }, // once per finished pan or zoom
|
|
2447
|
+
zoomControls: true, minimap: true,
|
|
2448
|
+
renameInPlace: true, // double-click a title to rename it
|
|
2449
|
+
collapsible: true, // a fold button on every header
|
|
2450
|
+
});
|
|
2451
|
+
editor.load({ graph }, { viewport: savedView });
|
|
2452
|
+
setInterval(() => editor.setNodeValues({ mix: { badge: '0.42', tone: 'teal', ports: { out: '0.42' } } }), 250);
|
|
2453
|
+
```
|
|
2454
|
+
|
|
2455
|
+
`setNodeValues` records nothing and leaves the focus where it is, so a host
|
|
2456
|
+
can paint as often as its values change.
|
|
2457
|
+
|
|
2458
|
+
**Where it was checked.** `tests/flow_modeler_demo.test.mjs` mounts the demo
|
|
2459
|
+
under jsdom: what it shows, its live badges, and this section's list against
|
|
2460
|
+
the options. `tests/flow_kit_vocabulary.test.mjs` §4 holds every flow and
|
|
2461
|
+
canvas source the demo imports, and the demo itself, to the vocabulary gate.
|
|
2462
|
+
In headless Edge, `demo/flow_modeler_probe.mjs` opens the page by a real click
|
|
2463
|
+
on its link in `demo/index.html` and drives it with real input:
|
|
2464
|
+
- The bodies were drawn at their own size below their ports; every line left
|
|
2465
|
+
its output dot and its arrowhead ended on its input dot, within 1px; the
|
|
2466
|
+
group frame was round both sources and nothing else; a dashed line had a
|
|
2467
|
+
dash pattern and a label was the element at its own centre.
|
|
2468
|
+
- The badges changed in each of three seconds while a settings field, focused
|
|
2469
|
+
by a real click, kept the focus, and nothing was recorded.
|
|
2470
|
+
- A real drag from an output to an input added one line, dot to dot, as one
|
|
2471
|
+
undo entry. The gesture line appears above the canvas when a drag starts and
|
|
2472
|
+
moves the canvas down by its height, so the probe aims at the input where it
|
|
2473
|
+
is drawn once the line has started, as a reader would.
|
|
2474
|
+
- A real click and Shift+click and a real Ctrl+G drew a second frame round
|
|
2475
|
+
exactly the two steps; real Ctrl+C and Ctrl+V put two copies two grid steps
|
|
2476
|
+
right of and below the originals, with the line between them; a real click
|
|
2477
|
+
on Arrange ran every line left to right, the sources first and the displays
|
|
2478
|
+
last, with no two steps overlapping. Each was one undo entry.
|
|
2479
|
+
- A real Ctrl+Z after each, last first, put the steps back where they were on
|
|
2480
|
+
the canvas, took the copies, the second frame and the dragged line away, and
|
|
2481
|
+
left the graph the text it was before each step.
|
|
2482
|
+
|
|
2483
|
+
|
|
2171
2484
|
### The outline editor
|
|
2172
2485
|
|
|
2173
2486
|
`createOutlineEditor` draws a logic flow as a list of blocks: *If … otherwise*,
|
|
@@ -2834,6 +3147,52 @@ title; default "Settings"). The markup is `.twm-settings` >
|
|
|
2834
3147
|
`.twm-settings__row` (`--modified`); `flexdesk.css` styles it from tokens.
|
|
2835
3148
|
`tests/settings_view.test.mjs` covers it in jsdom.
|
|
2836
3149
|
|
|
3150
|
+
## The settings categories outside the page: `SettingsCategories` (0.17.2)
|
|
3151
|
+
|
|
3152
|
+
A `SettingsView` made with `sidebar: false` draws no sidebar: no heading, no
|
|
3153
|
+
filter box, no category list and no *Reset all*, only the chosen category's
|
|
3154
|
+
settings (or the filter's matches). `new SettingsCategories({ view })` (in
|
|
3155
|
+
`@flexdesk/widgets`) draws those parts for that view wherever it is mounted,
|
|
3156
|
+
for example in an app's left navigator:
|
|
3157
|
+
|
|
3158
|
+
```js
|
|
3159
|
+
import { SettingsView, SettingsCategories } from '@flexdesk/widgets';
|
|
3160
|
+
|
|
3161
|
+
const view = new SettingsView({ store, eventBus: bus, sidebar: false });
|
|
3162
|
+
view.mount(pageHost);
|
|
3163
|
+
const cats = new SettingsCategories({ view });
|
|
3164
|
+
cats.mount(navigatorHost);
|
|
3165
|
+
|
|
3166
|
+
const off = view.subscribe(({ category, query }) => { /* e.g. a tab title */ });
|
|
3167
|
+
// …
|
|
3168
|
+
off();
|
|
3169
|
+
cats.dispose();
|
|
3170
|
+
view.dispose();
|
|
3171
|
+
```
|
|
3172
|
+
|
|
3173
|
+
It keeps no state of its own. Typing in its filter box is `view.setQuery()`,
|
|
3174
|
+
clicking a category is `view.selectCategory()` and its *Reset all* is
|
|
3175
|
+
`view.resetAll()`, which asks the view's `confirm` first and changes nothing
|
|
3176
|
+
unless it resolves true. It redraws from the view whenever the view's category,
|
|
3177
|
+
filter or registered categories change, so the two agree whichever side moved
|
|
3178
|
+
them: `view.selectCategory('window')` marks that category in the list, and
|
|
3179
|
+
`view.setQuery('shadow')` puts the text in its filter box. As in the view's own
|
|
3180
|
+
sidebar, choosing a category clears the filter, and no category is marked while
|
|
3181
|
+
the filter is in effect. Its clicks and keystrokes act once the view is mounted.
|
|
3182
|
+
|
|
3183
|
+
Two methods on `SettingsView` serve it and anything else that follows the view:
|
|
3184
|
+
`subscribe(fn)` calls `fn({ category, query })` after each such change and
|
|
3185
|
+
answers the unsubscribe; `isFiltering()` is true while the filter's trimmed
|
|
3186
|
+
text is not empty. `settingsViewContent({ …, sidebar: false })` passes the
|
|
3187
|
+
option through. A view made without `sidebar` (or with `sidebar: true`) draws
|
|
3188
|
+
exactly as in 0.17.1.
|
|
3189
|
+
|
|
3190
|
+
The markup is `.twm-settings-categories` > `.twm-settings__search`,
|
|
3191
|
+
`.twm-settings__nav` (`button.twm-settings__cat`, `--active`) and
|
|
3192
|
+
`.twm-settings__reset-all`, the same parts and classes as the sidebar's;
|
|
3193
|
+
`flexdesk.css` styles it, leaving its width and padding to the host.
|
|
3194
|
+
`tests/settings_view.test.mjs` §11–§14 cover it.
|
|
3195
|
+
|
|
2837
3196
|
## A step wizard: `Wizard` (0.17.0)
|
|
2838
3197
|
|
|
2839
3198
|
`new Wizard({ steps, data, onFinish, onCancel, labels })` (in `@flexdesk/widgets`)
|
|
@@ -3229,6 +3588,25 @@ job.on('end', () => nc.update(id, { progress: 1, severity: 'success', message: '
|
|
|
3229
3588
|
|
|
3230
3589
|
`tests/notification_progress.test.mjs` holds it in jsdom.
|
|
3231
3590
|
|
|
3591
|
+
### Toast cards are styled and clickable (0.17.2)
|
|
3592
|
+
|
|
3593
|
+
A toast card is `.twm-notification` with `.twm-notification--<severity>`
|
|
3594
|
+
(`info`, `success`, `warn`, `error`, `fatal`), and the "+N more" summary is
|
|
3595
|
+
`.twm-notification.twm-notification--summary`. `flexdesk.css` gives the card its
|
|
3596
|
+
background, a left border in the severity's colour (`--color-success`,
|
|
3597
|
+
`--color-warning`, `--color-danger`; info keeps the blue) and the entry
|
|
3598
|
+
animation `twm-notification-in`, which the sheet now defines. The card sets
|
|
3599
|
+
`pointer-events: auto` over the root's `none`, so its buttons and its
|
|
3600
|
+
click-to-close work.
|
|
3601
|
+
|
|
3602
|
+
Until 0.17.2 the card carried the bare `notification` / `notification--<severity>`
|
|
3603
|
+
classes, which no FlexDesk sheet styled: the card had no background and no
|
|
3604
|
+
border, and inherited `pointer-events: none`, so its buttons could not be
|
|
3605
|
+
clicked. A consumer stylesheet that targeted `.notification` should target
|
|
3606
|
+
`.twm-notification` instead. The live region is `.twm-notification-center__live-region`.
|
|
3607
|
+
`tests/notification_toast_styles.test.mjs` holds the classes, the cascade and
|
|
3608
|
+
the sheets.
|
|
3609
|
+
|
|
3232
3610
|
### Alerts with history: a log, a pane, a tab host and a bell (0.14.0)
|
|
3233
3611
|
|
|
3234
3612
|
A toast is the moment. The **alert log** keeps what came in until it is
|
|
@@ -4140,6 +4518,24 @@ build so that a consumer with no bundler can load the library through a plain im
|
|
|
4140
4518
|
map. See `.gitignore` — it says so there too, because it is the kind of thing someone
|
|
4141
4519
|
"tidies up" later.
|
|
4142
4520
|
|
|
4521
|
+
### The `development` condition resolves in the installed package (0.17.1)
|
|
4522
|
+
|
|
4523
|
+
Every JavaScript entry in the `exports` map has two conditions: `import`, the
|
|
4524
|
+
prebuilt bundle under `dist/`, and `development`, the source barrel at the
|
|
4525
|
+
package root (`core.js`, `host.js`, `wm.js`, `widgets.js`, `charts.js`,
|
|
4526
|
+
`tiles.js`, `editor.js`, `canvas.js`, `flow.js`), which re-exports `src/`.
|
|
4527
|
+
Up to 0.17.0 those nine barrels were missing from `files`, so the published
|
|
4528
|
+
package did not contain them and a resolver that took the `development` branch
|
|
4529
|
+
(`require.resolve` under `--conditions=development`, for one) got a path to
|
|
4530
|
+
nothing. From 0.17.1 they ship, beside the `src/` they import.
|
|
4531
|
+
|
|
4532
|
+
Nothing about which file an `import` resolves to has changed: `import` is
|
|
4533
|
+
listed first, so an ES import still gets `dist/`, with or without the
|
|
4534
|
+
`development` condition.
|
|
4535
|
+
|
|
4536
|
+
A suite (`tests/exports_packed.test.mjs`) runs `npm pack --dry-run` and fails
|
|
4537
|
+
if any target of any condition in the `exports` map is not in the tarball.
|
|
4538
|
+
|
|
4143
4539
|
## Licence
|
|
4144
4540
|
|
|
4145
4541
|
MIT. See [`LICENSE`](LICENSE) and [`THIRD_PARTY_LICENSES.md`](THIRD_PARTY_LICENSES.md).
|
package/canvas.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @flexdesk/canvas — the canvas kernel: a pannable, zoomable surface with
|
|
3
|
+
* marquee selection, the edge router (orthogonal routing, crow's feet, a
|
|
4
|
+
* loop's return line, a layered Arrange) and a generic graph layer (nodes and
|
|
5
|
+
* their ports, connection legality with a policy hook, snapshot-free history
|
|
6
|
+
* over a closed action list).
|
|
7
|
+
*
|
|
8
|
+
* It knows nothing about what the nodes ARE. A consumer's own rules — which
|
|
9
|
+
* ports connect, what a collapsed node shows — are subclasses and policies it
|
|
10
|
+
* passes in.
|
|
11
|
+
*
|
|
12
|
+
* HAND-WRITTEN, not generated. The other barrels say "GENERATED by
|
|
13
|
+
* scripts/gen_barrels.py", and that script is not in this repository (nor is
|
|
14
|
+
* scripts/css_extract.py), so this one is kept by hand: add a module here when
|
|
15
|
+
* you add one under src/canvas/.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export * from './src/canvas/index.js';
|
package/charts.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @flexdesk/charts — Plotly-backed charts: the wrapper, the chart types, subplot layouts,
|
|
3
|
+
* downsampling, the plot grid, the plot-config editor, and the two plot windows.
|
|
4
|
+
*
|
|
5
|
+
* Plotly is a PEER dependency — 4.4 MB this bundle deliberately does NOT
|
|
6
|
+
* contain. Call setPlotlySource() with the path to your copy.
|
|
7
|
+
*
|
|
8
|
+
* GENERATED by scripts/gen_barrels.py. Do not hand-edit.
|
|
9
|
+
*
|
|
10
|
+
* 9 modules.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export * from './src/charts/chart_types.js';
|
|
14
|
+
export * from './src/charts/data_series_plot_window.js';
|
|
15
|
+
export * from './src/charts/downsample.js';
|
|
16
|
+
export * from './src/charts/plot_config.js';
|
|
17
|
+
export * from './src/charts/plot_config_editor.js';
|
|
18
|
+
export * from './src/charts/plot_grid.js';
|
|
19
|
+
export * from './src/charts/plot_popout_window.js';
|
|
20
|
+
export * from './src/charts/plotly_wrapper.js';
|
|
21
|
+
export * from './src/charts/series_transforms.js';
|
package/core.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @flexdesk/core — The kernel: event bus, settings, logging, state machine, state guard.
|
|
3
|
+
*
|
|
4
|
+
* GENERATED by scripts/gen_barrels.py. Do not hand-edit.
|
|
5
|
+
*
|
|
6
|
+
* 6 modules.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export * from './src/core/event_bus.js';
|
|
10
|
+
export * from './src/core/logging.js';
|
|
11
|
+
export * from './src/core/settings.js';
|
|
12
|
+
export * from './src/core/state_guard.js';
|
|
13
|
+
export * from './src/core/state_guard_config.js';
|
|
14
|
+
export * from './src/core/state_machine.js';
|
package/css/base.css
CHANGED
|
@@ -275,6 +275,28 @@
|
|
|
275
275
|
flex-direction: column;
|
|
276
276
|
gap: var(--space-lg);
|
|
277
277
|
}
|
|
278
|
+
/* A toast card. The root takes no pointer events (it overlays the page), so the
|
|
279
|
+
* card takes them back: without this its buttons cannot be clicked (BUG-0033). */
|
|
280
|
+
.twm-notification {
|
|
281
|
+
pointer-events: auto;
|
|
282
|
+
background: #1f1f20;
|
|
283
|
+
border: 1px solid #2f2f31;
|
|
284
|
+
border-left: 4px solid #4fc1ff;
|
|
285
|
+
border-radius: var(--radius-lg);
|
|
286
|
+
padding: var(--space-lg) 14px;
|
|
287
|
+
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
|
|
288
|
+
animation: twm-notification-in 160ms ease-out;
|
|
289
|
+
min-width: 280px;
|
|
290
|
+
cursor: pointer;
|
|
291
|
+
}
|
|
292
|
+
.twm-notification--success { border-left-color: var(--color-success, #3fb950); }
|
|
293
|
+
.twm-notification--warn { border-left-color: var(--color-warning, #ff9800); }
|
|
294
|
+
.twm-notification--error,
|
|
295
|
+
.twm-notification--fatal { border-left-color: var(--color-danger, #c74e4e); }
|
|
296
|
+
@keyframes twm-notification-in {
|
|
297
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
298
|
+
to { opacity: 1; transform: translateY(0); }
|
|
299
|
+
}
|
|
278
300
|
.twm-notification__header {
|
|
279
301
|
display: flex;
|
|
280
302
|
align-items: center;
|
|
@@ -425,7 +447,7 @@
|
|
|
425
447
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
|
|
426
448
|
display: flex;
|
|
427
449
|
flex-direction: column;
|
|
428
|
-
animation: notification-in 160ms ease-out;
|
|
450
|
+
animation: twm-notification-in 160ms ease-out;
|
|
429
451
|
}
|
|
430
452
|
.twm-notification-history__header {
|
|
431
453
|
display: flex;
|