flexdesk 0.14.0 → 0.15.0

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 (190) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +54 -3
  3. package/THIRD_PARTY_LICENSES.md +0 -0
  4. package/css/base.css +0 -0
  5. package/css/flexdesk.css +34 -0
  6. package/css/overrides.css +0 -0
  7. package/css/reset.css +0 -0
  8. package/css/tokens.css +0 -0
  9. package/dist/canvas.js +0 -0
  10. package/dist/canvas.js.map +0 -0
  11. package/dist/charts.js +2 -2
  12. package/dist/charts.js.map +0 -0
  13. package/dist/chunk-3PHCPZHT.js +0 -0
  14. package/dist/chunk-3PHCPZHT.js.map +0 -0
  15. package/dist/chunk-6JSOVNID.js +0 -0
  16. package/dist/chunk-6JSOVNID.js.map +0 -0
  17. package/dist/chunk-CWPMLX67.js +0 -0
  18. package/dist/chunk-CWPMLX67.js.map +0 -0
  19. package/dist/{chunk-M2L4XW2U.js → chunk-FBGP5KN5.js} +33 -21
  20. package/dist/chunk-FBGP5KN5.js.map +7 -0
  21. package/dist/chunk-FL5KFNQH.js +0 -0
  22. package/dist/chunk-FL5KFNQH.js.map +0 -0
  23. package/dist/chunk-FOOS3T5L.js +0 -0
  24. package/dist/chunk-FOOS3T5L.js.map +0 -0
  25. package/dist/chunk-JYWURG5T.js +0 -0
  26. package/dist/chunk-JYWURG5T.js.map +0 -0
  27. package/dist/chunk-NLTOY57M.js +0 -0
  28. package/dist/chunk-NLTOY57M.js.map +0 -0
  29. package/dist/chunk-NNFTETK3.js +0 -0
  30. package/dist/chunk-NNFTETK3.js.map +0 -0
  31. package/dist/chunk-O5OHMWBB.js +0 -0
  32. package/dist/chunk-O5OHMWBB.js.map +0 -0
  33. package/dist/{chunk-IYMNDPWH.js → chunk-RQM5Z2MU.js} +2 -2
  34. package/dist/{chunk-IYMNDPWH.js.map → chunk-RQM5Z2MU.js.map} +0 -0
  35. package/dist/chunk-SN4LSOAR.js +0 -0
  36. package/dist/chunk-SN4LSOAR.js.map +0 -0
  37. package/dist/chunk-WVFGV5FT.js +0 -0
  38. package/dist/chunk-WVFGV5FT.js.map +0 -0
  39. package/dist/core.js +0 -0
  40. package/dist/core.js.map +0 -0
  41. package/dist/editor.js +0 -0
  42. package/dist/editor.js.map +0 -0
  43. package/dist/flexdesk.css +34 -0
  44. package/dist/flow.js +1 -1
  45. package/dist/flow.js.map +0 -0
  46. package/dist/host.js +28 -5
  47. package/dist/host.js.map +2 -2
  48. package/dist/reset.css +0 -0
  49. package/dist/tile_registry-6WZPTUZV.js +0 -0
  50. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  51. package/dist/tiles.js +11 -3
  52. package/dist/tiles.js.map +2 -2
  53. package/dist/tokens.css +0 -0
  54. package/dist/widgets.js +101 -10
  55. package/dist/widgets.js.map +2 -2
  56. package/dist/wm.js +0 -0
  57. package/dist/wm.js.map +0 -0
  58. package/package.json +1 -1
  59. package/src/canvas/canvas_adapter.js +0 -0
  60. package/src/canvas/edge_router.js +0 -0
  61. package/src/canvas/graph/connector_router.js +0 -0
  62. package/src/canvas/graph/history_service.js +0 -0
  63. package/src/canvas/graph/node_platform.js +0 -0
  64. package/src/canvas/index.js +0 -0
  65. package/src/charts/chart_types.js +0 -0
  66. package/src/charts/data_series_plot_window.js +0 -0
  67. package/src/charts/downsample.js +0 -0
  68. package/src/charts/plot_config.js +0 -0
  69. package/src/charts/plot_popout_window.js +0 -0
  70. package/src/charts/plotly_wrapper.js +0 -0
  71. package/src/core/event_bus.js +0 -0
  72. package/src/core/logging.js +0 -0
  73. package/src/core/settings.js +0 -0
  74. package/src/core/state_guard.js +0 -0
  75. package/src/core/state_guard_config.js +0 -0
  76. package/src/core/state_machine.js +0 -0
  77. package/src/editor/editor_pane.js +0 -0
  78. package/src/editor/monaco_editor_factory.js +0 -0
  79. package/src/editor/monaco_loader.js +0 -0
  80. package/src/editor/notebook_search.js +0 -0
  81. package/src/editor/notebook_tab_bar.js +0 -0
  82. package/src/editor/notebook_undo_manager.js +0 -0
  83. package/src/editor/split_pane_container.js +0 -0
  84. package/src/flow/canvas_editor/editor.js +0 -0
  85. package/src/flow/canvas_editor/index.js +0 -0
  86. package/src/flow/canvas_editor/layout.js +0 -0
  87. package/src/flow/canvas_editor/strings.js +0 -0
  88. package/src/flow/kit/always_before.js +0 -0
  89. package/src/flow/kit/catalogue.js +0 -0
  90. package/src/flow/kit/chip_input.js +0 -0
  91. package/src/flow/kit/dom.js +0 -0
  92. package/src/flow/kit/findings.js +0 -0
  93. package/src/flow/kit/history.js +0 -0
  94. package/src/flow/kit/index.js +0 -0
  95. package/src/flow/kit/keys.js +0 -0
  96. package/src/flow/kit/logic_graph.js +0 -0
  97. package/src/flow/kit/references.js +0 -0
  98. package/src/flow/kit/settings_panel.js +0 -0
  99. package/src/flow/kit/settings_schema.js +0 -0
  100. package/src/flow/kit/step_picker.js +0 -0
  101. package/src/flow/kit/strings.js +0 -0
  102. package/src/flow/kit/value_picker.js +0 -0
  103. package/src/flow/kit/widgets.js +0 -0
  104. package/src/flow/lanes/editor.js +0 -0
  105. package/src/flow/lanes/index.js +0 -0
  106. package/src/flow/lanes/layout.js +0 -0
  107. package/src/flow/lanes/pipeline.js +0 -0
  108. package/src/flow/lanes/preview.js +0 -0
  109. package/src/flow/lanes/strings.js +0 -0
  110. package/src/flow/outline/editor.js +0 -0
  111. package/src/flow/outline/index.js +0 -0
  112. package/src/flow/outline/mapping.js +0 -0
  113. package/src/flow/outline/menu.js +0 -0
  114. package/src/flow/outline/operations.js +0 -0
  115. package/src/flow/outline/recognise.js +0 -0
  116. package/src/flow/outline/strings.js +0 -0
  117. package/src/help/help_modal.js +0 -0
  118. package/src/help/help_registry.js +0 -0
  119. package/src/host/host.js +2 -1
  120. package/src/host/photino_host.js +31 -3
  121. package/src/host/photino_host.md +56 -3
  122. package/src/host/pywebview_host.js +0 -0
  123. package/src/tiles/chart_tiles.js +14 -1
  124. package/src/tiles/config_schema.js +0 -0
  125. package/src/tiles/layout_persistence.js +0 -0
  126. package/src/tiles/tile_base.js +0 -0
  127. package/src/tiles/tile_grid.js +0 -0
  128. package/src/tiles/tile_registry.js +0 -0
  129. package/src/tiling/bottom_panel_tabs.js +0 -0
  130. package/src/tiling/command_palette.js +0 -0
  131. package/src/tiling/content_registry.js +0 -0
  132. package/src/tiling/desktops.js +0 -0
  133. package/src/tiling/entity_sources.js +0 -0
  134. package/src/tiling/keymap.js +0 -0
  135. package/src/tiling/kind_taxonomy.js +0 -0
  136. package/src/tiling/landing_table.js +0 -0
  137. package/src/tiling/loading_overlay.js +0 -0
  138. package/src/tiling/nav_panel.js +0 -0
  139. package/src/tiling/page_factory.js +0 -0
  140. package/src/tiling/panel_keys.js +0 -0
  141. package/src/tiling/shell.js +0 -0
  142. package/src/tiling/tab_strip.js +0 -0
  143. package/src/tiling/tile_breadcrumb.js +0 -0
  144. package/src/tiling/tile_renderer.js +0 -0
  145. package/src/tiling/tile_tab_menu.js +0 -0
  146. package/src/tiling/tile_tree.js +0 -0
  147. package/src/tiling/wm.js +0 -0
  148. package/src/tiling/zoom.js +0 -0
  149. package/src/ui/base/component_base.js +0 -0
  150. package/src/ui/base/controller_base.js +0 -0
  151. package/src/ui/base/page_base.js +0 -0
  152. package/src/ui/components/about_dialog.js +0 -0
  153. package/src/ui/components/action_dropdown.js +0 -0
  154. package/src/ui/components/alert_log.js +0 -0
  155. package/src/ui/components/alerts_pane.js +0 -0
  156. package/src/ui/components/attribute_list_editor.js +0 -0
  157. package/src/ui/components/autocomplete_field.js +0 -0
  158. package/src/ui/components/column_fit.js +0 -0
  159. package/src/ui/components/computing_status_window.js +0 -0
  160. package/src/ui/components/confirm_dialog.js +0 -0
  161. package/src/ui/components/context_menu.js +0 -0
  162. package/src/ui/components/data_table.js +32 -14
  163. package/src/ui/components/detail_header.js +0 -0
  164. package/src/ui/components/drag_reorder.js +0 -0
  165. package/src/ui/components/fact_table.js +0 -0
  166. package/src/ui/components/gallery_picker.js +0 -0
  167. package/src/ui/components/inline_renamer.js +0 -0
  168. package/src/ui/components/managed_window.js +0 -0
  169. package/src/ui/components/modal.js +0 -0
  170. package/src/ui/components/notification_history.js +0 -0
  171. package/src/ui/components/record_list.js +0 -0
  172. package/src/ui/components/row_activation.js +0 -0
  173. package/src/ui/components/slide_out_panel.js +0 -0
  174. package/src/ui/components/slider_field.js +0 -0
  175. package/src/ui/components/sortable_list.js +0 -0
  176. package/src/ui/components/table_filter_chips.js +0 -0
  177. package/src/ui/components/table_state_store.js +0 -0
  178. package/src/ui/components/table_values.js +0 -0
  179. package/src/ui/components/toast.js +0 -0
  180. package/src/ui/components/tree_view.js +0 -0
  181. package/src/ui/components/window_taskbar.js +0 -0
  182. package/src/ui/controllers/panel_state_machine.js +0 -0
  183. package/src/ui/controllers/window_chrome_controller.js +106 -11
  184. package/src/ui/notification_center.js +0 -0
  185. package/src/ui/utils/auto_scrollbars.js +0 -0
  186. package/src/ui/utils/overlay_scrollbar.js +0 -0
  187. package/src/ui/utils/raf_resize_observer.js +0 -0
  188. package/src/ui/utils/scroll_lock.js +0 -0
  189. package/src/ui/utils/tooltip_service.js +0 -0
  190. package/dist/chunk-M2L4XW2U.js.map +0 -7
