@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.
Files changed (140) hide show
  1. package/L1_Foundation/L1_Foundation.js +13 -0
  2. package/L1_Foundation/bootstrap/bootstrap.js +11 -0
  3. package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
  4. package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
  5. package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
  6. package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
  7. package/L1_Foundation/bootstrap/module_registry.js +102 -0
  8. package/L1_Foundation/bootstrap/prism_loader.js +164 -0
  9. package/L1_Foundation/config/client_config.js +110 -0
  10. package/L1_Foundation/config/config.js +7 -0
  11. package/L1_Foundation/connection/connection.js +8 -0
  12. package/L1_Foundation/connection/websocket_connection.js +122 -0
  13. package/L1_Foundation/constants/bifrost_constants.js +284 -0
  14. package/L1_Foundation/constants/constants.js +7 -0
  15. package/L1_Foundation/logger/logger.js +10 -0
  16. package/L2_Handling/L2_Handling.js +15 -0
  17. package/L2_Handling/cache/cache.js +22 -0
  18. package/L2_Handling/cache/cache_constants.js +69 -0
  19. package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
  20. package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
  21. package/L2_Handling/cache/orchestration/orchestration.js +12 -0
  22. package/L2_Handling/cache/storage/session_manager.js +289 -0
  23. package/L2_Handling/cache/storage/storage.js +10 -0
  24. package/L2_Handling/cache/storage/storage_manager.js +590 -0
  25. package/L2_Handling/display/composite/composite.js +13 -0
  26. package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
  27. package/L2_Handling/display/composite/swiper_renderer.js +564 -0
  28. package/L2_Handling/display/composite/terminal_renderer.js +922 -0
  29. package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
  30. package/L2_Handling/display/display.js +30 -0
  31. package/L2_Handling/display/feedback/feedback.js +11 -0
  32. package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
  33. package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
  34. package/L2_Handling/display/inputs/button_renderer.js +634 -0
  35. package/L2_Handling/display/inputs/form_renderer.js +583 -0
  36. package/L2_Handling/display/inputs/input_renderer.js +658 -0
  37. package/L2_Handling/display/inputs/inputs.js +12 -0
  38. package/L2_Handling/display/navigation/menu_renderer.js +206 -0
  39. package/L2_Handling/display/navigation/navigation.js +11 -0
  40. package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
  41. package/L2_Handling/display/orchestration/orchestration.js +11 -0
  42. package/L2_Handling/display/orchestration/renderer.js +430 -0
  43. package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
  44. package/L2_Handling/display/outputs/alert_renderer.js +161 -0
  45. package/L2_Handling/display/outputs/audio_renderer.js +94 -0
  46. package/L2_Handling/display/outputs/card_renderer.js +229 -0
  47. package/L2_Handling/display/outputs/code_renderer.js +66 -0
  48. package/L2_Handling/display/outputs/dl_renderer.js +131 -0
  49. package/L2_Handling/display/outputs/header_renderer.js +162 -0
  50. package/L2_Handling/display/outputs/icon_renderer.js +107 -0
  51. package/L2_Handling/display/outputs/image_renderer.js +145 -0
  52. package/L2_Handling/display/outputs/list_renderer.js +190 -0
  53. package/L2_Handling/display/outputs/outputs.js +19 -0
  54. package/L2_Handling/display/outputs/table_renderer.js +765 -0
  55. package/L2_Handling/display/outputs/text_renderer.js +818 -0
  56. package/L2_Handling/display/outputs/typography_renderer.js +293 -0
  57. package/L2_Handling/display/outputs/video_renderer.js +116 -0
  58. package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
  59. package/L2_Handling/display/primitives/form_primitives.js +526 -0
  60. package/L2_Handling/display/primitives/generic_containers.js +109 -0
  61. package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
  62. package/L2_Handling/display/primitives/link_primitives.js +552 -0
  63. package/L2_Handling/display/primitives/lists_primitives.js +262 -0
  64. package/L2_Handling/display/primitives/media_primitives.js +383 -0
  65. package/L2_Handling/display/primitives/primitives.js +19 -0
  66. package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
  67. package/L2_Handling/display/primitives/table_primitives.js +528 -0
  68. package/L2_Handling/display/primitives/typography_primitives.js +175 -0
  69. package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
  70. package/L2_Handling/display/specialized/specialized.js +10 -0
  71. package/L2_Handling/hooks/hooks.js +9 -0
  72. package/L2_Handling/hooks/menu_integration.js +57 -0
  73. package/L2_Handling/hooks/widget_hook_manager.js +292 -0
  74. package/L2_Handling/message/message.js +8 -0
  75. package/L2_Handling/message/message_handler.js +701 -0
  76. package/L2_Handling/navigation/navigation.js +8 -0
  77. package/L2_Handling/navigation/navigation_manager.js +403 -0
  78. package/L2_Handling/zhooks/features/cache_live.js +287 -0
  79. package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
  80. package/L2_Handling/zhooks/zhooks_manager.js +65 -0
  81. package/L2_Handling/zvaf/zvaf.js +8 -0
  82. package/L2_Handling/zvaf/zvaf_manager.js +334 -0
  83. package/L3_Abstraction/L3_Abstraction.js +12 -0
  84. package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
  85. package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
  86. package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
  87. package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
  88. package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
  89. package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
  90. package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
  91. package/L3_Abstraction/renderer/renderer.js +1 -0
  92. package/L3_Abstraction/session/session.js +1 -0
  93. package/L4_Orchestration/L4_Orchestration.js +11 -0
  94. package/L4_Orchestration/client/client.js +1 -0
  95. package/L4_Orchestration/facade/facade.js +9 -0
  96. package/L4_Orchestration/facade/manager_registry.js +118 -0
  97. package/L4_Orchestration/facade/renderer_registry.js +274 -0
  98. package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
  99. package/L4_Orchestration/lifecycle/initializer.js +135 -0
  100. package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
  101. package/L4_Orchestration/rendering/facade.js +94 -0
  102. package/L4_Orchestration/rendering/rendering.js +7 -0
  103. package/LICENSE +21 -0
  104. package/README.md +82 -0
  105. package/bifrost_client.js +204 -0
  106. package/bifrost_core.js +1686 -0
  107. package/docs/ARCHITECTURE.md +111 -0
  108. package/docs/PROTOCOL.md +106 -0
  109. package/docs/RENDERERS.md +101 -0
  110. package/docs/SECURITY.md +92 -0
  111. package/package.json +24 -0
  112. package/syntax/prism-zconfig.js +41 -0
  113. package/syntax/prism-zenv.js +69 -0
  114. package/syntax/prism-zolo-theme.css +288 -0
  115. package/syntax/prism-zolo.js +380 -0
  116. package/syntax/prism-zschema.js +38 -0
  117. package/syntax/prism-zspark.js +25 -0
  118. package/syntax/prism-zui.js +68 -0
  119. package/zSys/accessibility/accessibility.js +10 -0
  120. package/zSys/accessibility/emoji_accessibility.js +173 -0
  121. package/zSys/dom/block_utils.js +122 -0
  122. package/zSys/dom/container_utils.js +370 -0
  123. package/zSys/dom/dom.js +13 -0
  124. package/zSys/dom/dom_utils.js +328 -0
  125. package/zSys/dom/encoding_utils.js +117 -0
  126. package/zSys/dom/style_utils.js +71 -0
  127. package/zSys/errors/error_display.js +299 -0
  128. package/zSys/errors/errors.js +10 -0
  129. package/zSys/theme/color_utils.js +274 -0
  130. package/zSys/theme/dark_mode_utils.js +272 -0
  131. package/zSys/theme/size_utils.js +256 -0
  132. package/zSys/theme/spacing_utils.js +405 -0
  133. package/zSys/theme/theme.js +14 -0
  134. package/zSys/theme/zbase.css +1735 -0
  135. package/zSys/theme/zbase_inject.js +161 -0
  136. package/zSys/theme/ztheme_utils.js +305 -0
  137. package/zSys/validation/error_boundary.js +201 -0
  138. package/zSys/validation/validation.js +11 -0
  139. package/zSys/validation/validation_utils.js +238 -0
  140. package/zSys/zSys.js +14 -0
