flexdesk 0.16.1 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2435 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/canvas.js +18 -0
  6. package/charts.js +21 -0
  7. package/core.js +14 -0
  8. package/css/base.css +1834 -75
  9. package/css/flexdesk.css +3165 -159
  10. package/css/overrides.css +16 -0
  11. package/css/reset.css +0 -0
  12. package/css/tokens.css +0 -0
  13. package/dist/canvas.js +6 -239
  14. package/dist/canvas.js.map +3 -3
  15. package/dist/charts.js +707 -18
  16. package/dist/charts.js.map +4 -4
  17. package/dist/chunk-3PHCPZHT.js +0 -0
  18. package/dist/chunk-3PHCPZHT.js.map +0 -0
  19. package/dist/chunk-4A7SJC3T.js +503 -0
  20. package/dist/chunk-4A7SJC3T.js.map +7 -0
  21. package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
  22. package/dist/chunk-CVMCZZX2.js.map +7 -0
  23. package/dist/chunk-D3COLZZ2.js +1311 -0
  24. package/dist/chunk-D3COLZZ2.js.map +7 -0
  25. package/dist/chunk-FOOS3T5L.js +0 -0
  26. package/dist/chunk-FOOS3T5L.js.map +0 -0
  27. package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
  28. package/dist/chunk-GOQLR5WR.js.map +7 -0
  29. package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
  30. package/dist/chunk-HNU6MM2B.js.map +7 -0
  31. package/dist/chunk-HYJYYEPK.js +375 -0
  32. package/dist/chunk-HYJYYEPK.js.map +7 -0
  33. package/dist/chunk-JKZLNKG6.js +2041 -0
  34. package/dist/chunk-JKZLNKG6.js.map +7 -0
  35. package/dist/chunk-JYWURG5T.js +0 -0
  36. package/dist/chunk-JYWURG5T.js.map +0 -0
  37. package/dist/{chunk-FBGP5KN5.js → chunk-LWV23YVE.js} +5073 -532
  38. package/dist/chunk-LWV23YVE.js.map +7 -0
  39. package/dist/chunk-N7HC3ADV.js +30 -0
  40. package/dist/chunk-N7HC3ADV.js.map +7 -0
  41. package/dist/{chunk-RQM5Z2MU.js → chunk-RMTRWWEO.js} +633 -311
  42. package/dist/chunk-RMTRWWEO.js.map +7 -0
  43. package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
  44. package/dist/chunk-UURZ37SD.js.map +7 -0
  45. package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
  46. package/dist/chunk-UWO43BZU.js.map +7 -0
  47. package/dist/chunk-WVFGV5FT.js +0 -0
  48. package/dist/chunk-WVFGV5FT.js.map +0 -0
  49. package/dist/core.js +170 -118
  50. package/dist/core.js.map +2 -2
  51. package/dist/editor.js +156 -70
  52. package/dist/editor.js.map +2 -2
  53. package/dist/flexdesk.css +3165 -159
  54. package/dist/flow.js +2789 -306
  55. package/dist/flow.js.map +4 -4
  56. package/dist/host.js +109 -3
  57. package/dist/host.js.map +3 -3
  58. package/dist/reset.css +0 -0
  59. package/dist/tile_registry-6WZPTUZV.js +0 -0
  60. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  61. package/dist/tiles.js +657 -234
  62. package/dist/tiles.js.map +4 -4
  63. package/dist/tokens.css +0 -0
  64. package/dist/widgets.js +3290 -2124
  65. package/dist/widgets.js.map +4 -4
  66. package/dist/wm.js +1227 -347
  67. package/dist/wm.js.map +4 -4
  68. package/editor.js +20 -0
  69. package/flow.js +21 -0
  70. package/host.js +14 -0
  71. package/package.json +12 -1
  72. package/src/canvas/canvas_adapter.js +5 -0
  73. package/src/canvas/edge_router.js +0 -0
  74. package/src/canvas/graph/connector_router.js +0 -0
  75. package/src/canvas/graph/history_service.js +0 -0
  76. package/src/canvas/graph/node_platform.js +0 -0
  77. package/src/canvas/index.js +1 -0
  78. package/src/canvas/layered_layout.js +244 -0
  79. package/src/charts/chart_types.js +0 -0
  80. package/src/charts/data_series_plot_window.js +0 -0
  81. package/src/charts/downsample.js +2 -2
  82. package/src/charts/plot_config.js +53 -29
  83. package/src/charts/plot_config_editor.js +627 -0
  84. package/src/charts/plot_grid.js +449 -0
  85. package/src/charts/plot_popout_window.js +103 -317
  86. package/src/charts/plotly_wrapper.js +22 -2
  87. package/src/charts/series_transforms.js +206 -0
  88. package/src/core/event_bus.js +0 -0
  89. package/src/core/logging.js +1 -1
  90. package/src/core/settings.js +105 -19
  91. package/src/core/state_guard.js +93 -51
  92. package/src/core/state_guard_config.js +85 -74
  93. package/src/core/state_machine.js +0 -0
  94. package/src/editor/editor_pane.js +3 -3
  95. package/src/editor/monaco_editor_factory.js +124 -60
  96. package/src/editor/monaco_loader.js +103 -28
  97. package/src/editor/notebook_search.js +0 -0
  98. package/src/editor/notebook_tab_bar.js +38 -52
  99. package/src/editor/notebook_undo_manager.js +0 -0
  100. package/src/editor/split_pane_container.js +6 -3
  101. package/src/flow/canvas_editor/connections.js +90 -0
  102. package/src/flow/canvas_editor/editor.js +2351 -153
  103. package/src/flow/canvas_editor/index.js +2 -1
  104. package/src/flow/canvas_editor/layout.js +119 -12
  105. package/src/flow/canvas_editor/strings.js +80 -0
  106. package/src/flow/kit/always_before.js +0 -0
  107. package/src/flow/kit/catalogue.js +97 -3
  108. package/src/flow/kit/chip_input.js +0 -0
  109. package/src/flow/kit/dom.js +0 -0
  110. package/src/flow/kit/findings.js +0 -0
  111. package/src/flow/kit/history.js +0 -0
  112. package/src/flow/kit/index.js +3 -3
  113. package/src/flow/kit/keys.js +7 -1
  114. package/src/flow/kit/logic_graph.js +97 -15
  115. package/src/flow/kit/references.js +0 -0
  116. package/src/flow/kit/settings_panel.js +0 -0
  117. package/src/flow/kit/settings_schema.js +2 -1
  118. package/src/flow/kit/step_picker.js +0 -0
  119. package/src/flow/kit/strings.js +3 -1
  120. package/src/flow/kit/value_picker.js +0 -0
  121. package/src/flow/kit/widgets.js +86 -109
  122. package/src/flow/lanes/editor.js +136 -14
  123. package/src/flow/lanes/index.js +3 -1
  124. package/src/flow/lanes/layout.js +0 -0
  125. package/src/flow/lanes/pipeline.js +44 -2
  126. package/src/flow/lanes/preview.js +0 -0
  127. package/src/flow/lanes/strings.js +1 -0
  128. package/src/flow/outline/editor.js +0 -0
  129. package/src/flow/outline/index.js +0 -0
  130. package/src/flow/outline/mapping.js +0 -0
  131. package/src/flow/outline/menu.js +0 -0
  132. package/src/flow/outline/operations.js +0 -0
  133. package/src/flow/outline/recognise.js +0 -0
  134. package/src/flow/outline/strings.js +0 -0
  135. package/src/help/help_modal.js +168 -42
  136. package/src/help/help_registry.js +12 -1
  137. package/src/host/external_links.js +0 -0
  138. package/src/host/file_picker.js +117 -0
  139. package/src/host/host.js +10 -2
  140. package/src/host/photino_host.js +9 -0
  141. package/src/host/photino_host.md +21 -3
  142. package/src/host/pywebview_host.js +10 -0
  143. package/src/tiles/chart_tiles.js +53 -245
  144. package/src/tiles/config_schema.js +8 -0
  145. package/src/tiles/figure_tile.js +322 -0
  146. package/src/tiles/layout_persistence.js +3 -3
  147. package/src/tiles/list_tiles.js +7 -7
  148. package/src/tiles/scope_picker.js +0 -0
  149. package/src/tiles/tile_base.js +1 -1
  150. package/src/tiles/tile_config_form.js +442 -0
  151. package/src/tiles/tile_grid.js +2 -0
  152. package/src/tiles/tile_registry.js +0 -0
  153. package/src/tiling/bottom_panel_tabs.js +67 -99
  154. package/src/tiling/command_palette.js +64 -19
  155. package/src/tiling/content_registry.js +4 -1
  156. package/src/tiling/desktops.js +11 -1
  157. package/src/tiling/entity_sources.js +0 -0
  158. package/src/tiling/keymap.js +403 -128
  159. package/src/tiling/kind_taxonomy.js +78 -0
  160. package/src/tiling/landing_table.js +0 -0
  161. package/src/tiling/loading_overlay.js +0 -0
  162. package/src/tiling/nav_panel.js +59 -10
  163. package/src/tiling/page_factory.js +5 -5
  164. package/src/tiling/panel_availability.js +49 -0
  165. package/src/tiling/panel_keys.js +0 -0
  166. package/src/tiling/shell.js +114 -12
  167. package/src/tiling/tab_strip.js +0 -0
  168. package/src/tiling/tile_breadcrumb.js +0 -0
  169. package/src/tiling/tile_renderer.js +105 -9
  170. package/src/tiling/tile_tab_menu.js +0 -0
  171. package/src/tiling/tile_tree.js +0 -0
  172. package/src/tiling/wm.js +562 -30
  173. package/src/tiling/zoom.js +0 -0
  174. package/src/ui/base/component_base.js +0 -0
  175. package/src/ui/base/controller_base.js +0 -0
  176. package/src/ui/base/page_base.js +0 -0
  177. package/src/ui/components/about_dialog.js +0 -0
  178. package/src/ui/components/action_dropdown.js +30 -2
  179. package/src/ui/components/alert_log.js +0 -0
  180. package/src/ui/components/alerts_pane.js +0 -0
  181. package/src/ui/components/attribute_list_editor.js +0 -0
  182. package/src/ui/components/autocomplete_field.js +0 -0
  183. package/src/ui/components/cell_editor_host.js +293 -0
  184. package/src/ui/components/cell_editors.js +480 -0
  185. package/src/ui/components/cell_grid.js +761 -0
  186. package/src/ui/components/cell_paste.js +243 -0
  187. package/src/ui/components/cell_selection.js +220 -0
  188. package/src/ui/components/choice_controls.js +351 -0
  189. package/src/ui/components/color_field.js +211 -0
  190. package/src/ui/components/color_list_field.js +210 -0
  191. package/src/ui/components/column_fit.js +0 -0
  192. package/src/ui/components/column_groups.js +314 -0
  193. package/src/ui/components/column_order.js +142 -0
  194. package/src/ui/components/computing_status_window.js +98 -25
  195. package/src/ui/components/confirm_dialog.js +81 -38
  196. package/src/ui/components/context_menu.js +2 -2
  197. package/src/ui/components/conversation_view.js +440 -0
  198. package/src/ui/components/crosstab.js +289 -0
  199. package/src/ui/components/data_table.js +696 -71
  200. package/src/ui/components/detail_header.js +22 -11
  201. package/src/ui/components/document_outline.js +397 -0
  202. package/src/ui/components/drag_reorder.js +0 -0
  203. package/src/ui/components/fact_table.js +0 -0
  204. package/src/ui/components/fill_handle.js +322 -0
  205. package/src/ui/components/gallery_picker.js +19 -8
  206. package/src/ui/components/header_actions.js +55 -0
  207. package/src/ui/components/inline_renamer.js +0 -0
  208. package/src/ui/components/job_progress.js +51 -0
  209. package/src/ui/components/managed_window.js +27 -4
  210. package/src/ui/components/menu_bar.js +745 -0
  211. package/src/ui/components/modal.js +449 -75
  212. package/src/ui/components/notification_history.js +3 -3
  213. package/src/ui/components/progress.js +483 -0
  214. package/src/ui/components/record_list.js +0 -0
  215. package/src/ui/components/row_activation.js +0 -0
  216. package/src/ui/components/section_stack.js +298 -0
  217. package/src/ui/components/series.js +173 -0
  218. package/src/ui/components/settings_view.js +634 -0
  219. package/src/ui/components/slide_out_panel.js +43 -6
  220. package/src/ui/components/slider_field.js +275 -18
  221. package/src/ui/components/sortable_list.js +0 -0
  222. package/src/ui/components/staged_edits.js +361 -0
  223. package/src/ui/components/staged_history.js +172 -0
  224. package/src/ui/components/table_filter_chips.js +0 -0
  225. package/src/ui/components/table_state_store.js +0 -0
  226. package/src/ui/components/table_structure.js +182 -0
  227. package/src/ui/components/table_values.js +0 -0
  228. package/src/ui/components/tabs.js +497 -0
  229. package/src/ui/components/tag_input.js +215 -0
  230. package/src/ui/components/toast.js +25 -8
  231. package/src/ui/components/toggle_field.js +175 -0
  232. package/src/ui/components/tree_view.js +55 -34
  233. package/src/ui/components/tsv.js +74 -0
  234. package/src/ui/components/window_taskbar.js +0 -0
  235. package/src/ui/components/wizard.js +563 -0
  236. package/src/ui/controllers/window_chrome_controller.js +2 -2
  237. package/src/ui/notification_center.js +375 -119
  238. package/src/ui/utils/auto_scrollbars.js +0 -0
  239. package/src/ui/utils/markdown.js +709 -0
  240. package/src/ui/utils/native_context_menu.js +0 -0
  241. package/src/ui/utils/overlay_scrollbar.js +16 -12
  242. package/src/ui/utils/raf_resize_observer.js +0 -0
  243. package/src/ui/utils/scroll_lock.js +0 -0
  244. package/src/ui/utils/strip_scroll.js +55 -0
  245. package/src/ui/utils/tooltip_service.js +0 -8
  246. package/tiles.js +27 -0
  247. package/widgets.js +69 -0
  248. package/wm.js +28 -0
  249. package/dist/chunk-6JSOVNID.js.map +0 -7
  250. package/dist/chunk-B5Q53LAJ.js.map +0 -7
  251. package/dist/chunk-FBGP5KN5.js.map +0 -7
  252. package/dist/chunk-FL5KFNQH.js.map +0 -7
  253. package/dist/chunk-NLTOY57M.js.map +0 -7
  254. package/dist/chunk-NNFTETK3.js +0 -633
  255. package/dist/chunk-NNFTETK3.js.map +0 -7
  256. package/dist/chunk-O5OHMWBB.js +0 -284
  257. package/dist/chunk-O5OHMWBB.js.map +0 -7
  258. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  259. package/dist/chunk-SN4LSOAR.js.map +0 -7
  260. package/dist/chunk-W6FBJLPW.js +0 -367
  261. package/dist/chunk-W6FBJLPW.js.map +0 -7
  262. package/src/ui/controllers/panel_state_machine.js +0 -634