package/dist/wm.js CHANGED
File without changes
package/dist/wm.js.map CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flexdesk",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "A tiling window manager and widget set for desktop-class web applications. Zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/host/host.js CHANGED
@@ -19,7 +19,8 @@ export const HOST_CONTRACT = Object.freeze({
19
19
  window: Object.freeze({
20
20
  required: Object.freeze(['chrome', 'minimize', 'close', 'isMaximized', 'setMaximized',
21
21
  'isFullscreen', 'toggleFullscreen', 'getBounds', 'setBounds']),
22
- optional: Object.freeze(['startNativeDrag']),
22
+ // `chrome()` answers 'native' or 'custom', or a promise of one.
23
+ optional: Object.freeze(['startNativeDrag', 'startNativeResize']),
23
24
  }),
24
25
  dialogs: Object.freeze({ required: Object.freeze(['saveFile']), optional: Object.freeze([]) }),
25
26
  state: Object.freeze({ required: Object.freeze(['read', 'write']), optional: Object.freeze([]) }),
@@ -99,6 +99,10 @@ export function createPhotinoTransport({ external, timeoutMs = 10000, onEvent =
99
99
  * @param {number} [opts.timeoutMs=10000]
100
100
  * @param {(name: string, value: any) => void} [opts.onEvent]
101
101
  * @param {object} [opts.logger=console]
102
+ * @param {'auto'|'native'|'custom'} [opts.chrome='auto'] who draws the window's
103
+ * title bar and borders. `auto` asks the native side once (`host.describe`,
104
+ * its `frame` field), the first time somebody needs to know; `native`
105
+ * or `custom` fixes the answer without asking. See photino_host.md.
102
106
  * @param {object|null} [opts.window] replace the window capability, or `null` to leave it out
103
107
  * @param {object|null} [opts.dialogs] replace the dialogs capability, or `null` to leave it out
104
108
  * @param {object|null} [opts.state] replace the state capability, or `null` to leave it out
@@ -110,6 +114,7 @@ export function createPhotinoHost(opts = {}) {
110
114
  timeoutMs = 10000,
111
115
  onEvent = null,
112
116
  logger = console,
117
+ chrome = 'auto',
113
118
  } = opts;
114
119
  if (!isPhotinoWindow(external)) return NULL_HOST;
115
120
 
@@ -121,11 +126,32 @@ export function createPhotinoHost(opts = {}) {
121
126
  };
122
127
  const fire = (cap, op, ...args) => { void soft(null, cap, op, ...args); };
123
128
 
129
+ // Who draws the frame. Asked once and remembered: a string once known, the
130
+ // pending question before that. A native side that predates `frame`, or
131
+ // doesn't answer within 3 s, has native chrome.
132
+ let frame = chrome === 'custom' || chrome === 'native' ? chrome : null;
133
+ let asking = null;
134
+ const askFrame = () => {
135
+ if (frame) return frame;
136
+ asking ??= request('host', 'describe', [], { wait: 3000 })
137
+ .then((d) => (d && d.frame === 'custom' ? 'custom' : 'native'), () => 'native')
138
+ .then((f) => { frame = f; return f; });
139
+ return asking;
140
+ };
141
+ // A drag or resize handed to the OS: the reply comes when the mouse is
142
+ // released, however long that takes. Refused (a native side without the op,
143
+ // or a window with native chrome) means "not handled": the caller does it in JS.
144
+ const gesture = async (op, ...args) => {
145
+ try { await request('window', op, args, { wait: 0 }); return true; }
146
+ catch (e) { return false; }
147
+ };
148
+
124
149
  const defaults = {
125
150
  window: {
126
- // Photino draws a decorated, native window on every OS: FlexDesk's
127
- // own min/max/close buttons would be a second set.
128
- chrome: () => 'native',
151
+ // 'native' (the OS draws the frame: FlexDesk's own buttons would be
152
+ // a second set) or 'custom' (the native side made the window
153
+ // frameless). A promise until the native side has said which.
154
+ chrome: () => askFrame(),
129
155
  minimize: () => { fire('window', 'minimize'); },
130
156
  close: () => { fire('window', 'close'); },
131
157
  isMaximized: async () => (await soft(false, 'window', 'isMaximized')) === true,
@@ -137,6 +163,8 @@ export function createPhotinoHost(opts = {}) {
137
163
  if (!b || typeof b !== 'object') return null;
138
164
  return { x: b.x, y: b.y, width: b.width, height: b.height };
139
165
  },
166
+ startNativeDrag: () => gesture('startDrag'),
167
+ startNativeResize: (edge) => gesture('startResize', String(edge || '')),
140
168
  // Not async, like pywebview's: a rAF hot path during a drag; nobody awaits.
141
169
  setBounds({ x = null, y = null, width = null, height = null } = {}) {
142
170
  fire('window', 'setBounds', { x, y, width, height });
@@ -28,6 +28,7 @@ host → page {"event": "window.maximizedChanged", "value": true}
28
28
 
29
29
  | `cap.op` | `args` | reply `value` | Photino |
30
30
  |---|---|---|---|
31
+ | `host.describe` | `[]` | `{frame: "native" \| "custom", …}` | asked once, for `chrome()` (see below) |
31
32
  | `window.minimize` | `[]` | anything | `Minimized = true` |
32
33
  | `window.close` | `[]` | anything | `Close()` |
33
34
  | `window.isMaximized` | `[]` | `bool` | `Maximized` |
@@ -36,13 +37,65 @@ host → page {"event": "window.maximizedChanged", "value": true}
36
37
  | `window.toggleFullscreen` | `[]` | `bool`, the state now | `FullScreen = !FullScreen` |
37
38
  | `window.getBounds` | `[]` | `{x, y, width, height}` | `Location`, `Size` |
38
39
  | `window.setBounds` | `[{x, y, width, height}]`, any of them `null` = leave alone | anything | `Location`, `Size` |
40
+ | `window.startDrag` | `[]` | anything, **after mouse-up** | custom chrome only: the OS move loop (below) |
41
+ | `window.startResize` | `[edge]`: `n` `s` `e` `w` `ne` `nw` `se` `sw` | anything, **after mouse-up** | custom chrome only: the OS size loop (below) |
39
42
  | `dialogs.saveFile` | `[{data, filename, kind}]` | see below | `ShowSaveFile(...)` + write |
40
43
  | `state.read` | `[key]` | the stored value, or `null` | a file per key |
41
44
  | `state.write` | `[key, value]` | `true` | a file per key |
42
45
 
43
- `window.chrome()` is answered by the adapter itself (`'native'`): Photino draws a
44
- native title bar, so FlexDesk draws no buttons of its own. `startNativeDrag` is
45
- not offered.
46
+ ## Native or custom chrome
47
+
48
+ A FlexDesk app runs in one of three ways, and the page needs no switch for it:
49
+
50
+ | | title bar and borders | `chrome()` | FlexDesk draws |
51
+ |---|---|---|---|
52
+ | a browser tab | the browser's | (no Photino host) | nothing |
53
+ | a Photino window, native frame | the OS's | `'native'` | nothing |
54
+ | a Photino window, custom frame | none: the page's top bar | `'custom'` | buttons, resize handles, drag |
55
+
56
+ `window.chrome()` asks the native side once, with `host.describe`, the first
57
+ time it's called (that is `WindowChromeController.initialize()`). It resolves
58
+ `'custom'` when the reply's `frame` is `"custom"`, and `'native'` for anything
59
+ else: no `frame` field, an `ok: false`, no answer within 3 s. An app that knows
60
+ already can say so: `createPhotinoHost({ chrome: 'custom' })` or `'native'`
61
+ answers without asking.
62
+
63
+ **What the native side does for a custom frame (Windows).** Don't use
64
+ Photino's `SetChromeless(true)`: that makes a bare popup window, with no
65
+ snapping, no Win+arrows, no shadow and square corners, and Photino then reports
66
+ it as fullscreen. Keep the normal window and stop drawing its frame:
67
+
68
+ 1. After the window exists, add `WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX |
69
+ WS_MAXIMIZEBOX | WS_SYSMENU` to its style, and subclass it
70
+ (`SetWindowSubclass`). These styles are what make Windows treat it as a
71
+ normal window: Aero Snap, Win+arrows, the shadow, the rounded corners on
72
+ Windows 11.
73
+ 2. In the subclass, answer `WM_NCCALCSIZE` (with `wParam` true) with `0`, so
74
+ the whole window is client area and nothing of the frame is drawn. While
75
+ maximised (`IsZoomed`), set the proposed rect to the monitor's work area
76
+ first: a maximised window is larger than the monitor by the frame's
77
+ thickness, and the taskbar must stay visible.
78
+ 3. Answer `WM_NCACTIVATE` with `DefSubclassProc(h, msg, wParam, -1)`, so
79
+ Windows repaints no caption on activation.
80
+ 4. `DwmExtendFrameIntoClientArea` with a top margin of 1 keeps DWM drawing
81
+ the shadow. `DWMWA_WINDOW_CORNER_PREFERENCE = DWMWCP_ROUND` asks for the
82
+ rounded corners.
83
+ 5. Call `SetWindowPos(..., SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE |
84
+ SWP_NOZORDER)` to apply it.
85
+
86
+ The WebView covers the whole window, so the window never sees the mouse. That
87
+ is what `startDrag` and `startResize` are for. The page asks when the mouse
88
+ goes down on its top bar or on a resize handle. On the window's thread, call
89
+ `ReleaseCapture()`, then `SendMessage(hwnd, WM_NCLBUTTONDOWN, hit, cursor)`
90
+ with `HTCAPTION` for a drag, or `HTTOP` / `HTBOTTOM` / `HTLEFT` / `HTRIGHT` /
91
+ `HTTOPLEFT` / … for the edge. Windows then runs its own move or size loop,
92
+ exactly as for a click on a real caption or border, and `SendMessage` returns
93
+ when the mouse is released. Reply then. The page waits without a timeout, and a
94
+ reply of `ok: false` makes it move or resize the window from JS instead.
95
+
96
+ `describe`'s reply says `frame: "custom"` only when all of this was done. A
97
+ Photino window on Linux or macOS keeps its native frame and says `"native"`, or
98
+ leaves `frame` out.
46
99
 
47
100
  **`dialogs.saveFile`.** `kind` is the file type the caller is saving, `'csv'`
48
101
  or `'png'` today. `data` is the file's text for `csv` and the file's bytes,
File without changes
@@ -528,9 +528,22 @@ export class ChartTile extends TileBase {
528
528
  try {
529
529
  const P = await ensurePlotly();
530
530
  const uri = await P.toImage(this._plotEl, { format: 'png', scale: 2 });
531
+ const filename = `${slug(this.config.title || this.constructor.TITLE)}.png`;
532
+ // The native Save dialog when the host has one (a desktop window).
533
+ // `saveFile` answers null when there is no dialog behind it, which
534
+ // means "download it yourself", not a failed save.
535
+ const dialogs = this.host?.dialogs;
536
+ const result = dialogs
537
+ ? await dialogs.saveFile({ data: uri.split(',')[1], filename, kind: 'png' })
538
+ : null;
539
+ if (result) {
540
+ if (result.ok) this._notify('Download', `Chart saved to ${result.path}`, 'success');
541
+ else if (!result.cancelled) this._notify('Download failed', result.error || 'The image could not be saved', 'error');
542
+ return;
543
+ }
531
544
  const a = document.createElement('a');
532
545
  a.href = uri;
533
- a.download = `${slug(this.config.title || this.constructor.TITLE)}.png`;
546
+ a.download = filename;
534
547
  a.click();
535
548
  } catch (err) {
536
549
  this._notify('Download failed', `The image could not be made: ${err?.message || err}`, 'error');
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/tiling/wm.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -327,7 +327,8 @@ const AUTOSIZE_MAX_PX = 520;
327
327
  * @property {Function} [onRowClick] - Row click handler: (rowIdx, row, ev) => void. Receives the original (unfiltered) row index.
328
328
  * @property {Function} [onRowContextMenu] - Row right-click handler: (rowIdx, row, ev) => void. Fires before the default context menu; call ev.preventDefault() to suppress the default.
329
329
  * @property {Function} [onCellContextMenu] - Cell right-click handler: (colIdx, rowIdx, value, td, ev) => void. Fires before onRowContextMenu; same suppression semantics.
330
- * @property {boolean} [showExportButton=false] - Adds a "CSV" download button to the pagination strip that invokes `downloadCSV()`.
330
+ * @property {boolean} [showExportButton=false] - Adds a "CSV" download button to the pagination strip that invokes `downloadCSV()`. The strip is drawn for it even when the table fits on one page (0.14.1).
331
+ * @property {string|function(DataTable): string} [exportFilename='data.csv'] - The file the export button saves (0.14.1); in a desktop host, the name the Save dialog suggests.
331
332
  * @property {Object} [host] - A Host (see ui/js/host/host.js). Its `dialogs` capability backs `downloadCSV()`'s native save dialog; without it CSV export falls back to a Blob download.
332
333
  * @property {Object} [stateStore] - `{ ready(), get(key), set(key, state) }` — REQUIRED when `persistKey` is set. Built by `createTableStateStore()`.
333
334
  * @property {'container'|'content'} [columnFit='container'] - How the automatic fit pass sizes a column the user has NOT dragged. 'container' squeezes every column into the wrap: a 360px cap, a more generous one for the first column, and water-fill-shrink toward the 40px floor when they do not fit. 'content' sizes each column to its own content through the same `_autoWidth` a grip double-click uses (520px cap) and lets the body scroll sideways rather than squeezing. A pinned column is honoured verbatim under both.
@@ -466,6 +467,7 @@ export class DataTable {
466
467
  onRowContextMenu: null,
467
468
  onCellContextMenu: null,
468
469
  showExportButton: false,
470
+ exportFilename: null,
469
471
  // The host port. TOP LEVEL, deliberately — NOT inside `services`,
470
472
  // which most call sites never pass. Supplies the native save
471
473
  // dialog for downloadCSV(); absent => Blob fallback.
@@ -2623,7 +2625,7 @@ export class DataTable {
2623
2625
  // Don't render the strip when there's only one page — the row
2624
2626
  // count is uninformative (the table itself shows every row) and
2625
2627
  // the nav buttons would all be disabled.
2626
- if (totalPages <= 1 && !isFiltered) return null;
2628
+ if (totalPages <= 1 && !isFiltered) return this._createExportOnlyStrip();
2627
2629
  const currentPage = Math.floor(offset / pageSize) + 1;
2628
2630
  const hasPrev = offset > 0;
2629
2631
  const hasNext = offset + pageSize < effectiveTotal;
@@ -2719,16 +2721,35 @@ export class DataTable {
2719
2721
  // CSV export button (opt-in). Re-uses the existing downloadCSV
2720
2722
  // method which already handles the host save dialog + browser
2721
2723
  // fallback.
2722
- if (this.config.showExportButton) {
2723
- const exportBtn = this._createPaginationBtn(
2724
- 'download', 'Download visible rows as CSV', false,
2725
- () => this.downloadCSV('data.csv'));
2726
- el.appendChild(exportBtn);
2727
- }
2724
+ if (this.config.showExportButton) el.appendChild(this._createExportBtn());
2728
2725
 
2729
2726
  return el;
2730
2727
  }
2731
2728
 
2729
+ /** The export button: downloadCSV() under the configured file name. */
2730
+ _createExportBtn() {
2731
+ const name = this.config.exportFilename;
2732
+ const filename = (typeof name === 'function' ? name(this) : name) || 'data.csv';
2733
+ return this._createPaginationBtn('download', 'Download visible rows as CSV', false,
2734
+ () => this.downloadCSV(filename));
2735
+ }
2736
+
2737
+ /**
2738
+ * The strip on a table with one page: nothing, as always, unless the export
2739
+ * button needs somewhere to be. Then the button alone, with no page
2740
+ * buttons (0.14.1).
2741
+ */
2742
+ _createExportOnlyStrip() {
2743
+ if (!this.config.showExportButton) return null;
2744
+ const el = document.createElement('div');
2745
+ el.className = 'twm-pagination-controls twm-pagination-controls--export-only';
2746
+ el.style.cssText = 'display:flex; align-items:center; gap:6px; padding:2px 8px; border-bottom:1px solid #2a2a2a; font-size:11px;';
2747
+ const spacer = document.createElement('div');
2748
+ spacer.style.flex = '1';
2749
+ el.append(spacer, this._createExportBtn());
2750
+ return el;
2751
+ }
2752
+
2732
2753
  _createPaginationBtn(icon, tooltip, disabled, onClick, textLabel = null) {
2733
2754
  const btn = document.createElement('button');
2734
2755
  btn.type = 'button';
@@ -4738,8 +4759,8 @@ export class DataTable {
4738
4759
  const pn = this._pageNav({ offset, held: drawn, total, hasMore: hasMoreKnown, pageSize });
4739
4760
  const { hasPrev, hasNext, page } = pn;
4740
4761
  const totalPages = pn.pages;
4741
- if (known && totalPages <= 1 && !isFiltered && !hasPrev) return null;
4742
- if (!known && !hasPrev && !hasNext) return null;
4762
+ if (known && totalPages <= 1 && !isFiltered && !hasPrev) return this._createExportOnlyStrip();
4763
+ if (!known && !hasPrev && !hasNext) return this._createExportOnlyStrip();
4743
4764
  const maxJump = Number.isFinite(c.maxJumpOffset) ? c.maxJumpOffset : null;
4744
4765
 
4745
4766
  const go = (newOffset) => {
@@ -4817,10 +4838,7 @@ export class DataTable {
4817
4838
  jumpBtn.dataset.pager = 'go';
4818
4839
 
4819
4840
  el.append(info, spacer, firstBtn, prevBtn, pageInfo, nextBtn, lastBtn, jumpInput, jumpBtn);
4820
- if (c.showExportButton) {
4821
- el.appendChild(this._createPaginationBtn('download', 'Download visible rows as CSV', false,
4822
- () => this.downloadCSV('data.csv')));
4823
- }
4841
+ if (c.showExportButton) el.appendChild(this._createExportBtn());
4824
4842
  return el;
4825
4843
  }
4826
4844
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes