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,314 @@
1
+ /**
2
+ * column_groups.js — STORY-0066. DataTable's COLUMN GROUPS (`columnGroups`).
3
+ * INTERNAL to the table: imported by `data_table.js` (and its span helpers by
4
+ * `crosstab.js`) only, and not part of the widgets barrel — it reads the
5
+ * table's private fields.
6
+ *
7
+ * A group is `{key, label?, columns: number[]}` (data column indices). The
8
+ * groups draw a second header row above the column headers, one cell a run of
9
+ * shown columns of the same group spanning them, a blank cell over the row
10
+ * number / select column and over any run of columns in no group.
11
+ *
12
+ * ── WHY A TABLE OF ITS OWN ────────────────────────────────────────────────
13
+ *
14
+ * The group row is a separate `<table class="twm-dt-groups">` in the header
15
+ * wrap, above the header table, rather than a first row of the header's
16
+ * `<thead>`. Every width pass of the table (`_measureNaturalWidths`,
17
+ * `_syncHeaderWidths`, the resize drag, `_applyTableWidth`) reads and writes
18
+ * the header rows CELL BY CELL at the column's index, and under
19
+ * `table-layout: fixed` the first row decides the track — a row of spanning
20
+ * cells in either place would misalign every column. So the group table has a
21
+ * `<colgroup>` of its own, one `<col>` a drawn column, and `align()` copies the
22
+ * header's widths, table width, layout and sideways scroll onto it after every
23
+ * pass that changes them (DataTable `_syncHeaderScroll`). The header wrap
24
+ * scrolls both tables together.
25
+ *
26
+ * ── AN EMPTY GROUP KEEPS A PLACEHOLDER COLUMN ─────────────────────────────
27
+ *
28
+ * A group with no columns would have no cell to draw, so it gets one drawn
29
+ * column of its own: a header cell `th.twm-dt-col--placeholder` (holding the
30
+ * group's add control under `onColumnAdd`), an empty filter cell, and an
31
+ * empty `td.twm-dt-cell--placeholder` in every body row. It is NO DATA
32
+ * column: `_dataIdxOfDom` answers null for it, it is not in a copy, the CSV,
33
+ * `ctx.colIdx` or the cell mode. It stands after the shown columns of the
34
+ * groups listed before it.
35
+ *
36
+ * A table without `columnGroups` never builds this.
37
+ */
38
+
39
+ import { callOption, headerActionButton } from './header_actions.js';
40
+
41
+ /**
42
+ * A row of spanning cells, cleaned (shared with STORY-0091's crosstab header
43
+ * rows): the data columns that exist, each column in the first cell that lists
44
+ * it, `key`, a label, and `index`, the cell's place in `spec` (the consumer's
45
+ * object is looked up there when drawing, never kept). Under `requireKey` a
46
+ * cell without a key is dropped; a cell without a label is labelled
47
+ * `defaultLabel(cell)`.
48
+ */
49
+ export function normalizeSpans(spec, n, { requireKey = false, defaultLabel = () => '' } = {}) {
50
+ const taken = new Set();
51
+ const out = [];
52
+ (Array.isArray(spec) ? spec : []).forEach((g, index) => {
53
+ if (!g || (requireKey && g.key == null)) return;
54
+ const columns = [];
55
+ for (const c of Array.isArray(g.columns) ? g.columns : []) {
56
+ const i = Number(c);
57
+ if (!Number.isInteger(i) || i < 0 || i >= n || taken.has(i)) continue;
58
+ taken.add(i);
59
+ columns.push(i);
60
+ }
61
+ out.push({ key: g.key, label: g.label == null ? defaultLabel(g) : String(g.label), columns, index });
62
+ });
63
+ return out;
64
+ }
65
+
66
+ /** The groups the table was given, cleaned: a key each, the key as the label
67
+ * when none is given. */
68
+ function normalize(spec, n) {
69
+ return normalizeSpans(spec, n, { requireKey: true, defaultLabel: (g) => String(g.key) });
70
+ }
71
+
72
+ /** A spanning header cell over a run of columns in no group (or no cell). */
73
+ export function blankSpanCell(span) {
74
+ const th = document.createElement('th');
75
+ th.className = 'twm-dt-group twm-dt-group--none';
76
+ th.colSpan = span;
77
+ return th;
78
+ }
79
+
80
+ /** A spanning header cell holding a label, its tooltip `title`. */
81
+ export function labelSpanCell(span, label, title = label) {
82
+ const th = document.createElement('th');
83
+ th.className = 'twm-dt-group';
84
+ th.colSpan = span;
85
+ th.title = title;
86
+ const el = document.createElement('span');
87
+ el.className = 'twm-dt-group__label';
88
+ el.textContent = label;
89
+ th.appendChild(el);
90
+ return th;
91
+ }
92
+
93
+ export class ColumnGroups {
94
+ /** @param {import('./data_table.js').DataTable} table */
95
+ constructor(table) {
96
+ this.table = table;
97
+ this._memoKey = null;
98
+ this._memo = null;
99
+ this._el = null;
100
+ }
101
+
102
+ _cfg() {
103
+ return this.table.config;
104
+ }
105
+
106
+ /**
107
+ * The drawn columns after the lead, in order: `{col, group}` for a shown
108
+ * data column (`group` the index in `groups`, or null) and
109
+ * `{placeholder: true, group}` for an empty group's column. The data
110
+ * columns come in the order the table draws them (STORY-0086's
111
+ * `columnOrder`, hidden ones left out). Memoised on the groups' content,
112
+ * the column count and the drawn columns.
113
+ */
114
+ layout() {
115
+ const cfg = this._cfg();
116
+ const n = cfg.headers?.length || 0;
117
+ const shown = this.table._shownColumns();
118
+ const spec = Array.isArray(cfg.columnGroups) ? cfg.columnGroups : [];
119
+ const key = `${n}|${shown.join(',')}|${spec.map((g) => (g ? `${g.key}\x01${g.label}\x01${(g.columns || []).join(',')}` : '')).join('\x02')}`;
120
+ if (key === this._memoKey) return this._memo;
121
+
122
+ const groups = normalize(spec, n);
123
+ const groupOf = new Array(n).fill(null);
124
+ groups.forEach((g, gi) => { for (const c of g.columns) groupOf[c] = gi; });
125
+ const empty = groups.map((g) => g.columns.length === 0);
126
+ const placed = new Set();
127
+ const slots = [];
128
+ const placeEmptyBefore = (gi) => {
129
+ for (let e = 0; e < gi; e++) {
130
+ if (empty[e] && !placed.has(e)) { placed.add(e); slots.push({ placeholder: true, group: e }); }
131
+ }
132
+ };
133
+ for (const c of shown) {
134
+ const gi = groupOf[c];
135
+ if (gi !== null) placeEmptyBefore(gi);
136
+ slots.push({ col: c, group: gi });
137
+ }
138
+ placeEmptyBefore(groups.length);
139
+
140
+ const slotOfCol = new Map();
141
+ slots.forEach((s, i) => { if (!s.placeholder) slotOfCol.set(s.col, i); });
142
+ const placeholders = slots.filter((s) => s.placeholder).length;
143
+ this._memoKey = key;
144
+ this._memo = { groups, slots, slotOfCol, placeholders };
145
+ return this._memo;
146
+ }
147
+
148
+ /** How many placeholder columns are drawn. */
149
+ placeholderCount() {
150
+ return this.layout().placeholders;
151
+ }
152
+
153
+ /** A shown data column's place after the lead, or -1 when the layout
154
+ * has no placeholder (the table's own arithmetic then holds). */
155
+ slotOf(colIdx) {
156
+ const l = this.layout();
157
+ if (!l.placeholders) return -1;
158
+ return l.slotOfCol.has(colIdx) ? l.slotOfCol.get(colIdx) : -1;
159
+ }
160
+
161
+ /** What the drawn column `i` after the lead is: the data column, null for
162
+ * a placeholder or past the end, `undefined` when the layout has no
163
+ * placeholder (the table's own arithmetic then holds). */
164
+ slotAt(i) {
165
+ const l = this.layout();
166
+ if (!l.placeholders) return undefined;
167
+ const s = l.slots[i];
168
+ return s && !s.placeholder ? s.col : null;
169
+ }
170
+
171
+ /** Part of the table's column signature: where the placeholders stand. */
172
+ sig() {
173
+ const l = this.layout();
174
+ if (!l.placeholders) return '';
175
+ return `|g${l.slots.map((s, i) => (s.placeholder ? i : '')).filter((x) => x !== '').join(',')}`;
176
+ }
177
+
178
+ /** The drawn positions (after the lead) of the placeholders, ascending,
179
+ * with their groups. */
180
+ _placeholderSlots() {
181
+ const out = [];
182
+ this.layout().slots.forEach((s, i) => { if (s.placeholder) out.push({ at: i, group: s.group }); });
183
+ return out;
184
+ }
185
+
186
+ /** Put the placeholders' cells into a drawn row whose cells are the lead
187
+ * and the shown data columns, in order. */
188
+ _insert(tr, make) {
189
+ const lead = this.table._lead().all;
190
+ for (const { at, group } of this._placeholderSlots()) {
191
+ const ref = tr.children[lead + at] || null;
192
+ tr.insertBefore(make(this.layout().groups[group]), ref);
193
+ }
194
+ }
195
+
196
+ /** STORY-0066. A body row gets an empty cell under each placeholder. */
197
+ fillRow(tr) {
198
+ if (!this.placeholderCount()) return;
199
+ this._insert(tr, () => {
200
+ const td = document.createElement('td');
201
+ td.className = 'twm-dt-cell--placeholder';
202
+ return td;
203
+ });
204
+ }
205
+
206
+ /**
207
+ * After the header is built and in the header table: the placeholders'
208
+ * header and filter cells, and the group table above the header table.
209
+ */
210
+ decorate(headerWrap, headerTable) {
211
+ this._el = null;
212
+ if (!headerWrap || !headerTable) return;
213
+ const thead = headerTable.querySelector('thead');
214
+ const labelRow = thead?.querySelector('tr');
215
+ if (!labelRow) return;
216
+ if (this.placeholderCount()) {
217
+ this._insert(labelRow, (g) => this._placeholderHead(g));
218
+ for (const tr of thead.querySelectorAll('tr.twm-data-table__filter-row')) {
219
+ this._insert(tr, () => {
220
+ const th = document.createElement('th');
221
+ th.className = 'data-table__filter-cell data-table__filter-cell--empty twm-dt-col--placeholder';
222
+ return th;
223
+ });
224
+ }
225
+ }
226
+ const groupTable = this._buildGroupTable(headerTable);
227
+ headerWrap.insertBefore(groupTable, headerTable);
228
+ this._el = groupTable;
229
+ this.align();
230
+ }
231
+
232
+ _placeholderHead(group) {
233
+ const th = document.createElement('th');
234
+ th.className = 'text twm-dt-col--placeholder';
235
+ if (typeof this._cfg().onColumnAdd === 'function') {
236
+ th.appendChild(this._addButton(group));
237
+ }
238
+ return th;
239
+ }
240
+
241
+ _addButton(group) {
242
+ return headerActionButton('twm-dt-group__add', { icon: 'add', title: `Add column to ${group.label}` },
243
+ () => callOption(this._cfg(), 'onColumnAdd', group.key));
244
+ }
245
+
246
+ _buildGroupTable(headerTable) {
247
+ const { groups, slots } = this.layout();
248
+ const lead = this.table._lead().all;
249
+ const table = document.createElement('table');
250
+ table.className = `${headerTable.className} twm-dt-groups`;
251
+ const colgroup = document.createElement('colgroup');
252
+ for (let i = 0; i < lead + slots.length; i++) colgroup.appendChild(document.createElement('col'));
253
+ table.appendChild(colgroup);
254
+ const thead = document.createElement('thead');
255
+ const tr = document.createElement('tr');
256
+ tr.className = 'twm-dt-group-row';
257
+ if (lead) {
258
+ const th = document.createElement('th');
259
+ th.className = 'twm-dt-group twm-dt-group--lead';
260
+ th.colSpan = lead;
261
+ tr.appendChild(th);
262
+ }
263
+ // Runs of the same group (or of no group) are one cell.
264
+ let i = 0;
265
+ while (i < slots.length) {
266
+ const gi = slots[i].group;
267
+ let j = i + 1;
268
+ while (j < slots.length && slots[j].group === gi) j++;
269
+ tr.appendChild(gi === null ? blankSpanCell(j - i) : this._groupCell(groups[gi], j - i, slots[i].placeholder));
270
+ i = j;
271
+ }
272
+ thead.appendChild(tr);
273
+ table.appendChild(thead);
274
+ return table;
275
+ }
276
+
277
+ _groupCell(group, span, empty) {
278
+ const th = labelSpanCell(span, group.label);
279
+ if (empty) th.classList.add('twm-dt-group--empty');
280
+
281
+ const actions = document.createElement('span');
282
+ actions.className = 'twm-dt-group__actions';
283
+ const cfg = this._cfg();
284
+ // The consumer's group object from the live config: the layout is
285
+ // memoised on key, label and columns, so a group whose other fields
286
+ // changed is still asked about with its current object.
287
+ const items = callOption(cfg, 'groupActions', cfg.columnGroups[group.index], { key: group.key, columns: group.columns.slice(), table: this.table });
288
+ for (const item of Array.isArray(items) ? items.filter(Boolean) : []) {
289
+ actions.appendChild(headerActionButton('twm-dt-group__action', item,
290
+ () => callOption(cfg, 'onGroupAction', item.action, group.key)));
291
+ }
292
+ if (typeof cfg.onColumnAdd === 'function') actions.appendChild(this._addButton(group));
293
+ if (actions.childNodes.length) th.appendChild(actions);
294
+ return th;
295
+ }
296
+
297
+ /** The group table follows the header table: the same column widths,
298
+ * table width, layout and sideways offset. */
299
+ align() {
300
+ const groupTable = this._el;
301
+ const headerTable = this.table._headerTableEl;
302
+ if (!groupTable || !headerTable) return;
303
+ const labelRow = headerTable.querySelector('thead > tr');
304
+ const cols = groupTable.querySelector('colgroup')?.children || [];
305
+ for (let i = 0; i < cols.length; i++) {
306
+ const th = labelRow?.children[i];
307
+ const w = th ? parseFloat(th.style.width) : NaN;
308
+ cols[i].style.width = Number.isFinite(w) ? `${w}px` : '';
309
+ }
310
+ groupTable.style.width = headerTable.style.width;
311
+ groupTable.style.tableLayout = headerTable.style.tableLayout;
312
+ groupTable.style.transform = headerTable.style.transform;
313
+ }
314
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * column_order.js — STORY-0086. The order DataTable draws its data columns in
3
+ * (`reorderable`, `columnOrder`, `moveColumn`, `getColumnOrder`), and the drag
4
+ * on the header that changes it. Internal to the table, like `cell_grid.js`.
5
+ *
6
+ * ══ AN ORDER IS A LIST OF DATA INDICES ══════════════════════════════════
7
+ *
8
+ * The order is every data column's index (hidden ones included), in the order
9
+ * shown: `[0, 3, 1, 2]` draws column 3 second. Everything else the table keeps
10
+ * by column — `hiddenColumns`, the sort, the filters, `renderCell`'s `colIdx`,
11
+ * the cell mode's `col` — stays a data index and so stays on its column. Only
12
+ * what is BY DRAWN POSITION (the cells of a row, the widths) follows the order.
13
+ *
14
+ * A table with no order asked for (`null`) draws 0, 1, 2, … as in 0.16. An
15
+ * order that is not a permutation of today's columns — a saved one from when
16
+ * the table had a different number of columns — is no order: the natural one
17
+ * is drawn.
18
+ */
19
+
20
+ /** Whether `order` is a permutation of `0..n-1`. */
21
+ export function isColumnOrder(order, n) {
22
+ if (!Array.isArray(order) || order.length !== n) return false;
23
+ const seen = new Set();
24
+ for (const c of order) {
25
+ if (!Number.isInteger(c) || c < 0 || c >= n || seen.has(c)) return false;
26
+ seen.add(c);
27
+ }
28
+ return true;
29
+ }
30
+
31
+ /** `0..n-1`. */
32
+ export function naturalOrder(n) {
33
+ return Array.from({ length: Math.max(0, n) }, (_, i) => i);
34
+ }
35
+
36
+ /**
37
+ * The data indices of a row of `count` cells in the order drawn: `order`
38
+ * where it holds, and any cell past the headers (a row longer than them) after
39
+ * it, as 0.16 drew them.
40
+ */
41
+ export function cellOrder(order, count) {
42
+ if (!order) return naturalOrder(count);
43
+ const out = order.filter((c) => c < count);
44
+ for (let c = order.length; c < count; c++) out.push(c);
45
+ return out;
46
+ }
47
+
48
+ /** `order` with the column at position `from` taken out and put back at
49
+ * position `to` (positions in `order`). A copy; `order` is not changed. */
50
+ export function moveInOrder(order, from, to) {
51
+ const next = order.slice();
52
+ const [moved] = next.splice(from, 1);
53
+ next.splice(to, 0, moved);
54
+ return next;
55
+ }
56
+
57
+ /**
58
+ * The header drag. Every data column's header cell is draggable; dropping one
59
+ * on another data column's header moves it to that column's place. A press
60
+ * that starts on a resize grip is the resize's, never a drag. The row-number
61
+ * and select columns are no data columns: they do not drag and take no drop,
62
+ * so nothing lands in front of them.
63
+ */
64
+ export class ColumnReorder {
65
+ /** @param {import('./data_table.js').DataTable} table */
66
+ constructor(table) {
67
+ this.table = table;
68
+ /** The data column being dragged, or null. */
69
+ this.dragging = null;
70
+ /** Whether the last press on a header cell was on its resize grip. */
71
+ this.onGrip = false;
72
+ }
73
+
74
+ /** Wire the header row of a fresh render. Its cells are new each render,
75
+ * so nothing needs taking off. */
76
+ install(headRow) {
77
+ if (!headRow) return;
78
+ [...headRow.children].forEach((th, domIdx) => {
79
+ const col = this.table._dataIdxOfDom(domIdx);
80
+ if (col === null) return;
81
+ th.draggable = true;
82
+ th.classList.add('twm-dt-col--movable');
83
+ // Capture: the grip stops its own mousedown, so a listener on the
84
+ // way up would never hear it.
85
+ th.addEventListener('mousedown', (ev) => {
86
+ this.onGrip = !!ev.target?.closest?.('.twm-dt-col-resizer');
87
+ }, true);
88
+ th.addEventListener('dragstart', (ev) => {
89
+ if (this.onGrip || ev.target?.closest?.('.twm-dt-col-resizer')) {
90
+ ev.preventDefault();
91
+ return;
92
+ }
93
+ this.dragging = col;
94
+ try {
95
+ ev.dataTransfer?.setData('text/plain', String(col));
96
+ if (ev.dataTransfer) ev.dataTransfer.effectAllowed = 'move';
97
+ } catch (_) { /* a drag with no data transfer */ }
98
+ th.classList.add('twm-dt-col--dragging');
99
+ });
100
+ th.addEventListener('dragover', (ev) => {
101
+ if (this.dragging === null || this.dragging === col) return;
102
+ ev.preventDefault(); // a drop is welcome here
103
+ if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
104
+ // Moved right it lands AFTER the column it is dropped on.
105
+ const order = this.table.getColumnOrder();
106
+ th.classList.add('twm-dt-col--drop');
107
+ th.classList.toggle('twm-dt-col--drop-after', order.indexOf(col) > order.indexOf(this.dragging));
108
+ });
109
+ th.addEventListener('dragleave', () => th.classList.remove('twm-dt-col--drop', 'twm-dt-col--drop-after'));
110
+ th.addEventListener('drop', (ev) => {
111
+ th.classList.remove('twm-dt-col--drop', 'twm-dt-col--drop-after');
112
+ const from = this.dragging;
113
+ this.dragging = null;
114
+ if (from === null || from === col) return;
115
+ ev.preventDefault();
116
+ const order = this.table.getColumnOrder();
117
+ this.table.moveColumn(order.indexOf(from), order.indexOf(col));
118
+ });
119
+ th.addEventListener('dragend', () => {
120
+ th.classList.remove('twm-dt-col--dragging');
121
+ this.dragging = null;
122
+ });
123
+ });
124
+ }
125
+
126
+ /** "Move left" / "Move right" for the header menu of data column `col`:
127
+ * past the shown column next to it. */
128
+ menuItems(col) {
129
+ const shown = this.table._shownColumns();
130
+ const at = shown.indexOf(col);
131
+ if (at < 0) return [];
132
+ const order = this.table.getColumnOrder();
133
+ const step = (to) => () => this.table.moveColumn(order.indexOf(col), order.indexOf(to));
134
+ return [
135
+ { label: 'Move left', icon: 'arrow_back', action: 'move-left', disabled: at === 0,
136
+ onChoose: at === 0 ? undefined : step(shown[at - 1]) },
137
+ { label: 'Move right', icon: 'arrow_forward', action: 'move-right',
138
+ disabled: at === shown.length - 1,
139
+ onChoose: at === shown.length - 1 ? undefined : step(shown[at + 1]) },
140
+ ];
141
+ }
142
+ }
@@ -1,53 +1,102 @@
1
1
  /**
2
2
  * Computing Status Window
3
3
  *
4
- * Lightweight ManagedWindow that shows a spinner and status message
5
- * while a long-running computation is in progress. Returns a handle
6
- * with update() and close() methods.
4
+ * Lightweight ManagedWindow that shows a spinner (or a progress bar) and a
5
+ * status message while a long-running computation is in progress. Returns a
6
+ * handle with update(), setProgress(), setStep(), complete(), cancelled(),
7
+ * close() and a `result` promise.
7
8
  *
8
9
  * Optionally shows a Cancel button when an onCancel callback is provided.
10
+ * Only one is open at a time: opening another closes the first, which then
11
+ * settles as cancelled (its onCancel is not called).
9
12
  */
10
13
 
11
14
  import { ManagedWindow } from './managed_window.js';
15
+ import { mountJobProgress } from './job_progress.js';
12
16
 
13
17
  let _activeWindow = null;
14
18
 
15
19
  /**
16
20
  * Show a computing status window.
21
+ *
22
+ * `result` settles once: `complete({ok, message})` gives `done` or `error`
23
+ * (the window stays, with a Close button, until closed), `cancelled()` gives
24
+ * `cancelled`, and so does closing the window while it runs (by its ×,
25
+ * Escape, `close()` or opening another). Closing never calls `onCancel`: only
26
+ * the Cancel button does. `progress` and `steps` are fixed when the window
27
+ * opens: `setProgress` throws if it was opened without `progress`, and
28
+ * `setStep` if it was opened without `steps`.
29
+ *
17
30
  * @param {Object} options
18
31
  * @param {string} [options.title='Computing...'] - Window title
19
- * @param {string} [options.message=''] - Status message below spinner
32
+ * @param {string} [options.message=''] - Status message below spinner (markup; `update` takes text)
20
33
  * @param {string} [options.icon='hourglass_top'] - Material icon name
21
34
  * @param {Function} [options.onCancel] - If provided, shows a Cancel button. Called once when clicked.
22
- * @returns {{ update(msg: string): void, close(): void }}
35
+ * @param {boolean|Object} [options.progress] - A ProgressBar in place of the spinner: `true` or its options.
36
+ * @param {Array} [options.steps] - StepProgress steps, shown above the message.
37
+ * @returns {{ update(msg: string): void, setProgress(next: number|Object): void,
38
+ * setStep(which: string|number, opts?: {pass?: number, passes?: number}): void,
39
+ * complete(result: {ok: boolean, message?: string}): void, cancelled(msg?: string): void,
40
+ * close(): void, result: Promise<{status: 'done'|'error'|'cancelled', message: string}> }}
23
41
  */
24
- export function showComputingWindow({ title = 'Computing...', message = '', icon = 'hourglass_top', onCancel } = {}) {
42
+ export function showComputingWindow({
43
+ title = 'Computing...', message = '', icon = 'hourglass_top', onCancel,
44
+ progress = null, steps = null,
45
+ } = {}) {
25
46
  if (_activeWindow) {
26
47
  _activeWindow.close();
27
48
  _activeWindow = null;
28
49
  }
29
50
 
51
+ let finished = false;
52
+ let settle;
53
+ const result = new Promise((resolve) => { settle = resolve; });
54
+
30
55
  const content = document.createElement('div');
31
56
  content.className = 'twm-computing-status';
32
- content.innerHTML = `
33
- <div class="twm-computing-status__spinner"></div>
34
- <div class="twm-computing-status__message">${message}</div>
35
- `;
57
+ const spinnerEl = document.createElement('div');
58
+ spinnerEl.className = 'twm-computing-status__spinner';
59
+ const msgEl = document.createElement('div');
60
+ msgEl.className = 'twm-computing-status__message';
61
+ // The opening message is markup, as it always was; every later one is text.
62
+ msgEl.innerHTML = message;
63
+ content.appendChild(spinnerEl);
64
+ const job = mountJobProgress(content, { progress, steps, spinner: spinnerEl });
65
+ content.appendChild(msgEl);
36
66
 
67
+ let cancelBtn = null;
37
68
  if (typeof onCancel === 'function') {
38
- const btn = document.createElement('button');
39
- btn.className = 'twm-computing-status__cancel-btn';
40
- btn.textContent = 'Cancel';
41
- btn.addEventListener('click', () => {
42
- btn.disabled = true;
43
- const msgEl = content.querySelector('.twm-computing-status__message');
44
- if (msgEl) msgEl.textContent = 'Cancelling...';
69
+ cancelBtn = document.createElement('button');
70
+ cancelBtn.className = 'twm-computing-status__cancel-btn';
71
+ cancelBtn.textContent = 'Cancel';
72
+ cancelBtn.addEventListener('click', () => {
73
+ cancelBtn.disabled = true;
74
+ msgEl.textContent = 'Cancelling...';
45
75
  onCancel();
46
76
  }, { once: true });
47
- content.appendChild(btn);
77
+ content.appendChild(cancelBtn);
48
78
  }
49
79
 
50
- _activeWindow = new ManagedWindow({
80
+ const closeBtn = document.createElement('button');
81
+ closeBtn.type = 'button';
82
+ closeBtn.className = 'twm-computing-status__cancel-btn twm-computing-status__close-btn';
83
+ closeBtn.textContent = 'Close';
84
+ closeBtn.hidden = true;
85
+ content.appendChild(closeBtn);
86
+
87
+ const finish = (status, text) => {
88
+ finished = true;
89
+ spinnerEl.hidden = true;
90
+ if (cancelBtn) cancelBtn.hidden = true;
91
+ msgEl.textContent = text;
92
+ msgEl.classList.toggle('is-ok', status === 'done');
93
+ msgEl.classList.toggle('is-err', status === 'error');
94
+ msgEl.classList.toggle('is-cancelled', status === 'cancelled');
95
+ job.finish(status);
96
+ settle({ status, message: text });
97
+ };
98
+
99
+ const win = new ManagedWindow({
51
100
  id: 'computing-status-window',
52
101
  title,
53
102
  icon,
@@ -55,21 +104,45 @@ export function showComputingWindow({ title = 'Computing...', message = '', icon
55
104
  minWidth: 320,
56
105
  minHeight: 140,
57
106
  defaultWidth: 380,
58
- defaultHeight: onCancel ? 190 : 160,
107
+ defaultHeight: (onCancel ? 190 : 160) + (job.bar ? 30 : 0) + (job.steps ? 40 : 0),
59
108
  canMinimize: true,
60
109
  canMaximize: false,
61
110
  canResize: false,
62
111
  canDrag: true,
63
112
  modal: false,
64
- onClose: () => { _activeWindow = null; },
113
+ onClose: () => {
114
+ if (_activeWindow === win) _activeWindow = null;
115
+ if (finished) return;
116
+ finish('cancelled', 'Cancelled.');
117
+ },
65
118
  });
66
-
67
- _activeWindow.show();
119
+ closeBtn.addEventListener('click', () => win.close());
120
+ _activeWindow = win;
121
+ win.show();
68
122
 
69
123
  return {
124
+ result,
70
125
  update(msg) {
71
- const el = content.querySelector('.twm-computing-status__message');
72
- if (el) el.textContent = msg;
126
+ if (finished) return;
127
+ msgEl.textContent = msg;
128
+ },
129
+ setProgress(next) {
130
+ if (finished) return;
131
+ job.setProgress(next);
132
+ },
133
+ setStep(which, opts) {
134
+ if (finished) return;
135
+ job.setStep(which, opts);
136
+ },
137
+ complete(res) {
138
+ if (finished) return;
139
+ finish(res?.ok ? 'done' : 'error', res?.message || '');
140
+ closeBtn.hidden = false;
141
+ },
142
+ cancelled(msg) {
143
+ if (finished) return;
144
+ finish('cancelled', msg || 'Cancelled.');
145
+ closeBtn.hidden = false;
73
146
  },
74
147
  close() {
75
148
  if (_activeWindow) {