flexdesk 0.16.0 → 0.17.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 (251) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2261 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/css/base.css +1834 -75
  6. package/css/flexdesk.css +3373 -238
  7. package/css/overrides.css +16 -0
  8. package/css/reset.css +0 -0
  9. package/css/tokens.css +0 -0
  10. package/dist/canvas.js +6 -239
  11. package/dist/canvas.js.map +3 -3
  12. package/dist/charts.js +709 -18
  13. package/dist/charts.js.map +4 -4
  14. package/dist/chunk-3PHCPZHT.js +0 -0
  15. package/dist/chunk-3PHCPZHT.js.map +0 -0
  16. package/dist/chunk-4A7SJC3T.js +503 -0
  17. package/dist/chunk-4A7SJC3T.js.map +7 -0
  18. package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
  19. package/dist/chunk-CVMCZZX2.js.map +7 -0
  20. package/dist/chunk-D3COLZZ2.js +1311 -0
  21. package/dist/chunk-D3COLZZ2.js.map +7 -0
  22. package/dist/chunk-FOOS3T5L.js +0 -0
  23. package/dist/chunk-FOOS3T5L.js.map +0 -0
  24. package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
  25. package/dist/chunk-GOQLR5WR.js.map +7 -0
  26. package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
  27. package/dist/chunk-HNU6MM2B.js.map +7 -0
  28. package/dist/chunk-HYJYYEPK.js +375 -0
  29. package/dist/chunk-HYJYYEPK.js.map +7 -0
  30. package/dist/chunk-JKZLNKG6.js +2041 -0
  31. package/dist/chunk-JKZLNKG6.js.map +7 -0
  32. package/dist/chunk-JYWURG5T.js +0 -0
  33. package/dist/chunk-JYWURG5T.js.map +0 -0
  34. package/dist/{chunk-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
  35. package/dist/chunk-MCIXFNVF.js.map +7 -0
  36. package/dist/chunk-N7HC3ADV.js +30 -0
  37. package/dist/chunk-N7HC3ADV.js.map +7 -0
  38. package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
  39. package/dist/chunk-Q36EPT2R.js.map +7 -0
  40. package/dist/chunk-QWQJUDPD.js +610 -0
  41. package/dist/chunk-QWQJUDPD.js.map +7 -0
  42. package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
  43. package/dist/chunk-UURZ37SD.js.map +7 -0
  44. package/dist/chunk-UWO43BZU.js +111 -0
  45. package/dist/chunk-UWO43BZU.js.map +7 -0
  46. package/dist/chunk-WVFGV5FT.js +0 -0
  47. package/dist/chunk-WVFGV5FT.js.map +0 -0
  48. package/dist/core.js +170 -118
  49. package/dist/core.js.map +2 -2
  50. package/dist/editor.js +156 -70
  51. package/dist/editor.js.map +2 -2
  52. package/dist/flexdesk.css +3373 -238
  53. package/dist/flow.js +2789 -306
  54. package/dist/flow.js.map +4 -4
  55. package/dist/host.js +109 -3
  56. package/dist/host.js.map +3 -3
  57. package/dist/reset.css +0 -0
  58. package/dist/tile_registry-6WZPTUZV.js +0 -0
  59. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  60. package/dist/tiles.js +1131 -253
  61. package/dist/tiles.js.map +4 -4
  62. package/dist/tokens.css +0 -0
  63. package/dist/widgets.js +3254 -2112
  64. package/dist/widgets.js.map +4 -4
  65. package/dist/wm.js +1227 -345
  66. package/dist/wm.js.map +4 -4
  67. package/package.json +3 -1
  68. package/src/canvas/canvas_adapter.js +5 -0
  69. package/src/canvas/edge_router.js +0 -0
  70. package/src/canvas/graph/connector_router.js +0 -0
  71. package/src/canvas/graph/history_service.js +0 -0
  72. package/src/canvas/graph/node_platform.js +0 -0
  73. package/src/canvas/index.js +1 -0
  74. package/src/canvas/layered_layout.js +244 -0
  75. package/src/charts/chart_types.js +0 -0
  76. package/src/charts/data_series_plot_window.js +0 -0
  77. package/src/charts/downsample.js +2 -2
  78. package/src/charts/plot_config.js +53 -29
  79. package/src/charts/plot_config_editor.js +627 -0
  80. package/src/charts/plot_grid.js +449 -0
  81. package/src/charts/plot_popout_window.js +103 -317
  82. package/src/charts/plotly_wrapper.js +22 -2
  83. package/src/charts/series_transforms.js +206 -0
  84. package/src/core/event_bus.js +0 -0
  85. package/src/core/logging.js +1 -1
  86. package/src/core/settings.js +105 -19
  87. package/src/core/state_guard.js +93 -51
  88. package/src/core/state_guard_config.js +85 -74
  89. package/src/core/state_machine.js +0 -0
  90. package/src/editor/editor_pane.js +3 -3
  91. package/src/editor/monaco_editor_factory.js +124 -60
  92. package/src/editor/monaco_loader.js +103 -28
  93. package/src/editor/notebook_search.js +0 -0
  94. package/src/editor/notebook_tab_bar.js +38 -52
  95. package/src/editor/notebook_undo_manager.js +0 -0
  96. package/src/editor/split_pane_container.js +6 -3
  97. package/src/flow/canvas_editor/connections.js +90 -0
  98. package/src/flow/canvas_editor/editor.js +2351 -153
  99. package/src/flow/canvas_editor/index.js +2 -1
  100. package/src/flow/canvas_editor/layout.js +119 -12
  101. package/src/flow/canvas_editor/strings.js +80 -0
  102. package/src/flow/kit/always_before.js +0 -0
  103. package/src/flow/kit/catalogue.js +97 -3
  104. package/src/flow/kit/chip_input.js +0 -0
  105. package/src/flow/kit/dom.js +0 -0
  106. package/src/flow/kit/findings.js +0 -0
  107. package/src/flow/kit/history.js +0 -0
  108. package/src/flow/kit/index.js +3 -3
  109. package/src/flow/kit/keys.js +7 -1
  110. package/src/flow/kit/logic_graph.js +97 -15
  111. package/src/flow/kit/references.js +0 -0
  112. package/src/flow/kit/settings_panel.js +0 -0
  113. package/src/flow/kit/settings_schema.js +2 -1
  114. package/src/flow/kit/step_picker.js +0 -0
  115. package/src/flow/kit/strings.js +3 -1
  116. package/src/flow/kit/value_picker.js +0 -0
  117. package/src/flow/kit/widgets.js +86 -109
  118. package/src/flow/lanes/editor.js +136 -14
  119. package/src/flow/lanes/index.js +3 -1
  120. package/src/flow/lanes/layout.js +0 -0
  121. package/src/flow/lanes/pipeline.js +44 -2
  122. package/src/flow/lanes/preview.js +0 -0
  123. package/src/flow/lanes/strings.js +1 -0
  124. package/src/flow/outline/editor.js +0 -0
  125. package/src/flow/outline/index.js +0 -0
  126. package/src/flow/outline/mapping.js +0 -0
  127. package/src/flow/outline/menu.js +0 -0
  128. package/src/flow/outline/operations.js +0 -0
  129. package/src/flow/outline/recognise.js +0 -0
  130. package/src/flow/outline/strings.js +0 -0
  131. package/src/help/help_modal.js +168 -42
  132. package/src/help/help_registry.js +12 -1
  133. package/src/host/external_links.js +0 -0
  134. package/src/host/file_picker.js +117 -0
  135. package/src/host/host.js +10 -2
  136. package/src/host/photino_host.js +9 -0
  137. package/src/host/photino_host.md +21 -3
  138. package/src/host/pywebview_host.js +10 -0
  139. package/src/tiles/chart_tiles.js +53 -245
  140. package/src/tiles/config_schema.js +8 -0
  141. package/src/tiles/figure_tile.js +322 -0
  142. package/src/tiles/layout_persistence.js +3 -3
  143. package/src/tiles/list_tiles.js +422 -0
  144. package/src/tiles/scope_picker.js +96 -0
  145. package/src/tiles/tile_base.js +12 -1
  146. package/src/tiles/tile_config_form.js +442 -0
  147. package/src/tiles/tile_grid.js +38 -1
  148. package/src/tiles/tile_registry.js +0 -0
  149. package/src/tiling/bottom_panel_tabs.js +67 -99
  150. package/src/tiling/command_palette.js +64 -19
  151. package/src/tiling/content_registry.js +4 -1
  152. package/src/tiling/desktops.js +11 -1
  153. package/src/tiling/entity_sources.js +0 -0
  154. package/src/tiling/keymap.js +403 -128
  155. package/src/tiling/kind_taxonomy.js +78 -0
  156. package/src/tiling/landing_table.js +0 -0
  157. package/src/tiling/loading_overlay.js +0 -0
  158. package/src/tiling/nav_panel.js +59 -10
  159. package/src/tiling/page_factory.js +5 -5
  160. package/src/tiling/panel_availability.js +49 -0
  161. package/src/tiling/panel_keys.js +0 -0
  162. package/src/tiling/shell.js +114 -12
  163. package/src/tiling/tab_strip.js +0 -0
  164. package/src/tiling/tile_breadcrumb.js +0 -0
  165. package/src/tiling/tile_renderer.js +105 -9
  166. package/src/tiling/tile_tab_menu.js +0 -0
  167. package/src/tiling/tile_tree.js +0 -0
  168. package/src/tiling/wm.js +562 -30
  169. package/src/tiling/zoom.js +0 -0
  170. package/src/ui/base/component_base.js +0 -0
  171. package/src/ui/base/controller_base.js +0 -0
  172. package/src/ui/base/page_base.js +0 -0
  173. package/src/ui/components/about_dialog.js +0 -0
  174. package/src/ui/components/action_dropdown.js +30 -2
  175. package/src/ui/components/alert_log.js +0 -0
  176. package/src/ui/components/alerts_pane.js +0 -0
  177. package/src/ui/components/attribute_list_editor.js +0 -0
  178. package/src/ui/components/autocomplete_field.js +0 -0
  179. package/src/ui/components/cell_editor_host.js +211 -0
  180. package/src/ui/components/cell_grid.js +648 -0
  181. package/src/ui/components/cell_selection.js +220 -0
  182. package/src/ui/components/choice_controls.js +351 -0
  183. package/src/ui/components/color_field.js +211 -0
  184. package/src/ui/components/color_list_field.js +210 -0
  185. package/src/ui/components/column_fit.js +0 -0
  186. package/src/ui/components/column_groups.js +314 -0
  187. package/src/ui/components/column_order.js +142 -0
  188. package/src/ui/components/computing_status_window.js +98 -25
  189. package/src/ui/components/confirm_dialog.js +81 -38
  190. package/src/ui/components/context_menu.js +2 -2
  191. package/src/ui/components/conversation_view.js +440 -0
  192. package/src/ui/components/crosstab.js +289 -0
  193. package/src/ui/components/data_table.js +601 -62
  194. package/src/ui/components/detail_header.js +22 -11
  195. package/src/ui/components/document_outline.js +397 -0
  196. package/src/ui/components/drag_reorder.js +0 -0
  197. package/src/ui/components/fact_table.js +0 -0
  198. package/src/ui/components/fill_handle.js +322 -0
  199. package/src/ui/components/gallery_picker.js +19 -8
  200. package/src/ui/components/header_actions.js +55 -0
  201. package/src/ui/components/inline_renamer.js +0 -0
  202. package/src/ui/components/job_progress.js +51 -0
  203. package/src/ui/components/managed_window.js +27 -4
  204. package/src/ui/components/menu_bar.js +745 -0
  205. package/src/ui/components/modal.js +449 -75
  206. package/src/ui/components/notification_history.js +3 -3
  207. package/src/ui/components/progress.js +483 -0
  208. package/src/ui/components/record_list.js +0 -0
  209. package/src/ui/components/row_activation.js +0 -0
  210. package/src/ui/components/section_stack.js +298 -0
  211. package/src/ui/components/series.js +173 -0
  212. package/src/ui/components/settings_view.js +634 -0
  213. package/src/ui/components/slide_out_panel.js +43 -6
  214. package/src/ui/components/slider_field.js +275 -18
  215. package/src/ui/components/sortable_list.js +26 -4
  216. package/src/ui/components/staged_edits.js +343 -0
  217. package/src/ui/components/staged_history.js +172 -0
  218. package/src/ui/components/table_filter_chips.js +0 -0
  219. package/src/ui/components/table_state_store.js +0 -0
  220. package/src/ui/components/table_structure.js +182 -0
  221. package/src/ui/components/table_values.js +0 -0
  222. package/src/ui/components/tabs.js +497 -0
  223. package/src/ui/components/tag_input.js +215 -0
  224. package/src/ui/components/toast.js +25 -8
  225. package/src/ui/components/toggle_field.js +175 -0
  226. package/src/ui/components/tree_view.js +55 -34
  227. package/src/ui/components/window_taskbar.js +0 -0
  228. package/src/ui/components/wizard.js +563 -0
  229. package/src/ui/controllers/window_chrome_controller.js +2 -2
  230. package/src/ui/notification_center.js +375 -119
  231. package/src/ui/utils/auto_scrollbars.js +0 -0
  232. package/src/ui/utils/markdown.js +709 -0
  233. package/src/ui/utils/native_context_menu.js +0 -0
  234. package/src/ui/utils/overlay_scrollbar.js +16 -12
  235. package/src/ui/utils/raf_resize_observer.js +0 -0
  236. package/src/ui/utils/scroll_lock.js +0 -0
  237. package/src/ui/utils/strip_scroll.js +55 -0
  238. package/src/ui/utils/tooltip_service.js +0 -8
  239. package/dist/chunk-6JSOVNID.js.map +0 -7
  240. package/dist/chunk-CWPMLX67.js +0 -475
  241. package/dist/chunk-CWPMLX67.js.map +0 -7
  242. package/dist/chunk-FBGP5KN5.js.map +0 -7
  243. package/dist/chunk-FL5KFNQH.js.map +0 -7
  244. package/dist/chunk-NLTOY57M.js.map +0 -7
  245. package/dist/chunk-NNFTETK3.js +0 -633
  246. package/dist/chunk-NNFTETK3.js.map +0 -7
  247. package/dist/chunk-O5OHMWBB.js +0 -284
  248. package/dist/chunk-O5OHMWBB.js.map +0 -7
  249. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  250. package/dist/chunk-SN4LSOAR.js.map +0 -7
  251. package/src/ui/controllers/panel_state_machine.js +0 -634
@@ -0,0 +1,322 @@
1
+ /**
2
+ * fill_handle.js — STORY-0089. DataTable's FILL HANDLE (`fillHandle: true`).
3
+ * INTERNAL to the table, like `cell_grid.js`: built by the cell grid, and
4
+ * reading the table through it.
5
+ *
6
+ * A small square on the bottom-right corner of the cell range. Dragging it
7
+ * up, down, left or right over the cells beyond the range previews the cells
8
+ * it covers (`twm-dt-cell--fill-preview`) and, on release, STAGES their new
9
+ * values (`staged_edits.js`) as ONE undo entry. What the values are is
10
+ * `series.js`'s: each line of the range — a column for a fill up or down, a
11
+ * row for a fill left or right — is a seed, continued as a series or repeated
12
+ * in a cycle; Ctrl (Cmd) held at release repeats it.
13
+ *
14
+ * A target cell that `editable` refuses is SKIPPED: nothing is staged in it,
15
+ * and it is drawn `twm-dt-cell--fill-skipped` until the selection moves, the
16
+ * next fill, or new rows.
17
+ */
18
+
19
+ import { projectSeries } from './series.js';
20
+
21
+ const key = (rowIdx, colIdx) => `${rowIdx}:${colIdx}`;
22
+
23
+ export class FillHandle {
24
+ /** @param {import('./cell_grid.js').CellGrid} grid */
25
+ constructor(grid) {
26
+ this.grid = grid;
27
+ /** The handle element; made on first use, moved into the corner cell. */
28
+ this.el = null;
29
+ /** The `<td>` holding the handle, or null. */
30
+ this.corner = null;
31
+ /** `rowIdx:colIdx` of the cells the last fill skipped. */
32
+ this.skipped = new Set();
33
+ /** `pos:colIdx` of the cells a drag covers now. */
34
+ this.preview = new Set();
35
+ /** The drag in progress: `{range, plan, unbind}`, or null. */
36
+ this.drag = null;
37
+ /** Takes the click guard of the last press on the handle off, or null. */
38
+ this._unguard = null;
39
+ }
40
+
41
+ // ── painting (called by the grid for every cell it paints) ───────────
42
+
43
+ /** One cell: its preview and skipped marks, and the handle when it is
44
+ * the range's corner. */
45
+ paintCell(td, pos, col) {
46
+ const rowIdx = td.parentNode?.__rowIndex;
47
+ td.classList.toggle('twm-dt-cell--fill-preview', this.preview.has(`${pos}:${col}`));
48
+ td.classList.toggle('twm-dt-cell--fill-skipped', this.skipped.has(key(rowIdx, col)));
49
+ const r = this.grid.sel.range();
50
+ if (r && r.bottom === pos && r.right === col) this._place(td);
51
+ else if (td === this.corner) this._unplace();
52
+ }
53
+
54
+ _handle() {
55
+ if (this.el) return this.el;
56
+ const el = document.createElement('div');
57
+ el.className = 'twm-dt-fill-handle';
58
+ el.setAttribute('aria-hidden', 'true');
59
+ el.title = 'Drag to fill; hold Ctrl to repeat instead of continuing a series';
60
+ el.addEventListener('pointerdown', (ev) => this._begin(ev));
61
+ this.el = el;
62
+ return el;
63
+ }
64
+
65
+ _place(td) {
66
+ if (this.grid.isEditing() || this.grid.staged.committing) { this._unplace(); return; }
67
+ if (this.corner && this.corner !== td) this.corner.classList.remove('twm-dt-cell--fill-corner');
68
+ this.corner = td;
69
+ td.classList.add('twm-dt-cell--fill-corner');
70
+ const el = this._handle();
71
+ if (el.parentNode !== td) td.appendChild(el);
72
+ }
73
+
74
+ _unplace() {
75
+ this.corner?.classList.remove('twm-dt-cell--fill-corner');
76
+ this.corner = null;
77
+ this.el?.remove();
78
+ }
79
+
80
+ // ── what a fill covers ───────────────────────────────────────────────
81
+
82
+ /**
83
+ * The fill that reaching cell `to` (`{row, col}`, position and data
84
+ * column) from `range` makes: `{direction, count}`, or null when `to` is
85
+ * inside the range's rows and columns. The direction is the one the cell
86
+ * lies furthest out in; up and down win a tie. Clamped to the page drawn.
87
+ */
88
+ plan(range, to) {
89
+ if (!range || !to) return null;
90
+ const cols = this.grid.columns();
91
+ const [first, last] = this.grid.span();
92
+ const row = Math.max(first, Math.min(last, Number(to.row)));
93
+ const ordL = cols.indexOf(range.left);
94
+ const ordR = cols.indexOf(range.right);
95
+ const ordTo = cols.indexOf(to.col);
96
+ const out = [
97
+ ['down', row - range.bottom],
98
+ ['up', range.top - row],
99
+ ['right', ordTo < 0 ? 0 : ordTo - ordR],
100
+ ['left', ordTo < 0 ? 0 : ordL - ordTo],
101
+ ];
102
+ let best = null;
103
+ for (const [direction, count] of out) {
104
+ if (count > 0 && (!best || count > best.count)) best = { direction, count };
105
+ }
106
+ return best;
107
+ }
108
+
109
+ /** The cells a plan covers, as `{pos, col}`, line by line in fill order:
110
+ * `[{seed: [{pos, col}], targets: [{pos, col}]}]`. */
111
+ _lines(range, { direction, count }) {
112
+ const cols = this.grid.columns();
113
+ const ordL = cols.indexOf(range.left);
114
+ const ordR = cols.indexOf(range.right);
115
+ const lines = [];
116
+ if (direction === 'down' || direction === 'up') {
117
+ const down = direction === 'down';
118
+ for (let o = ordL; o <= ordR; o++) {
119
+ const col = cols[o];
120
+ const seed = [];
121
+ for (let p = range.top; p <= range.bottom; p++) seed.push({ pos: p, col });
122
+ if (!down) seed.reverse();
123
+ const targets = [];
124
+ for (let i = 1; i <= count; i++) {
125
+ targets.push({ pos: down ? range.bottom + i : range.top - i, col });
126
+ }
127
+ lines.push({ seed, targets });
128
+ }
129
+ } else {
130
+ const right = direction === 'right';
131
+ for (let p = range.top; p <= range.bottom; p++) {
132
+ const seed = [];
133
+ for (let o = ordL; o <= ordR; o++) seed.push({ pos: p, col: cols[o] });
134
+ if (!right) seed.reverse();
135
+ const targets = [];
136
+ for (let i = 1; i <= count; i++) {
137
+ const o = right ? ordR + i : ordL - i;
138
+ if (o >= 0 && o < cols.length) targets.push({ pos: p, col: cols[o] });
139
+ }
140
+ lines.push({ seed, targets });
141
+ }
142
+ }
143
+ return lines;
144
+ }
145
+
146
+ // ── the fill ─────────────────────────────────────────────────────────
147
+
148
+ /**
149
+ * Fill from the range to cell `to`, staging the values as ONE undo
150
+ * entry. Returns `{direction, count, staged, skipped}` (`skipped` the
151
+ * cells `editable` refused, `{row, col, rowIdx}`), or null when nothing
152
+ * was filled: no range, `to` inside it, an editor open or a commit out.
153
+ */
154
+ fill(to, { forceCopy = false } = {}) {
155
+ const g = this.grid;
156
+ g._sync();
157
+ const range = g.sel.range();
158
+ const plan = this.plan(range, to);
159
+ if (!plan || g.staged.committing || g.isEditing()) return null;
160
+ const cells = [];
161
+ const skipped = [];
162
+ for (const { seed, targets } of this._lines(range, plan)) {
163
+ const values = projectSeries(
164
+ seed.map(({ pos, col }) => g.shownValue(g.rowIndexAt(pos), col)),
165
+ targets.length, { forceCopy });
166
+ targets.forEach(({ pos, col }, i) => {
167
+ const rowIdx = g.rowIndexAt(pos);
168
+ if (g.canEdit(pos, col)) cells.push({ rowIdx, colIdx: col, value: values[i] });
169
+ else skipped.push({ row: pos, col, rowIdx });
170
+ });
171
+ }
172
+ const staged = g.staged.stageMany(cells, `fill ${cells.length} cell(s)`);
173
+ // The range grows over what was filled, the cursor at its far end.
174
+ const cols = g.columns();
175
+ const ordL = cols.indexOf(range.left);
176
+ const ordR = cols.indexOf(range.right);
177
+ const { direction, count } = plan;
178
+ if (direction === 'down') g.set({ row: range.bottom + count, col: range.right }, { row: range.top, col: range.left });
179
+ else if (direction === 'up') g.set({ row: range.top - count, col: range.left }, { row: range.bottom, col: range.right });
180
+ else if (direction === 'right') g.set({ row: range.bottom, col: cols[Math.min(cols.length - 1, ordR + count)] }, { row: range.top, col: range.left });
181
+ else g.set({ row: range.top, col: cols[Math.max(0, ordL - count)] }, { row: range.bottom, col: range.right });
182
+ // Marked after the selection moved, which clears the last fill's marks.
183
+ for (const s of skipped) this.skipped.add(key(s.rowIdx, s.col));
184
+ if (skipped.length) g.paint();
185
+ return { direction, count, staged, skipped };
186
+ }
187
+
188
+ /** The selection moved, or new rows came: the skipped marks are over. */
189
+ clearMarks() {
190
+ this.skipped.clear();
191
+ }
192
+
193
+ // ── the drag ─────────────────────────────────────────────────────────
194
+
195
+ _begin(ev) {
196
+ if (ev.button != null && ev.button !== 0) return;
197
+ ev.preventDefault();
198
+ ev.stopPropagation();
199
+ const g = this.grid;
200
+ g._sync();
201
+ const range = g.sel.range();
202
+ if (!range || this.drag || g.isEditing() || g.staged.committing) return;
203
+ const doc = this.el.ownerDocument;
204
+ this._guardClick(doc);
205
+ const onMove = (move) => this._move(move);
206
+ const onUp = (up) => {
207
+ const plan = this.drag?.plan;
208
+ this._end();
209
+ if (plan) this.fill(this._reach(range, plan), { forceCopy: !!(up.ctrlKey || up.metaKey) });
210
+ try { g.table._tableEl?.focus({ preventScroll: true }); } catch (_) { /* detached */ }
211
+ };
212
+ const onCancel = () => this._end();
213
+ const onKey = (k) => { if (k.key === 'Escape') { k.preventDefault(); k.stopPropagation(); this._end(); } };
214
+ doc.addEventListener('pointermove', onMove);
215
+ doc.addEventListener('pointerup', onUp);
216
+ doc.addEventListener('pointercancel', onCancel);
217
+ doc.addEventListener('keydown', onKey, true);
218
+ this.drag = {
219
+ range,
220
+ plan: null,
221
+ unbind: () => {
222
+ doc.removeEventListener('pointermove', onMove);
223
+ doc.removeEventListener('pointerup', onUp);
224
+ doc.removeEventListener('pointercancel', onCancel);
225
+ doc.removeEventListener('keydown', onKey, true);
226
+ },
227
+ };
228
+ }
229
+
230
+ /**
231
+ * A press on the handle and its release make, in a browser, a click (and
232
+ * on a second press a dblclick) of their own, on the element under both:
233
+ * the corner cell, the row, the body. Filled, cancelled by Escape or
234
+ * released where it began, a drag is no click on the table: from the
235
+ * press, the next click in the document is swallowed in the capture
236
+ * phase, and so is every dblclick until the task the release is handled
237
+ * in is over. A cancelled pointer makes no click; the guard ends with it.
238
+ */
239
+ _guardClick(doc) {
240
+ this._unguard?.();
241
+ const view = doc.defaultView;
242
+ let timer = null;
243
+ const swallow = (c) => { c.stopPropagation(); c.preventDefault(); };
244
+ const swallowOnce = (c) => { swallow(c); doc.removeEventListener('click', swallowOnce, true); };
245
+ const released = () => {
246
+ doc.removeEventListener('pointerup', released, true);
247
+ doc.removeEventListener('pointercancel', unguard, true);
248
+ timer = view.setTimeout(unguard, 0);
249
+ };
250
+ const unguard = () => {
251
+ if (timer !== null) view.clearTimeout(timer);
252
+ doc.removeEventListener('click', swallowOnce, true);
253
+ doc.removeEventListener('dblclick', swallow, true);
254
+ doc.removeEventListener('pointerup', released, true);
255
+ doc.removeEventListener('pointercancel', unguard, true);
256
+ if (this._unguard === unguard) this._unguard = null;
257
+ };
258
+ doc.addEventListener('click', swallowOnce, true);
259
+ doc.addEventListener('dblclick', swallow, true);
260
+ doc.addEventListener('pointerup', released, true);
261
+ doc.addEventListener('pointercancel', unguard, true);
262
+ this._unguard = unguard;
263
+ }
264
+
265
+ /** The cell a plan reaches, as `fill` takes it. */
266
+ _reach(range, { direction, count }) {
267
+ const cols = this.grid.columns();
268
+ if (direction === 'down') return { row: range.bottom + count, col: range.right };
269
+ if (direction === 'up') return { row: range.top - count, col: range.left };
270
+ if (direction === 'right') return { row: range.bottom, col: cols[cols.indexOf(range.right) + count] };
271
+ return { row: range.top, col: cols[cols.indexOf(range.left) - count] };
272
+ }
273
+
274
+ /** The data cell under the pointer: the event's target, or (a touch,
275
+ * whose events stay on the handle) the element at its point. */
276
+ _cellUnder(ev) {
277
+ const tbody = this.grid.table._tbodyEl;
278
+ const hit = (el) => {
279
+ const td = el?.closest?.('td');
280
+ if (!td || td.__colIdx === undefined || !tbody?.contains(td)) return null;
281
+ const pos = td.parentNode?.__cellPos;
282
+ return pos === undefined ? null : { row: pos, col: td.__colIdx };
283
+ };
284
+ let cell = ev.target === this.el ? null : hit(ev.target);
285
+ const doc = this.el.ownerDocument;
286
+ if (!cell && typeof doc.elementFromPoint === 'function') {
287
+ cell = hit(doc.elementFromPoint(ev.clientX, ev.clientY));
288
+ }
289
+ return cell;
290
+ }
291
+
292
+ _move(ev) {
293
+ const d = this.drag;
294
+ if (!d) return;
295
+ const cell = this._cellUnder(ev);
296
+ if (!cell) return;
297
+ d.plan = this.plan(d.range, cell);
298
+ this.preview.clear();
299
+ if (d.plan) {
300
+ for (const { targets } of this._lines(d.range, d.plan)) {
301
+ for (const t of targets) this.preview.add(`${t.pos}:${t.col}`);
302
+ }
303
+ }
304
+ this.grid.paint();
305
+ }
306
+
307
+ _end() {
308
+ const d = this.drag;
309
+ if (!d) return;
310
+ this.drag = null;
311
+ d.unbind();
312
+ this.preview.clear();
313
+ this.grid.paint();
314
+ }
315
+
316
+ dispose() {
317
+ this._end();
318
+ this._unguard?.();
319
+ this._unplace();
320
+ this.skipped.clear();
321
+ }
322
+ }
@@ -6,7 +6,8 @@
6
6
  * called a "template"). The picker is mounted into a host element