@@ -0,0 +1,74 @@
1
+ /**
2
+ * tsv.js — STORY-0085 (writing), STORY-0090 (reading). Tab-separated text,
3
+ * the format Excel and Google Sheets put on the clipboard as `text/plain` and
4
+ * read back from it. DataTable's cell mode copies a range with `toTSV` and
5
+ * pastes a block read with `fromTSV`.
6
+ *
7
+ * QUOTING. Excel quotes a cell that holds a tab, a newline (LF or CR) or a
8
+ * double quote, and doubles every quote inside it: `say "hi"` is written
9
+ * `"say ""hi"""`. Any other cell is written as it is, so a plain grid stays
10
+ * plain text. A writer that does not quote turns a cell holding a tab into two
11
+ * cells, and every column after it lands one to the right when pasted.
12
+ *
13
+ * LINES. Rows are joined with `\n` and there is no newline after the last
14
+ * row: one cell copied is that cell's text alone.
15
+ *
16
+ * READING. Character by character, never by splitting: a quoted cell may hold
17
+ * tabs and newlines, and a split that cannot know it shifts every column after
18
+ * such a cell into the wrong place, silently. A quote opens a quoted cell only
19
+ * at the start of a cell; inside one a doubled quote is a quote. A CR outside
20
+ * a quoted cell is dropped, so CRLF lines read as LF ones. Excel ends its text
21
+ * with a newline: the empty line that leaves (and any further empty lines at
22
+ * the end) is dropped, so a paste never carries a row nobody copied.
23
+ */
24
+
25
+ /** One cell's text: `null` and `undefined` are empty, anything else is
26
+ * `String(value)`, quoted the way Excel quotes it when it has to be. */
27
+ export function tsvCell(value) {
28
+ const text = value === null || value === undefined ? '' : String(value);
29
+ if (/[\t\n\r"]/.test(text)) return `"${text.replace(/"/g, '""')}"`;
30
+ return text;
31
+ }
32
+
33
+ /** A matrix (an array of rows, each an array of cells) as TSV. */
34
+ export function toTSV(matrix) {
35
+ if (!Array.isArray(matrix)) return '';
36
+ return matrix
37
+ .map((row) => (Array.isArray(row) ? row : [row]).map(tsvCell).join('\t'))
38
+ .join('\n');
39
+ }
40
+
41
+ /**
42
+ * Tab-separated text as a matrix of strings, one array per line. Empty text
43
+ * is `[]`; a line with no tab is one cell; empty cells are kept (`'a\t\tb'`
44
+ * is three cells). Lines may differ in length: each is as long as it is.
45
+ */
46
+ export function fromTSV(text) {
47
+ if (text === null || text === undefined || text === '') return [];
48
+ const src = String(text);
49
+ const rows = [];
50
+ let row = [];
51
+ let cell = '';
52
+ let quoted = false;
53
+ for (let i = 0; i < src.length; i++) {
54
+ const ch = src[i];
55
+ if (quoted) {
56
+ if (ch === '"' && src[i + 1] === '"') { cell += '"'; i++; }
57
+ else if (ch === '"') quoted = false;
58
+ else cell += ch;
59
+ } else if (ch === '"' && cell === '') quoted = true;
60
+ else if (ch === '\t') { row.push(cell); cell = ''; }
61
+ else if (ch === '\r') { /* CRLF reads as LF */ }
62
+ else if (ch === '\n') { row.push(cell); rows.push(row); row = []; cell = ''; }
63
+ else cell += ch;
64
+ }
65
+ row.push(cell);
66
+ rows.push(row);
67
+ // Excel's closing newline leaves an empty line behind: it is no row.
68
+ while (rows.length > 1) {
69
+ const last = rows[rows.length - 1];
70
+ if (last.length === 1 && last[0] === '') rows.pop();
71
+ else break;
72
+ }
73
+ return rows;
74
+ }
File without changes
@@ -0,0 +1,563 @@
1
+ /**
2
+ * Wizard — a multi-step flow: a step indicator, a body slot and a footer with
3
+ * Cancel, Back and Next/Finish, every step gated by its own `canAdvance`.
4
+ *
5
+ * Usage:
6
+ * const wizard = new Wizard({
7
+ * data: { file: null }, // yours; every hook sees it as ctx.data
8
+ * steps: [
9
+ * { id: 'source', label: 'Source', icon: 'upload_file',
10
+ * render(bodyEl, ctx) { … ; return () => { … } }, // optional cleanup function
11
+ * canAdvance: (ctx) => ctx.data.file
12
+ * ? { ok: true } : { ok: false, hint: 'Choose a file first' } },
13
+ * { id: 'review', label: 'Review', render(bodyEl, ctx) { … } },
14
+ * ],
15
+ * onFinish: async (ctx) => { … }, // rejects → the step shows the error
16
+ * onCancel: (ctx) => { … },
17
+ * });
18
+ * wizard.mount(hostEl); // inline …
19
+ * wizard.openInWindow({ id: 'import', title: 'Import' }); // … or in a window
20
+ *
21
+ * Steps are drawn done / current / upcoming / error and joined by a connector.
22
+ * A done (or errored) step BEFORE the current one is a button that jumps back;
23
+ * a step after it is inert, so the only way forward is Next, through the gate.
24
+ *
25
+ * `canAdvance(ctx)` is asked whenever the footer is drawn, and `revalidate()`
26
+ * draws it again — the body is rendered once per visit, never by a revalidate,
27
+ * so an input the user is typing into keeps its focus and its value.
28
+ *
29
+ * Every move goes through a core StateMachine:
30
+ *
31
+ * step ──move──▶ step (guard: back to a visited step, or forward
32
+ * │ one step through the gate)
33
+ * ├──finish──▶ finishing ──resolve──▶ finished (closes)
34
+ * │ │
35
+ * │ └──reject──▶ step (the step shows the error)
36
+ * └──cancel──▶ cancelled (closes, calls onCancel)
37
+ *
38
+ * While `finishing`, every button is disabled and no move is accepted, so an
39
+ * async finish cannot be raced by a click. `finished` and `cancelled` are final:
40
+ * onCancel runs once however many times Cancel is pressed.
41
+ *
42
+ * State lives in this instance only; the DOM carries classes and ARIA, never a
43
+ * data attribute.
44
+ */
45
+ import { ComponentBase } from '../base/component_base.js';
46
+ import { StateMachine } from '../../core/state_machine.js';
47
+ import { ManagedWindow } from './managed_window.js';
48
+
49
+ const DEFAULT_LABELS = Object.freeze({
50
+ back: 'Back',
51
+ next: 'Next',
52
+ finish: 'Finish',
53
+ cancel: 'Cancel',
54
+ });
55
+
56
+ const OPEN = 'step';
57
+ const FINISHING = 'finishing';
58
+ const FINISHED = 'finished';
59
+ const CANCELLED = 'cancelled';
60
+
61
+ /** Steps own one override: an error and its message. Everything else about a
62
+ * step's look follows from where it is relative to the current one. */
63
+ const STEP_STATES = Object.freeze(['error']);
64
+
65
+ const isFinal = (state) => state === FINISHED || state === CANCELLED;
66
+
67
+ /** A step's render may hand back a cleanup function, run when the step is
68
+ * left and on dispose(). */
69
+ function runCleanup(cleanup) {
70
+ if (typeof cleanup === 'function') cleanup();
71
+ }
72
+
73
+ function messageOf(err) {
74
+ if (err && typeof err.message === 'string' && err.message) return err.message;
75
+ return err == null ? 'Failed' : String(err);
76
+ }
77
+
78
+ export class Wizard extends ComponentBase {
79
+
80
+ /** @type {Array<object>} */ #steps;
81
+ /** @type {object} */ #labels;
82
+ /** @type {Function|null} */ #onFinish;
83
+ /** @type {Function|null} */ #onCancel;
84
+ /** @type {object} */ #ctx;
85
+ /** @type {StateMachine} */ #machine;
86
+ /** @type {number} */ #index = 0;
87
+ /** @type {Map<string,{state:string,message:string}>} */ #overrides = new Map();
88
+ /** @type {*} */ #cleanup = null;
89
+ /** @type {Promise} */ #queue = Promise.resolve();
90
+ /** @type {ManagedWindow|null} */ #window = null;
91
+ /** @type {boolean} */ #disposed = false;
92
+
93
+ /** @type {HTMLElement|null} */ #root = null;
94
+ /** @type {HTMLElement[]} */ #stepItems = [];
95
+ /** @type {HTMLButtonElement[]} */#stepButtons = [];
96
+ /** @type {HTMLElement[]} */ #connectors = [];
97
+ /** @type {HTMLElement|null} */ #body = null;
98
+ /** @type {HTMLElement|null} */ #message = null;
99
+ /** @type {HTMLButtonElement|null} */ #cancelBtn = null;
100
+ /** @type {HTMLButtonElement|null} */ #backBtn = null;
101
+ /** @type {HTMLButtonElement|null} */ #nextBtn = null;
102
+
103
+ /**
104
+ * @param {object} opts
105
+ * @param {Array<{id:string,label:string,icon?:string,
106
+ * render:(bodyEl:HTMLElement, ctx:object)=>(Function|void),
107
+ * canAdvance?:(ctx:object)=>{ok:boolean,hint?:string}}>} opts.steps
108
+ * @param {object} [opts.data={}] Your state; every hook reads it as `ctx.data`.
109
+ * @param {Function} [opts.onFinish] `(ctx) => void|Promise`. A rejection marks the
110
+ * current step as error with the rejection's message.
111
+ * @param {Function} [opts.onCancel] `(ctx) => void`, called once when the wizard is cancelled.
112
+ * @param {object} [opts.labels] `{back, next, finish, cancel}` button labels.
113
+ * @param {object} [opts.eventBus] Passed to the StateMachine (its `fsm:*` events).
114
+ * @param {object} [opts.logger] Passed to the StateMachine.
115
+ */
116
+ constructor({ steps, data = {}, onFinish = null, onCancel = null, labels = {},
117
+ eventBus = null, logger = null } = {}) {
118
+ super({ eventBus, logger });
119
+ if (!Array.isArray(steps) || steps.length === 0) {
120
+ throw new Error('Wizard requires at least one step');
121
+ }
122
+ const ids = new Set();
123
+ for (const step of steps) {
124
+ if (!step || typeof step.id !== 'string' || !step.id) {
125
+ throw new Error('Wizard: every step needs a non-empty string id');
126
+ }
127
+ if (ids.has(step.id)) throw new Error(`Wizard: duplicate step id "${step.id}"`);
128
+ if (typeof step.render !== 'function') {
129
+ throw new Error(`Wizard: step "${step.id}" needs a render(bodyEl, ctx) function`);
130
+ }
131
+ ids.add(step.id);
132
+ }
133
+ this.#steps = steps.slice();
134
+ this.#labels = { ...DEFAULT_LABELS, ...labels };
135
+ this.#onFinish = onFinish;
136
+ this.#onCancel = onCancel;
137
+
138
+ const wizard = this;
139
+ this.#ctx = Object.freeze({
140
+ data,
141
+ get step() { return wizard.currentStep; },
142
+ get index() { return wizard.currentIndex; },
143
+ revalidate: () => this.revalidate(),
144
+ next: () => this.next(),
145
+ back: () => this.back(),
146
+ goTo: (target) => this.goTo(target),
147
+ setStepState: (id, state, message) => this.setStepState(id, state, message),
148
+ });
149
+
150
+ this.#machine = new StateMachine({
151
+ name: 'wizard',
152
+ initialState: OPEN,
153
+ eventBus,
154
+ logger,
155
+ states: {
156
+ [OPEN]: {
157
+ transitions: {
158
+ move: {
159
+ target: OPEN,
160
+ guard: ({ payload }) => this.#canMoveTo(payload.to),
161
+ action: ({ payload }) => this.#enterStep(payload.to),
162
+ },
163
+ finish: {
164
+ target: FINISHING,
165
+ guard: () => this.#isLast() && this.#gate().ok,
166
+ action: () => { this.#overrides.delete(this.currentStep); },
167
+ },
168
+ cancel: { target: CANCELLED },
169
+ },
170
+ },
171
+ [FINISHING]: {
172
+ transitions: {
173
+ resolve: { target: FINISHED },
174
+ reject: {
175
+ target: OPEN,
176
+ action: ({ payload }) => {
177
+ this.#overrides.set(this.currentStep,
178
+ { state: 'error', message: messageOf(payload.error) });
179
+ },
180
+ },
181
+ },
182
+ },
183
+ [FINISHED]: { onEnter: () => this.#close() },
184
+ [CANCELLED]: {
185
+ onEnter: () => {
186
+ this.#close();
187
+ this.#onCancel?.(this.#ctx);
188
+ },
189
+ },
190
+ },
191
+ });
192
+ }
193
+
194
+ // ─── Lifecycle ──────────────────────────────────────────────────────────
195
+
196
+ /**
197
+ * Mount inline into `container` and render the first step.
198
+ * @param {HTMLElement} container
199
+ * @returns {Wizard}
200
+ */
201
+ mount(container) {
202
+ if (this._mounted) return this;
203
+ this.#build();
204
+ container.appendChild(this.#root);
205
+ this._mounted = true;
206
+ this.#renderBody();
207
+ this.#paint();
208
+ return this;
209
+ }
210
+
211
+ /**
212
+ * Mount in a ManagedWindow and show it. The window's close button and
213
+ * Escape cancel the wizard (and are refused while a finish is running);
214
+ * finishing or cancelling closes the window.
215
+ * @param {object} [opts] ManagedWindow options: id, title, icon, defaultWidth,
216
+ * defaultHeight, minWidth, minHeight, container, modal (default true).
217
+ * @returns {ManagedWindow}
218
+ */
219
+ openInWindow({ id = 'twm-wizard', title = 'Wizard', icon, defaultWidth = 720,
220
+ defaultHeight = 520, minWidth = 420, minHeight = 320,
221
+ container, modal = true } = {}) {
222
+ if (this._mounted) throw new Error('Wizard is already mounted');
223
+ this.#build();
224
+ this._mounted = true;
225
+ this.#window = new ManagedWindow({
226
+ id, title, icon, defaultWidth, defaultHeight, minWidth, minHeight,
227
+ container, modal,
228
+ content: this.#root,
229
+ canMinimize: false,
230
+ beforeClose: () => {
231
+ if (isFinal(this.state)) return true;
232
+ void this.cancel();
233
+ return false;
234
+ },
235
+ });
236
+ this.#window.show();
237
+ this.#renderBody();
238
+ this.#paint();
239
+ return this.#window;
240
+ }
241
+
242
+ /** Tear down (and close the window, if any) without calling any callback.
243
+ * Every later move, finish or cancel is refused. */
244
+ dispose() {
245
+ if (this.#disposed) return;
246
+ this.#disposed = true;
247
+ const win = this.#window;
248
+ this.#teardown();
249
+ win?.close({ force: true });
250
+ super.dispose();
251
+ }
252
+
253
+ // ─── Public API ─────────────────────────────────────────────────────────
254
+
255
+ /** The wizard's root element (null before mount). */
256
+ get element() { return this.#root; }
257
+
258
+ /** The body slot the current step renders into. */
259
+ get bodyElement() { return this.#body; }
260
+
261
+ /** The ManagedWindow, when opened with `openInWindow`. */
262
+ get window() { return this.#window; }
263
+
264
+ /** The object every hook receives: `{data, step, index, revalidate, next, back, goTo, setStepState}`. */
265
+ get context() { return this.#ctx; }
266
+
267
+ /** The current step's id. */
268
+ get currentStep() { return this.#steps[this.#index].id; }
269
+
270
+ /** The current step's index. */
271
+ get currentIndex() { return this.#index; }
272
+
273
+ /** 'step' | 'finishing' | 'finished' | 'cancelled' (null for a moment after construction). */
274
+ get state() { return this.#machine.getState(); }
275
+
276
+ /** True while an async finish is running. */
277
+ get busy() { return this.state === FINISHING; }
278
+
279
+ /**
280
+ * Ask the current step's `canAdvance` again and redraw the indicator and
281
+ * the footer. The body is NOT re-rendered.
282
+ */
283
+ revalidate() {
284
+ this.#paint();
285
+ }
286
+
287
+ /** Forward one step, if the current step's gate allows it. Resolves true when it moved. */
288
+ next() {
289
+ return this.#move(() => this.#index + 1);
290
+ }
291
+
292
+ /** Back one step. Resolves true when it moved. */
293
+ back() {
294
+ return this.#move(() => this.#index - 1);
295
+ }
296
+
297
+ /**
298
+ * Move to a step by id or index: any step before the current one, or the
299
+ * next one through the gate. Resolves true when it moved.
300
+ * @param {string|number} target
301
+ */
302
+ goTo(target) {
303
+ return this.#move(() => this.#resolveIndex(target));
304
+ }
305
+
306
+ /**
307
+ * Mark a step as errored (`'error'`, with a message shown while it is the
308
+ * current step), or clear the mark (`null`).
309
+ * @param {string} id
310
+ * @param {'error'|null} state
311
+ * @param {string} [message]
312
+ */
313
+ setStepState(id, state, message = '') {
314
+ const i = this.#resolveIndex(id);
315
+ if (i < 0 || i >= this.#steps.length) throw new Error(`Wizard: no step "${id}"`);
316
+ const key = this.#steps[i].id;
317
+ if (state == null) this.#overrides.delete(key);
318
+ else if (STEP_STATES.includes(state)) this.#overrides.set(key, { state, message: String(message ?? '') });
319
+ else throw new Error(`Wizard: unknown step state "${state}" (expected 'error' or null)`);
320
+ this.#paint();
321
+ }
322
+
323
+ /**
324
+ * Run `onFinish` from the last step, if its gate allows. The buttons are
325
+ * disabled until it settles; resolved, the wizard closes; rejected, the
326
+ * step is marked as error with the message. Resolves true when it finished.
327
+ */
328
+ finish() {
329
+ return this.#enqueue(async () => {
330
+ if (this.state !== OPEN) return false;
331
+ const { transitioned } = await this.#machine.transition('finish');
332
+ if (!transitioned) return false;
333
+ this.#paint();
334
+ try {
335
+ await this.#onFinish?.(this.#ctx);
336
+ } catch (error) {
337
+ await this.#machine.transition('reject', { error });
338
+ this.#paint();
339
+ return false;
340
+ }
341
+ await this.#machine.transition('resolve');
342
+ return true;
343
+ });
344
+ }
345
+
346
+ /**
347
+ * Close the wizard and call `onCancel` — once. Refused while a finish is
348
+ * running. Resolves true when it cancelled.
349
+ */
350
+ cancel() {
351
+ return this.#enqueue(async () => {
352
+ if (this.state !== OPEN) return false;
353
+ const { transitioned } = await this.#machine.transition('cancel');
354
+ return transitioned;
355
+ });
356
+ }
357
+
358
+ // ─── Moves ──────────────────────────────────────────────────────────────
359
+
360
+ /** Moves run one at a time, so a double click cannot start a transition
361
+ * inside another. The target is computed when the move runs, not when it
362
+ * was asked for. A request made while a finish is running (or after the
363
+ * wizard closed) is refused there and then — it is not queued behind the
364
+ * finish to run once it settles. */
365
+ #enqueue(fn) {
366
+ if (this.#disposed || this.busy || isFinal(this.state)) return Promise.resolve(false);
367
+ const run = this.#queue.then(() => (this.#disposed ? false : fn()));
368
+ this.#queue = run.catch(() => {});
369
+ return run;
370
+ }
371
+
372
+ #move(targetOf) {
373
+ return this.#enqueue(async () => {
374
+ if (this.state !== OPEN) return false;
375
+ const to = targetOf();
376
+ if (!this.#canMoveTo(to)) return false;
377
+ const { transitioned } = await this.#machine.transition('move', { to });
378
+ this.#paint();
379
+ return transitioned;
380
+ });
381
+ }
382
+
383
+ #canMoveTo(to) {
384
+ if (!Number.isInteger(to) || to < 0 || to >= this.#steps.length) return false;
385
+ if (to < this.#index) return true;
386
+ return to === this.#index + 1 && this.#gate().ok;
387
+ }
388
+
389
+ #enterStep(to) {
390
+ this.#index = to;
391
+ this.#renderBody();
392
+ }
393
+
394
+ #resolveIndex(target) {
395
+ if (typeof target === 'number') return Number.isInteger(target) ? target : -1;
396
+ return this.#steps.findIndex((s) => s.id === target);
397
+ }
398
+
399
+ #isLast() {
400
+ return this.#index === this.#steps.length - 1;
401
+ }
402
+
403
+ /** The current step's gate, normalised to `{ok, hint}`. */
404
+ #gate() {
405
+ const step = this.#steps[this.#index];
406
+ if (typeof step.canAdvance !== 'function') return { ok: true, hint: '' };
407
+ const r = step.canAdvance(this.#ctx) ?? {};
408
+ return { ok: r.ok === true, hint: typeof r.hint === 'string' ? r.hint : '' };
409
+ }
410
+
411
+ // ─── DOM ────────────────────────────────────────────────────────────────
412
+
413
+ #build() {
414
+ const root = document.createElement('div');
415
+ root.className = 'twm-wizard';
416
+
417
+ const steps = document.createElement('ol');
418
+ steps.className = 'twm-wizard__steps';
419
+ this.#steps.forEach((step, i) => {
420
+ if (i > 0) {
421
+ const connector = document.createElement('li');
422
+ connector.className = 'twm-wizard__connector';
423
+ connector.setAttribute('role', 'presentation');
424
+ connector.setAttribute('aria-hidden', 'true');
425
+ steps.appendChild(connector);
426
+ this.#connectors.push(connector);
427
+ }
428
+ const item = document.createElement('li');
429
+ item.className = 'twm-wizard__step';
430
+ const btn = document.createElement('button');
431
+ btn.type = 'button';
432
+ btn.className = 'twm-wizard__step-btn';
433
+ const marker = document.createElement('span');
434
+ marker.className = 'twm-wizard__step-marker';
435
+ const label = document.createElement('span');
436
+ label.className = 'twm-wizard__step-label';
437
+ label.textContent = step.label ?? step.id;
438
+ btn.append(marker, label);
439
+ btn.addEventListener('click', () => this.#onStepClick(i));
440
+ item.appendChild(btn);
441
+ steps.appendChild(item);
442
+ this.#stepItems.push(item);
443
+ this.#stepButtons.push(btn);
444
+ });
445
+
446
+ this.#body = document.createElement('div');
447
+ this.#body.className = 'twm-wizard__body';
448
+
449
+ const footer = document.createElement('div');
450
+ footer.className = 'twm-wizard__footer';
451
+ this.#message = document.createElement('div');
452
+ this.#message.className = 'twm-wizard__message';
453
+ this.#message.setAttribute('aria-live', 'polite');
454
+
455
+ const actions = document.createElement('div');
456
+ actions.className = 'twm-wizard__actions';
457
+ this.#cancelBtn = this.#button('twm-wizard__cancel', this.#labels.cancel, () => { void this.cancel(); });
458
+ this.#backBtn = this.#button('twm-wizard__back', this.#labels.back, () => { void this.back(); });
459
+ this.#nextBtn = this.#button('twm-wizard__next twm-btn--primary', this.#labels.next, () => {
460
+ void (this.#isLast() ? this.finish() : this.next());
461
+ });
462
+ actions.append(this.#cancelBtn, this.#backBtn, this.#nextBtn);
463
+ footer.append(this.#message, actions);
464
+
465
+ root.append(steps, this.#body, footer);
466
+ this.#root = root;
467
+ }
468
+
469
+ #button(cls, text, onClick) {
470
+ const btn = document.createElement('button');
471
+ btn.type = 'button';
472
+ btn.className = `twm-btn ${cls}`;
473
+ btn.textContent = text;
474
+ btn.addEventListener('click', onClick);
475
+ return btn;
476
+ }
477
+
478
+ #onStepClick(i) {
479
+ if (i < this.#index) void this.goTo(i);
480
+ }
481
+
482
+ #renderBody() {
483
+ runCleanup(this.#cleanup);
484
+ this.#cleanup = null;
485
+ this.#body.replaceChildren();
486
+ const step = this.#steps[this.#index];
487
+ this.#cleanup = step.render(this.#body, this.#ctx) ?? null;
488
+ }
489
+
490
+ /** The visual state of step `i`: an override, else its position. */
491
+ #stepState(i) {
492
+ const override = this.#overrides.get(this.#steps[i].id);
493
+ if (override) return override.state;
494
+ if (i < this.#index) return 'done';
495
+ return i === this.#index ? 'current' : 'upcoming';
496
+ }
497
+
498
+ #paint() {
499
+ if (!this.#root) return;
500
+ const busy = this.busy;
501
+ this.#root.classList.toggle('twm-wizard--busy', busy);
502
+ this.#root.setAttribute('aria-busy', busy ? 'true' : 'false');
503
+
504
+ this.#steps.forEach((step, i) => {
505
+ const state = this.#stepState(i);
506
+ const item = this.#stepItems[i];
507
+ for (const s of ['done', 'current', 'upcoming', 'error']) {
508
+ item.classList.toggle(`twm-wizard__step--${s}`, s === state);
509
+ }
510
+ item.classList.toggle('twm-wizard__step--active', i === this.#index);
511
+ const btn = this.#stepButtons[i];
512
+ btn.disabled = busy || i >= this.#index;
513
+ if (i === this.#index) btn.setAttribute('aria-current', 'step');
514
+ else btn.removeAttribute('aria-current');
515
+ const override = this.#overrides.get(step.id);
516
+ btn.title = override?.message || step.label || step.id;
517
+
518
+ const marker = btn.firstChild;
519
+ const glyph = state === 'done' ? 'check' : state === 'error' ? 'error' : (step.icon || '');
520
+ marker.classList.toggle('material-symbols-outlined', Boolean(glyph));
521
+ marker.textContent = glyph || String(i + 1);
522
+ if (i > 0) {
523
+ this.#connectors[i - 1].classList.toggle('twm-wizard__connector--done', i <= this.#index);
524
+ }
525
+ });
526
+
527
+ const gate = this.#gate();
528
+ const last = this.#isLast();
529
+ this.#nextBtn.textContent = last ? this.#labels.finish : this.#labels.next;
530
+ this.#nextBtn.disabled = busy || !gate.ok;
531
+ this.#nextBtn.title = gate.ok ? '' : gate.hint;
532
+ this.#backBtn.disabled = busy || this.#index === 0;
533
+ this.#cancelBtn.disabled = busy;
534
+
535
+ const error = this.#overrides.get(this.currentStep);
536
+ const text = error ? error.message : (gate.ok ? '' : gate.hint);
537
+ this.#message.textContent = text;
538
+ this.#message.classList.toggle('twm-wizard__message--error', Boolean(error));
539
+ this.#message.classList.toggle('twm-wizard__message--hint', !error && Boolean(text));
540
+ if (error) this.#message.setAttribute('role', 'alert');
541
+ else this.#message.removeAttribute('role');
542
+ }
543
+
544
+ #close() {
545
+ const win = this.#window;
546
+ this.#teardown();
547
+ this._mounted = false;
548
+ win?.close({ force: true });
549
+ }
550
+
551
+ #teardown() {
552
+ runCleanup(this.#cleanup);
553
+ this.#cleanup = null;
554
+ this.#root?.remove();
555
+ this.#root = null;
556
+ this.#body = null;
557
+ this.#message = null;
558
+ this.#stepItems = [];
559
+ this.#stepButtons = [];
560
+ this.#connectors = [];
561
+ this.#cancelBtn = this.#backBtn = this.#nextBtn = null;
562
+ }
563
+ }
@@ -99,7 +99,7 @@ export class WindowChromeController {
99
99
 
100
100
  /**
101
101
  * Draw minimise, maximise/restore and close, unless the page has its own
102
- * `#win-*` buttons (EcoSim's markup), which are then wired instead.
102
+ * `#win-*` buttons (an embedder's own markup), which are then wired instead.
103
103
  */
104
104
  #createWindowControls() {
105
105
  if (document.getElementById('win-minimize') || document.getElementById('win-close')) return;
@@ -403,7 +403,7 @@ export class WindowChromeController {
403
403
 
404
404
  // What never starts a drag: anything you can click or type into. An
405
405
  // embedder adds its own with `noDrag`. (`.window-controls` and
406
- // `.panel-toggles` are kept for EcoSim-shaped markup.)
406
+ // `.panel-toggles` are kept for embedders' own markup of that shape.)
407
407
  const NO_DRAG_SELECTOR = [
408
408
  '.no-drag', 'button', 'a', 'input', 'select', 'textarea', 'label',
409
409
  '[role="button"]', '[role="menuitem"]', '[role="tab"]', '[contenteditable="true"]',