flexdesk 0.10.0 → 0.11.1

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/README.md CHANGED
@@ -794,6 +794,97 @@ disposed.
794
794
  dispose when the table is put back. Before 0.7.0, a table taken out, put back
795
795
  and taken out again could be disposed by the first departure's timer.
796
796
 
797
+ ### DataTable: your own right-click items, a render hook, the keyboard, a remembered page
798
+
799
+ These options let an application drop a private copy of the DataTable kept for
800
+ them. Every one is off until you set it, and a table that sets none of them
801
+ draws and behaves as in 0.9.1.
802
+
803
+ ```js
804
+ const queue = DataTable.mount(host, {
805
+ headers, rows, pagination: true, sortable: true, filterable: true,
806
+ stateStore, persistKey: `queue:${view}`,
807
+ persistPage: !!savedView, // an ad-hoc view shares its key: start it on page 1
808
+ getRowKey: (row) => row[0],
809
+ contextMenuItems: (row, rowIdx, ctx) => [
810
+ { label: ctx.selection.length > 1 ? `Open ${ctx.selection.length}` : 'Open',
811
+ icon: 'open_in_new', action: 'open' },
812
+ { separator: true },
813
+ { label: 'Delete…', icon: 'delete', action: 'delete', danger: true,
814
+ disabled: !canDelete(row), title: canDelete(row) ? '' : 'Closed records stay' },
815
+ ],
816
+ onContextMenuAction: (action, rowIdx, row, ctx) => run(action, ctx.selectedRows),
817
+ onRender: (t) => status.textContent = `${t.getDisplayedRows().length} of ${rows.length} shown`,
818
+ keyboardNav: true,
819
+ onRowKeyDown: (rowIdx, row, ev) => {
820
+ if (row && ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) { openElsewhere(row); return true; }
821
+ if (row && ev.altKey && ev.key.toLowerCase() === 't') { openInTab(row); return true; }
822
+ },
823
+ onRowActivate: (i, row) => open(row),
824
+ activateOn: ['click', 'enter'],
825
+ });
826
+ queue.focus();
827
+ ```
828
+
829
+ **The right-click menu**
830
+
831
+ | Option | What it does |
832
+ |---|---|
833
+ | `contextMenuItems` | Your items in the table's own right-click menu: an array, or `(row, rowIdx, ctx) => items`, asked on every right-click of a row. An item is `{ label, icon, action, disabled, danger, title }` or `{ separator: true }`, drawn like the copy items (`disabled` greys it out, `danger` draws it red, `title` says why). `ctx` is `{ rowIdx, row, colIdx, value, selection, selectedRows }`: the row and the cell under the pointer (`colIdx` is null on the row-number column) and the selection, which by then includes the row. Your items come first; with `copyable`, a separator and the two copy items follow. Set without `copyable`, the menu has your items only, and a row you give no items gets the browser's own menu. |
834
+ | `onContextMenuAction(action, rowIdx, row, ctx)` | Called when one of your items is chosen, after the menu has closed. The copy items never call it, so an action may be named anything. |
835
+
836
+ **What the table shows**
837
+
838
+ | Option | What it does |
839
+ |---|---|
840
+ | `onRender(table)` | Called at the end of every render: the first one, `setData`, a sort, a filter, a page. A hook that throws is reported on the console and the table still draws. |
841
+ | `getDisplayedRows()` | The rows the table shows, filtered and sorted, across every page and not only the one on screen. It returns a copy. With server-side paging it is the page held. |
842
+
843
+ **The keyboard**
844
+
845
+ | Option | What it does |
846
+ |---|---|
847
+ | `keyboardNav: true` | On the focused table, ArrowUp and ArrowDown move the selection one row through the page shown, in the order shown, and Home and End go to its first and last row. With Shift they select the range from the anchor, as Shift-click does (not on a `selectable: 'single'` table). The row is scrolled into view and `onSelectionChange` is told. Keys pressed in a control inside the table are left alone. Needs `selectable`. |
848
+ | `onRowKeyDown(rowIdx, row, ev)` | Called for every key pressed on the focused table, before the table's own keys, with the row Enter would open (the one row selected, the selection's anchor, or else the active row) or `null`. Return `true` to take the key: its default is prevented and nothing after it sees it, neither the table (Enter's `onRowActivate`, the arrows, copy) nor the page. Use it for the keys `activateOn` does not take, such as Ctrl+Enter or Alt+T. |
849
+ | `focus()` | Puts the keyboard focus on the table's body without scrolling the page. Returns `false` when nothing is drawn yet. |
850
+
851
+ **A remembered page**
852
+
853
+ | Option | What it does |
854
+ |---|---|
855
+ | `persistPage: true` | With `persistKey` and `stateStore`, saves the client page with the sort and filters, and opens the table on it again. If the rows have shrunk since, it opens on the last page. A new filter, and a sort under `resetPageOnSort`, go to the first page and save that. Rows given after mounting (`rows: []`, then `setData`) still open on the saved page. Server-side paging is not affected: its page is your `offset`. Off by default, because a key shared by different row sets, such as ad-hoc filters or search results, must not reopen on a page of the previous list. Without it, the saved state is exactly what 0.9.1 saves. |
856
+
857
+ ### DataTable: columns hidden until somebody asks for them
858
+
859
+ A column can be part of the row data without being drawn. People choose which
860
+ columns they see from the header's right-click menu, and the choice is
861
+ remembered. A table that sets none of these options draws every column, as in
862
+ 0.9.1.
863
+
864
+ ```js
865
+ const list = DataTable.mount(host, {
866
+ headers: ['Id', 'Summary', 'Status', ...customFields.map((f) => f.label)],
867
+ rows,
868
+ hiddenColumns: customFields.map((_, i) => 3 + i), // not shown until picked
869
+ columnChooser: true,
870
+ stateStore, persistKey: 'bugs:queue',
871
+ onColumnsChange: (hidden) => console.log('hidden now', hidden),
872
+ });
873
+ list.setColumnVisible(3, true);
874
+ ```
875
+
876
+ | Option | What it does |
877
+ |---|---|
878
+ | `hiddenColumns: [colIdx, ...]` | These columns are not drawn: no header, no filter box, no cell, no width in the fit, and they are left out of a copy and of `downloadCSV()`. They stay in the row data, so `renderCell`, `contextMenuItems`, `sortBy` and `setFilter` still see them. The last column shown can never be hidden; a list that hides all of them keeps the first. `setData({ hiddenColumns })` applies a new list. Column indices are positions, so a new `headers` list keeps the set by position. |
879
+ | `columnChooser: true` | A right-click on the header opens a menu that lists every column, with a check mark on those shown. Choosing one shows or hides it. The last column shown is greyed out and says why. Without the option the header keeps the browser's menu. |
880
+ | `setColumnVisible(colIdx, on)` | Shows or hides a column from code. Returns whether anything changed. The widths of the other columns, dragged or fitted, stay with their columns; a column shown again is measured. |
881
+ | `getHiddenColumns()` | The hidden columns' indices, ascending. |
882
+ | `onColumnsChange(hidden)` | Called after every change of the set: the chooser, `setColumnVisible` and `setData({ hiddenColumns })`. |
883
+ | With `persistKey` | The set is saved with the sort and the filters and restored on mount. A saved set wins over `hiddenColumns`, so a column somebody chose stays chosen. A table without these options saves exactly what 0.9.1 saves. |
884
+
885
+ A column sorted on and then hidden keeps the order; its arrow comes back with
886
+ the column.
887
+
797
888
  ### Scrollbars
798
889
 
799
890
  `installAutoScrollbars(root)` (from `@flexdesk/widgets`) replaces the browser's
@@ -1426,6 +1517,105 @@ card starts no native drag (a `dragstart` counter at 0) and moves nothing. Real
1426
1517
  arrows, Delete, Ctrl+Z and Backspace do what is said above, and three real
1427
1518
  keystrokes ask the preview once.
1428
1519
 
1520
+ ## Forms: `openForm`
1521
+
1522
+ `openForm({ title, fields, defaults, submitLabel })` (in `@flexdesk/widgets`)
1523
+ opens a modal form and resolves with `{ [name]: value }`, or `null` when it is
1524
+ cancelled. Fields are `text`, `number`, `password`, `select`, `textarea`,
1525
+ `checkbox` and `picker`; `{ section: 'Label' }` entries are headings. Each of
1526
+ the four additions below is opt-in, and a form that uses none of them renders
1527
+ and submits exactly as in 0.9.1.
1528
+
1529
+ ### A form that adapts: `onFieldChange`
1530
+
1531
+ ```js
1532
+ openForm({
1533
+ fields: [
1534
+ { name: 'type', label: 'Type', type: 'select', options: ['bug', 'story'] },
1535
+ { section: 'Story', name: 'story' }, // a NAMED section can be hidden
1536
+ { name: 'points', label: 'Points', type: 'number', required: true },
1537
+ ],
1538
+ onFieldChange: (name, value, api) => {
1539
+ const story = api.get('type') === 'story';
1540
+ api.setVisible('story', story);
1541
+ api.setVisible('points', story);
1542
+ },
1543
+ });
1544
+ ```
1545
+
1546
+ `onFieldChange(name, value, api)` is called once when the form opens (with
1547
+ `name === null`, so the form starts consistent with its defaults) and after
1548
+ every field's `change`. `api` addresses fields and named sections by name:
1549
+
1550
+ - `setVisible(name, on)` hides or shows the row. A hidden row keeps its value
1551
+ and is still submitted, but it is not asked about: its `required` and its
1552
+ `validator` stand down until it is shown again.
1553
+ - `get(name)` reads the control as it stands: a checkbox as a boolean,
1554
+ anything else as its string.
1555
+ - `set(name, value)` writes it. It does not call `onFieldChange` back, so a
1556
+ hook that fills one field from another cannot loop.
1557
+
1558
+ A hook that throws is logged and the form carries on.
1559
+
1560
+ ### Hidden rows stay hidden
1561
+
1562
+ `.twm-modal__row` is a grid, and a grid rule beats the `display: none` that
1563
+ the `hidden` attribute gets from the browser, so a hidden row stayed on
1564
+ screen. `flexdesk.css` now has `.twm-modal__row[hidden],
1565
+ .twm-modal__section[hidden] { display: none !important }`, and `setVisible`
1566
+ also sets the row's inline `display`, so it holds under a stylesheet of your
1567
+ own as well.
1568
+
1569
+ ### A value you look up: `type: 'picker'`
1570
+
1571
+ ```js
1572
+ { name: 'parent', label: 'Parent', type: 'picker', required: true,
1573
+ pick: async (current) => ({ value: 'EPIC-0001', label: 'EPIC-0001 — Auth rewrite' }),
1574
+ displayFor: (value) => titleOf(value) }
1575
+ ```
1576
+
1577
+ The row shows a read-only box and a search button. The button, or a click on
1578
+ the box, awaits `pick(currentValue)`. Return `{ value, label }` (the label is
1579
+ shown, the value is submitted), a bare value (shown through `displayFor`, or
1580
+ as itself), or `null` to leave the field as it was. `displayFor(value)` gives
1581
+ the text for the value the form opens with and for `api.set`. The value is
1582
+ kept in a hidden input, which the browser's own `required` check skips, so
1583
+ `required: true` on a picker is checked by the form instead. The markup is
1584
+ `.twm-picker` > `.twm-picker__display` + `.twm-btn.twm-picker__btn`.
1585
+
1586
+ ### A select you may type into: `create`
1587
+
1588
+ A `select` with a `create` spec is a type-ahead combobox: pick an option or
1589
+ type a new value. `create: { onCreate, hint }` makes the typed value into an
1590
+ entity before the form resolves, and shows "↳ will create *hint* …" while you
1591
+ type. `create: true` is the plain case: the typed value is submitted as typed,
1592
+ with no hint, because nothing is created. Until now `create: true` failed the
1593
+ submit with "onCreate is not a function".
1594
+
1595
+ `tests/open_form.test.mjs` mounts the real `openForm` in jsdom and covers all
1596
+ four, plus a plain form submitting as before.
1597
+
1598
+ ## Notifications: `NotificationCenter`
1599
+
1600
+ Toasts, from `toast:show` on the bus (or `toastInfo` / `toastWarn` /
1601
+ `toastError` / `toastSuccess`) or `show()`. A payload with `persistent: true`
1602
+ stays until its × is clicked. `history` keeps every notification shown, and
1603
+ `NotificationHistory` puts it behind a bell in the bottom bar.
1604
+
1605
+ *New in 0.11.0:* **`overflow: 'collapse'`.** By default, a toast past
1606
+ `maxVisible` (3) pushes the oldest off the screen. With `'collapse'`, the toasts
1607
+ on screen stay, and the rest wait behind one **"+N more"** toast whose count
1608
+ follows them. Each comes in as a visible one closes. A sticky alert is never
1609
+ pushed off unseen. Clicking the summary calls `summaryAction(held)` and emits
1610
+ `notification:summary` with `{held}`, so a history panel can open. Every toast
1611
+ is in `history` as it arrives, waiting or not.
1612
+
1613
+ ```js
1614
+ const nc = new NotificationCenter({ eventBus, maxVisible: 3, overflow: 'collapse',
1615
+ summaryAction: () => history.open() });
1616
+ nc.mount(document.body);
1617
+ ```
1618
+
1429
1619
  ## The host port
1430
1620
 
1431
1621
  FlexDesk never touches `pywebview`, `fetch`, `localStorage` or the filesystem. It
package/css/base.css CHANGED
@@ -6001,7 +6001,7 @@ button.twm-about-dialog__btn--close:focus {
6001
6001
  /* ── the popover both pickers are drawn in ──────────────────────────── */
6002
6002
  .twm-flow-popover {
6003
6003
  position: fixed;
6004
- z-index: var(--z-popover);
6004
+ z-index: var(--z-over-windows, 7100); /* over the managed window it opened from */
6005
6005
  display: flex;
6006
6006
  flex-direction: column;
6007
6007
  box-sizing: border-box;
@@ -7328,7 +7328,7 @@ button.twm-about-dialog__btn--close:focus {
7328
7328
  /* the grip drag: a ghost under the pointer, and a drop line naming the place */
7329
7329
  .twm-flow-outline__ghost {
7330
7330
  position: fixed;
7331
- z-index: var(--z-popover);
7331
+ z-index: var(--z-over-windows, 7100); /* over the managed window it opened from */
7332
7332
  box-sizing: border-box;
7333
7333
  display: flex;
7334
7334
  align-items: center;
package/css/flexdesk.css CHANGED
@@ -279,6 +279,22 @@
279
279
  flex-direction: column;
280
280
  gap: var(--space-lg);
281
281
  }
282
+
283
+ /* NotificationCenter `overflow: 'collapse'`: the toasts past maxVisible, as one. */
284
+ .twm-notification--summary {
285
+ display: block;
286
+ width: 100%;
287
+ padding: 6px 12px;
288
+ font: inherit;
289
+ font-size: 12px;
290
+ text-align: left;
291
+ color: var(--text-secondary, #9a9a9a);
292
+ background: var(--surface-2, #2d2d2d);
293
+ border: 1px dashed var(--border-strong, #383838);
294
+ border-radius: var(--radius-md, 6px);
295
+ cursor: pointer;
296
+ }
297
+ .twm-notification--summary:hover { color: var(--text-primary, #e0e0e0); }
282
298
  .twm-notification__header {
283
299
  display: flex;
284
300
  align-items: center;
@@ -5065,6 +5081,36 @@ button.twm-about-dialog__btn--close:focus {
5065
5081
  color: var(--ea-accent);
5066
5082
  font-style: italic;
5067
5083
  }
5084
+ /* Picker = a read-only display of the looked-up value + its search button.
5085
+ * The display takes the field-well styling of the text inputs above. */
5086
+ .twm-managed-window--modal .twm-picker {
5087
+ display: flex;
5088
+ align-items: center;
5089
+ gap: 4px;
5090
+ min-width: 0;
5091
+ }
5092
+ .twm-managed-window--modal .twm-picker .twm-picker__display {
5093
+ flex: 1 1 auto;
5094
+ min-width: 0;
5095
+ cursor: pointer;
5096
+ }
5097
+ .twm-managed-window--modal .twm-picker__btn {
5098
+ flex: 0 0 auto;
5099
+ min-height: 22px;
5100
+ padding: 0 6px;
5101
+ border-radius: 0;
5102
+ }
5103
+ .twm-managed-window--modal .twm-picker__btn .material-symbols-outlined {
5104
+ font-size: 15px;
5105
+ }
5106
+ /* A row (or named section) hidden by openForm's `setVisible`. `[hidden]`
5107
+ * carries only the UA sheet's `display: none`, which the grid and flex rules
5108
+ * on `.twm-modal__row` / `.twm-modal__section` beat — so a hidden row stayed
5109
+ * on screen. */
5110
+ .twm-modal__row[hidden],
5111
+ .twm-modal__section[hidden] {
5112
+ display: none !important;
5113
+ }
5068
5114
  /* Action strip — true footer band, flush against the modal edges,
5069
5115
  * darker chrome bg, top border + 1px highlight above. Right-aligned
5070
5116
  * buttons, primary stays rightmost. */
@@ -8490,7 +8536,7 @@ tr:hover .twm-row-action { opacity: 1; }
8490
8536
  /* ── the popover both pickers are drawn in ──────────────────────────── */
8491
8537
  .twm-flow-popover {
8492
8538
  position: fixed;
8493
- z-index: var(--z-popover);
8539
+ z-index: var(--z-over-windows, 7100); /* over the managed window it opened from */
8494
8540
  display: flex;
8495
8541
  flex-direction: column;
8496
8542
  box-sizing: border-box;
@@ -9817,7 +9863,7 @@ tr:hover .twm-row-action { opacity: 1; }
9817
9863
  /* the grip drag: a ghost under the pointer, and a drop line naming the place */
9818
9864
  .twm-flow-outline__ghost {
9819
9865
  position: fixed;
9820
- z-index: var(--z-popover);
9866
+ z-index: var(--z-over-windows, 7100); /* over the managed window it opened from */
9821
9867
  box-sizing: border-box;
9822
9868
  display: flex;
9823
9869
  align-items: center;
package/css/tokens.css CHANGED
@@ -115,6 +115,10 @@
115
115
  --z-popover: 200;
116
116
  --z-modal: 1000;
117
117
  --z-toast: 10000;
118
+ /* What opens FROM a managed window and must draw over it: managed windows
119
+ stack from 6000 to 6999 (managed_window.js), so a popover at
120
+ --z-popover would open behind the window that opened it. */
121
+ --z-over-windows: 7100;
118
122
 
119
123
  /* ── Transitions ──────────────────────────────────────────── */
120
124
  --transition-fast: 120ms ease;
package/dist/charts.js CHANGED
@@ -39,10 +39,10 @@ import {
39
39
  setPlotlySource,
40
40
  uid,
41
41
  updateChart
42
- } from "./chunk-WKTYKXWF.js";
42
+ } from "./chunk-EHHTKD7B.js";
43
43
  import {
44
44
  createRafResizeObserver
45
- } from "./chunk-JLCJWCRS.js";
45
+ } from "./chunk-6CGH73BC.js";
46
46
  import {
47
47
  ManagedWindow
48
48
  } from "./chunk-6JSOVNID.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  modalHost
3
- } from "./chunk-6NG7YVBG.js";
3
+ } from "./chunk-6EZPPN4E.js";
4
4
 
5
5
  // src/help/help_registry.js
6
6
  var EMPTY_PROVIDER = Object.freeze({
@@ -472,4 +472,4 @@ export {
472
472
  showContextMenu,
473
473
  hideContextMenu
474
474
  };
475
- //# sourceMappingURL=chunk-H4LT43RS.js.map
475
+ //# sourceMappingURL=chunk-5QPGYHBN.js.map