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.
Files changed (45) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +399 -3
  3. package/canvas.js +18 -0
  4. package/charts.js +21 -0
  5. package/core.js +14 -0
  6. package/css/base.css +23 -1
  7. package/css/flexdesk.css +61 -1
  8. package/dist/charts.js +3 -5
  9. package/dist/charts.js.map +1 -1
  10. package/dist/{chunk-Q36EPT2R.js → chunk-LWV23YVE.js} +1614 -138
  11. package/dist/chunk-LWV23YVE.js.map +7 -0
  12. package/dist/{chunk-MCIXFNVF.js → chunk-RMTRWWEO.js} +2 -2
  13. package/dist/flexdesk.css +61 -1
  14. package/dist/flow.js +2 -2
  15. package/dist/flow.js.map +2 -2
  16. package/dist/tiles.js +2 -2
  17. package/dist/widgets.js +186 -55
  18. package/dist/widgets.js.map +2 -2
  19. package/dist/wm.js +90 -10
  20. package/dist/wm.js.map +2 -2
  21. package/editor.js +20 -0
  22. package/flow.js +21 -0
  23. package/host.js +14 -0
  24. package/package.json +10 -1
  25. package/src/flow/canvas_editor/editor.js +1 -1
  26. package/src/tiling/command_palette.js +46 -7
  27. package/src/tiling/content_registry.js +3 -0
  28. package/src/tiling/entity_sources.js +10 -0
  29. package/src/tiling/wm.js +65 -3
  30. package/src/ui/components/cell_editor_host.js +100 -18
  31. package/src/ui/components/cell_editors.js +480 -0
  32. package/src/ui/components/cell_grid.js +121 -8
  33. package/src/ui/components/cell_paste.js +243 -0
  34. package/src/ui/components/data_table.js +96 -10
  35. package/src/ui/components/settings_view.js +192 -51
  36. package/src/ui/components/staged_edits.js +19 -1
  37. package/src/ui/components/tsv.js +74 -0
  38. package/src/ui/notification_center.js +5 -5
  39. package/tiles.js +27 -0
  40. package/widgets.js +69 -0
  41. package/wm.js +28 -0
  42. package/dist/chunk-Q36EPT2R.js.map +0 -7
  43. package/dist/chunk-QWQJUDPD.js +0 -610
  44. package/dist/chunk-QWQJUDPD.js.map +0 -7
  45. /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()`, Ctrl+C | Still about rows: `getSelection()` returns the rows the range touches, `onSelectionChange` hears them, and Ctrl+C and the copy menu copy them. `setSelection(indices)` selects the cells of those rows, first to last as shown. |
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. The default is a text box (`createTextEditor`). |
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;