@zolomedia/bifrost-client 1.7.74
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/L1_Foundation/L1_Foundation.js +13 -0
- package/L1_Foundation/bootstrap/bootstrap.js +11 -0
- package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
- package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
- package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
- package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
- package/L1_Foundation/bootstrap/module_registry.js +102 -0
- package/L1_Foundation/bootstrap/prism_loader.js +164 -0
- package/L1_Foundation/config/client_config.js +110 -0
- package/L1_Foundation/config/config.js +7 -0
- package/L1_Foundation/connection/connection.js +8 -0
- package/L1_Foundation/connection/websocket_connection.js +122 -0
- package/L1_Foundation/constants/bifrost_constants.js +284 -0
- package/L1_Foundation/constants/constants.js +7 -0
- package/L1_Foundation/logger/logger.js +10 -0
- package/L2_Handling/L2_Handling.js +15 -0
- package/L2_Handling/cache/cache.js +22 -0
- package/L2_Handling/cache/cache_constants.js +69 -0
- package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
- package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
- package/L2_Handling/cache/orchestration/orchestration.js +12 -0
- package/L2_Handling/cache/storage/session_manager.js +289 -0
- package/L2_Handling/cache/storage/storage.js +10 -0
- package/L2_Handling/cache/storage/storage_manager.js +590 -0
- package/L2_Handling/display/composite/composite.js +13 -0
- package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
- package/L2_Handling/display/composite/swiper_renderer.js +564 -0
- package/L2_Handling/display/composite/terminal_renderer.js +922 -0
- package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
- package/L2_Handling/display/display.js +30 -0
- package/L2_Handling/display/feedback/feedback.js +11 -0
- package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
- package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
- package/L2_Handling/display/inputs/button_renderer.js +634 -0
- package/L2_Handling/display/inputs/form_renderer.js +583 -0
- package/L2_Handling/display/inputs/input_renderer.js +658 -0
- package/L2_Handling/display/inputs/inputs.js +12 -0
- package/L2_Handling/display/navigation/menu_renderer.js +206 -0
- package/L2_Handling/display/navigation/navigation.js +11 -0
- package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
- package/L2_Handling/display/orchestration/orchestration.js +11 -0
- package/L2_Handling/display/orchestration/renderer.js +430 -0
- package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
- package/L2_Handling/display/outputs/alert_renderer.js +161 -0
- package/L2_Handling/display/outputs/audio_renderer.js +94 -0
- package/L2_Handling/display/outputs/card_renderer.js +229 -0
- package/L2_Handling/display/outputs/code_renderer.js +66 -0
- package/L2_Handling/display/outputs/dl_renderer.js +131 -0
- package/L2_Handling/display/outputs/header_renderer.js +162 -0
- package/L2_Handling/display/outputs/icon_renderer.js +107 -0
- package/L2_Handling/display/outputs/image_renderer.js +145 -0
- package/L2_Handling/display/outputs/list_renderer.js +190 -0
- package/L2_Handling/display/outputs/outputs.js +19 -0
- package/L2_Handling/display/outputs/table_renderer.js +765 -0
- package/L2_Handling/display/outputs/text_renderer.js +818 -0
- package/L2_Handling/display/outputs/typography_renderer.js +293 -0
- package/L2_Handling/display/outputs/video_renderer.js +116 -0
- package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
- package/L2_Handling/display/primitives/form_primitives.js +526 -0
- package/L2_Handling/display/primitives/generic_containers.js +109 -0
- package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
- package/L2_Handling/display/primitives/link_primitives.js +552 -0
- package/L2_Handling/display/primitives/lists_primitives.js +262 -0
- package/L2_Handling/display/primitives/media_primitives.js +383 -0
- package/L2_Handling/display/primitives/primitives.js +19 -0
- package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
- package/L2_Handling/display/primitives/table_primitives.js +528 -0
- package/L2_Handling/display/primitives/typography_primitives.js +175 -0
- package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
- package/L2_Handling/display/specialized/specialized.js +10 -0
- package/L2_Handling/hooks/hooks.js +9 -0
- package/L2_Handling/hooks/menu_integration.js +57 -0
- package/L2_Handling/hooks/widget_hook_manager.js +292 -0
- package/L2_Handling/message/message.js +8 -0
- package/L2_Handling/message/message_handler.js +701 -0
- package/L2_Handling/navigation/navigation.js +8 -0
- package/L2_Handling/navigation/navigation_manager.js +403 -0
- package/L2_Handling/zhooks/features/cache_live.js +287 -0
- package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
- package/L2_Handling/zhooks/zhooks_manager.js +65 -0
- package/L2_Handling/zvaf/zvaf.js +8 -0
- package/L2_Handling/zvaf/zvaf_manager.js +334 -0
- package/L3_Abstraction/L3_Abstraction.js +12 -0
- package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
- package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
- package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
- package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
- package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
- package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
- package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
- package/L3_Abstraction/renderer/renderer.js +1 -0
- package/L3_Abstraction/session/session.js +1 -0
- package/L4_Orchestration/L4_Orchestration.js +11 -0
- package/L4_Orchestration/client/client.js +1 -0
- package/L4_Orchestration/facade/facade.js +9 -0
- package/L4_Orchestration/facade/manager_registry.js +118 -0
- package/L4_Orchestration/facade/renderer_registry.js +274 -0
- package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
- package/L4_Orchestration/lifecycle/initializer.js +135 -0
- package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
- package/L4_Orchestration/rendering/facade.js +94 -0
- package/L4_Orchestration/rendering/rendering.js +7 -0
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/bifrost_client.js +204 -0
- package/bifrost_core.js +1686 -0
- package/docs/ARCHITECTURE.md +111 -0
- package/docs/PROTOCOL.md +106 -0
- package/docs/RENDERERS.md +101 -0
- package/docs/SECURITY.md +92 -0
- package/package.json +24 -0
- package/syntax/prism-zconfig.js +41 -0
- package/syntax/prism-zenv.js +69 -0
- package/syntax/prism-zolo-theme.css +288 -0
- package/syntax/prism-zolo.js +380 -0
- package/syntax/prism-zschema.js +38 -0
- package/syntax/prism-zspark.js +25 -0
- package/syntax/prism-zui.js +68 -0
- package/zSys/accessibility/accessibility.js +10 -0
- package/zSys/accessibility/emoji_accessibility.js +173 -0
- package/zSys/dom/block_utils.js +122 -0
- package/zSys/dom/container_utils.js +370 -0
- package/zSys/dom/dom.js +13 -0
- package/zSys/dom/dom_utils.js +328 -0
- package/zSys/dom/encoding_utils.js +117 -0
- package/zSys/dom/style_utils.js +71 -0
- package/zSys/errors/error_display.js +299 -0
- package/zSys/errors/errors.js +10 -0
- package/zSys/theme/color_utils.js +274 -0
- package/zSys/theme/dark_mode_utils.js +272 -0
- package/zSys/theme/size_utils.js +256 -0
- package/zSys/theme/spacing_utils.js +405 -0
- package/zSys/theme/theme.js +14 -0
- package/zSys/theme/zbase.css +1735 -0
- package/zSys/theme/zbase_inject.js +161 -0
- package/zSys/theme/ztheme_utils.js +305 -0
- package/zSys/validation/error_boundary.js +201 -0
- package/zSys/validation/validation.js +11 -0
- package/zSys/validation/validation_utils.js +238 -0
- package/zSys/zSys.js +14 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProgressBarRenderer - Render progress bars with percentage and ETA
|
|
3
|
+
*
|
|
4
|
+
* Terminal-first implementation matching backend zDisplay.progress_bar()
|
|
5
|
+
*
|
|
6
|
+
* Backend Events (from display_event_timebased.py):
|
|
7
|
+
* - progress_bar: Update progress
|
|
8
|
+
* - progress_complete: Finish progress
|
|
9
|
+
*
|
|
10
|
+
* Terminal Paradigm:
|
|
11
|
+
* - Carriage return (\r) updates: Overwrites same line for smooth updates
|
|
12
|
+
* - ETA calculation: Based on elapsed time and remaining work
|
|
13
|
+
* - Visual bars: 80% (2m 30s remaining)
|
|
14
|
+
*
|
|
15
|
+
* Bifrost Paradigm:
|
|
16
|
+
* - WebSocket events trigger updates
|
|
17
|
+
* - CSS transitions for smooth width changes
|
|
18
|
+
* - Multiple progress bars can be active simultaneously
|
|
19
|
+
* - Striped/animated variants for indeterminate or processing states
|
|
20
|
+
*
|
|
21
|
+
* Features:
|
|
22
|
+
* - Determinate progress (0-100%)
|
|
23
|
+
* - Percentage display (show_percentage)
|
|
24
|
+
* - ETA display (show_eta)
|
|
25
|
+
* - Color variants (primary, success, danger, warning, info)
|
|
26
|
+
* - Striped/animated variants
|
|
27
|
+
* - Height variants (using size_utils)
|
|
28
|
+
* - Auto-removal on completion
|
|
29
|
+
*
|
|
30
|
+
* @see https://github.com/ZoloAi/zTheme/blob/main/Manual/ztheme-progress.html
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// Layer 2: Utilities
|
|
34
|
+
import { getBackgroundClass } from '../../../zSys/theme/color_utils.js';
|
|
35
|
+
import { applyHeight } from '../../../zSys/theme/size_utils.js';
|
|
36
|
+
import { withErrorBoundary } from '../../../zSys/validation/error_boundary.js';
|
|
37
|
+
|
|
38
|
+
// Layer 3: Primitives
|
|
39
|
+
import { createDiv, createSpan } from '../primitives/generic_containers.js';
|
|
40
|
+
|
|
41
|
+
// Constants
|
|
42
|
+
import { TIMEOUTS, COLORS } from '../../../L1_Foundation/constants/bifrost_constants.js';
|
|
43
|
+
|
|
44
|
+
export default class ProgressBarRenderer {
|
|
45
|
+
/**
|
|
46
|
+
* @param {Object} logger - Logger instance
|
|
47
|
+
*/
|
|
48
|
+
constructor(logger) {
|
|
49
|
+
this.logger = logger;
|
|
50
|
+
this._activeProgressBars = new Map(); // Track active progress bars by progressId
|
|
51
|
+
|
|
52
|
+
// Wrap render method with error boundary
|
|
53
|
+
if (typeof this.render === 'function') {
|
|
54
|
+
const originalRender = this.render.bind(this);
|
|
55
|
+
this.render = withErrorBoundary(originalRender, {
|
|
56
|
+
component: 'ProgressBarRenderer',
|
|
57
|
+
logger: this.logger
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Start or update a progress bar (progress_bar event)
|
|
64
|
+
* @param {Object} event - Progress bar event
|
|
65
|
+
* @param {string} event.progressId - Unique progress bar ID
|
|
66
|
+
* @param {string} event.label - Progress bar label text
|
|
67
|
+
* @param {number} event.current - Current progress value
|
|
68
|
+
* @param {number} event.total - Total value (max progress)
|
|
69
|
+
* @param {boolean} [event.showPercentage=true] - Show percentage text
|
|
70
|
+
* @param {boolean} [event.showETA=true] - Show ETA text
|
|
71
|
+
* @param {string} [event.eta] - ETA string from backend
|
|
72
|
+
* @param {string} [event.color='primary'] - Progress bar color
|
|
73
|
+
* @param {string} [event.container='#app'] - Target container selector
|
|
74
|
+
* @param {boolean} [event.striped=false] - Use striped variant
|
|
75
|
+
* @param {boolean} [event.animated=false] - Animate stripes
|
|
76
|
+
* @param {string} [event.height='md'] - Height (sm, md, lg)
|
|
77
|
+
* @returns {HTMLElement} Progress bar container element
|
|
78
|
+
*/
|
|
79
|
+
render(event) {
|
|
80
|
+
const {
|
|
81
|
+
progressId,
|
|
82
|
+
label = 'Processing...',
|
|
83
|
+
current = 0,
|
|
84
|
+
total = 100,
|
|
85
|
+
eta = null,
|
|
86
|
+
color = 'primary',
|
|
87
|
+
container = '#app',
|
|
88
|
+
striped = false,
|
|
89
|
+
animated = false,
|
|
90
|
+
height = 'md'
|
|
91
|
+
} = event;
|
|
92
|
+
|
|
93
|
+
// Accept backend snake_case (show_percentage / show_eta) as well as camelCase.
|
|
94
|
+
const showPercentage = event.showPercentage ?? event.show_percentage ?? true;
|
|
95
|
+
const showETA = event.showETA ?? event.show_eta ?? false;
|
|
96
|
+
|
|
97
|
+
this.logger.log('[ProgressBarRenderer] Rendering progress:', {
|
|
98
|
+
progressId,
|
|
99
|
+
label,
|
|
100
|
+
progress: `${current}/${total}`
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Check if progress bar already exists
|
|
104
|
+
let progressContainer;
|
|
105
|
+
if (this._activeProgressBars.has(progressId)) {
|
|
106
|
+
// Update existing progress bar
|
|
107
|
+
progressContainer = this._activeProgressBars.get(progressId).element;
|
|
108
|
+
this._updateProgressBar(progressContainer, current, total, showPercentage, eta, showETA);
|
|
109
|
+
} else {
|
|
110
|
+
// Create new progress bar
|
|
111
|
+
progressContainer = this._createProgressBarContainer(
|
|
112
|
+
progressId,
|
|
113
|
+
label,
|
|
114
|
+
current,
|
|
115
|
+
total,
|
|
116
|
+
showPercentage,
|
|
117
|
+
showETA,
|
|
118
|
+
eta,
|
|
119
|
+
color,
|
|
120
|
+
striped,
|
|
121
|
+
animated,
|
|
122
|
+
height
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Find target container. The backend default is the literal "default"
|
|
126
|
+
// (not a selector) — treat that (and any unresolved selector) as the app
|
|
127
|
+
// root so a streamed bar lands in the page flow, never orphaned on <body>.
|
|
128
|
+
const selector = (container && container !== 'default') ? container : '#app';
|
|
129
|
+
const targetElement = document.querySelector(selector)
|
|
130
|
+
|| document.querySelector('#app')
|
|
131
|
+
|| document.body;
|
|
132
|
+
targetElement.appendChild(progressContainer);
|
|
133
|
+
|
|
134
|
+
// Track active progress bar
|
|
135
|
+
this._activeProgressBars.set(progressId, {
|
|
136
|
+
element: progressContainer,
|
|
137
|
+
label,
|
|
138
|
+
startTime: Date.now()
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check if complete
|
|
143
|
+
if (current >= total) {
|
|
144
|
+
this.complete({ progressId });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.logger.log('[ProgressBarRenderer] Progress updated successfully');
|
|
148
|
+
return progressContainer;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build a progress bar element WITHOUT appending it (inline/declarative use).
|
|
153
|
+
*
|
|
154
|
+
* The orchestrator's renderZDisplayEvent() switch returns a node that the
|
|
155
|
+
* caller places in the page flow — same contract as image/table renderers.
|
|
156
|
+
* We still register the bar by progressId so a later streamed update (a wizard
|
|
157
|
+
* advancing, same id) finds and animates this exact element in place.
|
|
158
|
+
*
|
|
159
|
+
* Accepts both camelCase (showPercentage) and the backend's snake_case
|
|
160
|
+
* (show_percentage); leaving `total` off yields an indeterminate striped bar.
|
|
161
|
+
* @param {Object} event - Progress bar event (declarative or streamed)
|
|
162
|
+
* @returns {HTMLElement} Progress bar container element (not yet attached)
|
|
163
|
+
*/
|
|
164
|
+
renderInline(event) {
|
|
165
|
+
const {
|
|
166
|
+
progressId = `progress-${Date.now()}`,
|
|
167
|
+
label = 'Processing...',
|
|
168
|
+
current = 0,
|
|
169
|
+
color = 'primary',
|
|
170
|
+
height = 'md',
|
|
171
|
+
eta = null
|
|
172
|
+
} = event;
|
|
173
|
+
|
|
174
|
+
const showPercentage = event.showPercentage ?? event.show_percentage ?? true;
|
|
175
|
+
const showETA = event.showETA ?? event.show_eta ?? false;
|
|
176
|
+
|
|
177
|
+
// Indeterminate (no total) → a full striped/animated bar that reads as "working".
|
|
178
|
+
let { total, striped = false, animated = false } = event;
|
|
179
|
+
if (!total) {
|
|
180
|
+
total = 100;
|
|
181
|
+
striped = true;
|
|
182
|
+
animated = true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Re-render of a bar we already drew (a wizard advancing past its gate
|
|
186
|
+
// re-emits the SAME progressId at 100%). Update it in place and return null
|
|
187
|
+
// so the caller never appends a duplicate — the existing node animates to its
|
|
188
|
+
// new width where it sits (the bar stays at the top of the wizard).
|
|
189
|
+
if (event.progressId && this._activeProgressBars.has(event.progressId)) {
|
|
190
|
+
const existing = this._activeProgressBars.get(event.progressId).element;
|
|
191
|
+
if (existing && document.contains(existing)) {
|
|
192
|
+
this._updateProgressBar(existing, current, total, showPercentage, eta, showETA);
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const element = this._createProgressBarContainer(
|
|
198
|
+
progressId, label, current, total,
|
|
199
|
+
showPercentage, showETA, eta, color, striped, animated, height
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Register so streamed updates (same progressId) update this node in place.
|
|
203
|
+
this._activeProgressBars.set(progressId, {
|
|
204
|
+
element,
|
|
205
|
+
label,
|
|
206
|
+
startTime: Date.now()
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return element;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Complete a progress bar (progress_complete event)
|
|
214
|
+
* @param {Object} event - Progress complete event
|
|
215
|
+
* @param {string} event.progressId - Unique progress bar ID
|
|
216
|
+
*/
|
|
217
|
+
complete(event) {
|
|
218
|
+
const { progressId } = event;
|
|
219
|
+
|
|
220
|
+
this.logger.log('[ProgressBarRenderer] Completing progress:', progressId);
|
|
221
|
+
|
|
222
|
+
const progressData = this._activeProgressBars.get(progressId);
|
|
223
|
+
if (!progressData) {
|
|
224
|
+
this.logger.warn('[ProgressBarRenderer] Progress bar not found:', progressId);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const { element } = progressData;
|
|
229
|
+
|
|
230
|
+
// Auto-remove after configured timeout
|
|
231
|
+
setTimeout(() => {
|
|
232
|
+
if (element.parentNode) {
|
|
233
|
+
element.style.transition = 'opacity 0.3s';
|
|
234
|
+
element.style.opacity = '0';
|
|
235
|
+
setTimeout(() => {
|
|
236
|
+
if (element.parentNode) {
|
|
237
|
+
element.parentNode.removeChild(element);
|
|
238
|
+
}
|
|
239
|
+
}, TIMEOUTS.FADE_TRANSITION);
|
|
240
|
+
}
|
|
241
|
+
}, TIMEOUTS.AUTO_REMOVE_PROGRESS);
|
|
242
|
+
|
|
243
|
+
// Remove from active progress bars
|
|
244
|
+
this._activeProgressBars.delete(progressId);
|
|
245
|
+
|
|
246
|
+
this.logger.log('[ProgressBarRenderer] Progress completed successfully');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Create progress bar container using primitives
|
|
251
|
+
* @private
|
|
252
|
+
*/
|
|
253
|
+
_createProgressBarContainer(
|
|
254
|
+
progressId,
|
|
255
|
+
label,
|
|
256
|
+
current,
|
|
257
|
+
total,
|
|
258
|
+
showPercentage,
|
|
259
|
+
showETA,
|
|
260
|
+
eta,
|
|
261
|
+
color,
|
|
262
|
+
striped,
|
|
263
|
+
animated,
|
|
264
|
+
height
|
|
265
|
+
) {
|
|
266
|
+
// Main container (using primitive)
|
|
267
|
+
const container = createDiv({
|
|
268
|
+
id: progressId,
|
|
269
|
+
class: 'zProgress-container zMy-3'
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Label row (using primitives)
|
|
273
|
+
const labelRow = createDiv({
|
|
274
|
+
class: 'zD-flex zFlex-justify-between zMb-1'
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const labelSpan = createSpan({
|
|
278
|
+
class: 'zText-dark zFw-bold'
|
|
279
|
+
});
|
|
280
|
+
labelSpan.textContent = label;
|
|
281
|
+
|
|
282
|
+
const infoSpan = createSpan({
|
|
283
|
+
class: 'zText-muted',
|
|
284
|
+
'data-info': 'progress-info'
|
|
285
|
+
});
|
|
286
|
+
infoSpan.textContent = this._formatProgressInfo(current, total, showPercentage, eta, showETA);
|
|
287
|
+
|
|
288
|
+
labelRow.appendChild(labelSpan);
|
|
289
|
+
labelRow.appendChild(infoSpan);
|
|
290
|
+
|
|
291
|
+
// Progress bar wrapper (using primitive + zTheme classes)
|
|
292
|
+
const progressWrapper = createDiv({
|
|
293
|
+
class: 'zProgress'
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Apply height using size_utils (consolidated to avoid duplication)
|
|
297
|
+
// TODO: Add .zProgress-sm, .zProgress-md, .zProgress-lg to zTheme
|
|
298
|
+
const PROGRESS_HEIGHT_MAP = {
|
|
299
|
+
sm: '0.5rem', // SIZE_SCALE[2]
|
|
300
|
+
md: '1rem', // SIZE_SCALE[4]
|
|
301
|
+
lg: '1.5rem' // SIZE_SCALE[6]
|
|
302
|
+
};
|
|
303
|
+
if (PROGRESS_HEIGHT_MAP[height]) {
|
|
304
|
+
applyHeight(progressWrapper, PROGRESS_HEIGHT_MAP[height]);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Progress bar (using primitive + composition pattern)
|
|
308
|
+
const progressBar = createDiv({
|
|
309
|
+
class: 'zProgress-bar',
|
|
310
|
+
role: 'progressbar',
|
|
311
|
+
'aria-valuenow': current,
|
|
312
|
+
'aria-valuemin': '0',
|
|
313
|
+
'aria-valuemax': total,
|
|
314
|
+
'data-bar': 'progress-bar'
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Apply color using color_utils (Layer 2 composition)
|
|
318
|
+
const bgClass = getBackgroundClass(color);
|
|
319
|
+
progressBar.classList.add(bgClass);
|
|
320
|
+
|
|
321
|
+
// Apply variants
|
|
322
|
+
if (striped) {
|
|
323
|
+
progressBar.classList.add('zProgress-bar-striped');
|
|
324
|
+
}
|
|
325
|
+
if (animated) {
|
|
326
|
+
progressBar.classList.add('zProgress-bar-animated');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Set initial width
|
|
330
|
+
const percentage = Math.min(100, Math.max(0, (current / total) * 100));
|
|
331
|
+
progressBar.style.width = `${percentage}%`;
|
|
332
|
+
progressBar.style.transition = 'width 0.3s ease-in-out';
|
|
333
|
+
|
|
334
|
+
// Assemble
|
|
335
|
+
progressWrapper.appendChild(progressBar);
|
|
336
|
+
container.appendChild(labelRow);
|
|
337
|
+
container.appendChild(progressWrapper);
|
|
338
|
+
|
|
339
|
+
return container;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Update existing progress bar
|
|
344
|
+
* @private
|
|
345
|
+
*/
|
|
346
|
+
_updateProgressBar(container, current, total, showPercentage, eta, showETA) {
|
|
347
|
+
// Update progress bar width
|
|
348
|
+
const progressBar = container.querySelector('[data-bar="progress-bar"]');
|
|
349
|
+
if (progressBar) {
|
|
350
|
+
const percentage = Math.min(100, Math.max(0, (current / total) * 100));
|
|
351
|
+
|
|
352
|
+
// Step-oriented vs time-oriented. A real step landing (finite total,
|
|
353
|
+
// still in progress) means this is determinate progress — it should
|
|
354
|
+
// "just slowly fill", not borrow the indeterminate/time treatment. Drop
|
|
355
|
+
// the marching marquee + flowing stripes so only the smooth width
|
|
356
|
+
// transition (zbase: width 0.45s) shows. Time/indeterminate bars never
|
|
357
|
+
// reach here with current < total, so they keep their motion.
|
|
358
|
+
const determinate = Number.isFinite(total) && total > 0 && current < total;
|
|
359
|
+
if (determinate) {
|
|
360
|
+
const track = container.querySelector('.zProgress');
|
|
361
|
+
if (track) track.classList.remove('zProgress--indeterminate');
|
|
362
|
+
progressBar.classList.remove('zProgress-bar-striped', 'zProgress-bar-animated');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
progressBar.style.width = `${percentage}%`;
|
|
366
|
+
progressBar.setAttribute('aria-valuenow', current);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Update info text
|
|
370
|
+
const infoSpan = container.querySelector('[data-info="progress-info"]');
|
|
371
|
+
if (infoSpan) {
|
|
372
|
+
infoSpan.textContent = this._formatProgressInfo(current, total, showPercentage, eta, showETA);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Format progress info text (percentage + ETA)
|
|
378
|
+
* @private
|
|
379
|
+
*/
|
|
380
|
+
_formatProgressInfo(current, total, showPercentage, eta, showETA) {
|
|
381
|
+
const parts = [];
|
|
382
|
+
|
|
383
|
+
if (showPercentage) {
|
|
384
|
+
const percentage = Math.min(100, Math.max(0, (current / total) * 100));
|
|
385
|
+
parts.push(`${Math.round(percentage)}%`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (showETA && eta) {
|
|
389
|
+
parts.push(`ETA: ${eta}`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return parts.join(' • ');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Get active progress bar count
|
|
397
|
+
* @returns {number} Number of active progress bars
|
|
398
|
+
*/
|
|
399
|
+
getActiveCount() {
|
|
400
|
+
return this._activeProgressBars.size;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Clear all progress bars (cleanup utility)
|
|
405
|
+
*/
|
|
406
|
+
clearAll() {
|
|
407
|
+
this.logger.log('[ProgressBarRenderer] Clearing all progress bars');
|
|
408
|
+
|
|
409
|
+
for (const [_progressId, data] of this._activeProgressBars) {
|
|
410
|
+
if (data.element.parentNode) {
|
|
411
|
+
data.element.parentNode.removeChild(data.element);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
this._activeProgressBars.clear();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpinnerRenderer - Render loading spinners
|
|
3
|
+
*
|
|
4
|
+
* Terminal-first implementation matching backend zDisplay.spinner()
|
|
5
|
+
*
|
|
6
|
+
* Backend Events (from display_event_timebased.py):
|
|
7
|
+
* - spinner_start: { event, spinnerId, label, style, container }
|
|
8
|
+
* - spinner_stop: { event, spinnerId }
|
|
9
|
+
*
|
|
10
|
+
* Spinner Styles (matching terminal):
|
|
11
|
+
* - dots:
|
|
12
|
+
* - line: - \ | /
|
|
13
|
+
* - arc:
|
|
14
|
+
* - bouncingBall: ( ) ( ) ( ) ( )
|
|
15
|
+
* - simple: . .. ...
|
|
16
|
+
*
|
|
17
|
+
* Terminal Paradigm:
|
|
18
|
+
* - Context manager: with display.spinner("Loading"): do_work()
|
|
19
|
+
* - Background thread animates frames
|
|
20
|
+
* - Auto-cleanup with checkmark on completion
|
|
21
|
+
*
|
|
22
|
+
* Bifrost Paradigm:
|
|
23
|
+
* - WebSocket events trigger start/stop
|
|
24
|
+
* - CSS animations handle visual feedback
|
|
25
|
+
* - Multiple spinners can be active simultaneously
|
|
26
|
+
*
|
|
27
|
+
* Features:
|
|
28
|
+
* - Size variants (sm, md, lg)
|
|
29
|
+
* - Color variants (primary, secondary, success, danger, warning, info)
|
|
30
|
+
* - CSS animations (no JavaScript required after rendering)
|
|
31
|
+
* - Auto-cleanup on stop
|
|
32
|
+
*
|
|
33
|
+
* @see https://github.com/ZoloAi/zTheme/blob/main/Manual/ztheme-spinners.html
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
// Layer 2: Utilities
|
|
37
|
+
import { applySpinnerSize } from '../../../zSys/theme/size_utils.js';
|
|
38
|
+
|
|
39
|
+
// Layer 3: Primitives
|
|
40
|
+
import { createDiv, createSpan } from '../primitives/generic_containers.js';
|
|
41
|
+
|
|
42
|
+
// Constants
|
|
43
|
+
import { TIMEOUTS, COLORS } from '../../../L1_Foundation/constants/bifrost_constants.js';
|
|
44
|
+
|
|
45
|
+
export default class SpinnerRenderer {
|
|
46
|
+
/**
|
|
47
|
+
* @param {Object} logger - Logger instance
|
|
48
|
+
*/
|
|
49
|
+
constructor(logger) {
|
|
50
|
+
this.logger = logger;
|
|
51
|
+
this._activeSpinners = new Map(); // Track active spinners by spinnerId
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Start a spinner (spinner_start event)
|
|
56
|
+
* @param {Object} event - Spinner start event
|
|
57
|
+
* @param {string} event.spinnerId - Unique spinner ID
|
|
58
|
+
* @param {string} event.label - Spinner label text
|
|
59
|
+
* @param {string} event.style - Spinner style (dots, line, arc, etc.)
|
|
60
|
+
* @param {string} event.container - Target container selector
|
|
61
|
+
* @param {string} [event.size='md'] - Spinner size (sm, md, lg)
|
|
62
|
+
* @param {string} [event.color='primary'] - Spinner color
|
|
63
|
+
* @returns {HTMLElement} Spinner container element
|
|
64
|
+
*/
|
|
65
|
+
start(event) {
|
|
66
|
+
const {
|
|
67
|
+
spinnerId,
|
|
68
|
+
label = 'Loading...',
|
|
69
|
+
style = 'dots',
|
|
70
|
+
container = '#app',
|
|
71
|
+
size = 'md',
|
|
72
|
+
color = 'primary'
|
|
73
|
+
} = event;
|
|
74
|
+
|
|
75
|
+
this.logger.log('[SpinnerRenderer] Starting spinner:', { spinnerId, label, style });
|
|
76
|
+
|
|
77
|
+
// Create spinner structure using primitives
|
|
78
|
+
const spinnerContainer = this._createSpinnerContainer(spinnerId, label, size, color);
|
|
79
|
+
|
|
80
|
+
// Find target container
|
|
81
|
+
const targetElement = document.querySelector(container) || document.body;
|
|
82
|
+
targetElement.appendChild(spinnerContainer);
|
|
83
|
+
|
|
84
|
+
// Track active spinner
|
|
85
|
+
this._activeSpinners.set(spinnerId, {
|
|
86
|
+
element: spinnerContainer,
|
|
87
|
+
label,
|
|
88
|
+
startTime: Date.now()
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
this.logger.log('[SpinnerRenderer] Spinner started successfully');
|
|
92
|
+
return spinnerContainer;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Stop a spinner (spinner_stop event)
|
|
97
|
+
* @param {Object} event - Spinner stop event
|
|
98
|
+
* @param {string} event.spinnerId - Unique spinner ID
|
|
99
|
+
* @param {boolean} [event.success=true] - Whether operation succeeded
|
|
100
|
+
* @param {string} [event.message] - Optional completion message
|
|
101
|
+
*/
|
|
102
|
+
stop(event) {
|
|
103
|
+
const { spinnerId, success = true, message } = event;
|
|
104
|
+
|
|
105
|
+
this.logger.log('[SpinnerRenderer] Stopping spinner:', { spinnerId, success });
|
|
106
|
+
|
|
107
|
+
const spinnerData = this._activeSpinners.get(spinnerId);
|
|
108
|
+
if (!spinnerData) {
|
|
109
|
+
this.logger.warn('[SpinnerRenderer] Spinner not found:', spinnerId);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const { element, label, startTime } = spinnerData;
|
|
114
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
115
|
+
|
|
116
|
+
// Replace spinner with completion message
|
|
117
|
+
this._replaceWithCompletion(element, label, success, message, duration);
|
|
118
|
+
|
|
119
|
+
// Remove from active spinners
|
|
120
|
+
this._activeSpinners.delete(spinnerId);
|
|
121
|
+
|
|
122
|
+
this.logger.log('[SpinnerRenderer] Spinner stopped successfully');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Create spinner container using primitives
|
|
127
|
+
* @private
|
|
128
|
+
*/
|
|
129
|
+
_createSpinnerContainer(spinnerId, label, size, color) {
|
|
130
|
+
// Main container (using primitive)
|
|
131
|
+
const container = createDiv({
|
|
132
|
+
id: spinnerId,
|
|
133
|
+
class: 'zSpinner-container zD-flex zFlex-items-center zGap-2 zMy-2'
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Determine spinner classes
|
|
137
|
+
// zTheme has .zSpinner-border-sm for small, but no -md or -lg
|
|
138
|
+
// TODO: Add .zSpinner-border-md and .zSpinner-border-lg to zTheme
|
|
139
|
+
// See: https://github.com/ZoloAi/zTheme/blob/main/Manual/ztheme-spinners.html
|
|
140
|
+
let spinnerClass = 'zSpinner-border';
|
|
141
|
+
if (size === 'sm') {
|
|
142
|
+
spinnerClass = 'zSpinner-border-sm'; // Use zTheme's built-in small size
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Spinner element (using primitive + zTheme classes)
|
|
146
|
+
const spinner = createDiv({
|
|
147
|
+
class: `${spinnerClass} zText-${color}`,
|
|
148
|
+
role: 'status'
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// For md/lg sizes, apply inline styles using size_utils
|
|
152
|
+
// TODO: Remove this once zTheme has full size scale (.zSpinner-border-md, .zSpinner-border-lg)
|
|
153
|
+
if (size === 'md' || size === 'lg') {
|
|
154
|
+
applySpinnerSize(spinner, size);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Accessibility label (using primitive)
|
|
158
|
+
const srOnly = createSpan({
|
|
159
|
+
class: 'zVisually-hidden'
|
|
160
|
+
});
|
|
161
|
+
srOnly.textContent = 'Loading...';
|
|
162
|
+
spinner.appendChild(srOnly);
|
|
163
|
+
|
|
164
|
+
// Label text (using primitive)
|
|
165
|
+
const labelSpan = createSpan({
|
|
166
|
+
class: 'zSpinner-label zText-muted'
|
|
167
|
+
});
|
|
168
|
+
labelSpan.textContent = label;
|
|
169
|
+
|
|
170
|
+
// Assemble spinner
|
|
171
|
+
container.appendChild(spinner);
|
|
172
|
+
container.appendChild(labelSpan);
|
|
173
|
+
|
|
174
|
+
return container;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Replace spinner with completion message
|
|
179
|
+
* @private
|
|
180
|
+
*/
|
|
181
|
+
_replaceWithCompletion(element, label, success, message, duration) {
|
|
182
|
+
// Clear spinner content
|
|
183
|
+
element.innerHTML = '';
|
|
184
|
+
|
|
185
|
+
// Create completion icon (checkmark or X)
|
|
186
|
+
const icon = document.createElement('i');
|
|
187
|
+
icon.className = success
|
|
188
|
+
? 'bi bi-check-circle-fill zText-success'
|
|
189
|
+
: 'bi bi-x-circle-fill zText-danger';
|
|
190
|
+
icon.style.fontSize = '1.2rem';
|
|
191
|
+
|
|
192
|
+
// Create completion message (using primitive)
|
|
193
|
+
const messageSpan = createSpan({
|
|
194
|
+
class: success ? 'zText-success' : 'zText-danger'
|
|
195
|
+
});
|
|
196
|
+
messageSpan.textContent = message || `${label} ${success ? '[ok]' : ''}`;
|
|
197
|
+
|
|
198
|
+
// Create duration badge (using primitive)
|
|
199
|
+
const durationBadge = createSpan({
|
|
200
|
+
class: 'zBadge zBadge-secondary zMs-2'
|
|
201
|
+
});
|
|
202
|
+
durationBadge.textContent = `${duration}s`;
|
|
203
|
+
|
|
204
|
+
// Assemble completion message
|
|
205
|
+
element.appendChild(icon);
|
|
206
|
+
element.appendChild(document.createTextNode(' '));
|
|
207
|
+
element.appendChild(messageSpan);
|
|
208
|
+
element.appendChild(durationBadge);
|
|
209
|
+
|
|
210
|
+
// Remove classes
|
|
211
|
+
element.className = 'zSpinner-complete zD-flex zFlex-items-center zGap-2 zMy-2';
|
|
212
|
+
|
|
213
|
+
// Auto-remove after configured timeout (optional)
|
|
214
|
+
setTimeout(() => {
|
|
215
|
+
if (element.parentNode) {
|
|
216
|
+
element.style.transition = 'opacity 0.3s';
|
|
217
|
+
element.style.opacity = '0';
|
|
218
|
+
setTimeout(() => {
|
|
219
|
+
if (element.parentNode) {
|
|
220
|
+
element.parentNode.removeChild(element);
|
|
221
|
+
}
|
|
222
|
+
}, TIMEOUTS.FADE_TRANSITION);
|
|
223
|
+
}
|
|
224
|
+
}, TIMEOUTS.AUTO_REMOVE_SPINNER);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Stop all active spinners (cleanup utility)
|
|
229
|
+
* @param {boolean} [success=true] - Whether operations succeeded
|
|
230
|
+
*/
|
|
231
|
+
stopAll(success = true) {
|
|
232
|
+
this.logger.log('[SpinnerRenderer] Stopping all spinners');
|
|
233
|
+
|
|
234
|
+
for (const [spinnerId, _data] of this._activeSpinners) {
|
|
235
|
+
this.stop({ spinnerId, success });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get active spinner count
|
|
241
|
+
* @returns {number} Number of active spinners
|
|
242
|
+
*/
|
|
243
|
+
getActiveCount() {
|
|
244
|
+
return this._activeSpinners.size;
|
|
245
|
+
}
|
|
246
|
+
}
|