@@ -0,0 +1,634 @@
1
+ /**
2
+ *
3
+ * Button Renderer - Interactive Button Input Events
4
+ *
5
+ *
6
+ * Terminal-First Design (Refactored Micro-Step 8):
7
+ * - Backend sends semantic color (danger, success, warning, etc.)
8
+ * - Terminal displays colored prompts matching semantic meaning
9
+ * - Bifrost renders buttons using zTheme button variants (.zBtn-primary, etc.)
10
+ *
11
+ * Renders button input events from zCLI backend. Creates interactive
12
+ * button elements with zTheme button component classes and WebSocket
13
+ * response handling.
14
+ *
15
+ * @module rendering/button_renderer
16
+ * @layer 3
17
+ * @pattern Strategy (single event type)
18
+ *
19
+ * Dependencies:
20
+ * - Layer 0: primitives/interactive_primitives.js (createButton)
21
+ * - Layer 2: dom_utils.js (createElement, replaceElement)
22
+ * - zTheme: Button component classes (.zBtn, .zBtn-primary, etc.)
23
+ *
24
+ * Exports:
25
+ * - ButtonRenderer: Class for rendering button events
26
+ *
27
+ * Example:
28
+ * ```javascript
29
+ * import ButtonRenderer from './button_renderer.js';
30
+ *
31
+ * const renderer = new ButtonRenderer(logger, client);
32
+ * renderer.render({
33
+ * label: 'Submit',
34
+ * action: 'process_form',
35
+ * color: 'primary',
36
+ * requestId: '123'
37
+ * }, 'zVaF');
38
+ * ```
39
+ */
40
+
41
+ // ─────────────────────────────────────────────────────────────────
42
+ // Imports
43
+ // ─────────────────────────────────────────────────────────────────
44
+
45
+ // Layer 0: Primitives
46
+ import { createButton } from '../primitives/interactive_primitives.js';
47
+ import { convertStyleToString } from '../../../zSys/dom/style_utils.js';
48
+
49
+ //
50
+ // Main Implementation
51
+ //
52
+
53
+ /**
54
+ * Renders button input events for zDisplay
55
+ *
56
+ * Handles the 'button' event type from zCLI backend, creating
57
+ * interactive button elements with zTheme styling and WebSocket
58
+ * response handling.
59
+ *
60
+ * @class
61
+ */
62
+ export default class ButtonRenderer {
63
+ /**
64
+ * Create a button renderer
65
+ * @param {Object} logger - Logger instance for debugging
66
+ * @param {Object} client - BifrostClient instance for sending responses
67
+ */
68
+ constructor(logger, client = null) {
69
+ if (!logger) {
70
+ throw new Error('[ButtonRenderer] logger is required');
71
+ }
72
+
73
+ this.logger = logger;
74
+ this.client = client;
75
+ this.defaultZone = 'zVaF-content';
76
+ }
77
+
78
+ /**
79
+ * Render a button input request
80
+ *
81
+ * Terminal-First Design:
82
+ * - Backend sends semantic color (danger, success, warning, primary, info, secondary)
83
+ * - Bifrost renders button with matching zTheme color class
84
+ *
85
+ * @param {Object} data - Button configuration
86
+ * @param {string} data.label - Button label text (or 'prompt')
87
+ * @param {string} data.action - Action identifier (or '#' for placeholder)
88
+ * @param {string} [data.color='primary'] - Button semantic color
89
+ * - primary: Default action (blue)
90
+ * - danger: Destructive action (red)
91
+ * - success: Positive action (green)
92
+ * - warning: Cautious action (yellow)
93
+ * - info: Informational (cyan)
94
+ * - secondary: Neutral (gray)
95
+ * @param {string} [data.zIcon] - Optional icon name (e.g., "bi-backspace") - renders instead of label
96
+ * @param {string} data.requestId - Request ID for response correlation
97
+ * @param {string} zone - Target DOM element ID
98
+ * @returns {HTMLElement|null} Created button container, or null if zone not found
99
+ */
100
+ render(data, zone) {
101
+ // Validate inputs
102
+ if (!data) {
103
+ this.logger.error('[ButtonRenderer] data is required');
104
+ return null;
105
+ }
106
+
107
+ // Extract data (support both direct and nested formats)
108
+ const requestId = data.requestId || data.data?.requestId;
109
+ const label = data.label || data.prompt || data.data?.prompt || 'Click Me';
110
+ let action = data.action || data.data?.action || null;
111
+
112
+ // zDelegate dict action — first-class dual-mode "internal rewiring" verb.
113
+ // {zDelegate: "$Block.Sub"} (or {zDelegate: {target: …}}) means: on click,
114
+ // run the target in place (routeless, AJAX-like). Two flavours, by target shape:
115
+ // • DOTTED ($Block.Section) → render the nested section IN PLACE within this
116
+ // carrier's parent key container (the way descending into a sub-block feels
117
+ // in CLI). Routed to client.zDelegateInline — no panel swap, no crumb push.
118
+ // • SINGLE ($Block) → routeless panel swap via the existing zDelta click path
119
+ // (used by menu options + Back affordances).
120
+ let delegateInline = null;
121
+ if (action && typeof action === 'object') {
122
+ if (action.zDelegate !== undefined) {
123
+ const spec = action.zDelegate;
124
+ const target = (spec && typeof spec === 'object')
125
+ ? (spec.target || spec.to || spec.zDelta)
126
+ : spec;
127
+ const norm = (typeof target === 'string' && target)
128
+ ? (target.startsWith('$') ? target : `$${target.replace(/^[%^~]/, '')}`)
129
+ : null;
130
+ if (norm && norm.replace(/^\$/, '').includes('.')) {
131
+ delegateInline = norm;
132
+ action = null;
133
+ this.logger.log('[ButtonRenderer] zDelegate (inline, dotted) →', delegateInline);
134
+ } else {
135
+ action = norm;
136
+ this.logger.log('[ButtonRenderer] zDelegate → routeless delta to:', action);
137
+ }
138
+ } else {
139
+ action = null; // unknown object action — ignore rather than crash
140
+ }
141
+ }
142
+ const rawColor = data.color || data.data?.color || 'primary';
143
+ const color = rawColor.toLowerCase(); // Normalize to lowercase for consistency
144
+ const type = data.type || data.data?.type || 'button'; // Default to 'button' for safety
145
+ const rawZIcon = data.zIcon || data.data?.zIcon || null;
146
+ // Normalize: zIcon may arrive as {name:'bi-...'} or {zDisplay:{name:'bi-...'}} if Python pre-processed YAML
147
+ const zIcon = typeof rawZIcon === 'string' ? rawZIcon
148
+ : (rawZIcon && typeof rawZIcon === 'object')
149
+ ? (rawZIcon.name || rawZIcon.content || rawZIcon.zDisplay?.name || null)
150
+ : null;
151
+
152
+ this.logger.log('[ButtonRenderer] Rendering button:', { requestId, label, action, color, type, zIcon });
153
+
154
+ // Create button primitive (just the button, no container)
155
+ const button = this._createButton(label, color, data._zClass, data._id, type, zIcon);
156
+ const customStyle = data._zStyle || data.data?._zStyle || null;
157
+ if (customStyle) {
158
+ const cssString = convertStyleToString(customStyle, this.logger);
159
+ if (cssString) {
160
+ button.style.cssText = cssString;
161
+ }
162
+ }
163
+ if (delegateInline) {
164
+ // Inline delegate: render the dotted target section within this carrier's
165
+ // parent key container, with a Back affordance — no route change, no panel
166
+ // swap. The carrier itself stays a normal button (no wizard-action wiring).
167
+ button.dataset.zdelegateInline = delegateInline;
168
+ button.addEventListener('click', () => {
169
+ const client = this.client || window.bifrostClient;
170
+ if (client?.zDelegateInline) {
171
+ client.zDelegateInline(delegateInline, button);
172
+ } else {
173
+ this.logger.warn('[ButtonRenderer] zDelegateInline unavailable on client');
174
+ }
175
+ });
176
+ } else {
177
+ const renderInline = data._renderInline || data.data?._renderInline || false;
178
+ // zProgress action-property (nested in the zBtn): a live bar that appears
179
+ // ONLY AFTER the click, for the duration of the plugin action. Unlike a
180
+ // zFunc (which auto-runs on render and shows its bar immediately), a button
181
+ // is dormant until pressed — so the bar is wired into the click, not here.
182
+ const progressSpec = data.zProgress ?? data.data?.zProgress ?? null;
183
+ this._attachClickHandler(button, requestId, label, true, type, action, renderInline, progressSpec);
184
+
185
+ // Mark step-key actions (non-plugin, non-placeholder) for wizard restart handling
186
+ if (action && action !== '#' && !action.startsWith('&')) {
187
+ button.dataset.wizardAction = action;
188
+ this.logger.log('[ButtonRenderer] Added wizard-action:', action, 'to button:', label);
189
+ }
190
+ }
191
+
192
+ // _zDelegate on a button → delegate label/value to a target input on click
193
+ // Suppress visual rendering (SSOT: _zDelegate always suppresses output)
194
+ if (data._zDelegate) {
195
+ button.dataset.zdelegate = data._zDelegate;
196
+ button.style.display = 'none'; // Hide but keep in DOM for wiring
197
+ this.logger.log('[ButtonRenderer] Button delegated to:', data._zDelegate, '| action:', action, '| suppressing visual output');
198
+ }
199
+
200
+ // NO cancel button in Bifrost! (Terminal-first: y/n, GUI: click or ignore)
201
+ // In terminal, button is y/n prompt. In GUI, button is click or don't click.
202
+ // We're asynchronous - user can just ignore the button.
203
+
204
+ // If zone is provided, append to DOM (legacy behavior for direct calls)
205
+ // If no zone, just return element (orchestrator pattern)
206
+ if (zone) {
207
+ const targetZone = zone || data.target || this.defaultZone;
208
+ const container = document.getElementById(targetZone);
209
+
210
+ if (!container) {
211
+ this.logger.error(`[ButtonRenderer] Zone not found: ${targetZone}`);
212
+ return button; // Still return element even if zone not found
213
+ }
214
+
215
+ // Add to page
216
+ container.appendChild(button);
217
+ this.logger.log('[ButtonRenderer] Button rendered and appended to zone');
218
+ }
219
+
220
+ this.logger.log('[ButtonRenderer] Button rendered successfully');
221
+ return button;
222
+ }
223
+
224
+ /**
225
+ * Create a single button element from primitives + zTheme button variants
226
+ *
227
+ * Architecture:
228
+ * - Layer 0.0: createButton() - Raw <button> element
229
+ * - Layer 3 (here): Apply zTheme button variant classes (.zBtn-primary, etc.)
230
+ *
231
+ * Terminal-First Design:
232
+ * - Uses semantic color from backend (matches terminal prompt color)
233
+ * - Maps to zTheme button variant classes for consistent styling
234
+ *
235
+ * @private
236
+ * @param {string} label - Button text
237
+ * @param {string} color - Button semantic color (primary, danger, success, warning, info, secondary)
238
+ * @param {string} [customClass] - Optional custom classes for layout (_zClass from YAML)
239
+ * @param {string} [customId] - Optional custom id for targeting (_id from YAML)
240
+ * @param {string} [type='button'] - Button type (button, submit, reset)
241
+ * @param {string} [zIcon] - Optional icon name (e.g., "bi-backspace") - renders instead of label
242
+ * @returns {HTMLElement} Button element
243
+ */
244
+ _createButton(label, color, customClass, customId, type = 'button', zIcon = null) {
245
+ this.logger.log(`[ButtonRenderer] Creating button "${label}" | color: ${color} | type: ${type} | id: ${customId || 'auto'} | icon: ${zIcon || 'none'}`);
246
+
247
+ // Layer 0.0: Create raw button primitive with attributes
248
+ const attrs = { class: 'zBtn' }; // Base button styling only (padding, border, etc.)
249
+ if (customId) {
250
+ attrs.id = customId;
251
+ } // Pass _id to primitive
252
+ const button = createButton(type, attrs);
253
+
254
+ // Render the button face.
255
+ // - Legacy discrete `zIcon` field (menus / standalone icon events): icon only.
256
+ // - Otherwise the `label` is icon-aware (SSOT, mirrors the server/zCLI): any
257
+ // `bi-*` token becomes a <i> glyph, every other token is literal text, order
258
+ // preserved — so `bi-gear`, `bi-gear Settings`, and `bi-x bi-y Done` all work.
259
+ if (zIcon) {
260
+ const iconName = zIcon.replace(/^bi-/, ''); // Strip 'bi-' prefix if present
261
+ const icon = document.createElement('i');
262
+ icon.className = `bi bi-${iconName}`;
263
+ button.appendChild(icon);
264
+ this.logger.log(`[ButtonRenderer] Rendered icon: bi-${iconName}`);
265
+ } else {
266
+ this._renderLabel(button, label);
267
+ }
268
+
269
+ // Apply semantic button variant classes (zTheme button components)
270
+ // Map zCLI semantic colors to zTheme button variant classes (.zBtn-primary, etc.)
271
+ const colorMap = {
272
+ 'primary': 'zBtn-primary', // Green (zCLI brand)
273
+ 'danger': 'zBtn-danger', // Red (destructive action)
274
+ 'success': 'zBtn-success', // Green (positive action)
275
+ 'warning': 'zBtn-warning', // Orange (cautious action)
276
+ 'info': 'zBtn-info', // Blue (informational)
277
+ 'secondary': 'zBtn-secondary' // Purple (secondary action)
278
+ };
279
+
280
+ const btnClass = colorMap[color] || 'zBtn-primary';
281
+ button.classList.add(btnClass);
282
+ this.logger.log(`[ButtonRenderer] Applied button variant class: ${btnClass}`);
283
+
284
+ // Apply custom classes if provided (_zClass from YAML - for layout/spacing)
285
+ if (customClass) {
286
+ button.className += ` ${customClass}`;
287
+ this.logger.log(`[ButtonRenderer] Applied custom classes: ${customClass}`);
288
+ }
289
+
290
+ if (customId) {
291
+ this.logger.log(`[ButtonRenderer] Applied custom id: ${customId}`);
292
+ }
293
+
294
+ // Proper composition: Primitive + zTheme Button Variant = Styled Button
295
+ // Uses semantic button classes (.zBtn-primary) per zTheme conventions
296
+
297
+ return button;
298
+ }
299
+
300
+ /**
301
+ * Render an icon-aware label into a button (SSOT contract, mirrors zCLI).
302
+ *
303
+ * The label is split on whitespace; each `bi-*` token becomes a Bootstrap
304
+ * `<i>` glyph and every other token is rendered as literal text, in author
305
+ * order. This natively covers icon-only (`bi-gear`), icon + text
306
+ * (`bi-gear Settings`) and multi-icon (`bi-x bi-y Done`) labels. When the
307
+ * label is icon-only, an `aria-label` is derived from the icon name(s) so the
308
+ * button still has an accessible name.
309
+ *
310
+ * @private
311
+ * @param {HTMLElement} button - Target button element
312
+ * @param {string} label - Icon-aware label string
313
+ */
314
+ _renderLabel(button, label) {
315
+ const raw = (label === null || label === undefined) ? '' : String(label);
316
+ const tokens = raw.split(/\s+/).filter(Boolean);
317
+ const iconRe = /^bi-[a-z0-9]+(?:-[a-z0-9]+)*$/;
318
+
319
+ // No icon tokens → plain text label (fast path, preserves original spacing).
320
+ if (!tokens.some((t) => iconRe.test(t))) {
321
+ button.textContent = raw;
322
+ return;
323
+ }
324
+
325
+ const iconNames = [];
326
+ const words = [];
327
+ tokens.forEach((tok) => {
328
+ if (button.childNodes.length) {
329
+ button.appendChild(document.createTextNode(' '));
330
+ }
331
+ if (iconRe.test(tok)) {
332
+ const icon = document.createElement('i');
333
+ icon.className = `bi ${tok}`;
334
+ button.appendChild(icon);
335
+ iconNames.push(tok.replace(/^bi-/, '').replace(/-/g, ' '));
336
+ } else {
337
+ button.appendChild(document.createTextNode(tok));
338
+ words.push(tok);
339
+ }
340
+ });
341
+
342
+ // Accessible name: visible text wins; otherwise spell out the icon(s).
343
+ if (!words.length && iconNames.length) {
344
+ button.setAttribute('aria-label', iconNames.join(', '));
345
+ }
346
+ this.logger.log(`[ButtonRenderer] Rendered icon-aware label: icons=[${iconNames.join('|')}] text="${words.join(' ')}"`);
347
+ }
348
+
349
+ /**
350
+ * Attach click handler to button
351
+ * @private
352
+ * @param {HTMLElement} button - Button element
353
+ * @param {string} requestId - Request ID for response
354
+ * @param {string} originalLabel - Original button label
355
+ * @param {boolean} value - Response value (true for primary, false for cancel)
356
+ * @param {string} type - Button type (button, submit, reset)
357
+ * @param {string} action - Optional action string (e.g., "&plugin.func(zHat[0])")
358
+ */
359
+ _attachClickHandler(button, requestId, originalLabel, value, type = 'button', action = null, renderInline = false, progressSpec = null) {
360
+ button.addEventListener('click', (event) => {
361
+ this.logger.log(`[ButtonRenderer] Button clicked: "${button.textContent}" (type: ${type}, value: ${value}, action: ${action})`);
362
+ this.logger.log(`[ButtonRenderer] Button clicked: ${button.textContent} (type: ${type}, value: ${value}, action: ${action})`);
363
+
364
+ // For submit/reset buttons, let the form handle it naturally
365
+ if (type === 'submit' || type === 'reset') {
366
+ this.logger.log(`[ButtonRenderer] ${type} button - letting form handle submission`);
367
+ this.logger.log(`[ButtonRenderer] ${type} button - letting form handle submission`);
368
+ // Don't preventDefault, don't send WebSocket response
369
+ // The form's submit event will fire and can be handled separately
370
+ return;
371
+ }
372
+
373
+ // zLink action — auto-redirect, mirrors CLI semantics
374
+ if (action && action.startsWith('zLink(')) {
375
+ const path = action.slice(6, -1).trim();
376
+ this.logger.log(`[ButtonRenderer] zLink action — navigating to: ${path}`);
377
+ const client = this.client || window.bifrostClient;
378
+ if (client?.zLink) {
379
+ const originKey = client.navOriginKey ? client.navOriginKey(button) : null;
380
+ client.zLink(path, originKey);
381
+ }
382
+ return;
383
+ }
384
+
385
+ // zBack action — navigate back one block, mirrors CLI semantics
386
+ if (action === 'zBack') {
387
+ this.logger.log('[ButtonRenderer] zBack action — navigating back');
388
+ const client = this.client || window.bifrostClient;
389
+ if (client?.zBack) {
390
+ client.zBack();
391
+ }
392
+ return;
393
+ }
394
+
395
+ // zDelta action — intra-file block hop, mirrors CLI semantics
396
+ if (action && (action.startsWith('zDelta(') || action.startsWith('$'))) {
397
+ const blockName = action.startsWith('zDelta(')
398
+ ? action.slice(7, -1).replace(/^\$/, '').trim()
399
+ : action.slice(1).trim();
400
+ this.logger.log(`[ButtonRenderer] zDelta action — block hop to: ${blockName}`);
401
+ const client = this.client || window.bifrostClient;
402
+ if (client?.zDelta) {
403
+ const originKey = client.navOriginKey ? client.navOriginKey(button) : null;
404
+ client.zDelta(blockName, originKey);
405
+ }
406
+ return;
407
+ }
408
+
409
+ // For regular buttons (type="button"), check if it has an action
410
+ if (action && action.startsWith('&')) {
411
+ this.logger.log(`[ButtonRenderer] Button has plugin action: ${action}`);
412
+
413
+ // zProgress on the button → run the plugin through the zFunc transport,
414
+ // which owns the live-bar lifecycle (indeterminate bar + elapsed, cleared
415
+ // on the correlated response). This gives the "bar only after click"
416
+ // behavior for free and keeps the plugin/probe/backend path identical to
417
+ // a zFunc call. Plain buttons (no zProgress) keep the button_action path.
418
+ const client = this.client || window.bifrostClient;
419
+ const orch = client?.zDisplayOrchestrator;
420
+ if (progressSpec && orch && typeof orch._executeZFunc === 'function') {
421
+ this.logger.log('[ButtonRenderer] zProgress button — routing via _executeZFunc');
422
+ button.disabled = true;
423
+ const host = button.parentElement || button;
424
+ orch._executeZFunc(action, host, progressSpec);
425
+ return;
426
+ }
427
+
428
+ // Collect wizard input values (look for sibling inputs in same wizard container)
429
+ const collectedValues = this._collectWizardValues(button);
430
+ this.logger.log(`[ButtonRenderer] Collected wizard values:`, collectedValues);
431
+
432
+ // Send button action event with collected values
433
+ this._sendButtonAction(requestId, action, collectedValues);
434
+ button.disabled = true; // prevent double-fire
435
+ } else if (requestId) {
436
+ // Interactive button awaiting a backend response (zDialog / zWizard).
437
+ // A parked wizard gate carries _renderInline: collect the pre-gate
438
+ // field values, pin the reveal to this button's own container (append),
439
+ // and send them alongside the truthy value. The button stays dumb — it
440
+ // just resolves its requestId; the runtime owns the wizard knowledge.
441
+ if (renderInline) {
442
+ const ctx = this._collectInlineContext(button);
443
+ if (ctx.container) {
444
+ const client = this.client || window.bifrostClient;
445
+ if (client) {
446
+ client._renderTarget = { el: ctx.container, mode: 'append', once: true };
447
+ }
448
+ }
449
+ this._sendResponse(requestId, value, ctx.values);
450
+ } else {
451
+ this.logger.log('[ButtonRenderer] Regular button - sending WebSocket response');
452
+ this._sendResponse(requestId, value);
453
+ }
454
+ button.disabled = true; // prevent double-submission
455
+ } else {
456
+ // Standalone / declarative button: no action and no pending request — a
457
+ // plain showcase button. Nothing to fire; leave it interactive.
458
+ this.logger.log('[ButtonRenderer] Button has no action and no pending request — no-op');
459
+ }
460
+
461
+ // NOTE: never rewrite button.textContent on click. The `[ok]` confirmation
462
+ // is a log signal only — rendering it injected text and wiped icon buttons.
463
+ });
464
+ }
465
+
466
+ /**
467
+ * Send button response to backend
468
+ * @private
469
+ * @param {string} requestId - Request ID
470
+ * @param {boolean} value - Response value
471
+ */
472
+ _sendResponse(requestId, value, values = null) {
473
+ // Try to get connection from client or global window object
474
+ const connection = this.client?.connection || window.bifrostClient?.connection;
475
+
476
+ if (!connection) {
477
+ this.logger.error('[ButtonRenderer] No WebSocket connection available');
478
+ return;
479
+ }
480
+
481
+ try {
482
+ const payload = { event: 'input_response', requestId, value };
483
+ if (values && Object.keys(values).length) {
484
+ payload.values = values;
485
+ }
486
+ connection.send(JSON.stringify(payload));
487
+
488
+ this.logger.log('[ButtonRenderer] Response sent:', payload);
489
+ } catch (error) {
490
+ this.logger.error('[ButtonRenderer] Failed to send response:', error);
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Collect inline-reveal context for a parked wizard gate button.
496
+ *
497
+ * Walks up to the button's container (nearest [data-zkey] ancestor) and
498
+ * harvests the value of every field inside it, keyed by each field's own
499
+ * enclosing [data-zkey] (the wizard step key, e.g. Ask_Name). Generic — the
500
+ * button forwards nearby field values; the runtime decides their meaning.
501
+ *
502
+ * @private
503
+ * @param {HTMLElement} button
504
+ * @returns {{container: HTMLElement|null, values: Object}}
505
+ */
506
+ _collectInlineContext(button) {
507
+ const values = {};
508
+ const fieldSel = 'input[type="text"], input[type="email"], input[type="number"], ' +
509
+ 'input[type="password"], input.zForm-control, textarea, select';
510
+
511
+ // Climb to the WIZARD container: the gate step's own [data-zkey] wraps only
512
+ // the button, so we keep walking up until we hit the ancestor [data-zkey]
513
+ // that actually holds the pre-gate fields (e.g. Types_Demo). That same
514
+ // ancestor is the in-place reveal target — the post-gate step lands beside
515
+ // its siblings, not inside the button.
516
+ let container = null;
517
+ let node = button.parentElement;
518
+ while (node) {
519
+ if (node.getAttribute && node.getAttribute('data-zkey') && node.querySelector(fieldSel)) {
520
+ container = node;
521
+ break;
522
+ }
523
+ node = node.parentElement;
524
+ }
525
+ if (!container) {
526
+ container = button.closest('[data-zkey]') || button.parentElement;
527
+ }
528
+ if (!container) {
529
+ return { container: null, values };
530
+ }
531
+
532
+ const containerKey = container.getAttribute('data-zkey');
533
+ container.querySelectorAll(fieldSel).forEach((field) => {
534
+ const keyEl = field.closest('[data-zkey]');
535
+ const key = keyEl ? keyEl.getAttribute('data-zkey') : (field.id || field.name);
536
+ if (key && key !== containerKey) {
537
+ values[key] = field.value || '';
538
+ }
539
+ });
540
+ this.logger.log('[ButtonRenderer] Inline gate context:', { container: containerKey, values });
541
+ return { container, values };
542
+ }
543
+
544
+ /**
545
+ * Collect wizard input values from sibling inputs
546
+ * Looks for input elements in the same container as the button
547
+ * @private
548
+ * @param {HTMLElement} button - Button element
549
+ * @returns {Array} Array of input values in order
550
+ */
551
+ _collectWizardValues(button) {
552
+ const values = [];
553
+
554
+ // Strategy: Look upward for wizard container markers, then fallback to parent
555
+ // 1. Try data-zwizard attribute (explicit wizard)
556
+ // 2. Try data-zkey containing "zWizard" (orchestrator pattern)
557
+ // 3. Fallback to nearest parent with multiple children
558
+
559
+ let container = button.closest('[data-zwizard]');
560
+
561
+ if (!container) {
562
+ // Try to find parent with data-zkey attribute (orchestrator pattern)
563
+ let current = button.parentElement;
564
+ while (current && !container) {
565
+ if (current.hasAttribute('data-zkey')) {
566
+ container = current;
567
+ break;
568
+ }
569
+ current = current.parentElement;
570
+ }
571
+ }
572
+
573
+ // Fallback to direct parent
574
+ if (!container) {
575
+ container = button.parentElement;
576
+ }
577
+
578
+ if (!container) {
579
+ this.logger.warn('[ButtonRenderer] [WARN] Could not find wizard container');
580
+ return values;
581
+ }
582
+
583
+ this.logger.log('[ButtonRenderer] Searching for inputs in container:', container.getAttribute('data-zkey') || container.id || container.className);
584
+
585
+ // Find all input elements in the same container (look for .zForm-control class from form_primitives)
586
+ const inputs = container.querySelectorAll('input.zForm-control, input[type="text"], input[type="email"], input[type="number"], textarea');
587
+
588
+ this.logger.log('[ButtonRenderer] Found', inputs.length, 'input(s)');
589
+
590
+ inputs.forEach((input, index) => {
591
+ const value = input.value || '';
592
+ this.logger.log(`[ButtonRenderer] Input ${index}: "${value}" (id: ${input.id}, placeholder: ${input.placeholder})`);
593
+ values.push(value);
594
+ });
595
+
596
+ return values;
597
+ }
598
+
599
+ /**
600
+ * Send button action event to backend
601
+ * @private
602
+ * @param {string} requestId - Request ID
603
+ * @param {string} action - Plugin action string (e.g., "&plugin.func(zHat[0])")
604
+ * @param {Array} collectedValues - Collected wizard input values
605
+ */
606
+ _sendButtonAction(requestId, action, collectedValues) {
607
+ // Try to get connection from client or global window object
608
+ const connection = this.client?.connection || window.bifrostClient?.connection;
609
+
610
+ if (!connection) {
611
+ this.logger.error('[ButtonRenderer] No WebSocket connection available');
612
+ this.logger.error('[ButtonRenderer] [ERROR] No WebSocket connection');
613
+ return;
614
+ }
615
+
616
+ try {
617
+ const payload = {
618
+ event: 'button_action',
619
+ requestId: requestId,
620
+ action: action,
621
+ collected_values: collectedValues
622
+ };
623
+
624
+ this.logger.log('[ButtonRenderer] Sending button action:', payload);
625
+ connection.send(JSON.stringify(payload));
626
+
627
+ this.logger.log('[ButtonRenderer] Button action sent:', payload);
628
+ } catch (error) {
629
+ this.logger.error('[ButtonRenderer] Failed to send button action:', error);
630
+ this.logger.error('[ButtonRenderer] [ERROR] Failed to send:', error);
631
+ }
632
+ }
633
+
634
+ }