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.
- package/CHANGELOG.md +93 -0
- package/LICENSE +0 -0
- package/README.md +2217 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3146 -164
- package/css/overrides.css +16 -0
- package/css/reset.css +0 -0
- package/css/tokens.css +0 -0
- package/dist/canvas.js +6 -239
- package/dist/canvas.js.map +3 -3
- package/dist/charts.js +709 -18
- package/dist/charts.js.map +4 -4
- package/dist/chunk-3PHCPZHT.js +0 -0
- package/dist/chunk-3PHCPZHT.js.map +0 -0
- package/dist/chunk-4A7SJC3T.js +503 -0
- package/dist/chunk-4A7SJC3T.js.map +7 -0
- package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
- package/dist/chunk-CVMCZZX2.js.map +7 -0
- package/dist/chunk-D3COLZZ2.js +1311 -0
- package/dist/chunk-D3COLZZ2.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +0 -0
- package/dist/chunk-FOOS3T5L.js.map +0 -0
- package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
- package/dist/chunk-GOQLR5WR.js.map +7 -0
- package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
- package/dist/chunk-HNU6MM2B.js.map +7 -0
- package/dist/chunk-HYJYYEPK.js +375 -0
- package/dist/chunk-HYJYYEPK.js.map +7 -0
- package/dist/chunk-JKZLNKG6.js +2041 -0
- package/dist/chunk-JKZLNKG6.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +0 -0
- package/dist/chunk-JYWURG5T.js.map +0 -0
- package/dist/{chunk-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
- package/dist/chunk-MCIXFNVF.js.map +7 -0
- package/dist/chunk-N7HC3ADV.js +30 -0
- package/dist/chunk-N7HC3ADV.js.map +7 -0
- package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
- package/dist/chunk-Q36EPT2R.js.map +7 -0
- package/dist/chunk-QWQJUDPD.js +610 -0
- package/dist/chunk-QWQJUDPD.js.map +7 -0
- package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
- package/dist/chunk-UURZ37SD.js.map +7 -0
- package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
- package/dist/chunk-UWO43BZU.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +0 -0
- package/dist/chunk-WVFGV5FT.js.map +0 -0
- package/dist/core.js +170 -118
- package/dist/core.js.map +2 -2
- package/dist/editor.js +156 -70
- package/dist/editor.js.map +2 -2
- package/dist/flexdesk.css +3146 -164
- package/dist/flow.js +2789 -306
- package/dist/flow.js.map +4 -4
- package/dist/host.js +109 -3
- package/dist/host.js.map +3 -3
- package/dist/reset.css +0 -0
- package/dist/tile_registry-6WZPTUZV.js +0 -0
- package/dist/tile_registry-6WZPTUZV.js.map +0 -0
- package/dist/tiles.js +657 -234
- package/dist/tiles.js.map +4 -4
- package/dist/tokens.css +0 -0
- package/dist/widgets.js +3252 -2124
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -347
- package/dist/wm.js.map +4 -4
- package/package.json +3 -1
- package/src/canvas/canvas_adapter.js +5 -0
- package/src/canvas/edge_router.js +0 -0
- package/src/canvas/graph/connector_router.js +0 -0
- package/src/canvas/graph/history_service.js +0 -0
- package/src/canvas/graph/node_platform.js +0 -0
- package/src/canvas/index.js +1 -0
- package/src/canvas/layered_layout.js +244 -0
- package/src/charts/chart_types.js +0 -0
- package/src/charts/data_series_plot_window.js +0 -0
- package/src/charts/downsample.js +2 -2
- package/src/charts/plot_config.js +53 -29
- package/src/charts/plot_config_editor.js +627 -0
- package/src/charts/plot_grid.js +449 -0
- package/src/charts/plot_popout_window.js +103 -317
- package/src/charts/plotly_wrapper.js +22 -2
- package/src/charts/series_transforms.js +206 -0
- package/src/core/event_bus.js +0 -0
- package/src/core/logging.js +1 -1
- package/src/core/settings.js +105 -19
- package/src/core/state_guard.js +93 -51
- package/src/core/state_guard_config.js +85 -74
- package/src/core/state_machine.js +0 -0
- package/src/editor/editor_pane.js +3 -3
- package/src/editor/monaco_editor_factory.js +124 -60
- package/src/editor/monaco_loader.js +103 -28
- package/src/editor/notebook_search.js +0 -0
- package/src/editor/notebook_tab_bar.js +38 -52
- package/src/editor/notebook_undo_manager.js +0 -0
- package/src/editor/split_pane_container.js +6 -3
- package/src/flow/canvas_editor/connections.js +90 -0
- package/src/flow/canvas_editor/editor.js +2351 -153
- package/src/flow/canvas_editor/index.js +2 -1
- package/src/flow/canvas_editor/layout.js +119 -12
- package/src/flow/canvas_editor/strings.js +80 -0
- package/src/flow/kit/always_before.js +0 -0
- package/src/flow/kit/catalogue.js +97 -3
- package/src/flow/kit/chip_input.js +0 -0
- package/src/flow/kit/dom.js +0 -0
- package/src/flow/kit/findings.js +0 -0
- package/src/flow/kit/history.js +0 -0
- package/src/flow/kit/index.js +3 -3
- package/src/flow/kit/keys.js +7 -1
- package/src/flow/kit/logic_graph.js +97 -15
- package/src/flow/kit/references.js +0 -0
- package/src/flow/kit/settings_panel.js +0 -0
- package/src/flow/kit/settings_schema.js +2 -1
- package/src/flow/kit/step_picker.js +0 -0
- package/src/flow/kit/strings.js +3 -1
- package/src/flow/kit/value_picker.js +0 -0
- package/src/flow/kit/widgets.js +86 -109
- package/src/flow/lanes/editor.js +136 -14
- package/src/flow/lanes/index.js +3 -1
- package/src/flow/lanes/layout.js +0 -0
- package/src/flow/lanes/pipeline.js +44 -2
- package/src/flow/lanes/preview.js +0 -0
- package/src/flow/lanes/strings.js +1 -0
- package/src/flow/outline/editor.js +0 -0
- package/src/flow/outline/index.js +0 -0
- package/src/flow/outline/mapping.js +0 -0
- package/src/flow/outline/menu.js +0 -0
- package/src/flow/outline/operations.js +0 -0
- package/src/flow/outline/recognise.js +0 -0
- package/src/flow/outline/strings.js +0 -0
- package/src/help/help_modal.js +168 -42
- package/src/help/help_registry.js +12 -1
- package/src/host/external_links.js +0 -0
- package/src/host/file_picker.js +117 -0
- package/src/host/host.js +10 -2
- package/src/host/photino_host.js +9 -0
- package/src/host/photino_host.md +21 -3
- package/src/host/pywebview_host.js +10 -0
- package/src/tiles/chart_tiles.js +53 -245
- package/src/tiles/config_schema.js +8 -0
- package/src/tiles/figure_tile.js +322 -0
- package/src/tiles/layout_persistence.js +3 -3
- package/src/tiles/list_tiles.js +7 -7
- package/src/tiles/scope_picker.js +0 -0
- package/src/tiles/tile_base.js +1 -1
- package/src/tiles/tile_config_form.js +442 -0
- package/src/tiles/tile_grid.js +2 -0
- package/src/tiles/tile_registry.js +0 -0
- package/src/tiling/bottom_panel_tabs.js +67 -99
- package/src/tiling/command_palette.js +64 -19
- package/src/tiling/content_registry.js +4 -1
- package/src/tiling/desktops.js +11 -1
- package/src/tiling/entity_sources.js +0 -0
- package/src/tiling/keymap.js +403 -128
- package/src/tiling/kind_taxonomy.js +78 -0
- package/src/tiling/landing_table.js +0 -0
- package/src/tiling/loading_overlay.js +0 -0
- package/src/tiling/nav_panel.js +59 -10
- package/src/tiling/page_factory.js +5 -5
- package/src/tiling/panel_availability.js +49 -0
- package/src/tiling/panel_keys.js +0 -0
- package/src/tiling/shell.js +114 -12
- package/src/tiling/tab_strip.js +0 -0
- package/src/tiling/tile_breadcrumb.js +0 -0
- package/src/tiling/tile_renderer.js +105 -9
- package/src/tiling/tile_tab_menu.js +0 -0
- package/src/tiling/tile_tree.js +0 -0
- package/src/tiling/wm.js +562 -30
- package/src/tiling/zoom.js +0 -0
- package/src/ui/base/component_base.js +0 -0
- package/src/ui/base/controller_base.js +0 -0
- package/src/ui/base/page_base.js +0 -0
- package/src/ui/components/about_dialog.js +0 -0
- package/src/ui/components/action_dropdown.js +30 -2
- package/src/ui/components/alert_log.js +0 -0
- package/src/ui/components/alerts_pane.js +0 -0
- package/src/ui/components/attribute_list_editor.js +0 -0
- package/src/ui/components/autocomplete_field.js +0 -0
- package/src/ui/components/cell_editor_host.js +211 -0
- package/src/ui/components/cell_grid.js +648 -0
- package/src/ui/components/cell_selection.js +220 -0
- package/src/ui/components/choice_controls.js +351 -0
- package/src/ui/components/color_field.js +211 -0
- package/src/ui/components/color_list_field.js +210 -0
- package/src/ui/components/column_fit.js +0 -0
- package/src/ui/components/column_groups.js +314 -0
- package/src/ui/components/column_order.js +142 -0
- package/src/ui/components/computing_status_window.js +98 -25
- package/src/ui/components/confirm_dialog.js +81 -38
- package/src/ui/components/context_menu.js +2 -2
- package/src/ui/components/conversation_view.js +440 -0
- package/src/ui/components/crosstab.js +289 -0
- package/src/ui/components/data_table.js +601 -62
- package/src/ui/components/detail_header.js +22 -11
- package/src/ui/components/document_outline.js +397 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/fact_table.js +0 -0
- package/src/ui/components/fill_handle.js +322 -0
- package/src/ui/components/gallery_picker.js +19 -8
- package/src/ui/components/header_actions.js +55 -0
- package/src/ui/components/inline_renamer.js +0 -0
- package/src/ui/components/job_progress.js +51 -0
- package/src/ui/components/managed_window.js +27 -4
- package/src/ui/components/menu_bar.js +745 -0
- package/src/ui/components/modal.js +449 -75
- package/src/ui/components/notification_history.js +3 -3
- package/src/ui/components/progress.js +483 -0
- package/src/ui/components/record_list.js +0 -0
- package/src/ui/components/row_activation.js +0 -0
- package/src/ui/components/section_stack.js +298 -0
- package/src/ui/components/series.js +173 -0
- package/src/ui/components/settings_view.js +634 -0
- package/src/ui/components/slide_out_panel.js +43 -6
- package/src/ui/components/slider_field.js +275 -18
- package/src/ui/components/sortable_list.js +0 -0
- package/src/ui/components/staged_edits.js +343 -0
- package/src/ui/components/staged_history.js +172 -0
- package/src/ui/components/table_filter_chips.js +0 -0
- package/src/ui/components/table_state_store.js +0 -0
- package/src/ui/components/table_structure.js +182 -0
- package/src/ui/components/table_values.js +0 -0
- package/src/ui/components/tabs.js +497 -0
- package/src/ui/components/tag_input.js +215 -0
- package/src/ui/components/toast.js +25 -8
- package/src/ui/components/toggle_field.js +175 -0
- package/src/ui/components/tree_view.js +55 -34
- package/src/ui/components/window_taskbar.js +0 -0
- package/src/ui/components/wizard.js +563 -0
- package/src/ui/controllers/window_chrome_controller.js +2 -2
- package/src/ui/notification_center.js +375 -119
- package/src/ui/utils/auto_scrollbars.js +0 -0
- package/src/ui/utils/markdown.js +709 -0
- package/src/ui/utils/native_context_menu.js +0 -0
- package/src/ui/utils/overlay_scrollbar.js +16 -12
- package/src/ui/utils/raf_resize_observer.js +0 -0
- package/src/ui/utils/scroll_lock.js +0 -0
- package/src/ui/utils/strip_scroll.js +55 -0
- package/src/ui/utils/tooltip_service.js +0 -8
- package/dist/chunk-6JSOVNID.js.map +0 -7
- package/dist/chunk-B5Q53LAJ.js.map +0 -7
- package/dist/chunk-FBGP5KN5.js.map +0 -7
- package/dist/chunk-FL5KFNQH.js.map +0 -7
- package/dist/chunk-NLTOY57M.js.map +0 -7
- package/dist/chunk-NNFTETK3.js +0 -633
- package/dist/chunk-NNFTETK3.js.map +0 -7
- package/dist/chunk-O5OHMWBB.js +0 -284
- package/dist/chunk-O5OHMWBB.js.map +0 -7
- package/dist/chunk-RQM5Z2MU.js.map +0 -7
- package/dist/chunk-SN4LSOAR.js.map +0 -7
- package/dist/chunk-W6FBJLPW.js +0 -367
- package/dist/chunk-W6FBJLPW.js.map +0 -7
- 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(() =>
|
|
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
|