7
7
  * and fires `onChange(entry)` whenever the selection changes.
8
8
  *
9
- * Each entry is the raw gallery dict from `pywebview.api.gallery_list`:
9
+ * The entries come from `opts.entries`, or else from the host's optional
10
+ * `gallery` capability (`host.gallery.list(kind)`, see host.js). Each entry is
10
11
  *
11
12
  * { id, kind, label, description, fields }
12
13
  *
@@ -17,6 +18,7 @@
17
18
  * Usage:
18
19
  *
19
20
  * const picker = await mountGalleryPicker(hostEl, {
21
+ * host, // or entries: [...]
20
22
  * kind: 'agent',
21
23
  * selectedId: 'blank',
22
24
  * onChange: (entry) => { … },
@@ -45,8 +47,11 @@ function esc(s) {
45
47
  * first entry wins.
46
48
  * @param {function} [opts.onChange] — `(entry) => void` fired on selection
47
49
  * change AND on initial mount.
48
- * @param {object[]} [opts.entries] — explicit entries (skips bridge fetch).
49
- * Useful for tests + offline previews.
50
+ * @param {object[]} [opts.entries] — explicit entries (the host is not asked).
51
+ * @param {object} [opts.host] — a Host; its `gallery.list(kind)` supplies
52
+ * the entries when `entries` is absent.
53
+ * With neither, the picker shows
54
+ * `emptyLabel` and warns once.
50
55
  * @param {string} [opts.emptyLabel] — copy shown when the catalog is empty
51
56
  * for that kind.
52
57
  * @returns {Promise<{getSelected, setSelectedById, dispose}>}
@@ -57,18 +62,24 @@ export async function mountGalleryPicker(hostEl, opts = {}) {
57
62
  selectedId = null,
58
63
  onChange = () => {},
59
64
  entries = null,
65
+ host = null,
60
66
  emptyLabel = 'No starters available for this kind.',
61
67
  } = opts;
62
68
  if (!hostEl) throw new Error('mountGalleryPicker: hostEl is required');
63
69
 
64
70
  let catalog = entries;
65
71
  if (!catalog) {
66
- const api = window.pywebview?.api;
67
- try {
68
- catalog = (await api?.gallery_list?.(kind)) || [];
69
- } catch (err) {
70
- console.warn('[gallery-picker] gallery_list failed', err);
72
+ const list = host?.gallery?.list;
73
+ if (typeof list !== 'function') {
74
+ console.warn('[gallery-picker] no entries and no host gallery capability; showing the empty label');
71
75
  catalog = [];
76
+ } else {
77
+ try {
78
+ catalog = (await list(kind)) || [];
79
+ } catch (err) {
80
+ console.warn('[gallery-picker] host.gallery.list failed', err);
81
+ catalog = [];
82
+ }
72
83
  }
73
84
  }
74
85
  catalog = Array.isArray(catalog) ? catalog : [];
@@ -0,0 +1,55 @@
1
+ /**
2
+ * header_actions.js — STORY-0066. The pieces DataTable's column groups
3
+ * (column_groups.js) and structural edits (table_structure.js) share: the
4
+ * consumer callback called behind a guard, and the action button drawn in a
5
+ * header cell. INTERNAL to the table, and not part of the widgets barrel.
6
+ */
7
+
8
+ /** Call the table option `name` (a function on `config`) with `args`. An
9
+ * option that is not a function answers undefined; one that throws is
10
+ * logged and answers undefined too, so a consumer's bug never breaks a
11
+ * render or a click. */
12
+ export function callOption(config, name, ...args) {
13
+ const fn = config?.[name];
14
+ if (typeof fn !== 'function') return undefined;
15
+ try {
16
+ return fn.apply(config, args);
17
+ } catch (err) {
18
+ console.error(`[DataTable] ${name} threw`, err);
19
+ return undefined;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * A button in a header cell for the item `{icon?, label?, title?, disabled?}`:
25
+ * the material-symbols icon, or the label when there is no icon; the title
26
+ * and aria-label from `title ?? label`; disabled when `disabled`. Its press
27
+ * is its own: neither the mousedown nor the click reaches the header (no
28
+ * sort, no drag under it).
29
+ */
30
+ export function headerActionButton(className, { icon, label, title, disabled } = {}, onClick) {
31
+ const b = document.createElement('button');
32
+ b.type = 'button';
33
+ b.className = className;
34
+ if (icon) {
35
+ const span = document.createElement('span');
36
+ span.className = 'material-symbols-outlined';
37
+ span.textContent = String(icon);
38
+ b.appendChild(span);
39
+ } else if (label != null) {
40
+ b.appendChild(document.createTextNode(String(label)));
41
+ }
42
+ const name = title ?? label;
43
+ if (name != null) {
44
+ b.title = String(name);
45
+ b.setAttribute('aria-label', String(name));
46
+ }
47
+ if (disabled) b.disabled = true;
48
+ b.addEventListener('mousedown', (ev) => ev.stopPropagation());
49
+ b.addEventListener('click', (ev) => {
50
+ ev.preventDefault();
51
+ ev.stopPropagation();
52
+ onClick(ev);
53
+ });
54
+ return b;
55
+ }
File without changes
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The progress block both job dialogs share — `showRunProgressDialog` and
3
+ * `showComputingWindow` — so a bar and a row of phases behave the same in
4
+ * either. Internal: not in the `widgets` barrel.
5
+ */
6
+
7
+ import { ProgressBar, StepProgress } from './progress.js';
8
+
9
+ /**
10
+ * Mount the optional phases and bar into `parent`, phases first.
11
+ *
12
+ * @param {HTMLElement} parent
13
+ * @param {{ progress?: boolean|Object, steps?: Array, spinner?: HTMLElement|null }} options
14
+ * `progress`: `true`, or ProgressBar options (its `onCancel` is ignored: the
15
+ * dialog owns cancelling). `steps`: StepProgress steps. `spinner`: hidden
16
+ * while a bar is shown. Both are fixed at mount: `setProgress` throws
17
+ * without `progress` and `setStep` without `steps`, so the window is sized
18
+ * for what it holds from the start.
19
+ */
20
+ export function mountJobProgress(parent, { progress = null, steps = null, spinner = null } = {}) {
21
+ const stepper = Array.isArray(steps) && steps.length
22
+ ? new StepProgress(parent, { steps, className: 'twm-job-progress__steps' })
23
+ : null;
24
+ let bar = null;
25
+ if (progress) {
26
+ const { onCancel: _ignored, ...rest } = typeof progress === 'object' ? progress : {};
27
+ bar = new ProgressBar(parent, { ...rest, className: 'twm-job-progress__bar' });
28
+ if (spinner) spinner.hidden = true;
29
+ }
30
+
31
+ return {
32
+ get bar() { return bar; },
33
+ get steps() { return stepper; },
34
+ /** `bar.update(next)`. */
35
+ setProgress(next) {
36
+ if (!bar) throw new Error('setProgress: this dialog was opened without `progress`');
37
+ bar.update(next);
38
+ },
39
+ /** `steps.setStep(which, opts)`. */
40
+ setStep(which, opts) {
41
+ if (!stepper) throw new Error('setStep: this dialog was opened without `steps`');
42
+ stepper.setStep(which, opts);
43
+ },
44
+ /** Put the bar and the phases in their final state. */
45
+ finish(status) {
46
+ if (status === 'done') { bar?.setState('done'); stepper?.complete(); }
47
+ else if (status === 'error') { bar?.setState('error'); stepper?.fail(); }
48
+ else bar?.setState('cancelled');
49
+ },
50
+ };
51
+ }
@@ -19,7 +19,11 @@
19
19
 
20
20
  import { getSetting } from '../../core/settings.js';
21
21
 
22
- const STORAGE_KEY = 'ecosim.managedWindows.v1';
22
+ /** Where window geometry lives when the embedder names no key. An embedder
23
+ * that saved geometry under another key keeps it by passing that key to
24
+ * `ManagedWindow.configure({ storageKey })` before the first window opens. */
25
+ export const DEFAULT_WINDOW_STORAGE_KEY = 'flexdesk.managedWindows.v1';
26
+ let _storageKey = DEFAULT_WINDOW_STORAGE_KEY;
23
27
  const BASE_Z_INDEX = 6000;
24
28
  const MAX_Z_INDEX = 6999;
25
29
 
@@ -70,7 +74,7 @@ let _stateCache = null;
70
74
  function _loadState() {
71
75
  if (_stateCache) return _stateCache;
72
76
  try {
73
- const raw = localStorage.getItem(STORAGE_KEY);
77
+ const raw = localStorage.getItem(_storageKey);
74
78
  _stateCache = raw ? JSON.parse(raw) : {};
75
79
  } catch {
76
80
  _stateCache = {};
@@ -81,7 +85,7 @@ function _loadState() {
81
85
  function _saveState(state) {
82
86
  _stateCache = state;
83
87
  try {
84
- localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
88
+ localStorage.setItem(_storageKey, JSON.stringify(state));
85
89
  } catch (err) {
86
90
  console.warn('[ManagedWindow] Failed to save state:', err);
87
91
  }
@@ -958,7 +962,7 @@ export class ManagedWindow {
958
962
  * x = max(minX, min(x, maxX)) = max(0, min(x, maxX)) ✓
959
963
  * y = max(minY, min(y, maxY)) = max(TOP, min(y, maxY)) ✓
960
964
  *
961
- * EcoAgent's and EcoSim's modals depend on that arithmetic; prove the
965
+ * Embedders' modals depend on that arithmetic; prove the
962
966
  * equivalence in review by substitution rather than by testing.
963
967
  */
964
968
  _bounds() {
@@ -1661,6 +1665,25 @@ export class ManagedWindow {
1661
1665
 
1662
1666
  // ========== Static Methods ==========
1663
1667
 
1668
+ /**
1669
+ * Choose where window geometry is saved and restored. Call once at boot,
1670
+ * before the first window opens; from then on windows read and write only
1671
+ * `storageKey`. The geometry already read from another key is dropped, so
1672
+ * a window shown afterwards restores what `storageKey` holds.
1673
+ *
1674
+ * @param {object} [opts]
1675
+ * @param {string} [opts.storageKey=DEFAULT_WINDOW_STORAGE_KEY]
1676
+ */
1677
+ static configure({ storageKey = DEFAULT_WINDOW_STORAGE_KEY } = {}) {
1678
+ _storageKey = storageKey;
1679
+ _stateCache = null;
1680
+ }
1681
+
1682
+ /** @returns {string} The localStorage key window geometry uses. */
1683
+ static get storageKey() {
1684
+ return _storageKey;
1685
+ }
1686
+
1664
1687
  /**
1665
1688
  * Get a window by ID.
1666
1689
  */