@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,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
|
+
}
|