@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,1759 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZDisplayOrchestrator - Central orchestrator for all declarative rendering
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - YAML → DOM rendering
|
|
6
|
+
* - Progressive chunk rendering
|
|
7
|
+
* - Block-level metadata
|
|
8
|
+
* - Recursive item rendering
|
|
9
|
+
* - zDisplay event routing (delegates to specialized renderers)
|
|
10
|
+
* - Navbar rendering
|
|
11
|
+
*
|
|
12
|
+
* Refactoring History:
|
|
13
|
+
* - Phase 2.1: Extracted from bifrost_client.js
|
|
14
|
+
* - Phase 4.4: Extracted metadata processor and group renderer to L3
|
|
15
|
+
* - Task 2.7 (Pre-NPM): Extracted input event handlers to L3 (1441 LOC → 887 LOC)
|
|
16
|
+
* - Phase 5 (Server-Side Intel): Removed ShorthandExpander — Python pre-expands all shorthands
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { TYPOGRAPHY } from '../../../L1_Foundation/constants/bifrost_constants.js';
|
|
20
|
+
import { WizardGateHandler } from '../../../L3_Abstraction/orchestrator/wizard_gate_handler.js';
|
|
21
|
+
import { NavBarBuilder } from '../../../L3_Abstraction/orchestrator/navbar_builder.js';
|
|
22
|
+
import { MetadataProcessor } from '../../../L3_Abstraction/orchestrator/metadata_processor.js';
|
|
23
|
+
import { GroupRenderer } from '../../../L3_Abstraction/orchestrator/group_renderer.js';
|
|
24
|
+
import { ContainerUnwrapper } from '../../../L3_Abstraction/orchestrator/container_unwrapper.js';
|
|
25
|
+
import { InputEventHandler } from '../../../L3_Abstraction/orchestrator/input_event_handler.js';
|
|
26
|
+
import { createSemanticElement } from '../primitives/semantic_element_primitive.js';
|
|
27
|
+
import { getAlertColorClass } from '../../../zSys/theme/ztheme_utils.js';
|
|
28
|
+
|
|
29
|
+
export class ZDisplayOrchestrator {
|
|
30
|
+
constructor(client) {
|
|
31
|
+
this.client = client;
|
|
32
|
+
this.logger = client.logger;
|
|
33
|
+
this.options = client.options;
|
|
34
|
+
this.wizardGateHandler = new WizardGateHandler(client, this.logger, this);
|
|
35
|
+
this.navBarBuilder = new NavBarBuilder(client, this.logger);
|
|
36
|
+
this.metadataProcessor = new MetadataProcessor(this.logger);
|
|
37
|
+
this.groupRenderer = new GroupRenderer(client, this.logger, this, this.metadataProcessor);
|
|
38
|
+
this.containerUnwrapper = new ContainerUnwrapper(this.logger);
|
|
39
|
+
this.inputEventHandler = new InputEventHandler(client, this.logger);
|
|
40
|
+
|
|
41
|
+
// Map of zfunc requestId → { inputEl, sendResponse } for in-flight input prompts
|
|
42
|
+
this._pendingZFuncInputs = new Map();
|
|
43
|
+
|
|
44
|
+
// Register WebSocket hooks for zFunc execution
|
|
45
|
+
client.hooks.register('onZFuncInput', (msg) => this._handleZFuncInput(msg));
|
|
46
|
+
client.hooks.register('onZFuncResponse', (msg) => this._handleZFuncResponse(msg));
|
|
47
|
+
|
|
48
|
+
// Map of zfunc requestId → resolve callback (for _executeZFunc promise)
|
|
49
|
+
this._zfuncResolvers = new Map();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Render an entire zVaF block from YAML data
|
|
54
|
+
* @param {Object} blockData - Block configuration from YAML
|
|
55
|
+
*/
|
|
56
|
+
async renderBlock(blockData) {
|
|
57
|
+
// Full block render = full navigation; reset any scoped render-target so the
|
|
58
|
+
// next chunk stream targets the freshly rendered DOM, not a stale pane/host.
|
|
59
|
+
this.client._renderTarget = null;
|
|
60
|
+
// Use stored reference (set by _initZVaFElements)
|
|
61
|
+
const contentElement = this.client._zVaFElement;
|
|
62
|
+
if (!contentElement) {
|
|
63
|
+
throw new Error('zVaF element not initialized');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Clear existing content
|
|
67
|
+
contentElement.innerHTML = '';
|
|
68
|
+
|
|
69
|
+
// Block-level metadata wrapper. SSOT: the _zHTML/_zClass/_zStyle/zId rules
|
|
70
|
+
// live in MetadataProcessor.applyMetadata — the same path zKey containers use —
|
|
71
|
+
// so blocks, keys, and events all read styling identically (no per-tier idiom).
|
|
72
|
+
let blockWrapper = contentElement;
|
|
73
|
+
if (blockData && typeof blockData === 'object' && this.metadataProcessor.hasBlockMetadata(blockData)) {
|
|
74
|
+
const blockMeta = this.metadataProcessor.extractMetadata(blockData);
|
|
75
|
+
const blockName = this.options.zBlock || 'zBlock';
|
|
76
|
+
const blockLevelDiv = createSemanticElement(blockMeta._zHTML || 'div', {}, this.logger);
|
|
77
|
+
this.metadataProcessor.applyMetadata(blockLevelDiv, blockMeta);
|
|
78
|
+
blockLevelDiv.setAttribute('data-zblock', blockName);
|
|
79
|
+
|
|
80
|
+
contentElement.appendChild(blockLevelDiv);
|
|
81
|
+
blockWrapper = blockLevelDiv; // Children render inside the block wrapper
|
|
82
|
+
|
|
83
|
+
this.logger.debug(`[ZDisplayOrchestrator] Created block-level wrapper`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Recursively render all items (await for navigation renderer loading)
|
|
87
|
+
await this.renderItems(blockData, blockWrapper);
|
|
88
|
+
|
|
89
|
+
// NOTE: zCard-body auto-enhancement REMOVED (2026-01-28)
|
|
90
|
+
// Users should explicitly declare _zClass: zCard-body when needed.
|
|
91
|
+
// The renderer should not be "smarter" than the declarative .zolo file.
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* SSOT render-target resolver — decides WHERE the next walker output paints.
|
|
96
|
+
* Returns a descriptor: { el, mode: 'replace'|'append', restoreNodes?, once }.
|
|
97
|
+
*
|
|
98
|
+
* Priority:
|
|
99
|
+
* 1. Explicit client._renderTarget (still attached to the DOM) — set by a
|
|
100
|
+
* producer: zDelegate inline (host + restoreNodes, once) or zDash panel
|
|
101
|
+
* (the exact pane element, persistent).
|
|
102
|
+
* 2. Auto-detect the active zDash pane (legacy #dashboard-panel-content).
|
|
103
|
+
* 3. zVaF root.
|
|
104
|
+
*
|
|
105
|
+
* Keeping the auto-detect as a fallback means a producer that forgets to set
|
|
106
|
+
* the target never regresses to a broken render.
|
|
107
|
+
* @returns {{el: HTMLElement, mode: string, restoreNodes?: Node[], once: boolean}}
|
|
108
|
+
*/
|
|
109
|
+
_resolveRenderTarget() {
|
|
110
|
+
const rt = this.client._renderTarget;
|
|
111
|
+
if (rt && rt.el && document.contains(rt.el)) {
|
|
112
|
+
return { mode: 'replace', once: false, ...rt };
|
|
113
|
+
}
|
|
114
|
+
const pane = document.querySelector('.zDash-panel .zTab-pane.zActive')
|
|
115
|
+
|| document.getElementById('dashboard-panel-content');
|
|
116
|
+
if (pane) return { el: pane, mode: 'replace', once: false };
|
|
117
|
+
return { el: this.client._zVaFElement, mode: 'replace', once: false };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Progressive chunk rendering (Terminal First philosophy)
|
|
122
|
+
* Appends chunks from backend as they arrive, stops at failed gates
|
|
123
|
+
* @param {Object} message - Chunk message from backend
|
|
124
|
+
*/
|
|
125
|
+
async renderChunkProgressive(message) {
|
|
126
|
+
try {
|
|
127
|
+
this.logger.debug('[ZDisplayOrchestrator] renderChunkProgressive called:', message.chunk_num);
|
|
128
|
+
const {chunk_num, keys, data, is_gate, gate_key} = message;
|
|
129
|
+
this._pendingGateKey = (is_gate && gate_key) ? gate_key : null;
|
|
130
|
+
|
|
131
|
+
this.logger.debug(`[ZDisplayOrchestrator] Rendering chunk #${chunk_num}`);
|
|
132
|
+
if (is_gate) {
|
|
133
|
+
this.logger.debug('[ZDisplayOrchestrator] Chunk contains gate');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// SSOT render-target: a single descriptor decides WHERE the chunk paints.
|
|
137
|
+
// Producers set client._renderTarget — zDelegate inline (host + restore,
|
|
138
|
+
// once) or zDash panel (the exact pane, persistent). The resolver prefers
|
|
139
|
+
// it, then auto-detects the active dash pane, then the zVaF root. zDelegate
|
|
140
|
+
// is just the render-target + restore variant of the same primitive.
|
|
141
|
+
const renderTarget = this._resolveRenderTarget();
|
|
142
|
+
const contentDiv = renderTarget.el;
|
|
143
|
+
|
|
144
|
+
if (!contentDiv) {
|
|
145
|
+
throw new Error('zVaF element not initialized. Ensure _initZVaFElements() was called.');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check if data has block-level metadata (_zClass, _zStyle, etc.)
|
|
149
|
+
const hasBlockMetadata = data && Object.keys(data).some(k => k.startsWith('_'));
|
|
150
|
+
|
|
151
|
+
// Determine the target container for rendering
|
|
152
|
+
let targetContainer = contentDiv;
|
|
153
|
+
|
|
154
|
+
// Clear loading state on first chunk (skip when the target is append-mode,
|
|
155
|
+
// e.g. a future accretion surface). Default mode is 'replace'.
|
|
156
|
+
if (chunk_num === 1 && renderTarget.mode !== 'append') {
|
|
157
|
+
contentDiv.innerHTML = '';
|
|
158
|
+
this.logger.debug('[ZDisplayOrchestrator] Cleared loading state');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (hasBlockMetadata && chunk_num === 1) {
|
|
162
|
+
// First chunk with block metadata: create a wrapper for the entire block
|
|
163
|
+
const blockName = message.zBlock || 'progressive'; // Use block name from backend
|
|
164
|
+
|
|
165
|
+
// Block-level metadata wrapper — same SSOT path as renderBlock / zKeys
|
|
166
|
+
// (MetadataProcessor.applyMetadata handles _zHTML/_zClass/_zStyle/zId).
|
|
167
|
+
const blockMeta = this.metadataProcessor.extractMetadata(data);
|
|
168
|
+
const blockWrapper = createSemanticElement(blockMeta._zHTML || 'div', {}, this.logger);
|
|
169
|
+
this.metadataProcessor.applyMetadata(blockWrapper, blockMeta);
|
|
170
|
+
blockWrapper.setAttribute('data-zblock', 'progressive');
|
|
171
|
+
blockWrapper.setAttribute('id', blockName);
|
|
172
|
+
|
|
173
|
+
contentDiv.appendChild(blockWrapper);
|
|
174
|
+
targetContainer = blockWrapper;
|
|
175
|
+
this.logger.debug(`[ZDisplayOrchestrator] Created block wrapper: ${blockName}`);
|
|
176
|
+
} else if (hasBlockMetadata && chunk_num > 1) {
|
|
177
|
+
// Subsequent chunks: find existing block wrapper
|
|
178
|
+
const existingWrapper = contentDiv.querySelector('[data-zblock="progressive"]');
|
|
179
|
+
if (existingWrapper) {
|
|
180
|
+
targetContainer = existingWrapper;
|
|
181
|
+
this.logger.debug(`[ZDisplayOrchestrator] Using existing block wrapper`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Render YAML data using existing rendering pipeline
|
|
186
|
+
// This preserves all styling, forms, zDisplay events, etc.
|
|
187
|
+
if (data && typeof data === 'object') {
|
|
188
|
+
// DEBUG: Log chunk data structure
|
|
189
|
+
this.logger.debug('[ZDisplayOrchestrator] Chunk data keys:', Object.keys(data));
|
|
190
|
+
for (const [key, value] of Object.entries(data)) {
|
|
191
|
+
if (!key.startsWith('_')) {
|
|
192
|
+
this.logger.debug(`[ZDisplayOrchestrator] ${key}:`, typeof value);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Progressive section render: a zOS page is a list of top-level sections,
|
|
196
|
+
// and we know the count from the declarative structure. Paint them one at
|
|
197
|
+
// a time, yielding to the browser between each, and report progress on the
|
|
198
|
+
// badge ("Rendering k/N"). The page visibly paints in — and the user feels
|
|
199
|
+
// it loading, which is the point.
|
|
200
|
+
//
|
|
201
|
+
// Guarded: only the plain "section list" shape is safe to split. Gated
|
|
202
|
+
// wizards (whole-dict gate detection), root-level ~menus (option content
|
|
203
|
+
// lives in sibling keys), and grouped blocks must render holistically — so
|
|
204
|
+
// they keep the exact current path. Single-section chunks aren't worth it.
|
|
205
|
+
const _topKeys = Object.keys(data).filter(k => !k.startsWith('_'));
|
|
206
|
+
const _meta = this.metadataProcessor.extractMetadata(data);
|
|
207
|
+
const _canSplit = _topKeys.length > 1
|
|
208
|
+
&& !this.wizardGateHandler.detectGateStep(data)
|
|
209
|
+
&& !this.groupRenderer.shouldRenderAsGroup(_meta)
|
|
210
|
+
&& !_topKeys.some(k => k.startsWith('~'));
|
|
211
|
+
|
|
212
|
+
if (_canSplit) {
|
|
213
|
+
const _total = _topKeys.length;
|
|
214
|
+
let _done = 0;
|
|
215
|
+
for (const _k of _topKeys) {
|
|
216
|
+
await this.renderItems({ [_k]: data[_k] }, targetContainer);
|
|
217
|
+
_done += 1;
|
|
218
|
+
try { await this.client._updateRenderState({ current: _done, total: _total }); }
|
|
219
|
+
catch (_e) { /* badge is best-effort chrome — never block render */ }
|
|
220
|
+
// Yield so the section paints and the badge updates before the next.
|
|
221
|
+
await new Promise(r => requestAnimationFrame(() => r()));
|
|
222
|
+
}
|
|
223
|
+
// Page painted in full → snap the badge back to the connected state.
|
|
224
|
+
try { await this.client._updateRenderState({ done: true }); } catch (_e) { /* best-effort */ }
|
|
225
|
+
} else {
|
|
226
|
+
await this.renderItems(data, targetContainer);
|
|
227
|
+
}
|
|
228
|
+
this._pendingGateKey = null;
|
|
229
|
+
this.logger.log(`[ZDisplayOrchestrator] Chunk #${chunk_num} rendered from YAML (${keys.length} keys)`);
|
|
230
|
+
|
|
231
|
+
// Initialize conditional rendering for any wizards with if conditions
|
|
232
|
+
this.logger.debug(`[ZDisplayOrchestrator] Checking wizard containers`);
|
|
233
|
+
try {
|
|
234
|
+
await this.client._ensureWizardConditionalRenderer();
|
|
235
|
+
this.logger.debug(`[ZDisplayOrchestrator] WizardConditionalRenderer ensured`);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
this.logger.error(`[ZDisplayOrchestrator] Failed to ensure WizardConditionalRenderer:`, err);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const wizardContainers = targetContainer.querySelectorAll('[data-zkey*="Wizard"], [data-zgroup="input-group"]');
|
|
241
|
+
this.logger.debug(`[ZDisplayOrchestrator] Found ${wizardContainers.length} wizard containers`);
|
|
242
|
+
|
|
243
|
+
if (wizardContainers.length > 0) {
|
|
244
|
+
this.logger.debug(`[ZDisplayOrchestrator] Initializing ${wizardContainers.length} wizard containers`);
|
|
245
|
+
wizardContainers.forEach((container, idx) => {
|
|
246
|
+
const containerId = container.id || container.getAttribute('data-zkey') || container.getAttribute('data-zgroup') || `container-${idx}`;
|
|
247
|
+
this.logger.debug(`[ZDisplayOrchestrator] Initializing wizard: ${containerId}`);
|
|
248
|
+
try {
|
|
249
|
+
this.client.wizardConditionalRenderer.initializeWizard(container);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
this.logger.error(`[ZDisplayOrchestrator] [ERROR] Failed to initialize wizard container ${containerId}:`, err);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
} else {
|
|
255
|
+
const conditionalElements = targetContainer.querySelectorAll('[data-zif]');
|
|
256
|
+
this.logger.debug(`[ZDisplayOrchestrator] No wizards, ${conditionalElements.length} conditional elements`);
|
|
257
|
+
|
|
258
|
+
if (conditionalElements.length > 0) {
|
|
259
|
+
this.logger.debug(`[ZDisplayOrchestrator] Found ${conditionalElements.length} conditional elements`);
|
|
260
|
+
const parentContainers = new Set();
|
|
261
|
+
conditionalElements.forEach((el, idx) => {
|
|
262
|
+
this.logger.debug(`[ZDisplayOrchestrator] Conditional element ${idx + 1}`);
|
|
263
|
+
// Find the closest container with data-zgroup or data-zkey containing "Wizard"
|
|
264
|
+
const parent = el.closest('[data-zgroup], [data-zkey*="Wizard"]');
|
|
265
|
+
if (parent && !parentContainers.has(parent)) {
|
|
266
|
+
parentContainers.add(parent);
|
|
267
|
+
const parentId = parent.id || parent.getAttribute('data-zkey') || parent.getAttribute('data-zgroup') || `parent-${idx}`;
|
|
268
|
+
this.logger.debug(`[ZDisplayOrchestrator] Initializing parent wizard: ${parentId}`);
|
|
269
|
+
try {
|
|
270
|
+
this.client.wizardConditionalRenderer.initializeWizard(parent);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
this.logger.error(`[ZDisplayOrchestrator] [ERROR] Failed to initialize parent wizard container ${parentId}:`, err);
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
this.logger.debug(`[ZDisplayOrchestrator] No parent found for conditional element ${idx + 1}`);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
} else {
|
|
279
|
+
this.logger.debug('[ZDisplayOrchestrator] No wizard containers or conditional elements found');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Re-initialize zTheme components after rendering new content
|
|
284
|
+
if (window.zTheme && typeof window.zTheme.initRangeSliders === 'function') {
|
|
285
|
+
window.zTheme.initRangeSliders();
|
|
286
|
+
this.logger.debug('[ZDisplayOrchestrator] Re-initialized range sliders');
|
|
287
|
+
}
|
|
288
|
+
if (window.zTheme && typeof window.zTheme.initAccordion === 'function') {
|
|
289
|
+
window.zTheme.initAccordion();
|
|
290
|
+
this.logger.debug('[ZDisplayOrchestrator] Re-initialized accordions');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Wire any _zDelegate buttons declared in this chunk
|
|
294
|
+
this._wireDelegates();
|
|
295
|
+
|
|
296
|
+
// once-target (zDelegate inline): if it carried restoreNodes, append a Back
|
|
297
|
+
// affordance that restores the carrier's original nodes (avatar img + Edit
|
|
298
|
+
// Picture button) in place, then clear the target so the next render
|
|
299
|
+
// targets the panel/root normally.
|
|
300
|
+
if (renderTarget.once) {
|
|
301
|
+
if (Array.isArray(renderTarget.restoreNodes)) {
|
|
302
|
+
this._appendInlineDelegateBack(targetContainer, {
|
|
303
|
+
host: renderTarget.el,
|
|
304
|
+
restoreNodes: renderTarget.restoreNodes,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
this.client._renderTarget = null;
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
this.logger.warn(`[ZDisplayOrchestrator] [WARN] Chunk #${chunk_num} has no YAML data to render`);
|
|
311
|
+
if (renderTarget.once) this.client._renderTarget = null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// If this is a gate chunk, log that we're waiting for backend
|
|
315
|
+
if (is_gate) {
|
|
316
|
+
this.logger.debug('[ZDisplayOrchestrator] Waiting for gate completion');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
} catch (error) {
|
|
320
|
+
this.logger.error('Failed to render chunk:', error);
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Recursively render YAML items (handles nested structures like implicit wizards)
|
|
328
|
+
* @param {Object} data - YAML data to render
|
|
329
|
+
* @param {HTMLElement} parentElement - Parent element to render into
|
|
330
|
+
*/
|
|
331
|
+
async renderItems(data, parentElement, currentPath = '') {
|
|
332
|
+
if (!data || typeof data !== 'object') {
|
|
333
|
+
this.logger.debug('[ZDisplayOrchestrator] renderItems: No data');
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
this.logger.debug('[ZDisplayOrchestrator] renderItems called with keys:', Object.keys(data));
|
|
338
|
+
|
|
339
|
+
// Check if parent already has block-level metadata applied (data-zblock attribute)
|
|
340
|
+
const _parentIsBlockWrapper = parentElement.hasAttribute && parentElement.hasAttribute('data-zblock');
|
|
341
|
+
|
|
342
|
+
// Extract metadata first (underscore-prefixed keys like _zClass)
|
|
343
|
+
// Delegated to MetadataProcessor (Phase 4.4a)
|
|
344
|
+
const metadata = this.metadataProcessor.extractMetadata(data);
|
|
345
|
+
|
|
346
|
+
// Detect gated wizard steps (keys with '!' gate modifier)
|
|
347
|
+
// Delegated to WizardGateHandler (Phase 4.2)
|
|
348
|
+
const gateStepKey = this.wizardGateHandler.detectGateStep(data);
|
|
349
|
+
if (gateStepKey) {
|
|
350
|
+
await this.wizardGateHandler.renderWizardGated(data, parentElement, gateStepKey, currentPath);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Expand shorthand syntax (zH1-zH6, zText, zUL, zOL, zTable, zMD, zImage, zURL)
|
|
355
|
+
// Check for _zGroup metadata - delegate to GroupRenderer (Phase 4.4b)
|
|
356
|
+
if (this.groupRenderer.shouldRenderAsGroup(metadata)) {
|
|
357
|
+
await this.groupRenderer.renderGroupedItems(data, metadata, parentElement, currentPath);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
//
|
|
362
|
+
// Regular (non-grouped) rendering continues below
|
|
363
|
+
//
|
|
364
|
+
|
|
365
|
+
// Track option keys that belong to a ~* menu — skip their sibling content rendering
|
|
366
|
+
// Include modifier-prefixed variants (^opt, $opt) used for bounce/anchor semantics
|
|
367
|
+
const menuOptionKeys = new Set();
|
|
368
|
+
for (const [k, v] of Object.entries(data)) {
|
|
369
|
+
if (k.startsWith('~') && k.includes('*') && Array.isArray(v)) {
|
|
370
|
+
v.forEach(opt => {
|
|
371
|
+
if (typeof opt !== 'string') return;
|
|
372
|
+
menuOptionKeys.add(opt);
|
|
373
|
+
menuOptionKeys.add('^' + opt);
|
|
374
|
+
menuOptionKeys.add('$' + opt);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Iterate through all keys in this level
|
|
380
|
+
for (const [key, value] of Object.entries(data)) {
|
|
381
|
+
const keyPath = currentPath ? `${currentPath}.${key}` : key;
|
|
382
|
+
|
|
383
|
+
// Handle metadata keys BEFORE skipping
|
|
384
|
+
if (key.startsWith('~')) {
|
|
385
|
+
// Navigation metadata: ~zNavBar*
|
|
386
|
+
if (key.startsWith('~zNavBar')) {
|
|
387
|
+
await this.renderNavBar(value, parentElement);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
// Menu shorthand: ~Key* with array of option strings
|
|
391
|
+
// Expand to zMenu structure using sibling content — SSOT: identical to longhand zMenu
|
|
392
|
+
if (key.includes('*') && Array.isArray(value) && value.every(item => typeof item === 'string')) {
|
|
393
|
+
const cleanKey = key.replace(/[~*^$]/g, '').trim();
|
|
394
|
+
// Track which options came from ^-prefixed keys (bounce semantics in Bifrost)
|
|
395
|
+
const bounceOptions = new Set(
|
|
396
|
+
value.filter(opt => typeof opt === 'string' && data['^' + opt] !== undefined)
|
|
397
|
+
);
|
|
398
|
+
const menuValue = {
|
|
399
|
+
title: cleanKey.replace(/_/g, ' '),
|
|
400
|
+
options: value,
|
|
401
|
+
bounceOptions,
|
|
402
|
+
// Option content may be stored under ^opt ($opt, opt) — try all modifier variants
|
|
403
|
+
...Object.fromEntries(value.map(opt => {
|
|
404
|
+
const val = data[opt] ?? data['^' + opt] ?? data['$' + opt] ?? data['~' + opt];
|
|
405
|
+
return [opt, val];
|
|
406
|
+
}).filter(([, v]) => v !== undefined)),
|
|
407
|
+
};
|
|
408
|
+
await this._renderZMenuBlock(cleanKey, menuValue, parentElement, keyPath);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
// Other ~ metadata keys - skip
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Skip option keys that are sibling content of a ~* menu
|
|
416
|
+
if (menuOptionKeys.has(key)) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Skip ONLY metadata attributes (not terminal-suppressed elements)
|
|
421
|
+
// _zClass, _zStyle, _zHTML, _zId, _zScripts are metadata attributes applied to parent
|
|
422
|
+
// But _Demo_Stack, _Live_Demo_Section are terminal-suppressed elements that SHOULD render in Bifrost
|
|
423
|
+
const METADATA_KEYS = ['_zClass', '_zStyle', '_zHTML', '_zId', 'zScripts', '_zScripts', 'zId'];
|
|
424
|
+
if (METADATA_KEYS.includes(key)) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// onChange is a declarative event binding (consumed at input render / stamped
|
|
429
|
+
// server-side into the input as zapi_url), never renderable content. Skip it so
|
|
430
|
+
// its zAPI/zFunc payload never paints or executes at render time.
|
|
431
|
+
if (key === 'onChange') {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
this.logger.debug(`Rendering item: ${key}`);
|
|
436
|
+
|
|
437
|
+
// Check if this value has its own metadata (for nested _zClass support)
|
|
438
|
+
let itemMetadata = {};
|
|
439
|
+
|
|
440
|
+
// Each zKey container should ONLY use its OWN _zClass/_zStyle/_zHTML/zId, never inherit from parent
|
|
441
|
+
// This ensures ProfilePicture doesn't get ProfileHeader's classes
|
|
442
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
443
|
+
if (value._zClass !== undefined || value._zStyle !== undefined || value._zHTML !== undefined || value.zId !== undefined || value._zId !== undefined) {
|
|
444
|
+
itemMetadata = {
|
|
445
|
+
_zClass: value._zClass,
|
|
446
|
+
_zStyle: value._zStyle,
|
|
447
|
+
_zHTML: value._zHTML,
|
|
448
|
+
zId: value.zId || value._zId, // _zId as fallback so both conventions work
|
|
449
|
+
};
|
|
450
|
+
this.logger.debug(`Found nested metadata for %s`, key, itemMetadata);
|
|
451
|
+
// DEBUG: Log organizational container metadata
|
|
452
|
+
if (key.startsWith('_Box_') || key.startsWith('_Visual_')) {
|
|
453
|
+
this.logger.log(`[METADATA] ${key}:`, {
|
|
454
|
+
_zClass: value._zClass,
|
|
455
|
+
_zStyle: value._zStyle,
|
|
456
|
+
hasZDisplay: !!value.zDisplay,
|
|
457
|
+
allKeys: Object.keys(value)
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// zLink: fire navigation immediately — mirrors CLI redirect semantics
|
|
464
|
+
if (key === 'zLink') {
|
|
465
|
+
const path = typeof value === 'string' ? value : (value.zLink || '');
|
|
466
|
+
if (path) {
|
|
467
|
+
this.logger.debug(`[ZDisplayOrchestrator] zLink redirect: ${path}`);
|
|
468
|
+
this.client.zLink(path);
|
|
469
|
+
}
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// zDelta: intra-file block hop — send execute_walker for the named block
|
|
474
|
+
if (key === 'zDelta') {
|
|
475
|
+
const blockName = typeof value === 'string' ? value : (value.zDelta || '');
|
|
476
|
+
if (blockName) {
|
|
477
|
+
this.logger.debug(`[ZDisplayOrchestrator] zDelta hop: ${blockName}`);
|
|
478
|
+
this.client.zDelta(blockName);
|
|
479
|
+
}
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// zLogger: app-level log — output to browser console (mirrors backend zos.app.log)
|
|
484
|
+
if (key === 'zLogger') {
|
|
485
|
+
let msg = '', level = 'INFO';
|
|
486
|
+
if (typeof value === 'string') {
|
|
487
|
+
msg = value;
|
|
488
|
+
} else if (value && typeof value === 'object') {
|
|
489
|
+
msg = String(value.message || '');
|
|
490
|
+
level = String(value.level || 'INFO').toUpperCase();
|
|
491
|
+
}
|
|
492
|
+
if (msg) {
|
|
493
|
+
if (level === 'ERROR' || level === 'CRITICAL') console.error('[zLog]', msg);
|
|
494
|
+
else if (level === 'WARNING') console.warn('[zLog]', msg);
|
|
495
|
+
else if (level === 'DEBUG') console.debug('[zLog]', msg);
|
|
496
|
+
else console.log('[zLog]', msg);
|
|
497
|
+
}
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// zMenu: route through unified renderer (same path as ~* shorthand)
|
|
502
|
+
if (key === 'zMenu') {
|
|
503
|
+
await this._renderZMenuBlock(key, value, parentElement, keyPath);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// zDialog: flat top-level form spec (e.g. the ^Edit_Profile block streams
|
|
508
|
+
// `zDialog: {title, model, fields, onSubmit, _dialogId}` as a direct key —
|
|
509
|
+
// not wrapped as value.zDialog). Render it as an inline form. Without this,
|
|
510
|
+
// the spec falls through to the generic object recursion and paints nothing.
|
|
511
|
+
if (key === 'zDialog' && value && typeof value === 'object') {
|
|
512
|
+
const formRenderer = await this.client._ensureFormRenderer();
|
|
513
|
+
const formElement = formRenderer.renderForm(value);
|
|
514
|
+
if (formElement) parentElement.appendChild(formElement);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// zProgress as a sibling of an action (zFunc) is an action-property — the
|
|
519
|
+
// bar is owned/drawn by _executeZFunc, never as standalone content. Skip it
|
|
520
|
+
// here so it doesn't fall through to generic rendering and paint junk.
|
|
521
|
+
// (Wizard zProgress arrives pre-expanded as {event: progress_bar}, not this key.)
|
|
522
|
+
if (key === 'zProgress' && (data.zFunc || data.zfunc)) {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// zFunc: execute a @zfunc plugin call and render result inline.
|
|
527
|
+
// Two grammars, parsed identically:
|
|
528
|
+
// sibling — zFunc: &plugin() + zProgress: true (zProgress on `data`)
|
|
529
|
+
// nested — zFunc: { src: &plugin(), zProgress: true|{label,color} }
|
|
530
|
+
// A zProgress (true | {label,color}) turns the ⏳ spinner into a live
|
|
531
|
+
// indeterminate bar for the duration of the backend call.
|
|
532
|
+
if (key === 'zFunc' || key === 'zfunc') {
|
|
533
|
+
const isObj = value && typeof value === 'object' && !Array.isArray(value);
|
|
534
|
+
const funcStr = isObj ? String(value.src ?? '') : String(value);
|
|
535
|
+
const progress = (isObj ? value.zProgress : undefined) ?? data.zProgress ?? null;
|
|
536
|
+
await this._executeZFunc(funcStr, parentElement, progress);
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// zH0–zH6 shorthand: stored as raw YAML in menu option content (not pre-expanded)
|
|
541
|
+
const headerShorthand = key.match(/^zH([0-6])$/);
|
|
542
|
+
if (headerShorthand) {
|
|
543
|
+
const indent = parseInt(headerShorthand[1]);
|
|
544
|
+
const label = typeof value === 'string'
|
|
545
|
+
? value
|
|
546
|
+
: (value?.label || value?.content || key.replace(/_/g, ' '));
|
|
547
|
+
const hEvt = { event: 'header', label, indent };
|
|
548
|
+
if (value?.color) hEvt.color = value.color;
|
|
549
|
+
const el = await this.renderZDisplayEvent(hEvt, parentElement);
|
|
550
|
+
if (el) parentElement.appendChild(el);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// zText shorthand: inline text paragraph — render directly, no event dispatch
|
|
555
|
+
if (key === 'zText' && value) {
|
|
556
|
+
const content = typeof value === 'string' ? value : (value?.content || '');
|
|
557
|
+
const color = value?.color;
|
|
558
|
+
const extraClass = (!Array.isArray(value) && value?._zClass) ? String(value._zClass) : '';
|
|
559
|
+
const p = document.createElement('p');
|
|
560
|
+
p.className = ['zText', 'zmy-1', extraClass].filter(Boolean).join(' ');
|
|
561
|
+
p.textContent = content;
|
|
562
|
+
if (color) p.classList.add(`zText-${color.toLowerCase()}`);
|
|
563
|
+
parentElement.appendChild(p);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Create container wrapper for this zKey (zTheme responsive layout)
|
|
568
|
+
const containerDiv = await this.createContainer(key, itemMetadata);
|
|
569
|
+
|
|
570
|
+
// Give container a data attribute for debugging
|
|
571
|
+
containerDiv.setAttribute('data-zkey', key);
|
|
572
|
+
// Set id for DevTools navigation and CSS targeting (unless custom zId provided)
|
|
573
|
+
if (!itemMetadata.zId) {
|
|
574
|
+
containerDiv.setAttribute('id', key);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Handle list/array values (sequential zDisplay events, zDialog forms, OR menus)
|
|
578
|
+
if (Array.isArray(value)) {
|
|
579
|
+
this.logger.log(`[ZDisplayOrchestrator] Detected list/array for key: ${key}, items: ${value.length}`);
|
|
580
|
+
this.logger.log(`Detected list/array for key: ${key}, items: ${value.length}`);
|
|
581
|
+
|
|
582
|
+
// Check if this is a menu (has * modifier and array of strings)
|
|
583
|
+
const isMenu = key.includes('*') && value.every(item => typeof item === 'string');
|
|
584
|
+
|
|
585
|
+
if (isMenu) {
|
|
586
|
+
this.logger.log(`[ZDisplayOrchestrator] Detected MENU: ${key}`);
|
|
587
|
+
this.logger.log(` Detected menu with ${value.length} options`);
|
|
588
|
+
|
|
589
|
+
// Load menu renderer and render the menu
|
|
590
|
+
const menuRenderer = await this.client._ensureMenuRenderer();
|
|
591
|
+
if (menuRenderer) {
|
|
592
|
+
// Prepare menu data (matching backend zMenu event format)
|
|
593
|
+
const menuData = {
|
|
594
|
+
menu_key: key,
|
|
595
|
+
options: value,
|
|
596
|
+
title: key.replace(/[*~^$]/g, '').trim() || 'Menu',
|
|
597
|
+
allow_back: true
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// Render menu into container
|
|
601
|
+
menuRenderer.renderMenuInline(menuData, containerDiv);
|
|
602
|
+
this.logger.log(`Menu rendered for ${key}`);
|
|
603
|
+
} else {
|
|
604
|
+
this.logger.error('[ZDisplayOrchestrator] [ERROR] MenuRenderer not available');
|
|
605
|
+
}
|
|
606
|
+
} else {
|
|
607
|
+
// Regular list/array - iterate through items
|
|
608
|
+
for (const item of value) {
|
|
609
|
+
if (item && item.zDisplay) {
|
|
610
|
+
this.logger.log('[ZDisplayOrchestrator] Rendering zDisplay event:', item.zDisplay.event);
|
|
611
|
+
this.logger.log(' Rendering zDisplay from list item:', item.zDisplay);
|
|
612
|
+
const element = await this.renderZDisplayEvent(item.zDisplay, containerDiv);
|
|
613
|
+
if (element) {
|
|
614
|
+
this.logger.log(' Appended element to container');
|
|
615
|
+
containerDiv.appendChild(element);
|
|
616
|
+
}
|
|
617
|
+
} else if (item && item.zDialog) {
|
|
618
|
+
this.logger.log(' Rendering zDialog from list item:', item.zDialog);
|
|
619
|
+
const formRenderer = await this.client._ensureFormRenderer();
|
|
620
|
+
const formElement = formRenderer.renderForm(item.zDialog);
|
|
621
|
+
if (formElement) {
|
|
622
|
+
this.logger.log(' Appended zDialog form to container');
|
|
623
|
+
containerDiv.appendChild(formElement);
|
|
624
|
+
}
|
|
625
|
+
} else if (item && typeof item === 'object') {
|
|
626
|
+
// Nested object in list - recurse
|
|
627
|
+
await this.renderItems(item, containerDiv, keyPath);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
} else if (value && value.zDisplay) {
|
|
632
|
+
// Check if this has a direct zDisplay event
|
|
633
|
+
this.logger.debug(`[renderItems] Direct zDisplay for ${key}`);
|
|
634
|
+
const element = await this.renderZDisplayEvent(value.zDisplay, containerDiv);
|
|
635
|
+
if (element) {
|
|
636
|
+
// Handle unwrapping - delegated to ContainerUnwrapper (Phase 4.4c)
|
|
637
|
+
if (element.nodeType === Node.COMMENT_NODE) {
|
|
638
|
+
parentElement.appendChild(element);
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const unwrapResult = this.containerUnwrapper.processUnwrapping(containerDiv, element, key);
|
|
643
|
+
if (!unwrapResult.shouldAppendContainer) {
|
|
644
|
+
parentElement.appendChild(unwrapResult.elementToAppend);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
// Container was kept, element already appended to container
|
|
648
|
+
}
|
|
649
|
+
} else if (value && value.event && typeof value.event === 'string') {
|
|
650
|
+
// Backend now sends unwrapped zDisplay events (direct event key, no zDisplay wrapper)
|
|
651
|
+
// Example: {event: 'zCrumbs', show: 'static', ...}
|
|
652
|
+
// Also handles zData: read resolved inline as {event: 'zTable', ...}
|
|
653
|
+
if (key === 'zData' && value.event === 'zTable') {
|
|
654
|
+
this.logger.log(`[renderItems] zData:read resolved inline → zTable (${value.rows?.length ?? 0} rows)`);
|
|
655
|
+
}
|
|
656
|
+
this.logger.debug(`[renderItems] Direct event: %s for %s`, value.event, key);
|
|
657
|
+
// Collapsed single-child container: the backend merges a styled wrapper
|
|
658
|
+
// (e.g. Demo {_zClass: zc-render, zIcon}) into one flat event, so _zClass
|
|
659
|
+
// lands on BOTH the container div (itemMetadata) and the event. For an
|
|
660
|
+
// inline icon that double-boxes the glyph — the container already owns the
|
|
661
|
+
// frame, so render the icon bare inside it.
|
|
662
|
+
let evtData = value;
|
|
663
|
+
if (value.event === 'icon' && (itemMetadata._zClass || itemMetadata._zStyle)) {
|
|
664
|
+
evtData = { ...value };
|
|
665
|
+
delete evtData._zClass;
|
|
666
|
+
delete evtData._zStyle;
|
|
667
|
+
}
|
|
668
|
+
const element = await this.renderZDisplayEvent(evtData, containerDiv);
|
|
669
|
+
if (element) {
|
|
670
|
+
// Handle unwrapping - delegated to ContainerUnwrapper (Phase 4.4c)
|
|
671
|
+
if (element.nodeType === Node.COMMENT_NODE) {
|
|
672
|
+
parentElement.appendChild(element);
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const unwrapResult = this.containerUnwrapper.processUnwrapping(containerDiv, element, key);
|
|
677
|
+
if (!unwrapResult.shouldAppendContainer) {
|
|
678
|
+
parentElement.appendChild(unwrapResult.elementToAppend);
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
// Container was kept, element already appended to container
|
|
682
|
+
}
|
|
683
|
+
} else if (value && value.zDialog) {
|
|
684
|
+
// Check if this has a direct zDialog form
|
|
685
|
+
this.logger.log(' Rendering zDialog from direct value:', value.zDialog);
|
|
686
|
+
const formRenderer = await this.client._ensureFormRenderer();
|
|
687
|
+
const formElement = formRenderer.renderForm(value.zDialog);
|
|
688
|
+
if (formElement) {
|
|
689
|
+
containerDiv.appendChild(formElement);
|
|
690
|
+
}
|
|
691
|
+
} else if (value && typeof value === 'object' && Object.keys(value).length > 0) {
|
|
692
|
+
// Nested structure — pre-append to parent BEFORE recursing so that any
|
|
693
|
+
// async children (e.g. _executeZFunc) can find DOM elements via querySelector
|
|
694
|
+
// or direct reference while the tree is being built.
|
|
695
|
+
if (key.startsWith('_')) {
|
|
696
|
+
this.logger.log(` [NON-GROUP] Processing organizational container: ${key}, nested keys:`, Object.keys(value));
|
|
697
|
+
} else {
|
|
698
|
+
this.logger.debug(`[ZDisplayOrchestrator] Recursing into nested object: %s`, key, Object.keys(value));
|
|
699
|
+
}
|
|
700
|
+
parentElement.appendChild(containerDiv);
|
|
701
|
+
await this.renderItems(value, containerDiv, keyPath);
|
|
702
|
+
if (key.startsWith('_') && containerDiv.children.length > 0) {
|
|
703
|
+
this.logger.log(`[NON-GROUP] Rendered organizational container ${key} with ${containerDiv.children.length} children`);
|
|
704
|
+
}
|
|
705
|
+
continue; // already appended — skip deferred append below
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Append container to parent if it has children OR if it carries its own
|
|
709
|
+
// styling/semantic intent (class, inline style, or non-div tag).
|
|
710
|
+
// This allows metadata-only blocks like:
|
|
711
|
+
// Primary_Cube:
|
|
712
|
+
// _zHTML: div
|
|
713
|
+
// _zClass: zBg-primary zRounded
|
|
714
|
+
// _zStyle: width:120px; height:120px
|
|
715
|
+
// NOTE: zCard-body auto-enhancement REMOVED (2026-01-28)
|
|
716
|
+
const hasChildren = containerDiv.children.length > 0;
|
|
717
|
+
const hasStyling = containerDiv.className || containerDiv.getAttribute('style');
|
|
718
|
+
const isSemanticTag = containerDiv.tagName.toLowerCase() !== 'div';
|
|
719
|
+
if (hasChildren || hasStyling || isSemanticTag) {
|
|
720
|
+
parentElement.appendChild(containerDiv);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Handle wizard_gate_result: populate and show the post-gate container.
|
|
728
|
+
* Delegated to WizardGateHandler (Phase 4.2)
|
|
729
|
+
* @param {Object} message - {gateKey, wizardPath, data}
|
|
730
|
+
*/
|
|
731
|
+
async handleWizardGateResult(message) {
|
|
732
|
+
await this.wizardGateHandler.handleWizardGateResult(message);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Resolve a _zDelegate path (e.g. "_GUI.Btn_Eq") to the target button element.
|
|
738
|
+
* Walks data-zkey attributes in sequence within the given scope (defaults to document).
|
|
739
|
+
* Scoping to the nearest [data-zblock] prevents cross-block collisions.
|
|
740
|
+
* @param {string} path - Dot-separated key path
|
|
741
|
+
* @param {Element|Document} [scope=document] - Root to search within
|
|
742
|
+
* @returns {HTMLElement|null}
|
|
743
|
+
*/
|
|
744
|
+
_resolveZDelegatePath(path, scope = document) {
|
|
745
|
+
const parts = path.split('.');
|
|
746
|
+
let el = scope;
|
|
747
|
+
for (const part of parts) {
|
|
748
|
+
el = el.querySelector(`[data-zkey="${part}"]`);
|
|
749
|
+
if (!el) {
|
|
750
|
+
this.logger.warn('[WizardGate] _zDelegate: key not found in DOM:', part, '(path:', path, ')');
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return el;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Unified zMenu block renderer — single code path for both longhand zMenu key
|
|
759
|
+
* and ~Key* shorthand. menuValue = { title, options: [...], [optKey]: content, ... }
|
|
760
|
+
*/
|
|
761
|
+
async _renderZMenuBlock(menuKey, menuValue, parentElement, keyPath) {
|
|
762
|
+
this.logger.debug('[ZDisplayOrchestrator] Rendering zMenu block:', menuKey);
|
|
763
|
+
const options = Array.isArray(menuValue.options) ? menuValue.options : [];
|
|
764
|
+
const bounceOptions = menuValue.bounceOptions instanceof Set ? menuValue.bounceOptions : new Set();
|
|
765
|
+
const title = menuValue.title || null;
|
|
766
|
+
|
|
767
|
+
const containerDiv = document.createElement('div');
|
|
768
|
+
containerDiv.setAttribute('data-zkey', menuKey);
|
|
769
|
+
containerDiv.setAttribute('id', menuKey);
|
|
770
|
+
|
|
771
|
+
if (title) {
|
|
772
|
+
const titleEl = document.createElement('p');
|
|
773
|
+
titleEl.className = 'zText-muted zSmall zmb-1';
|
|
774
|
+
titleEl.textContent = title;
|
|
775
|
+
containerDiv.appendChild(titleEl);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const nav = document.createElement('nav');
|
|
779
|
+
nav.className = 'zMenu-nav zmb-2';
|
|
780
|
+
nav.setAttribute('role', 'menu');
|
|
781
|
+
if (title) nav.setAttribute('aria-label', title);
|
|
782
|
+
|
|
783
|
+
const ul = document.createElement('ul');
|
|
784
|
+
ul.className = 'zNavbar-nav zd-flex zflex-column zgap-1 list-unstyled zm-0 zp-0';
|
|
785
|
+
nav.appendChild(ul);
|
|
786
|
+
containerDiv.appendChild(nav);
|
|
787
|
+
|
|
788
|
+
const placeholders = {};
|
|
789
|
+
options.forEach(optKey => {
|
|
790
|
+
const ph = document.createElement('div');
|
|
791
|
+
ph.className = 'zMenu-option-content';
|
|
792
|
+
ph.style.display = 'none';
|
|
793
|
+
ph.dataset.menuContent = optKey;
|
|
794
|
+
placeholders[optKey] = ph;
|
|
795
|
+
containerDiv.appendChild(ph);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// Reset menu to neutral — deselect all buttons, hide all content
|
|
799
|
+
const resetMenu = () => {
|
|
800
|
+
ul.querySelectorAll('button[data-key]').forEach(b => b.classList.remove('active'));
|
|
801
|
+
options.forEach(k => {
|
|
802
|
+
const ph = placeholders[k];
|
|
803
|
+
if (ph) { ph.style.display = 'none'; ph.innerHTML = ''; delete ph.dataset.rendered; }
|
|
804
|
+
});
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
options.forEach((optKey, idx) => {
|
|
808
|
+
const li = document.createElement('li');
|
|
809
|
+
li.className = 'zNav-item';
|
|
810
|
+
|
|
811
|
+
const btn = document.createElement('button');
|
|
812
|
+
btn.className = 'zNav-link zBtn w-100 text-start zp-2';
|
|
813
|
+
btn.setAttribute('role', 'menuitem');
|
|
814
|
+
btn.dataset.key = optKey;
|
|
815
|
+
// Strip the leading delta/bounce/anchor modifier ($ ^ ~) so the visible
|
|
816
|
+
// label reads cleanly ("$Edit_Profile" → "Edit Profile"). The raw optKey is
|
|
817
|
+
// preserved on data-key for selection/resolution.
|
|
818
|
+
const label = menuValue.labels?.[optKey] ?? optKey.replace(/^[$^~]+/, '').replace(/_/g, ' ');
|
|
819
|
+
btn.innerHTML = `<span class="zBadge zBadge-secondary me-2">${idx + 1}</span>${label}`;
|
|
820
|
+
|
|
821
|
+
btn.addEventListener('click', async () => {
|
|
822
|
+
// Guard: prevent re-entry while a zfunc is already in-flight for this option
|
|
823
|
+
if (btn.dataset.zfuncInFlight === '1') return;
|
|
824
|
+
|
|
825
|
+
// ── Server-driven menu (SSOT) ─────────────────────────────────────
|
|
826
|
+
// The menu carries a _menuId: its option content was deliberately NOT
|
|
827
|
+
// shipped (server owns the flow). Send the pick back; the server resumes
|
|
828
|
+
// the executor at the chosen key and falls through the siblings, streaming
|
|
829
|
+
// the result into this option's placeholder. The JS never decides flow.
|
|
830
|
+
if (menuValue._menuId) {
|
|
831
|
+
resetMenu();
|
|
832
|
+
btn.classList.add('active');
|
|
833
|
+
const ph = placeholders[optKey];
|
|
834
|
+
if (!ph) return;
|
|
835
|
+
ph.style.display = 'block';
|
|
836
|
+
ph.innerHTML = '';
|
|
837
|
+
ph.dataset.rendered = '1';
|
|
838
|
+
// Pin the resumed chunk(s) into this placeholder (replace, single-shot).
|
|
839
|
+
this.client._renderTarget = { el: ph, mode: 'replace', once: true };
|
|
840
|
+
const payload = {
|
|
841
|
+
event: 'menu_selection',
|
|
842
|
+
menu_id: menuValue._menuId,
|
|
843
|
+
menu_key: menuKey,
|
|
844
|
+
selected: optKey,
|
|
845
|
+
};
|
|
846
|
+
if (typeof this.client._sendWalker === 'function') {
|
|
847
|
+
this.client._sendWalker(payload);
|
|
848
|
+
} else {
|
|
849
|
+
this.client.connection.send(JSON.stringify(payload));
|
|
850
|
+
}
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// No inline sibling content → the option is a $-reference to a SEPARATE
|
|
855
|
+
// block (e.g. ~Profile_Actions* → ^Edit_Profile). Mirror CLI menu→block
|
|
856
|
+
// selection: navigate in place via zDelta (same route, content swap). The
|
|
857
|
+
// target block carries its own Back affordance (zDelegate $Profile).
|
|
858
|
+
if (menuValue[optKey] === undefined) {
|
|
859
|
+
const targetBlock = optKey.replace(/^[$^~]+/, '').trim();
|
|
860
|
+
if (targetBlock && this.client?.zDelta) {
|
|
861
|
+
this.logger.log('[ZMenu] option → zDelta block hop:', targetBlock);
|
|
862
|
+
this.client.zDelta(targetBlock);
|
|
863
|
+
}
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
resetMenu();
|
|
868
|
+
btn.classList.add('active');
|
|
869
|
+
|
|
870
|
+
const ph = placeholders[optKey];
|
|
871
|
+
if (!ph) return;
|
|
872
|
+
ph.style.display = 'block';
|
|
873
|
+
|
|
874
|
+
ph.dataset.rendered = '1';
|
|
875
|
+
const isBounce = bounceOptions.has(optKey);
|
|
876
|
+
|
|
877
|
+
btn.dataset.zfuncInFlight = '1';
|
|
878
|
+
try {
|
|
879
|
+
// Temporarily override _executeZFunc to capture the zFunc result
|
|
880
|
+
// so we can decide whether to show the Back button.
|
|
881
|
+
let zfuncResult = null;
|
|
882
|
+
const origExecuteZFunc = this._executeZFunc.bind(this);
|
|
883
|
+
this._executeZFunc = async (funcStr, parentEl) => {
|
|
884
|
+
const result = await origExecuteZFunc(funcStr, parentEl);
|
|
885
|
+
zfuncResult = result;
|
|
886
|
+
return result;
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
await this.renderItems({ [optKey]: menuValue[optKey] }, ph, [...keyPath, menuKey]);
|
|
890
|
+
|
|
891
|
+
this._executeZFunc = origExecuteZFunc;
|
|
892
|
+
|
|
893
|
+
if (isBounce) {
|
|
894
|
+
// For ^ bounce options: show zBack button unless zFunc returned 'exit'
|
|
895
|
+
const shouldBack = zfuncResult === null // no zFunc (ungated) — always show Back
|
|
896
|
+
|| (zfuncResult !== 'exit' && zfuncResult !== false && zfuncResult !== null);
|
|
897
|
+
|
|
898
|
+
if (shouldBack) {
|
|
899
|
+
this._appendZBackButton(ph, resetMenu, btn);
|
|
900
|
+
} else {
|
|
901
|
+
// exit result: hide the content entirely
|
|
902
|
+
ph.style.display = 'none';
|
|
903
|
+
ph.innerHTML = '';
|
|
904
|
+
delete ph.dataset.rendered;
|
|
905
|
+
btn.classList.remove('active');
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
} catch (e) {
|
|
909
|
+
this._executeZFunc = origExecuteZFunc;
|
|
910
|
+
this.logger.error(`[ZDisplayOrchestrator] zMenu option render error (${optKey}):`, e);
|
|
911
|
+
} finally {
|
|
912
|
+
delete btn.dataset.zfuncInFlight;
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
li.appendChild(btn);
|
|
917
|
+
ul.appendChild(li);
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
// Auto Back item for non-anchor (~ absent) menus — server signals via
|
|
921
|
+
// _allowBack, mirroring the zCLI allow_back rule. Clicking it collapses the
|
|
922
|
+
// drill-in this menu lives in, re-showing the parent menu (CLI: zBack from a
|
|
923
|
+
// sub-menu returns to the parent menu).
|
|
924
|
+
if (menuValue._allowBack) {
|
|
925
|
+
const backLi = document.createElement('li');
|
|
926
|
+
backLi.className = 'zNav-item';
|
|
927
|
+
const backNav = document.createElement('button');
|
|
928
|
+
backNav.className = 'zNav-link zBtn w-100 text-start zp-2';
|
|
929
|
+
backNav.setAttribute('role', 'menuitem');
|
|
930
|
+
backNav.dataset.key = 'zBack';
|
|
931
|
+
backNav.innerHTML = `<span class="zBadge zBadge-secondary me-2">${options.length + 1}</span>Back`;
|
|
932
|
+
backNav.addEventListener('click', () => {
|
|
933
|
+
const parentPh = containerDiv.closest('.zMenu-option-content');
|
|
934
|
+
if (parentPh) {
|
|
935
|
+
const ownerKey = parentPh.dataset.menuContent;
|
|
936
|
+
const ownerContainer = parentPh.parentElement;
|
|
937
|
+
parentPh.style.display = 'none';
|
|
938
|
+
parentPh.innerHTML = '';
|
|
939
|
+
delete parentPh.dataset.rendered;
|
|
940
|
+
if (ownerContainer && ownerKey) {
|
|
941
|
+
const ownerBtn = ownerContainer.querySelector(`button[data-key="${ownerKey}"]`);
|
|
942
|
+
if (ownerBtn) ownerBtn.classList.remove('active');
|
|
943
|
+
}
|
|
944
|
+
} else {
|
|
945
|
+
// Top-level non-anchor menu — nothing to collapse into; reset self.
|
|
946
|
+
resetMenu();
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
backLi.appendChild(backNav);
|
|
950
|
+
ul.appendChild(backLi);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
parentElement.appendChild(containerDiv);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Append a "← Back" button to a menu option's content placeholder.
|
|
958
|
+
* Clicking it resets the menu to neutral (no active option, content hidden).
|
|
959
|
+
*
|
|
960
|
+
* @param {HTMLElement} ph - The option content placeholder div
|
|
961
|
+
* @param {Function} resetMenu - Callback that deselects all options and hides content
|
|
962
|
+
* @param {HTMLElement} activeBtn - The currently active menu button (used for aria state)
|
|
963
|
+
*/
|
|
964
|
+
_appendZBackButton(ph, resetMenu, activeBtn) {
|
|
965
|
+
const backBtn = document.createElement('button');
|
|
966
|
+
backBtn.className = 'zBtn zBtn-sm zBtn-outline-secondary zmt-3';
|
|
967
|
+
backBtn.innerHTML = '← Back to menu';
|
|
968
|
+
backBtn.addEventListener('click', () => {
|
|
969
|
+
resetMenu();
|
|
970
|
+
}, { once: true });
|
|
971
|
+
ph.appendChild(backBtn);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* Append a Back affordance for an inline zDelegate. Clicking it restores the
|
|
976
|
+
* carrier's original child nodes (saved live, so their listeners survive) into
|
|
977
|
+
* the host container — collapsing the delegated section back to the carrier.
|
|
978
|
+
* Uses the shared .acct-back-action styling + a stable hook class.
|
|
979
|
+
* @param {HTMLElement} container - where the delegated fragment was rendered (the host)
|
|
980
|
+
* @param {{host: HTMLElement, restoreNodes: Node[]}} inlineDelegate
|
|
981
|
+
*/
|
|
982
|
+
_appendInlineDelegateBack(container, inlineDelegate) {
|
|
983
|
+
const backBtn = document.createElement('button');
|
|
984
|
+
backBtn.type = 'button';
|
|
985
|
+
backBtn.className = 'zBtn acct-back-action acct-inline-back zmt-3';
|
|
986
|
+
backBtn.innerHTML = '← Back';
|
|
987
|
+
backBtn.addEventListener('click', () => {
|
|
988
|
+
const { host, restoreNodes } = inlineDelegate;
|
|
989
|
+
if (host && Array.isArray(restoreNodes)) {
|
|
990
|
+
host.replaceChildren(...restoreNodes);
|
|
991
|
+
}
|
|
992
|
+
}, { once: true });
|
|
993
|
+
container.appendChild(backBtn);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Loop a wizard in the inline-gate path back to an earlier step.
|
|
998
|
+
*
|
|
999
|
+
* The inline gate (stamped requestId + render_chunk reveal) has no
|
|
1000
|
+
* data-wizard-gate DOM, so restartWizardFromGate's selectors don't apply.
|
|
1001
|
+
* Instead we operate on the wizard container (nearest ancestor [data-zkey]
|
|
1002
|
+
* holding an input field): drop every appended reveal group, reset the
|
|
1003
|
+
* targeted step's input, and re-arm the gate button. The server re-parks the
|
|
1004
|
+
* gate (same requestId), so re-clicking the gate re-runs the post-gate steps.
|
|
1005
|
+
*
|
|
1006
|
+
* @param {string} actionStep - The step key the loop-back targets (e.g. Ask_Name)
|
|
1007
|
+
* @param {HTMLElement} btn - The clicked loop-back button
|
|
1008
|
+
* @private
|
|
1009
|
+
*/
|
|
1010
|
+
_restartInlineWizard(actionStep, btn) {
|
|
1011
|
+
const fieldSel = 'input, textarea, select';
|
|
1012
|
+
|
|
1013
|
+
// Climb to the wizard container: the nearest ancestor [data-zkey] that holds
|
|
1014
|
+
// an input field (mirrors button_renderer._collectInlineContext).
|
|
1015
|
+
let container = null;
|
|
1016
|
+
let node = btn.parentElement;
|
|
1017
|
+
while (node) {
|
|
1018
|
+
if (node.getAttribute && node.getAttribute('data-zkey') && node.querySelector(fieldSel)) {
|
|
1019
|
+
container = node;
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
node = node.parentElement;
|
|
1023
|
+
}
|
|
1024
|
+
if (!container) container = btn.closest('[data-zkey]');
|
|
1025
|
+
if (!container) {
|
|
1026
|
+
this.logger.warn('[WizardLoop] container not found for loop-back:', actionStep);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Remove every revealed group (each gate resolve appended one). This also
|
|
1031
|
+
// removes the loop-back button itself — a fresh one renders on the next loop.
|
|
1032
|
+
container.querySelectorAll('.wizard-postgate').forEach((el) => el.remove());
|
|
1033
|
+
|
|
1034
|
+
// Re-arm the gate: re-enable any disabled buttons left in the container
|
|
1035
|
+
// (the gate button keeps its click listener + requestId).
|
|
1036
|
+
container.querySelectorAll('button[disabled]').forEach((b) => { b.disabled = false; });
|
|
1037
|
+
|
|
1038
|
+
// Reset the targeted step's input and focus it (CLI parity: loop to that step).
|
|
1039
|
+
const stepScope = container.querySelector(`[data-zkey="${actionStep}"]`);
|
|
1040
|
+
const stepInput = stepScope ? stepScope.querySelector(fieldSel) : null;
|
|
1041
|
+
if (stepInput) {
|
|
1042
|
+
stepInput.disabled = false;
|
|
1043
|
+
stepInput.value = '';
|
|
1044
|
+
setTimeout(() => stepInput.focus(), 50);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
this.logger.log('[WizardLoop] Restarted inline wizard from step:', actionStep);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Wire _zDelegate buttons for all gate wrappers that declare one.
|
|
1052
|
+
* Also handles button[data-zdelegate] → input replacement delegation.
|
|
1053
|
+
* Safe to call multiple times — skips already-wired gates via dataset.zdelegateWired.
|
|
1054
|
+
* Call after each chunk render and after wizard restart.
|
|
1055
|
+
*/
|
|
1056
|
+
_wireDelegates() {
|
|
1057
|
+
// ── Pass 0: hidden button → delegate button (wizard action) ─────────────
|
|
1058
|
+
// Handles buttons with both data-wizard-action and data-zdelegate
|
|
1059
|
+
// Example: Restart button delegates to AC button
|
|
1060
|
+
const hiddenActionButtons = document.querySelectorAll('button[data-wizard-action][data-zdelegate]:not([data-zdelegate-wired])');
|
|
1061
|
+
this.logger.log('[Delegate] Pass 0: Found', hiddenActionButtons.length, 'hidden action buttons');
|
|
1062
|
+
for (const hiddenBtn of hiddenActionButtons) {
|
|
1063
|
+
const targetPath = hiddenBtn.dataset.zdelegate;
|
|
1064
|
+
const wizardAction = hiddenBtn.dataset.wizardAction;
|
|
1065
|
+
|
|
1066
|
+
this.logger.log('[Delegate] Processing hidden button:', hiddenBtn, 'action:', wizardAction, 'target:', targetPath);
|
|
1067
|
+
|
|
1068
|
+
const targetContainer = this._resolveZDelegatePath(targetPath, document);
|
|
1069
|
+
if (!targetContainer) {
|
|
1070
|
+
this.logger.warn('[Delegate] Target container not found for:', targetPath);
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const targetBtn = targetContainer.querySelector('button') || targetContainer;
|
|
1075
|
+
if (!targetBtn) {
|
|
1076
|
+
this.logger.warn('[Delegate] Target button not found in container:', targetContainer);
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Wire target button to trigger wizard restart
|
|
1081
|
+
targetBtn.addEventListener('click', () => {
|
|
1082
|
+
this.logger.log('[Delegate] Target button clicked, restarting wizard from:', wizardAction);
|
|
1083
|
+
this.wizardGateHandler.restartWizardFromGate(wizardAction);
|
|
1084
|
+
});
|
|
1085
|
+
hiddenBtn.dataset.zdelegateWired = 'true';
|
|
1086
|
+
this.logger.log('[Delegate] Button action → button wired:', wizardAction, '→', targetPath);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// ── Pass 0b: plain wizard-action button (loop-back, no delegate) ─────────
|
|
1090
|
+
// A post-gate `zBtn` with `action: <stepKey>` and no _zDelegate — the
|
|
1091
|
+
// "Run again?" / "Play again" loop-back. In the inline-gate path there is no
|
|
1092
|
+
// data-wizard-gate wrapper, so wire it to re-enter the wizard in place:
|
|
1093
|
+
// clear this reveal, reset the targeted step's input, re-arm the gate.
|
|
1094
|
+
const actionButtons = document.querySelectorAll(
|
|
1095
|
+
'button[data-wizard-action]:not([data-zdelegate]):not([data-wizard-action-wired])'
|
|
1096
|
+
);
|
|
1097
|
+
for (const btn of actionButtons) {
|
|
1098
|
+
const targetStep = btn.dataset.wizardAction;
|
|
1099
|
+
btn.dataset.wizardActionWired = 'true';
|
|
1100
|
+
btn.addEventListener('click', () => this._restartInlineWizard(targetStep, btn));
|
|
1101
|
+
this.logger.log('[Delegate] Wizard-action (loop-back) button wired:', targetStep);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// ── Pass 1: button → input (replace value) ──────────────────────────────
|
|
1105
|
+
const delegateButtons = document.querySelectorAll('button[data-zdelegate]:not([data-zdelegate-wired])');
|
|
1106
|
+
for (const btn of delegateButtons) {
|
|
1107
|
+
const targetPath = btn.dataset.zdelegate;
|
|
1108
|
+
const targetContainer = this._resolveZDelegatePath(targetPath, document);
|
|
1109
|
+
if (!targetContainer) continue;
|
|
1110
|
+
|
|
1111
|
+
const targetInput = targetContainer.querySelector('input, textarea, select');
|
|
1112
|
+
if (!targetInput) continue;
|
|
1113
|
+
|
|
1114
|
+
btn.addEventListener('click', () => {
|
|
1115
|
+
targetInput.value = btn.textContent.trim();
|
|
1116
|
+
targetInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1117
|
+
});
|
|
1118
|
+
btn.dataset.zdelegateWired = 'true';
|
|
1119
|
+
this.logger.log('[Delegate] Button → input wired:', btn.textContent.trim(), '→', targetPath);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// ── Pass 2: zInput gate → delegate button (submit) ──────────────────────
|
|
1123
|
+
const gateWrappers = document.querySelectorAll('[data-wizard-gate][data-zdelegate]');
|
|
1124
|
+
for (const gateWrapper of gateWrappers) {
|
|
1125
|
+
if (gateWrapper.dataset.zdelegateWired) continue;
|
|
1126
|
+
|
|
1127
|
+
const delegatePath = gateWrapper.getAttribute('data-zdelegate');
|
|
1128
|
+
// Scope to the nearest [data-zblock] ancestor to avoid cross-block collisions
|
|
1129
|
+
const scope = gateWrapper.closest('[data-zblock]') || document;
|
|
1130
|
+
const delegateContainer = this._resolveZDelegatePath(delegatePath, scope);
|
|
1131
|
+
if (!delegateContainer) continue;
|
|
1132
|
+
const delegateBtn = delegateContainer.querySelector('button') || delegateContainer;
|
|
1133
|
+
|
|
1134
|
+
const cleanGateKey = gateWrapper.getAttribute('data-wizard-gate');
|
|
1135
|
+
// wizardPath lives on the companion post-gate container (set during _renderWizardGated)
|
|
1136
|
+
const postGateContainer = document.querySelector(`[data-wizard-post-gate="${cleanGateKey}"]`);
|
|
1137
|
+
const wizardPath = postGateContainer?.getAttribute('data-wizard-path') || '';
|
|
1138
|
+
|
|
1139
|
+
delegateBtn.addEventListener('click', async () => {
|
|
1140
|
+
const input = gateWrapper.querySelector('input, textarea, select');
|
|
1141
|
+
const value = input ? input.value.trim() : '';
|
|
1142
|
+
if (!value) return;
|
|
1143
|
+
|
|
1144
|
+
input.disabled = true;
|
|
1145
|
+
delegateBtn.disabled = true;
|
|
1146
|
+
|
|
1147
|
+
try {
|
|
1148
|
+
this.client.connection.send(JSON.stringify({
|
|
1149
|
+
event: 'wizard_gate_submit',
|
|
1150
|
+
wizardPath,
|
|
1151
|
+
gateKey: cleanGateKey,
|
|
1152
|
+
value,
|
|
1153
|
+
}));
|
|
1154
|
+
} catch (e) {
|
|
1155
|
+
this.logger.error('[WizardGate] Delegate submit error:', e);
|
|
1156
|
+
if (input) input.disabled = false;
|
|
1157
|
+
delegateBtn.disabled = false;
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
gateWrapper.dataset.zdelegateWired = 'true';
|
|
1162
|
+
this.logger.log('[WizardGate] Wired _zDelegate for gate:', cleanGateKey, '→', delegatePath);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* Create container wrapper for a zKey with zTheme responsive classes
|
|
1168
|
+
* Supports _zClass, _zStyle, and zId metadata for customization
|
|
1169
|
+
* @param {string} zKey - Key name for debugging
|
|
1170
|
+
* @param {Object} metadata - Metadata object with _zClass, _zStyle, zId
|
|
1171
|
+
* @returns {HTMLElement}
|
|
1172
|
+
*/
|
|
1173
|
+
async createContainer(zKey, metadata) {
|
|
1174
|
+
// Delegated to MetadataProcessor (Phase 4.4a)
|
|
1175
|
+
return this.metadataProcessor.createContainer(zKey, metadata, this.logger);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Render navbar DOM element (v1.6.1: Returns DOM element to preserve event listeners)
|
|
1180
|
+
* Delegated to NavBarBuilder (Phase 4.3)
|
|
1181
|
+
* @param {Array} items - Navbar items (e.g., ['zVaF', 'zAbout', '^zLogin'])
|
|
1182
|
+
* @returns {Promise<HTMLElement|null>} Navbar DOM element
|
|
1183
|
+
*/
|
|
1184
|
+
async renderMetaNavBarHTML(items) {
|
|
1185
|
+
return await this.navBarBuilder.renderMetaNavBarHTML(items, this.options);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* Render navigation bar from metadata (~zNavBar* in content)
|
|
1190
|
+
* Delegated to NavBarBuilder (Phase 4.3)
|
|
1191
|
+
* @param {Array} items - Navbar items
|
|
1192
|
+
* @param {HTMLElement} parentElement - Parent element to append to
|
|
1193
|
+
*/
|
|
1194
|
+
async renderNavBar(items, parentElement) {
|
|
1195
|
+
await this.navBarBuilder.renderNavBar(items, parentElement);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Render a single zDisplay event as DOM element
|
|
1200
|
+
* @param {Object} eventData - Event data with event type and content
|
|
1201
|
+
* @param {HTMLElement} [parentElement=null] - Optional parent element for context detection
|
|
1202
|
+
* @returns {Promise<HTMLElement>}
|
|
1203
|
+
*/
|
|
1204
|
+
async renderZDisplayEvent(eventData, parentElement = null) {
|
|
1205
|
+
const event = eventData.event;
|
|
1206
|
+
this.logger.debug(`[renderZDisplayEvent] Rendering event: ${event}`);
|
|
1207
|
+
let element;
|
|
1208
|
+
|
|
1209
|
+
switch (event) {
|
|
1210
|
+
case 'text': {
|
|
1211
|
+
// Use modular TypographyRenderer for text
|
|
1212
|
+
const textRenderer = await this.client._ensureTypographyRenderer();
|
|
1213
|
+
element = textRenderer.renderText(eventData);
|
|
1214
|
+
this.logger.log('[renderZDisplayEvent] Rendered text element');
|
|
1215
|
+
break;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
case 'rich_text': {
|
|
1219
|
+
// Markdown fenced code blocks (```lang) inside zMD render here, not via
|
|
1220
|
+
// the 'code' event — so kick off Prism the same way when a fence is present.
|
|
1221
|
+
if (!this.client._prismLoaded && typeof eventData.content === 'string' && eventData.content.includes('```')) {
|
|
1222
|
+
this.client._prismLoaded = true;
|
|
1223
|
+
this.client._loadPrismJS();
|
|
1224
|
+
}
|
|
1225
|
+
// Use TextRenderer for rich text with markdown parsing
|
|
1226
|
+
const textRenderer = await this.client._ensureTextRenderer();
|
|
1227
|
+
element = textRenderer.renderRichText(eventData);
|
|
1228
|
+
this.logger.debug('[renderZDisplayEvent] Rendered rich_text element');
|
|
1229
|
+
break;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
case 'code': {
|
|
1233
|
+
if (!this.client._prismLoaded) {
|
|
1234
|
+
this.client._prismLoaded = true;
|
|
1235
|
+
this.client._loadPrismJS();
|
|
1236
|
+
}
|
|
1237
|
+
const codeRenderer = await this.client._ensureCodeRenderer();
|
|
1238
|
+
element = codeRenderer.renderCode(eventData);
|
|
1239
|
+
this.logger.debug(`[renderZDisplayEvent] Rendered code block (language: ${eventData.language || 'text'})`);
|
|
1240
|
+
break;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
case 'json': {
|
|
1244
|
+
if (!this.client._prismLoaded) {
|
|
1245
|
+
this.client._prismLoaded = true;
|
|
1246
|
+
this.client._loadPrismJS();
|
|
1247
|
+
}
|
|
1248
|
+
// Render JSON data as a syntax-highlighted code block (language: json).
|
|
1249
|
+
const jsonCodeRenderer = await this.client._ensureCodeRenderer();
|
|
1250
|
+
const jsonStr = typeof eventData.data === 'string'
|
|
1251
|
+
? eventData.data
|
|
1252
|
+
: JSON.stringify(eventData.data, null, eventData.indent_size ?? 2);
|
|
1253
|
+
element = jsonCodeRenderer.renderCode({ event: 'code', language: 'json', content: jsonStr });
|
|
1254
|
+
this.logger.debug('[renderZDisplayEvent] Rendered json block');
|
|
1255
|
+
break;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
case 'header': {
|
|
1259
|
+
// Use modular TypographyRenderer for headers
|
|
1260
|
+
const headerRenderer = await this.client._ensureTypographyRenderer();
|
|
1261
|
+
element = headerRenderer.renderHeader(eventData);
|
|
1262
|
+
this.logger.debug(`[renderZDisplayEvent] Rendered header (level: %s)`, eventData.level || 1);
|
|
1263
|
+
break;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
case 'divider': {
|
|
1267
|
+
// Use modular TypographyRenderer for dividers
|
|
1268
|
+
const dividerRenderer = await this.client._ensureTypographyRenderer();
|
|
1269
|
+
element = dividerRenderer.renderDivider(eventData);
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
case 'button': {
|
|
1274
|
+
// Use modular ButtonRenderer for buttons
|
|
1275
|
+
const buttonRenderer = await this.client._ensureButtonRenderer();
|
|
1276
|
+
element = buttonRenderer.render(eventData);
|
|
1277
|
+
this.logger.debug(`[renderZDisplayEvent] Rendered button: ${eventData.label}`);
|
|
1278
|
+
break;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
case 'zURL': {
|
|
1282
|
+
// Use modular LinkRenderer for semantic links
|
|
1283
|
+
// Renamed from 'link' to distinguish from zLink (inter-file navigation)
|
|
1284
|
+
const { renderLink } = await import('../primitives/link_primitives.js');
|
|
1285
|
+
// SEPARATION OF CONCERNS: Primitive renders element, orchestrator handles grouping
|
|
1286
|
+
element = renderLink(eventData, null, this.client, this.logger);
|
|
1287
|
+
this.logger.debug(`[renderZDisplayEvent] Rendered zURL: ${eventData.label}`);
|
|
1288
|
+
break;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
case 'navbar_inline': {
|
|
1292
|
+
// Block-level (in-page) navbar rendered inline. The server (message_walker
|
|
1293
|
+
// ._inline_block_navbars) builds the bar HTML via build_nav_html — the SAME
|
|
1294
|
+
// SSOT as the chrome navbar — and ships it as `html`. We inject it and wire
|
|
1295
|
+
// the generic data-nav-action delegation (hamburger/dropdown/navigate).
|
|
1296
|
+
// zPsi items are emitted as native `#anchor` links (no data-nav-action), so
|
|
1297
|
+
// the browser scrolls to the matching section _zId for free.
|
|
1298
|
+
const wrapper = document.createElement('div');
|
|
1299
|
+
wrapper.className = eventData._zClass || 'zNavbar-inline-wrap';
|
|
1300
|
+
wrapper.innerHTML = eventData.html || '';
|
|
1301
|
+
const navEl = wrapper.querySelector('nav');
|
|
1302
|
+
if (navEl) {
|
|
1303
|
+
try {
|
|
1304
|
+
const { NavBarBuilder } = await import('../../../L3_Abstraction/orchestrator/navbar_builder.js');
|
|
1305
|
+
NavBarBuilder.wireNavBarEvents(navEl, this.client, this.logger);
|
|
1306
|
+
} catch (err) {
|
|
1307
|
+
this.logger.warn('[renderZDisplayEvent] navbar_inline wiring skipped:', err);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
element = wrapper;
|
|
1311
|
+
this.logger.debug('[renderZDisplayEvent] Rendered navbar_inline');
|
|
1312
|
+
break;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
case 'zTable': {
|
|
1316
|
+
// Use modular TableRenderer for tables
|
|
1317
|
+
const tableRenderer = await this.client._ensureTableRenderer();
|
|
1318
|
+
// Give the renderer a client reference so _handleTableNavigation can send
|
|
1319
|
+
tableRenderer.client = this.client;
|
|
1320
|
+
element = tableRenderer.render(eventData);
|
|
1321
|
+
|
|
1322
|
+
// For interactive tables: replace existing DOM node with same instance ID (in-place navigation).
|
|
1323
|
+
// data-table-id is a unique per-render instance ID so multiple same-model tables
|
|
1324
|
+
// on one page each target their own DOM node correctly.
|
|
1325
|
+
if (element && element.getAttribute('data-interactive') === 'true') {
|
|
1326
|
+
const instanceId = element.getAttribute('data-table-id');
|
|
1327
|
+
if (instanceId) {
|
|
1328
|
+
const existing = document.querySelector(`[data-table-id="${instanceId}"][data-interactive="true"]`);
|
|
1329
|
+
if (existing) {
|
|
1330
|
+
existing.replaceWith(element);
|
|
1331
|
+
element = null; // Prevent double-append below
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
this.logger.debug(`[renderZDisplayEvent] Rendered table: ${eventData.title || 'untitled'}`);
|
|
1337
|
+
break;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
case 'progress_bar':
|
|
1341
|
+
case 'progress_complete': {
|
|
1342
|
+
// Declarative zProgress (expands to {event: progress_bar}). Build inline
|
|
1343
|
+
// and return the node — the same place/return contract as image/table —
|
|
1344
|
+
// so it renders in the page flow instead of self-appending to <body>.
|
|
1345
|
+
const progressRenderer = await this.client._ensureProgressBarRenderer();
|
|
1346
|
+
element = progressRenderer.renderInline(eventData);
|
|
1347
|
+
this.logger.debug('[renderZDisplayEvent] Rendered progress bar (inline)');
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
case 'list': {
|
|
1352
|
+
// Use modular ListRenderer for lists
|
|
1353
|
+
const listRenderer = await this.client._ensureListRenderer();
|
|
1354
|
+
element = listRenderer.render(eventData);
|
|
1355
|
+
this.logger.debug(`[renderZDisplayEvent] Rendered list: ${eventData.items?.length || 0} items`);
|
|
1356
|
+
break;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
case 'dl': {
|
|
1360
|
+
// Use DLRenderer for description lists
|
|
1361
|
+
const dlRenderer = await this.client._ensureDLRenderer();
|
|
1362
|
+
element = dlRenderer.render(eventData);
|
|
1363
|
+
this.logger.log(`[renderZDisplayEvent] Rendered description list with ${eventData.items?.length || 0} items`);
|
|
1364
|
+
break;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
case 'image': {
|
|
1368
|
+
// Use modular ImageRenderer for images
|
|
1369
|
+
const imageRenderer = await this.client._ensureImageRenderer();
|
|
1370
|
+
element = imageRenderer.render(eventData);
|
|
1371
|
+
this.logger.debug(`[renderZDisplayEvent] Rendered image: ${eventData.src}`);
|
|
1372
|
+
break;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
case 'video': {
|
|
1376
|
+
// Use modular VideoRenderer for inline <video controls>
|
|
1377
|
+
const videoRenderer = await this.client._ensureVideoRenderer();
|
|
1378
|
+
element = videoRenderer.render(eventData);
|
|
1379
|
+
this.logger.debug(`[renderZDisplayEvent] Rendered video: ${eventData.src}`);
|
|
1380
|
+
break;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
case 'audio': {
|
|
1384
|
+
// Use modular AudioRenderer for inline <audio controls>
|
|
1385
|
+
const audioRenderer = await this.client._ensureAudioRenderer();
|
|
1386
|
+
element = audioRenderer.render(eventData);
|
|
1387
|
+
this.logger.debug(`[renderZDisplayEvent] Rendered audio: ${eventData.src}`);
|
|
1388
|
+
break;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
case 'icon': {
|
|
1392
|
+
// Use modular IconRenderer for Bootstrap Icons
|
|
1393
|
+
const iconRenderer = await this.client._ensureIconRenderer();
|
|
1394
|
+
// Return the renderer's own node (bare <i>, or a styled <span>) directly —
|
|
1395
|
+
// no extra empty wrapper. This lets the container-unwrapper collapse a
|
|
1396
|
+
// redundant parent frame (e.g. a single-child zc-render Demo) by class match.
|
|
1397
|
+
element = iconRenderer.render(eventData);
|
|
1398
|
+
this.logger.debug(`[renderZDisplayEvent] Rendered icon: ${eventData.name}`);
|
|
1399
|
+
break;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
case 'card': {
|
|
1403
|
+
// Use modular CardRenderer for cards
|
|
1404
|
+
const cardRenderer = await this.client._ensureCardRenderer();
|
|
1405
|
+
element = cardRenderer.renderCard(eventData);
|
|
1406
|
+
this.logger.log('[renderZDisplayEvent] Rendered card element');
|
|
1407
|
+
break;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
case 'zCrumbs': {
|
|
1411
|
+
// Breadcrumb navigation (multi-trail support)
|
|
1412
|
+
this.logger.log('[renderZDisplayEvent] zCrumbs case hit! eventData:', eventData);
|
|
1413
|
+
const navRenderer = await this.client._ensureNavigationRenderer();
|
|
1414
|
+
this.logger.log('[renderZDisplayEvent] NavRenderer ready, calling renderBreadcrumbs...');
|
|
1415
|
+
element = navRenderer.renderBreadcrumbs(eventData);
|
|
1416
|
+
this.logger.debug('[renderZDisplayEvent] Rendered breadcrumbs');
|
|
1417
|
+
break;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
case 'zDash': {
|
|
1421
|
+
// Dashboard with sidebar navigation.
|
|
1422
|
+
// We pass parentElement so DashboardRenderer appends #dashboard-panel-content to the
|
|
1423
|
+
// DOM immediately — the default panel WS request fires INSIDE render(), and by the
|
|
1424
|
+
// time the WS response arrives the element must already be queryable.
|
|
1425
|
+
const DashboardRenderer = (await import('../composite/dashboard_renderer.js')).default;
|
|
1426
|
+
const dashRenderer = new DashboardRenderer(this.logger, this.client);
|
|
1427
|
+
await dashRenderer.render(eventData, parentElement);
|
|
1428
|
+
element = null; // already appended by DashboardRenderer
|
|
1429
|
+
this.logger.log('[renderZDisplayEvent] Rendered dashboard element');
|
|
1430
|
+
break;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
case 'read_string':
|
|
1434
|
+
case 'read_password': {
|
|
1435
|
+
// Delegate to InputEventHandler
|
|
1436
|
+
element = await this.inputEventHandler.handleTextInput(event, eventData, parentElement);
|
|
1437
|
+
break;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
case 'read_bool': {
|
|
1441
|
+
// Delegate to InputEventHandler
|
|
1442
|
+
element = await this.inputEventHandler.handleBoolInput(event, eventData, parentElement);
|
|
1443
|
+
break;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
case 'selection': {
|
|
1447
|
+
// Delegate to InputEventHandler
|
|
1448
|
+
element = await this.inputEventHandler.handleSelection(event, eventData, parentElement);
|
|
1449
|
+
break;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
case 'zTerminal': {
|
|
1453
|
+
if (!this.client._prismLoaded) {
|
|
1454
|
+
this.client._prismLoaded = true;
|
|
1455
|
+
this.client._loadPrismJS();
|
|
1456
|
+
}
|
|
1457
|
+
// Code execution sandbox with syntax highlighting and Run button
|
|
1458
|
+
const terminalRenderer = await this.client._ensureTerminalRenderer();
|
|
1459
|
+
element = terminalRenderer.render(eventData);
|
|
1460
|
+
this.logger.log(`[renderZDisplayEvent] Rendered zTerminal: ${eventData.title || 'untitled'}`);
|
|
1461
|
+
break;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
case 'error':
|
|
1465
|
+
case 'warning':
|
|
1466
|
+
case 'success':
|
|
1467
|
+
case 'info': {
|
|
1468
|
+
// zSignals — semantic status feedback (zTheme: zSignal + zSignal-*)
|
|
1469
|
+
const colorClass = getAlertColorClass(event);
|
|
1470
|
+
element = document.createElement('div');
|
|
1471
|
+
element.className = `zSignal ${colorClass}`;
|
|
1472
|
+
element.setAttribute('role', 'alert');
|
|
1473
|
+
element.textContent = eventData.content || '';
|
|
1474
|
+
if (eventData.indent > 0) {
|
|
1475
|
+
element.style.marginLeft = `${eventData.indent}rem`;
|
|
1476
|
+
}
|
|
1477
|
+
this.logger.log(`[renderZDisplayEvent] Rendered ${event} signal`);
|
|
1478
|
+
break;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
default: {
|
|
1482
|
+
this.logger.warn(`Unknown zDisplay event: ${event}`);
|
|
1483
|
+
const { createDiv } = await import('../primitives/generic_containers.js');
|
|
1484
|
+
element = createDiv({
|
|
1485
|
+
class: 'zDisplay-unknown'
|
|
1486
|
+
});
|
|
1487
|
+
element.textContent = `[${event}] ${eventData.content || ''}`;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// ── SSOT: universal _zClass / _zStyle for EVERY event ────────────────────
|
|
1492
|
+
// The same applyMetadata path zBlocks and zKeys use, in APPEND mode so the
|
|
1493
|
+
// renderer's intrinsic classes (zText, bi-*, zTable …) survive. This is the
|
|
1494
|
+
// single place a LEAF event's styling is applied; per-renderer _zClass/_zStyle
|
|
1495
|
+
// handling is retired in favour of this. `color` stays contextual
|
|
1496
|
+
// (zText-* vs zBtn-* vs zSignal-*) and remains owned by each renderer.
|
|
1497
|
+
//
|
|
1498
|
+
// Composite renderers (media-with-caption → <figure>, zTable → wrapper) return
|
|
1499
|
+
// a wrapper but intentionally place _zClass on an INNER element; they mark the
|
|
1500
|
+
// wrapper `__zMetaScoped` so we skip it here and don't mis-target the frame.
|
|
1501
|
+
if (element && element.nodeType === Node.ELEMENT_NODE && !element.__zMetaScoped && eventData && typeof eventData === 'object') {
|
|
1502
|
+
this.metadataProcessor.applyMetadata(
|
|
1503
|
+
element,
|
|
1504
|
+
{ _zClass: eventData._zClass, _zStyle: eventData._zStyle },
|
|
1505
|
+
null,
|
|
1506
|
+
this.logger,
|
|
1507
|
+
{ append: true }
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
return element;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// ─── zFunc execution ────────────────────────────────────────────────────────
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* Execute a @zfunc plugin call via WebSocket and render the result inline.
|
|
1518
|
+
*
|
|
1519
|
+
* Flow:
|
|
1520
|
+
* 1. Render a spinner placeholder in parentElement
|
|
1521
|
+
* 2. Send execute_zfunc { zfunc, requestId } over WebSocket
|
|
1522
|
+
* 3. If the plugin calls input(), the backend emits request_input with
|
|
1523
|
+
* zfuncRequestId — handled by _handleZFuncInput (renders inline widget)
|
|
1524
|
+
* 4. When the plugin finishes, backend sends execute_zfunc_response —
|
|
1525
|
+
* handled by _handleZFuncResponse (resolves the promise here)
|
|
1526
|
+
* 5. Replace spinner with result text (or error)
|
|
1527
|
+
*
|
|
1528
|
+
* @param {string} funcStr - Plugin invocation string, e.g. "&confirm.ask()"
|
|
1529
|
+
* @param {HTMLElement} parentElement - DOM node to append output into
|
|
1530
|
+
*/
|
|
1531
|
+
/**
|
|
1532
|
+
* Build a live CSS border-spinner row (zProgress type: spinner) with a ticking
|
|
1533
|
+
* elapsed readout. Mirrors SpinnerRenderer's markup (zSpinner-border + label)
|
|
1534
|
+
* so the look matches the streamed-spinner SSOT; color is pinned to the same
|
|
1535
|
+
* --color-* tokens the progress bar uses (so secondary/info/etc. tint reliably).
|
|
1536
|
+
* @returns {{bar: HTMLElement, ticker: number|null}}
|
|
1537
|
+
*/
|
|
1538
|
+
_buildSpinnerProgress(label, color) {
|
|
1539
|
+
const COLOR_VARS = {
|
|
1540
|
+
primary: '--color-primary', secondary: '--color-secondary',
|
|
1541
|
+
success: '--color-success', info: '--color-info',
|
|
1542
|
+
warning: '--color-warning', danger: '--color-error', error: '--color-error',
|
|
1543
|
+
};
|
|
1544
|
+
const row = document.createElement('div');
|
|
1545
|
+
row.className = 'zSpinner-container zD-flex zFlex-items-center zGap-2 zMy-2';
|
|
1546
|
+
|
|
1547
|
+
const spin = document.createElement('div');
|
|
1548
|
+
spin.className = `zSpinner-border zText-${color}`;
|
|
1549
|
+
spin.setAttribute('role', 'status');
|
|
1550
|
+
const varName = COLOR_VARS[String(color).toLowerCase()];
|
|
1551
|
+
if (varName) spin.style.color = `var(${varName})`;
|
|
1552
|
+
|
|
1553
|
+
const lbl = document.createElement('span');
|
|
1554
|
+
lbl.className = 'zSpinner-label zText-muted';
|
|
1555
|
+
lbl.textContent = label; // STEPS/percent only — never seconds
|
|
1556
|
+
|
|
1557
|
+
row.appendChild(spin);
|
|
1558
|
+
row.appendChild(lbl);
|
|
1559
|
+
return { bar: row };
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
async _executeZFunc(funcStr, parentElement, progressSpec = null) {
|
|
1563
|
+
if (!funcStr.startsWith('&')) {
|
|
1564
|
+
this.logger.warn('[ZFunc] Skipping non-plugin zFunc value:', funcStr);
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
const requestId = `zfunc-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
1569
|
+
|
|
1570
|
+
// Spinner placeholder — replaced when response arrives
|
|
1571
|
+
const wrapper = document.createElement('div');
|
|
1572
|
+
wrapper.className = 'zfunc-wrapper zmy-2';
|
|
1573
|
+
wrapper.dataset.zfuncRequestId = requestId;
|
|
1574
|
+
|
|
1575
|
+
// zProgress sibling → render the SAME probe count, two looks (no marquee, no
|
|
1576
|
+
// seconds — steps/percent only). The probe walks zDispatch → zFunc, so a
|
|
1577
|
+
// direct action is 2 stops: dispatch lands the moment we send, then the
|
|
1578
|
+
// plugin runs server-side as a black box until the response. So the bar sits
|
|
1579
|
+
// at 1/2 (50%) while it runs — exactly like zCLI — and clears to the result.
|
|
1580
|
+
// - type: bar (default) → a real, determinate progress bar.
|
|
1581
|
+
// - type: spinner → an animated glyph.
|
|
1582
|
+
const progressTicker = null;
|
|
1583
|
+
if (progressSpec) {
|
|
1584
|
+
const spec = (typeof progressSpec === 'object') ? progressSpec : {};
|
|
1585
|
+
const label = spec.label || 'Working…';
|
|
1586
|
+
const color = spec.color || 'primary';
|
|
1587
|
+
const ptype = String(spec.type || 'bar').toLowerCase();
|
|
1588
|
+
try {
|
|
1589
|
+
if (ptype === 'spinner') {
|
|
1590
|
+
const { bar } = this._buildSpinnerProgress(label, color);
|
|
1591
|
+
wrapper.appendChild(bar);
|
|
1592
|
+
} else {
|
|
1593
|
+
const progressRenderer = await this.client._ensureProgressBarRenderer();
|
|
1594
|
+
const bar = progressRenderer.renderInline({
|
|
1595
|
+
progressId: `zfunc-progress-${requestId}`,
|
|
1596
|
+
label, color,
|
|
1597
|
+
current: 1, // dispatch cleared
|
|
1598
|
+
total: 2, // zDispatch → zFunc (probe stops)
|
|
1599
|
+
striped: false,
|
|
1600
|
+
animated: false,
|
|
1601
|
+
showPercentage: true, // percent, never seconds
|
|
1602
|
+
});
|
|
1603
|
+
if (bar) wrapper.appendChild(bar);
|
|
1604
|
+
}
|
|
1605
|
+
} catch (err) {
|
|
1606
|
+
this.logger.warn('[ZFunc] Progress indicator unavailable:', err);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
if (!wrapper.firstChild) {
|
|
1611
|
+
const spinner = document.createElement('span');
|
|
1612
|
+
spinner.className = 'zText-muted zSmall';
|
|
1613
|
+
spinner.textContent = '⏳ Running…';
|
|
1614
|
+
wrapper.appendChild(spinner);
|
|
1615
|
+
}
|
|
1616
|
+
parentElement.appendChild(wrapper);
|
|
1617
|
+
|
|
1618
|
+
// Register wrapper reference for _handleZFuncInput (direct ref, not DOM query)
|
|
1619
|
+
this._pendingZFuncInputs.set(requestId, wrapper);
|
|
1620
|
+
|
|
1621
|
+
// Register resolve callback — _handleZFuncResponse will call it
|
|
1622
|
+
const responsePromise = new Promise((resolve) => {
|
|
1623
|
+
this._zfuncResolvers.set(requestId, resolve);
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
// Send execute_zfunc to backend
|
|
1627
|
+
try {
|
|
1628
|
+
this.client.connection.send(JSON.stringify({
|
|
1629
|
+
event: 'execute_zfunc',
|
|
1630
|
+
zfunc: funcStr,
|
|
1631
|
+
requestId,
|
|
1632
|
+
}));
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
this.logger.error('[ZFunc] Failed to send execute_zfunc:', err);
|
|
1635
|
+
if (progressTicker) clearInterval(progressTicker);
|
|
1636
|
+
wrapper.innerHTML = `<span class="zText-danger zSmall">⚠ Failed to start: ${err.message}</span>`;
|
|
1637
|
+
this._zfuncResolvers.delete(requestId);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// Wait for backend response (resolved by _handleZFuncResponse)
|
|
1642
|
+
const response = await responsePromise;
|
|
1643
|
+
|
|
1644
|
+
// Clean up pending reference
|
|
1645
|
+
this._pendingZFuncInputs.delete(requestId);
|
|
1646
|
+
|
|
1647
|
+
// Stop the elapsed-time ticker before the bar is removed with the wrapper
|
|
1648
|
+
if (progressTicker) clearInterval(progressTicker);
|
|
1649
|
+
|
|
1650
|
+
// Clear spinner / progress bar / input widget
|
|
1651
|
+
wrapper.innerHTML = '';
|
|
1652
|
+
|
|
1653
|
+
if (response.success) {
|
|
1654
|
+
if (response.result) {
|
|
1655
|
+
const out = document.createElement('p');
|
|
1656
|
+
out.className = 'zfunc-result zmy-1';
|
|
1657
|
+
out.textContent = response.result;
|
|
1658
|
+
wrapper.appendChild(out);
|
|
1659
|
+
}
|
|
1660
|
+
} else {
|
|
1661
|
+
const err = document.createElement('span');
|
|
1662
|
+
err.className = 'zText-danger zSmall';
|
|
1663
|
+
err.textContent = `⚠ ${response.error || 'Unknown error'}`;
|
|
1664
|
+
wrapper.appendChild(err);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// Return the plugin result so callers (e.g. _renderZMenuBlock) can decide
|
|
1668
|
+
// whether to show a zBack button or hide the content (bounce vs exit semantics)
|
|
1669
|
+
return response.success ? (response.result ?? null) : null;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/**
|
|
1673
|
+
* Handle a request_input event scoped to a zFunc execution.
|
|
1674
|
+
* Renders an inline prompt widget inside the matching zfunc-wrapper div.
|
|
1675
|
+
*
|
|
1676
|
+
* For y/n prompts: two buttons (Yes / No).
|
|
1677
|
+
* For all other prompts: text input + Submit button.
|
|
1678
|
+
*
|
|
1679
|
+
* @param {Object} msg - WebSocket message with { requestId, prompt, zfuncRequestId }
|
|
1680
|
+
*/
|
|
1681
|
+
_handleZFuncInput(msg) {
|
|
1682
|
+
const { requestId, prompt, zfuncRequestId } = msg;
|
|
1683
|
+
|
|
1684
|
+
// Find wrapper via stored reference (direct ref avoids DOM query failure when
|
|
1685
|
+
// the wrapper's ancestor container hasn't been appended to the document yet)
|
|
1686
|
+
const wrapper = this._pendingZFuncInputs.get(zfuncRequestId)
|
|
1687
|
+
|| document.querySelector(`[data-zfunc-request-id="${zfuncRequestId}"]`);
|
|
1688
|
+
if (!wrapper) {
|
|
1689
|
+
this.logger.warn('[ZFunc] No wrapper found for zfuncRequestId:', zfuncRequestId);
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
const sendResponse = (value) => {
|
|
1694
|
+
this.client.connection.send(JSON.stringify({
|
|
1695
|
+
event: 'input_response',
|
|
1696
|
+
requestId, // backend's input request_id (routes to _pending_inputs)
|
|
1697
|
+
value,
|
|
1698
|
+
}));
|
|
1699
|
+
// Replace widget with "answered" indicator
|
|
1700
|
+
inputArea.innerHTML = `<span class="zText-muted zSmall">↩ ${value}</span>`;
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1703
|
+
// Build input widget
|
|
1704
|
+
const inputArea = document.createElement('div');
|
|
1705
|
+
inputArea.className = 'zfunc-input-area zp-2 zmt-1';
|
|
1706
|
+
|
|
1707
|
+
const promptEl = document.createElement('p');
|
|
1708
|
+
promptEl.className = 'zSmall zmb-1';
|
|
1709
|
+
promptEl.textContent = prompt || 'Input required:';
|
|
1710
|
+
inputArea.appendChild(promptEl);
|
|
1711
|
+
|
|
1712
|
+
// Generic zInput — SSOT: stdin maps to a plain text field regardless of prompt content
|
|
1713
|
+
const row = document.createElement('div');
|
|
1714
|
+
row.className = 'zd-flex zg-2 zalign-items-center';
|
|
1715
|
+
|
|
1716
|
+
const textInput = document.createElement('input');
|
|
1717
|
+
textInput.type = 'text';
|
|
1718
|
+
textInput.className = 'zForm-control zForm-control-sm';
|
|
1719
|
+
textInput.placeholder = 'Enter value…';
|
|
1720
|
+
|
|
1721
|
+
const submitBtn = document.createElement('button');
|
|
1722
|
+
submitBtn.className = 'zBtn zBtn-sm zBtn-primary';
|
|
1723
|
+
submitBtn.textContent = 'Submit';
|
|
1724
|
+
|
|
1725
|
+
const submit = () => sendResponse(textInput.value.trim());
|
|
1726
|
+
|
|
1727
|
+
submitBtn.addEventListener('click', submit, { once: true });
|
|
1728
|
+
textInput.addEventListener('keydown', (e) => {
|
|
1729
|
+
if (e.key === 'Enter') submit();
|
|
1730
|
+
}, { once: true });
|
|
1731
|
+
|
|
1732
|
+
row.appendChild(textInput);
|
|
1733
|
+
row.appendChild(submitBtn);
|
|
1734
|
+
inputArea.appendChild(row);
|
|
1735
|
+
|
|
1736
|
+
// Replace spinner with input widget
|
|
1737
|
+
wrapper.innerHTML = '';
|
|
1738
|
+
wrapper.appendChild(inputArea);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
/**
|
|
1742
|
+
* Handle execute_zfunc_response from backend — resolves the promise in _executeZFunc.
|
|
1743
|
+
*
|
|
1744
|
+
* @param {Object} msg - WebSocket message with { requestId, success, result?, error? }
|
|
1745
|
+
*/
|
|
1746
|
+
_handleZFuncResponse(msg) {
|
|
1747
|
+
const resolve = this._zfuncResolvers.get(msg.requestId);
|
|
1748
|
+
if (!resolve) {
|
|
1749
|
+
this.logger.warn('[ZFunc] No resolver found for requestId:', msg.requestId);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
this._zfuncResolvers.delete(msg.requestId);
|
|
1753
|
+
resolve(msg);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
export default ZDisplayOrchestrator;
|
|
1759
|
+
|