flexdesk 0.16.1 → 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 (252) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2217 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/css/base.css +1834 -75
  6. package/css/flexdesk.css +3146 -164
  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-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
  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 +3146 -164
  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 +657 -234
  61. package/dist/tiles.js.map +4 -4
  62. package/dist/tokens.css +0 -0
  63. package/dist/widgets.js +3252 -2124
  64. package/dist/widgets.js.map +4 -4
  65. package/dist/wm.js +1227 -347
  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 +7 -7
  144. package/src/tiles/scope_picker.js +0 -0
  145. package/src/tiles/tile_base.js +1 -1
  146. package/src/tiles/tile_config_form.js +442 -0
  147. package/src/tiles/tile_grid.js +2 -0
  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 +0 -0
  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-B5Q53LAJ.js.map +0 -7
  241. package/dist/chunk-FBGP5KN5.js.map +0 -7
  242. package/dist/chunk-FL5KFNQH.js.map +0 -7
  243. package/dist/chunk-NLTOY57M.js.map +0 -7
  244. package/dist/chunk-NNFTETK3.js +0 -633
  245. package/dist/chunk-NNFTETK3.js.map +0 -7
  246. package/dist/chunk-O5OHMWBB.js +0 -284
  247. package/dist/chunk-O5OHMWBB.js.map +0 -7
  248. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  249. package/dist/chunk-SN4LSOAR.js.map +0 -7
  250. package/dist/chunk-W6FBJLPW.js +0 -367
  251. package/dist/chunk-W6FBJLPW.js.map +0 -7
  252. package/src/ui/controllers/panel_state_machine.js +0 -634
