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
@@ -108,8 +108,8 @@ export class NotificationHistory {
108
108
  'workspace:save:failed', 'workspace:import:failed'];
109
109
 
110
110
  events.forEach(name => {
111
- this.eventBus.on(name, badgeUpdate);
112
- this._subscriptions.push(() => this.eventBus.off(name, badgeUpdate));
111
+ const handle = this.eventBus.on(name, badgeUpdate);
112
+ this._subscriptions.push(() => handle?.dispose?.());
113
113
  });
114
114
  }
115
115
 
@@ -245,7 +245,7 @@ export class NotificationHistory {
245
245
  const entry = history[i];
246
246
  const item = document.createElement('div');
247
247
  const severity = entry.severity || 'info';
248
- item.className = `twm-notification-history__item notification-history__item--${severity}`;
248
+ item.className = `twm-notification-history__item twm-notification-history__item--${severity}`;
249
249
 
250
250
  const itemHeader = document.createElement('div');
251
251
  itemHeader.className = 'twm-notification-history__item-header';
@@ -0,0 +1,483 @@
1
+ /**
2
+ * Determinate progress: a bar, a keyed list of bars, and a row of phases.
3
+ * Location: ui/components/progress.js
4
+ *
5
+ * Three primitives for long-running jobs, each a class that owns its own DOM
6
+ * and changes it IN PLACE — a caller that reports progress twenty times a
7
+ * second gets the same elements back every time, so a row it is hovering, a
8
+ * tooltip anchored to it and a CSS transition on the fill all survive.
9
+ *
10
+ * ProgressBar value/max or a 0..1 fraction, indeterminate, a label, a
11
+ * percent and a count readout, elapsed time, a cancel button,
12
+ * and a final state (done / error / cancelled).
13
+ * ProgressList keyed rows `{ key, name, completed, total?, state }`;
14
+ * `setItems` reconciles by key. A row without `total` is
15
+ * drawn against the largest item in the list.
16
+ * StepProgress ordered phases, each done / active / pending / error, the
17
+ * active one optionally reading "pass n of m".
18
+ *
19
+ * Nothing here knows what is being computed: names, labels and the stats line
20
+ * are the caller's text, always set with `textContent`. State lives on the
21
+ * instances; the DOM carries only classes and ARIA.
22
+ */
23
+
24
+ /** The states a ProgressBar can end in, besides `running`. */
25
+ export const PROGRESS_STATES = Object.freeze(['running', 'done', 'error', 'cancelled']);
26
+
27
+ /** The states a ProgressList row can be in. */
28
+ export const PROGRESS_ITEM_STATES = Object.freeze(['pending', 'running', 'done', 'failed']);
29
+
30
+ /** The states a StepProgress phase can be in. */
31
+ export const STEP_STATES = Object.freeze(['pending', 'active', 'done', 'error']);
32
+
33
+ const clamp01 = (x) => (x < 0 ? 0 : x > 1 ? 1 : x);
34
+ const isNum = (x) => typeof x === 'number' && Number.isFinite(x);
35
+
36
+ /** `m:ss`, or `h:mm:ss` from an hour on. */
37
+ export function formatElapsed(ms) {
38
+ const s = Math.max(0, Math.floor((ms || 0) / 1000));
39
+ const h = Math.floor(s / 3600);
40
+ const m = Math.floor((s % 3600) / 60);
41
+ const ss = String(s % 60).padStart(2, '0');
42
+ return h ? `${h}:${String(m).padStart(2, '0')}:${ss}` : `${m}:${ss}`;
43
+ }
44
+
45
+ function el(tag, className, text) {
46
+ const n = document.createElement(tag);
47
+ if (className) n.className = className;
48
+ if (text !== undefined) n.textContent = text;
49
+ return n;
50
+ }
51
+
52
+ /** Set text only when it changed, so an unchanged readout is not a mutation. */
53
+ function setText(node, text) {
54
+ const t = text == null ? '' : String(text);
55
+ if (node.textContent !== t) node.textContent = t;
56
+ }
57
+
58
+ function setWidth(node, fraction) {
59
+ const w = `${(clamp01(fraction) * 100).toFixed(2).replace(/\.?0+$/, '')}%`;
60
+ if (node.style.width !== w) node.style.width = w;
61
+ }
62
+
63
+ // ─── ProgressBar ─────────────────────────────────────────────────────────
64
+
65
+ /**
66
+ * @typedef {Object} ProgressBarOptions
67
+ * @property {number} [value=0] - A count when `max` is given, else a 0..1 fraction.
68
+ * @property {number|null} [max=null] - The count `value` runs to.
69
+ * @property {boolean} [indeterminate=false] - Busy, with no known end.
70
+ * @property {string} [label=''] - Text before the readout.
71
+ * @property {boolean} [showPercent=true] - "42%".
72
+ * @property {boolean|null} [showCount=null] - "21 / 50"; defaults to on when `max` is given.
73
+ * @property {boolean} [showElapsed=false] - A running "m:ss" from construction until a final state.
74
+ * @property {Function|null} [onCancel=null] - Shows a Cancel button; called once when clicked.
75
+ * @property {string} [cancelLabel='Cancel']
76
+ * @property {Function} [now=Date.now] - The clock, for the elapsed readout.
77
+ * @property {string} [className] - Added to the root.
78
+ */
79
+
80
+ export class ProgressBar {
81
+ /**
82
+ * @param {HTMLElement|null} host appended to, if given
83
+ * @param {ProgressBarOptions} [options]
84
+ */
85
+ constructor(host, options = {}) {
86
+ const {
87
+ value = 0, max = null, indeterminate = false, label = '',
88
+ showPercent = true, showCount = null, showElapsed = false,
89
+ onCancel = null, cancelLabel = 'Cancel', now = Date.now, className = '',
90
+ } = options;
91
+ this._value = isNum(value) ? value : 0;
92
+ this._max = isNum(max) && max > 0 ? max : null;
93
+ this._indeterminate = !!indeterminate;
94
+ this._label = label || '';
95
+ this._showPercent = showPercent !== false;
96
+ this._showCount = showCount;
97
+ this._showElapsed = !!showElapsed;
98
+ this._state = 'running';
99
+ this._now = typeof now === 'function' ? now : Date.now;
100
+ this._started = this._now();
101
+ this._stopped = null;
102
+ this._timer = null;
103
+ this._onCancel = typeof onCancel === 'function' ? onCancel : null;
104
+
105
+ const root = el('div', `twm-progress${className ? ` ${className}` : ''}`);
106
+ const head = el('div', 'twm-progress__head');
107
+ this._labelEl = el('span', 'twm-progress__label');
108
+ this._readoutEl = el('span', 'twm-progress__readout');
109
+ head.append(this._labelEl, this._readoutEl);
110
+ if (this._showElapsed) {
111
+ this._elapsedEl = el('span', 'twm-progress__elapsed');
112
+ head.appendChild(this._elapsedEl);
113
+ this._startTimer();
114
+ } else {
115
+ this._elapsedEl = null;
116
+ }
117
+ if (this._onCancel) {
118
+ const btn = el('button', 'twm-progress__cancel', cancelLabel);
119
+ btn.type = 'button';
120
+ btn.addEventListener('click', () => {
121
+ if (btn.disabled || this._state !== 'running') return;
122
+ btn.disabled = true;
123
+ this._onCancel();
124
+ });
125
+ this._cancelBtn = btn;
126
+ head.appendChild(btn);
127
+ } else {
128
+ this._cancelBtn = null;
129
+ }
130
+ this._track = el('div', 'twm-progress__track');
131
+ this._track.setAttribute('role', 'progressbar');
132
+ this._fill = el('div', 'twm-progress__fill');
133
+ this._track.appendChild(this._fill);
134
+ root.append(head, this._track);
135
+ this.element = root;
136
+ this._draw();
137
+ if (host) host.appendChild(root);
138
+ }
139
+
140
+ /** The bar's fraction 0..1, or null while indeterminate. */
141
+ get fraction() {
142
+ if (this._indeterminate) return null;
143
+ return clamp01(this._max ? this._value / this._max : this._value);
144
+ }
145
+
146
+ /** `running`, `done`, `error` or `cancelled`. */
147
+ get state() { return this._state; }
148
+
149
+ /** Milliseconds since construction, frozen at the final state. */
150
+ get elapsed() { return (this._stopped ?? this._now()) - this._started; }
151
+
152
+ /**
153
+ * Change the bar in place. A number is the new value (a count with `max`,
154
+ * else a fraction); an object may carry `value`, `max`, `label` and
155
+ * `indeterminate`. Ignored once the bar is in a final state.
156
+ */
157
+ update(next) {
158
+ if (this._state !== 'running') return this;
159
+ const o = isNum(next) ? { value: next } : (next || {});
160
+ if ('max' in o) this._max = isNum(o.max) && o.max > 0 ? o.max : null;
161
+ if (isNum(o.value)) { this._value = o.value; if (!('indeterminate' in o)) this._indeterminate = false; }
162
+ if ('indeterminate' in o) this._indeterminate = !!o.indeterminate;
163
+ if ('label' in o) this._label = o.label || '';
164
+ this._draw();
165
+ return this;
166
+ }
167
+
168
+ /**
169
+ * Put the bar in a final state (`done`, `error`, `cancelled`) or back to
170
+ * `running`. `done` fills it. A final state stops the elapsed clock and
171
+ * disables Cancel; `running` restarts the clock (still counting from
172
+ * construction) and enables Cancel again. `label` replaces the label when
173
+ * given.
174
+ */
175
+ setState(state, { label } = {}) {
176
+ if (!PROGRESS_STATES.includes(state)) throw new Error(`ProgressBar: unknown state "${state}"`);
177
+ this._state = state;
178
+ if (label !== undefined) this._label = label || '';
179
+ if (state === 'done') { this._indeterminate = false; this._value = this._max ?? 1; }
180
+ if (state === 'running') {
181
+ this._stopped = null;
182
+ if (this._showElapsed) this._startTimer();
183
+ } else {
184
+ this._stopped = this._now();
185
+ if (this._timer) { clearInterval(this._timer); this._timer = null; }
186
+ }
187
+ if (this._cancelBtn) this._cancelBtn.disabled = state !== 'running';
188
+ this._draw();
189
+ this._drawElapsed();
190
+ return this;
191
+ }
192
+
193
+ /** Tick the elapsed readout once a second, unless it already ticks. */
194
+ _startTimer() {
195
+ if (!this._timer) this._timer = setInterval(() => this._drawElapsed(), 1000);
196
+ }
197
+
198
+ /** Stop the elapsed clock and detach. */
199
+ dispose() {
200
+ this._showElapsed = false;
201
+ if (this._timer) { clearInterval(this._timer); this._timer = null; }
202
+ this.element.remove();
203
+ }
204
+
205
+ _draw() {
206
+ const root = this.element;
207
+ for (const s of PROGRESS_STATES) {
208
+ root.classList.toggle(`twm-progress--${s}`, this._state === s);
209
+ }
210
+ root.classList.toggle('twm-progress--indeterminate', this._indeterminate);
211
+ setText(this._labelEl, this._label);
212
+ const f = this.fraction;
213
+ const parts = [];
214
+ if (f !== null) {
215
+ if (this._showPercent) parts.push(`${Math.floor(f * 100)}%`);
216
+ const count = this._showCount ?? !!this._max;
217
+ if (count && this._max) parts.push(`${fmtCount(this._value)} / ${fmtCount(this._max)}`);
218
+ }
219
+ setText(this._readoutEl, parts.join(' · '));
220
+ if (f === null) {
221
+ if (this._fill.style.width) this._fill.style.width = '';
222
+ this._track.removeAttribute('aria-valuenow');
223
+ } else {
224
+ setWidth(this._fill, f);
225
+ this._track.setAttribute('aria-valuenow', String(Math.round(f * 100)));
226
+ }
227
+ this._track.setAttribute('aria-valuemin', '0');
228
+ this._track.setAttribute('aria-valuemax', '100');
229
+ if (this._label) this._track.setAttribute('aria-label', this._label);
230
+ this._drawElapsed();
231
+ }
232
+
233
+ _drawElapsed() {
234
+ if (this._elapsedEl) setText(this._elapsedEl, formatElapsed(this.elapsed));
235
+ }
236
+ }
237
+
238
+ function fmtCount(n) {
239
+ return Number.isInteger(n) ? String(n) : String(Math.round(n * 100) / 100);
240
+ }
241
+
242
+ // ─── ProgressList ────────────────────────────────────────────────────────
243
+
244
+ /**
245
+ * @typedef {Object} ProgressItem
246
+ * @property {string|number} key - Identity across `setItems` calls.
247
+ * @property {string} [name] - Shown as the row's label; defaults to the key.
248
+ * @property {number} [completed=0]
249
+ * @property {number} [total] - Absent: the row is drawn against the largest item.
250
+ * @property {'pending'|'running'|'done'|'failed'} [state='pending']
251
+ * @property {string} [detail] - Extra text after the count.
252
+ */
253
+
254
+ /**
255
+ * @typedef {Object} ProgressListOptions
256
+ * @property {ProgressItem[]} [items=[]]
257
+ * @property {boolean|Function} [stats=true] - The line under the rows: `true`
258
+ * for the default summary, `false` for none, or `(items) => string`.
259
+ * @property {boolean} [pulse=true] - Running rows pulse.
260
+ * @property {string} [emptyText=''] - Shown when there are no items.
261
+ * @property {string} [className] - Added to the root.
262
+ */
263
+
264
+ export class ProgressList {
265
+ /**
266
+ * @param {HTMLElement|null} host appended to, if given
267
+ * @param {ProgressListOptions} [options]
268
+ */
269
+ constructor(host, options = {}) {
270
+ const { items = [], stats = true, pulse = true, emptyText = '', className = '' } = options;
271
+ this._stats = stats;
272
+ this._emptyText = emptyText;
273
+ /** @type {Map<string, {item: ProgressItem, row: HTMLElement, name: HTMLElement, fill: HTMLElement, count: HTMLElement, state: HTMLElement}>} */
274
+ this._rows = new Map();
275
+ this._items = [];
276
+ const root = el('div', `twm-progress-list${pulse ? ' twm-progress-list--pulse' : ''}${className ? ` ${className}` : ''}`);
277
+ this._listEl = el('div', 'twm-progress-list__rows');
278
+ this._listEl.setAttribute('role', 'list');
279
+ this._emptyEl = el('div', 'twm-progress-list__empty');
280
+ this._statsEl = el('div', 'twm-progress-list__stats');
281
+ root.append(this._listEl, this._emptyEl, this._statsEl);
282
+ this.element = root;
283
+ this.setItems(items);
284
+ if (host) host.appendChild(root);
285
+ }
286
+
287
+ /** The items last given, in order. */
288
+ get items() { return this._items.slice(); }
289
+
290
+ /**
291
+ * Reconcile the rows with `items` by key: a known key keeps its row and
292
+ * has its text, width and state changed in place; a new key gets a row; a
293
+ * key that is gone loses its row. Rows follow the order given, and a row
294
+ * already in its place is not moved.
295
+ */
296
+ setItems(items) {
297
+ const list = Array.isArray(items) ? items : [];
298
+ const seen = new Set();
299
+ this._items = [];
300
+ for (const it of list) {
301
+ if (!it || it.key == null) continue;
302
+ const k = String(it.key);
303
+ if (seen.has(k)) continue;
304
+ seen.add(k);
305
+ this._items.push(it);
306
+ }
307
+ for (const [k, r] of this._rows) {
308
+ if (!seen.has(k)) { r.row.remove(); this._rows.delete(k); }
309
+ }
310
+ const scale = this._items.reduce((m, it) => {
311
+ const d = isNum(it.total) && it.total > 0 ? it.total : (isNum(it.completed) ? it.completed : 0);
312
+ return d > m ? d : m;
313
+ }, 0);
314
+ let prev = null;
315
+ for (const it of this._items) {
316
+ const k = String(it.key);
317
+ let r = this._rows.get(k);
318
+ if (!r) { r = this._makeRow(); this._rows.set(k, r); }
319
+ this._drawRow(r, it, scale);
320
+ const want = prev ? prev.nextSibling : this._listEl.firstChild;
321
+ if (want !== r.row) this._listEl.insertBefore(r.row, want);
322
+ prev = r.row;
323
+ }
324
+ this._emptyEl.hidden = this._items.length > 0 || !this._emptyText;
325
+ setText(this._emptyEl, this._emptyText);
326
+ const line = this._statsLine();
327
+ this._statsEl.hidden = !line;
328
+ setText(this._statsEl, line);
329
+ return this;
330
+ }
331
+
332
+ /** The row element for `key`, or null. */
333
+ rowFor(key) { return this._rows.get(String(key))?.row ?? null; }
334
+
335
+ dispose() { this.element.remove(); this._rows.clear(); }
336
+
337
+ _makeRow() {
338
+ const row = el('div', 'twm-progress-list__row');
339
+ row.setAttribute('role', 'listitem');
340
+ const name = el('span', 'twm-progress-list__name');
341
+ const track = el('div', 'twm-progress-list__track');
342
+ const fill = el('div', 'twm-progress-list__fill');
343
+ track.appendChild(fill);
344
+ const count = el('span', 'twm-progress-list__count');
345
+ const state = el('span', 'twm-progress-list__state');
346
+ row.append(state, name, track, count);
347
+ return { row, name, fill, count, state };
348
+ }
349
+
350
+ _drawRow(r, it, scale) {
351
+ const state = PROGRESS_ITEM_STATES.includes(it.state) ? it.state : 'pending';
352
+ for (const s of PROGRESS_ITEM_STATES) {
353
+ r.row.classList.toggle(`twm-progress-list__row--${s}`, s === state);
354
+ }
355
+ const done = isNum(it.completed) ? it.completed : 0;
356
+ const hasTotal = isNum(it.total) && it.total > 0;
357
+ const denom = hasTotal ? it.total : scale;
358
+ setWidth(r.fill, denom > 0 ? done / denom : 0);
359
+ setText(r.name, it.name ?? String(it.key));
360
+ const count = hasTotal ? `${fmtCount(done)} / ${fmtCount(it.total)}` : fmtCount(done);
361
+ setText(r.count, it.detail ? `${count} · ${it.detail}` : count);
362
+ if (r.state.title !== state) r.state.title = state;
363
+ }
364
+
365
+ _statsLine() {
366
+ if (this._stats === false) return '';
367
+ if (typeof this._stats === 'function') return String(this._stats(this.items) ?? '');
368
+ if (!this._items.length) return '';
369
+ const n = { done: 0, running: 0, failed: 0, pending: 0 };
370
+ let completed = 0;
371
+ for (const it of this._items) {
372
+ n[PROGRESS_ITEM_STATES.includes(it.state) ? it.state : 'pending'] += 1;
373
+ if (isNum(it.completed)) completed += it.completed;
374
+ }
375
+ const parts = [`${n.done} / ${this._items.length} done`];
376
+ if (n.running) parts.push(`${n.running} running`);
377
+ if (n.failed) parts.push(`${n.failed} failed`);
378
+ if (n.pending) parts.push(`${n.pending} pending`);
379
+ parts.push(`${fmtCount(completed)} completed`);
380
+ return parts.join(' · ');
381
+ }
382
+ }
383
+
384
+ // ─── StepProgress ────────────────────────────────────────────────────────
385
+
386
+ /**
387
+ * @typedef {Object} StepDef
388
+ * @property {string} [key] - How `setStep` names it; defaults to its index.
389
+ * @property {string} label
390
+ */
391
+
392
+ export class StepProgress {
393
+ /**
394
+ * @param {HTMLElement|null} host appended to, if given
395
+ * @param {{ steps?: Array<StepDef|string>, className?: string }} [options]
396
+ */
397
+ constructor(host, options = {}) {
398
+ const { steps = [], className = '' } = options;
399
+ const root = el('ol', `twm-steps${className ? ` ${className}` : ''}`);
400
+ this.element = root;
401
+ this._steps = steps.map((s, i) => {
402
+ const def = typeof s === 'string' ? { label: s } : (s || {});
403
+ const li = el('li', 'twm-steps__step');
404
+ const marker = el('span', 'twm-steps__marker');
405
+ const label = el('span', 'twm-steps__label', def.label ?? '');
406
+ const pass = el('span', 'twm-steps__pass');
407
+ pass.hidden = true;
408
+ li.append(marker, label, pass);
409
+ root.appendChild(li);
410
+ return { key: def.key != null ? String(def.key) : String(i), label: def.label ?? '', state: 'pending', pass: null, li, passEl: pass };
411
+ });
412
+ this._draw();
413
+ if (host) host.appendChild(root);
414
+ }
415
+
416
+ /** `[{ key, label, state, pass }]`, in order. */
417
+ get steps() {
418
+ return this._steps.map(({ key, label, state, pass }) => ({ key, label, state, pass }));
419
+ }
420
+
421
+ /** The index of the active phase, or -1. */
422
+ get activeIndex() { return this._steps.findIndex((s) => s.state === 'active'); }
423
+
424
+ /**
425
+ * Make one phase active: every phase before it is done, every phase after
426
+ * it pending. `pass` and `passes` read "pass n of m" on it.
427
+ * @param {string|number} which - A key, or an index.
428
+ * @param {{ pass?: number, passes?: number }} [opts]
429
+ */
430
+ setStep(which, { pass, passes } = {}) {
431
+ const i = this._index(which);
432
+ this._steps.forEach((s, j) => {
433
+ s.state = j < i ? 'done' : j === i ? 'active' : 'pending';
434
+ s.pass = j === i && isNum(pass) ? { n: pass, of: isNum(passes) ? passes : null } : null;
435
+ });
436
+ this._draw();
437
+ return this;
438
+ }
439
+
440
+ /** Mark a phase (the active one by default) as failed; phases after it stay pending. */
441
+ fail(which) {
442
+ const i = which === undefined ? this.activeIndex : this._index(which);
443
+ if (i < 0) return this;
444
+ this._steps.forEach((s, j) => {
445
+ if (j < i) s.state = 'done';
446
+ else if (j === i) s.state = 'error';
447
+ else s.state = 'pending';
448
+ if (j !== i) s.pass = null;
449
+ });
450
+ this._draw();
451
+ return this;
452
+ }
453
+
454
+ /** Every phase done. */
455
+ complete() {
456
+ for (const s of this._steps) { s.state = 'done'; s.pass = null; }
457
+ this._draw();
458
+ return this;
459
+ }
460
+
461
+ dispose() { this.element.remove(); }
462
+
463
+ _index(which) {
464
+ const i = typeof which === 'number'
465
+ ? which
466
+ : this._steps.findIndex((s) => s.key === String(which));
467
+ if (!Number.isInteger(i) || i < 0 || i >= this._steps.length) {
468
+ throw new Error(`StepProgress: no step "${which}"`);
469
+ }
470
+ return i;
471
+ }
472
+
473
+ _draw() {
474
+ for (const s of this._steps) {
475
+ for (const st of STEP_STATES) s.li.classList.toggle(`twm-steps__step--${st}`, s.state === st);
476
+ if (s.state === 'active') s.li.setAttribute('aria-current', 'step');
477
+ else s.li.removeAttribute('aria-current');
478
+ const text = s.pass ? (s.pass.of ? `pass ${s.pass.n} of ${s.pass.of}` : `pass ${s.pass.n}`) : '';
479
+ s.passEl.hidden = !text;
480
+ setText(s.passEl, text);
481
+ }
482
+ }
483
+ }
File without changes
File without changes