@@ -0,0 +1,220 @@
1
+ /**
2
+ * cell_selection.js — STORY-0003. The model under DataTable's CELL MODE
3
+ * (`selectionMode: 'cells'`): a cursor cell, a range from an anchor to that
4
+ * cursor, and every move the keys make over them. No DOM, so the rules can be
5
+ * tested — or drive another grid — without a table. DataTable's own half (the
6
+ * painting, the pointer, the keys, the editor) is `cell_grid.js`, which is
7
+ * internal to the table.
8
+ *
9
+ * ══ COORDINATES ═════════════════════════════════════════════════════════
10
+ *
11
+ * A cell is `{row, col}`. `row` is the row's POSITION in the order shown — the
12
+ * index into `getDisplayedRows()`, after the sort and the filters — because a
13
+ * range is a rectangle on the screen, not in the data. `col` is the DATA column
14
+ * index (the one `headers`, `renderCell` and `hiddenColumns` use); a range
15
+ * spans the columns SHOWN between its two edges, so a hidden column inside it
16
+ * is not part of it. The cursor moves only over the rows of the page drawn.
17
+ */
18
+
19
+ /**
20
+ * The selection model. Rows are positions `first..last`; columns are the data
21
+ * indices in `cols`, in the order they are shown. Both are handed in on every
22
+ * move (`setBounds`), because a filter, a page or a hidden column changes them
23
+ * under a selection that has to survive.
24
+ */
25
+ export class CellSelection {
26
+ constructor() {
27
+ /** @type {{row:number, col:number}|null} */
28
+ this.anchor = null;
29
+ /** @type {{row:number, col:number}|null} the cursor */
30
+ this.focus = null;
31
+ this.first = 0;
32
+ this.last = -1;
33
+ this.cols = [];
34
+ }
35
+
36
+ /** The rows (positions, inclusive) and columns (data indices, shown order)
37
+ * the cursor may stand on. A selection outside them is pulled in. */
38
+ setBounds(first, last, cols) {
39
+ this.first = first;
40
+ this.last = last;
41
+ this.cols = Array.isArray(cols) ? cols.slice() : [];
42
+ if (this.isEmptyGrid()) {
43
+ this.anchor = null;
44
+ this.focus = null;
45
+ return;
46
+ }
47
+ if (this.focus) this.focus = this._clamp(this.focus);
48
+ if (this.anchor) this.anchor = this._clamp(this.anchor);
49
+ }
50
+
51
+ isEmptyGrid() {
52
+ return this.last < this.first || this.cols.length === 0;
53
+ }
54
+
55
+ _ord(col) {
56
+ const i = this.cols.indexOf(col);
57
+ return i >= 0 ? i : 0;
58
+ }
59
+
60
+ _clamp({ row, col }) {
61
+ const r = Math.max(this.first, Math.min(this.last, row));
62
+ const c = this.cols.includes(col) ? col
63
+ : this.cols[Math.max(0, Math.min(this.cols.length - 1, this._nearestOrd(col)))];
64
+ return { row: r, col: c };
65
+ }
66
+
67
+ /** A column that is not shown any more: the shown one nearest its index. */
68
+ _nearestOrd(col) {
69
+ let best = 0;
70
+ for (let i = 0; i < this.cols.length; i++) if (this.cols[i] <= col) best = i;
71
+ return best;
72
+ }
73
+
74
+ _inside(row, ord) {
75
+ return row >= this.first && row <= this.last && ord >= 0 && ord < this.cols.length;
76
+ }
77
+
78
+ /** Put the cursor on one cell and collapse the range onto it. */
79
+ setCursor(row, col) {
80
+ if (this.isEmptyGrid()) return false;
81
+ this.focus = this._clamp({ row, col });
82
+ this.anchor = { ...this.focus };
83
+ return true;
84
+ }
85
+
86
+ /** Move the cursor to a cell and keep the anchor: the range grows. */
87
+ extendTo(row, col) {
88
+ if (!this.anchor) return this.setCursor(row, col);
89
+ if (this.isEmptyGrid()) return false;
90
+ this.focus = this._clamp({ row, col });
91
+ return true;
92
+ }
93
+
94
+ _go(cell, extend) {
95
+ return extend ? this.extendTo(cell.row, cell.col) : this.setCursor(cell.row, cell.col);
96
+ }
97
+
98
+ /** One step: the arrows. At an edge the cursor stays put. */
99
+ move(dRow, dCol, { extend = false } = {}) {
100
+ if (!this.focus) return this.setCursor(this.first, this.cols[0]);
101
+ const ord = Math.max(0, Math.min(this.cols.length - 1, this._ord(this.focus.col) + dCol));
102
+ const row = Math.max(this.first, Math.min(this.last, this.focus.row + dRow));
103
+ return this._go({ row, col: this.cols[ord] }, extend);
104
+ }
105
+
106
+ /**
107
+ * Ctrl+arrow: to the edge of the block of filled cells, as a spreadsheet
108
+ * does. From a filled cell whose neighbour is filled, to the last filled
109
+ * cell of that run; otherwise to the next filled cell along the line, or
110
+ * the edge of the grid when there is none.
111
+ *
112
+ * @param {number} dRow -1, 0 or 1
113
+ * @param {number} dCol -1, 0 or 1
114
+ * @param {(row:number, col:number) => boolean} filled
115
+ */
116
+ jump(dRow, dCol, filled, { extend = false } = {}) {
117
+ if (!this.focus) return this.setCursor(this.first, this.cols[0]);
118
+ let r = this.focus.row;
119
+ let o = this._ord(this.focus.col);
120
+ const isFilled = (row, ord) => !!filled(row, this.cols[ord]);
121
+ if (this._inside(r + dRow, o + dCol)) {
122
+ if (isFilled(r, o) && isFilled(r + dRow, o + dCol)) {
123
+ while (this._inside(r + dRow, o + dCol) && isFilled(r + dRow, o + dCol)) {
124
+ r += dRow; o += dCol;
125
+ }
126
+ } else {
127
+ r += dRow; o += dCol;
128
+ while (!isFilled(r, o) && this._inside(r + dRow, o + dCol)) {
129
+ r += dRow; o += dCol;
130
+ }
131
+ }
132
+ }
133
+ return this._go({ row: r, col: this.cols[o] }, extend);
134
+ }
135
+
136
+ /** Home / End: the first or last column of the cursor's row; with
137
+ * `corner` (Ctrl), the first or last cell of the grid. */
138
+ lineEnd(toEnd, { extend = false, corner = false } = {}) {
139
+ if (!this.focus) return this.setCursor(this.first, this.cols[0]);
140
+ const col = toEnd ? this.cols[this.cols.length - 1] : this.cols[0];
141
+ const row = corner ? (toEnd ? this.last : this.first) : this.focus.row;
142
+ return this._go({ row, col }, extend);
143
+ }
144
+
145
+ /** Tab: one cell right, wrapping to the first cell of the next row (Shift:
146
+ * left, wrapping to the last cell of the row before). The last cell of
147
+ * the grid (the first, going back) stays put. */
148
+ tab(back = false) {
149
+ if (!this.focus) return this.setCursor(this.first, this.cols[0]);
150
+ let row = this.focus.row;
151
+ let ord = this._ord(this.focus.col) + (back ? -1 : 1);
152
+ if (ord >= this.cols.length) {
153
+ if (row >= this.last) return this.setCursor(row, this.focus.col);
154
+ row += 1; ord = 0;
155
+ } else if (ord < 0) {
156
+ if (row <= this.first) return this.setCursor(row, this.focus.col);
157
+ row -= 1; ord = this.cols.length - 1;
158
+ }
159
+ return this.setCursor(row, this.cols[ord]);
160
+ }
161
+
162
+ /** Whether Tab (Shift+Tab with `back`) has nowhere to go: the cursor is
163
+ * on the grid's last cell (its first, going back). The grid then lets
164
+ * the key move focus on, out of the table. */
165
+ tabEnds(back = false) {
166
+ if (!this.focus || this.isEmptyGrid()) return false;
167
+ const col = back ? this.cols[0] : this.cols[this.cols.length - 1];
168
+ const row = back ? this.first : this.last;
169
+ return this.focus.row === row && this.focus.col === col;
170
+ }
171
+
172
+ /** Every cell of the grid: from its first cell to its last, which is
173
+ * where the cursor goes. */
174
+ selectAll() {
175
+ if (this.isEmptyGrid()) return false;
176
+ this.anchor = { row: this.first, col: this.cols[0] };
177
+ this.focus = { row: this.last, col: this.cols[this.cols.length - 1] };
178
+ return true;
179
+ }
180
+
181
+ /** The range collapses onto the cursor. */
182
+ collapse() {
183
+ if (!this.focus) return false;
184
+ const was = this.anchor && (this.anchor.row !== this.focus.row || this.anchor.col !== this.focus.col);
185
+ this.anchor = { ...this.focus };
186
+ return !!was;
187
+ }
188
+
189
+ clear() {
190
+ this.anchor = null;
191
+ this.focus = null;
192
+ }
193
+
194
+ /** `{top, bottom, left, right, columns}` — rows as positions, `left` and
195
+ * `right` and `columns` as data indices — or null with nothing selected. */
196
+ range() {
197
+ if (!this.focus || !this.anchor) return null;
198
+ const a = this._ord(this.anchor.col);
199
+ const b = this._ord(this.focus.col);
200
+ const lo = Math.min(a, b);
201
+ const hi = Math.max(a, b);
202
+ return {
203
+ top: Math.min(this.anchor.row, this.focus.row),
204
+ bottom: Math.max(this.anchor.row, this.focus.row),
205
+ left: this.cols[lo],
206
+ right: this.cols[hi],
207
+ columns: this.cols.slice(lo, hi + 1),
208
+ };
209
+ }
210
+
211
+ contains(row, col) {
212
+ const r = this.range();
213
+ if (!r || row < r.top || row > r.bottom) return false;
214
+ return r.columns.includes(col);
215
+ }
216
+
217
+ isCursor(row, col) {
218
+ return !!this.focus && this.focus.row === row && this.focus.col === col;
219
+ }
220
+ }
@@ -0,0 +1,351 @@
1
+ /**
2
+ * CHOICE CONTROLS — the one select, the one set of toggle chips and the one
3
+ * radio list, shared by `openForm` and the flow kit's settings widgets.
4
+ *
5
+ * Each draws a fixed list of options and reads back the option's OWN value —
6
+ * a number stays a number, an object stays the object — never the text of an
7
+ * `<option>` or a `value` attribute. That is why every control here keeps the
8
+ * values beside the DOM it draws and resolves the reader's choice by position.
9
+ *
10
+ * An option is a bare value, or `{ value, label?, description?, disabled?,
11
+ * reason? }`. Each also takes `{ group, options }` entries: `selectBox` draws
12
+ * one as an `<optgroup>`, the chips and the radio list as a `.twm-choice__group`
13
+ * heading over its options. A group with no options draws no heading there.
14
+ *
15
+ * A DISABLED OPTION IS SHOWN, WITH ITS REASON, AND IS NEVER CHOSEN BY VALUE:
16
+ * a stored value that names one is not selected (the select falls to its first
17
+ * enabled option, the radios to none, the chips leave it unpressed), unless
18
+ * `keepMissing` keeps it as a value the list does not offer. The reader cannot
19
+ * pick one either. What a caller does about a disabled option selected some
20
+ * other way (a script setting `.value`) is the caller's: `selectBox.entry()`
21
+ * and `choiceList.entry()` say which option is selected, disabled or not.
22
+ *
23
+ * `keepMissing` (the kit's rule, on by default): a stored value the list does
24
+ * not offer is KEPT, marked with `notFound(text)`, so a settings panel cannot
25
+ * silently turn it into the first entry on the next save. A form that should
26
+ * offer only what is listed passes `keepMissing: false`.
27
+ *
28
+ * `loose`: a stored value matches an option whose value has the same TEXT
29
+ * (`2` matches `'2'`), for a caller whose stored values may arrive as strings.
30
+ * Off by default: the kit's values keep their type, and `'2'` is not `2`.
31
+ * Text is only the fallback: an option whose value IS the stored value (`===`)
32
+ * wins over an earlier one that only reads the same (`findChoice`).
33
+ *
34
+ * Nothing here names a consumer's vocabulary; every word on screen is the
35
+ * caller's (`label`, `reason`, `description`, `notFound`).
36
+ */
37
+
38
+ /** A value as one line of text shows it: a string as itself, anything else as JSON. */
39
+ export const shownValue = (v) => (v === undefined ? '' : typeof v === 'string' ? v : JSON.stringify(v));
40
+
41
+ function el(tag, className, text) {
42
+ const n = document.createElement(tag);
43
+ if (className) n.className = className;
44
+ if (text !== undefined && text !== null) n.textContent = String(text);
45
+ return n;
46
+ }
47
+
48
+ const isGroup = (o) => o !== null && typeof o === 'object' && Array.isArray(o.options) && 'group' in o;
49
+
50
+ /** One option, normalised: `{value, label, description, disabled, reason}`. */
51
+ function choiceOption(o) {
52
+ if (o === null || typeof o !== 'object') {
53
+ return { value: o, label: shownValue(o), description: null, disabled: false, reason: null };
54
+ }
55
+ return {
56
+ value: o.value,
57
+ label: o.label != null ? String(o.label) : shownValue(o.value),
58
+ description: o.description != null ? String(o.description) : null,
59
+ disabled: Boolean(o.disabled),
60
+ reason: o.reason != null && o.reason !== '' ? String(o.reason) : null,
61
+ };
62
+ }
63
+
64
+ /** Every option of a list, groups flattened in order. */
65
+ export function flatChoices(options) {
66
+ const out = [];
67
+ for (const o of options || []) {
68
+ if (isGroup(o)) for (const inner of o.options) out.push(choiceOption(inner));
69
+ else out.push(choiceOption(o));
70
+ }
71
+ return out;
72
+ }
73
+
74
+ /**
75
+ * Where the chips and the radio list draw a group heading: the position in
76
+ * the flattened list of each non-empty group's first option, to its heading.
77
+ */
78
+ function groupHeads(options) {
79
+ const heads = new Map();
80
+ let at = 0;
81
+ for (const o of options || []) {
82
+ if (!isGroup(o)) { at += 1; continue; }
83
+ if (o.options.length) heads.set(at, String(o.group ?? ''));
84
+ at += o.options.length;
85
+ }
86
+ return heads;
87
+ }
88
+
89
+ const groupHeading = (text) => el('div', 'twm-choice__group', text);
90
+
91
+ /** Does stored `value` name option value `v`? */
92
+ function sameChoice(v, value, loose = false) {
93
+ if (v === value) return true;
94
+ if (!loose || v === undefined || value === undefined || v === null || value === null) return false;
95
+ return String(v) === String(value);
96
+ }
97
+
98
+ /**
99
+ * The position in `list` (normalised options) of the option stored value `v`
100
+ * names, or -1: the first whose value IS `v`, else — with `loose` — the first
101
+ * whose value has the same text. `from` skips the leading entries.
102
+ */
103
+ function choiceIndex(list, v, loose = false, from = 0) {
104
+ for (let i = from; i < list.length; i += 1) if (list[i].value === v) return i;
105
+ if (!loose) return -1;
106
+ for (let i = from; i < list.length; i += 1) if (sameChoice(list[i].value, v, true)) return i;
107
+ return -1;
108
+ }
109
+
110
+ /** The option of `list` stored value `v` names (`choiceIndex`), or undefined. */
111
+ export function findChoice(list, v, loose = false) {
112
+ const i = choiceIndex(list, v, loose);
113
+ return i >= 0 ? list[i] : undefined;
114
+ }
115
+
116
+ /** An option's text: its label, and the reason it is unavailable when it is. */
117
+ const optionText = (o) => (o.disabled && o.reason ? `${o.label} — ${o.reason}` : o.label);
118
+
119
+ /**
120
+ * A `<select>` over `options`.
121
+ *
122
+ * `empty` (text) draws a first option that stands for "no value" (`undefined`).
123
+ * `valueAttr`: what each `<option value>` carries — `'index'` (its position;
124
+ * the kit's) or `'value'` (the option's value as text, so a script that sets
125
+ * `.value = 'b'` picks option `'b'`). The value READ BACK is the option's own
126
+ * either way.
127
+ *
128
+ * Returns `{ box, valueOf(), entry(), select(value), setOptions(options, value?) }`:
129
+ * `select` re-selects by value (true when the value is offered and enabled);
130
+ * `setOptions` redraws the list and places `value` (default: the current
131
+ * value) the way the first draw places one, so with `keepMissing` a value the
132
+ * list lacks shows as its own not-found entry.
133
+ */
134
+ export function selectBox(options, value, {
135
+ empty = null, label = '', notFound = (v) => `${v} (not found)`, keepMissing = true,
136
+ valueAttr = 'index', loose = false, className = 'twm-flow-input twm-flow-input--select',
137
+ } = {}) {
138
+ const box = el('select', className);
139
+ if (label) box.setAttribute('aria-label', label);
140
+ let entries = [];
141
+
142
+ const add = (parent, o) => {
143
+ const attr = valueAttr === 'index' ? String(entries.length) : (o.value === undefined ? '' : String(o.value));
144
+ // `createElement`, not `new Option`: the constructor is a window
145
+ // global, and a page that installed only `document` has none.
146
+ const opt = el('option', '', optionText(o));
147
+ opt.value = attr;
148
+ if (o.disabled) {
149
+ opt.disabled = true;
150
+ if (o.reason) opt.title = o.reason;
151
+ }
152
+ parent.appendChild(opt);
153
+ entries.push(o);
154
+ };
155
+ const firstEnabled = () => {
156
+ const i = entries.findIndex((e) => !e.disabled);
157
+ return i >= 0 ? i : 0;
158
+ };
159
+ const select = (v) => {
160
+ const start = empty !== null ? 1 : 0;
161
+ const at = choiceIndex(entries, v, loose, start);
162
+ if (at < 0 || entries[at].disabled || entries[at].stray) return false;
163
+ box.selectedIndex = at;
164
+ return true;
165
+ };
166
+ const place = (v) => {
167
+ if (select(v)) return;
168
+ if (keepMissing && v !== undefined && v !== null && v !== '') {
169
+ // A stored value the list no longer offers is KEPT as its own option:
170
+ // it must not silently become the first entry on the next save.
171
+ add(box, { value: v, label: notFound(shownValue(v)), description: null, disabled: false, reason: null,
172
+ stray: true });
173
+ box.selectedIndex = entries.length - 1;
174
+ return;
175
+ }
176
+ box.selectedIndex = empty !== null ? 0 : firstEnabled();
177
+ };
178
+ const fill = (opts, v) => {
179
+ box.replaceChildren();
180
+ entries = [];
181
+ if (empty !== null) add(box, { value: undefined, label: String(empty), description: null, disabled: false,
182
+ reason: null });
183
+ for (const o of opts || []) {
184
+ if (isGroup(o)) {
185
+ const g = el('optgroup');
186
+ g.label = String(o.group ?? '');
187
+ for (const inner of o.options) add(g, choiceOption(inner));
188
+ box.appendChild(g);
189
+ } else {
190
+ add(box, choiceOption(o));
191
+ }
192
+ }
193
+ place(v);
194
+ };
195
+ const entry = () => (box.selectedIndex < 0 ? null : entries[box.selectedIndex] ?? null);
196
+ const valueOf = () => entry()?.value;
197
+
198
+ fill(options, value);
199
+ return {
200
+ box,
201
+ valueOf,
202
+ entry,
203
+ select,
204
+ setOptions(opts, v = valueOf()) { fill(opts, v); },
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Toggle chips over a fixed list: a multi-select. The value is the list of
210
+ * pressed values in the list's order, and `onChange(list | undefined)` hears
211
+ * each toggle (`undefined` when none is pressed). With `keepMissing`, a stored
212
+ * entry the list lacks is kept, marked, until it is toggled off. A disabled
213
+ * option is a chip that cannot be pressed, its reason under its label.
214
+ *
215
+ * Returns `{ el, valueOf(), setValue(list), setOptions(options) }`;
216
+ * `setOptions` keeps the pressed values the new list still offers.
217
+ */
218
+ export function toggleChips({ options, value, label, readOnly, onChange, notFound = (v) => `${v} (not found)`,
219
+ keepMissing = true, loose = false }) {
220
+ const root = el('div', 'twm-flow-togglechips');
221
+ root.setAttribute('role', 'group');
222
+ if (label) root.setAttribute('aria-label', label);
223
+ let shown = []; // the options as given, groups and all
224
+ let list = [];
225
+ let chosen = new Set();
226
+ let strays = [];
227
+
228
+ const emit = () => {
229
+ const out = valueOf();
230
+ onChange?.(out.length ? out : undefined);
231
+ };
232
+ const chipFor = (text, pressed, toggle, { stray = false, reason = null, disabled = false } = {}) => {
233
+ const b = el('button', `twm-flow-togglechip${stray ? ' twm-flow-togglechip--missing' : ''}`, text);
234
+ b.type = 'button';
235
+ b.setAttribute('aria-pressed', pressed ? 'true' : 'false');
236
+ b.disabled = Boolean(readOnly) || disabled;
237
+ if (reason) {
238
+ b.title = reason;
239
+ b.appendChild(el('span', 'twm-choice__reason', reason));
240
+ }
241
+ b.addEventListener('click', () => {
242
+ const on = toggle();
243
+ b.setAttribute('aria-pressed', on ? 'true' : 'false');
244
+ emit();
245
+ });
246
+ return b;
247
+ };
248
+ const draw = (opts, stored) => {
249
+ shown = opts || [];
250
+ list = flatChoices(shown);
251
+ const heads = groupHeads(shown);
252
+ const wanted = Array.isArray(stored) ? stored : [];
253
+ const offered = (v) => {
254
+ const o = findChoice(list, v, loose);
255
+ return o && !o.disabled ? o : undefined;
256
+ };
257
+ chosen = new Set(wanted.map(offered).filter(Boolean).map((o) => o.value));
258
+ strays = keepMissing ? wanted.filter((v) => !offered(v)) : [];
259
+ root.replaceChildren();
260
+ list.forEach((o, i) => {
261
+ if (heads.has(i)) root.appendChild(groupHeading(heads.get(i)));
262
+ root.appendChild(chipFor(o.label, chosen.has(o.value), () => {
263
+ if (o.disabled) return false; // never chosen, even with the button forced
264
+ if (chosen.has(o.value)) chosen.delete(o.value); else chosen.add(o.value);
265
+ return chosen.has(o.value);
266
+ }, { disabled: o.disabled, reason: o.disabled ? o.reason : null }));
267
+ });
268
+ for (const v of [...strays]) {
269
+ root.appendChild(chipFor(notFound(shownValue(v)), true, () => {
270
+ const i = strays.indexOf(v);
271
+ if (i >= 0) { strays.splice(i, 1); return false; }
272
+ strays.push(v);
273
+ return true;
274
+ }, { stray: true }));
275
+ }
276
+ };
277
+ const valueOf = () => [...list.filter((o) => chosen.has(o.value)).map((o) => o.value), ...strays];
278
+ draw(options, value);
279
+ return {
280
+ el: root,
281
+ valueOf,
282
+ setValue: (stored) => draw(shown, stored),
283
+ setOptions: (opts) => draw(opts, valueOf()),
284
+ };
285
+ }
286
+
287
+ /**
288
+ * A one-of-N as radio rows, each with its label — and a sentence under it when
289
+ * the option has a `description`, and its `reason` when it is disabled. `name`
290
+ * groups the radios (the caller's, so it is unique where it needs to be).
291
+ *
292
+ * Returns `{ el, focus(), valueOf(), entry(), select(value), setOptions(options) }`.
293
+ * `onChange(value)` hears the reader's choice. With `keepMissing`, a stored
294
+ * value the list lacks is named in a note under the list (and kept by the
295
+ * caller, since no radio holds it).
296
+ */
297
+ export function choiceList({ options, value, name, label, readOnly, onChange, notFound = (v) => `${v} (not found)`,
298
+ keepMissing = true, loose = false }) {
299
+ const root = el('div', 'twm-flow-choices');
300
+ root.setAttribute('role', 'radiogroup');
301
+ if (label) root.setAttribute('aria-label', label);
302
+ let list = [];
303
+ let radios = [];
304
+
305
+ const draw = (opts, current) => {
306
+ list = flatChoices(opts);
307
+ const heads = groupHeads(opts);
308
+ radios = [];
309
+ root.replaceChildren();
310
+ const named = choiceIndex(list, current, loose);
311
+ const at = named >= 0 && !list[named].disabled ? named : -1;
312
+ list.forEach((o, i) => {
313
+ if (heads.has(i)) root.appendChild(groupHeading(heads.get(i)));
314
+ const row = el('label', `twm-flow-choice${o.disabled ? ' twm-choice--disabled' : ''}`);
315
+ const radio = el('input', 'twm-flow-choice__radio');
316
+ radio.type = 'radio';
317
+ radio.name = name;
318
+ radio.checked = i === at;
319
+ radio.disabled = Boolean(readOnly) || o.disabled;
320
+ radio.addEventListener('change', () => { if (radio.checked) onChange?.(o.value); });
321
+ radios.push(radio);
322
+ const words = el('span', 'twm-flow-choice__words');
323
+ words.appendChild(el('span', 'twm-flow-choice__label', o.label));
324
+ if (o.description !== null) words.appendChild(el('span', 'twm-flow-choice__sub', o.description));
325
+ if (o.disabled && o.reason) words.appendChild(el('span', 'twm-choice__reason', o.reason));
326
+ row.append(radio, words);
327
+ root.appendChild(row);
328
+ });
329
+ if (keepMissing && current !== undefined && named < 0) {
330
+ root.appendChild(el('p', 'twm-flow-field__note', notFound(shownValue(current))));
331
+ }
332
+ };
333
+ const checkedAt = () => radios.findIndex((r) => r.checked);
334
+ const entry = () => list[checkedAt()] ?? null;
335
+ const api = {
336
+ el: root,
337
+ focus: () => (radios.find((r) => r.checked && !r.disabled) || radios.find((r) => !r.disabled))
338
+ ?.focus({ preventScroll: true }),
339
+ entry,
340
+ valueOf: () => entry()?.value,
341
+ select(v) {
342
+ const named = choiceIndex(list, v, loose);
343
+ const at = named >= 0 && !list[named].disabled ? named : -1;
344
+ radios.forEach((r, i) => { r.checked = i === at; });
345
+ return at >= 0;
346
+ },
347
+ setOptions(opts) { draw(opts, api.valueOf()); },
348
+ };
349
+ draw(options, value);
350
+ return api;
351
+ }