@zolomedia/bifrost-client 1.7.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/L1_Foundation/L1_Foundation.js +13 -0
  2. package/L1_Foundation/bootstrap/bootstrap.js +11 -0
  3. package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
  4. package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
  5. package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
  6. package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
  7. package/L1_Foundation/bootstrap/module_registry.js +102 -0
  8. package/L1_Foundation/bootstrap/prism_loader.js +164 -0
  9. package/L1_Foundation/config/client_config.js +110 -0
  10. package/L1_Foundation/config/config.js +7 -0
  11. package/L1_Foundation/connection/connection.js +8 -0
  12. package/L1_Foundation/connection/websocket_connection.js +122 -0
  13. package/L1_Foundation/constants/bifrost_constants.js +284 -0
  14. package/L1_Foundation/constants/constants.js +7 -0
  15. package/L1_Foundation/logger/logger.js +10 -0
  16. package/L2_Handling/L2_Handling.js +15 -0
  17. package/L2_Handling/cache/cache.js +22 -0
  18. package/L2_Handling/cache/cache_constants.js +69 -0
  19. package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
  20. package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
  21. package/L2_Handling/cache/orchestration/orchestration.js +12 -0
  22. package/L2_Handling/cache/storage/session_manager.js +289 -0
  23. package/L2_Handling/cache/storage/storage.js +10 -0
  24. package/L2_Handling/cache/storage/storage_manager.js +590 -0
  25. package/L2_Handling/display/composite/composite.js +13 -0
  26. package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
  27. package/L2_Handling/display/composite/swiper_renderer.js +564 -0
  28. package/L2_Handling/display/composite/terminal_renderer.js +922 -0
  29. package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
  30. package/L2_Handling/display/display.js +30 -0
  31. package/L2_Handling/display/feedback/feedback.js +11 -0
  32. package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
  33. package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
  34. package/L2_Handling/display/inputs/button_renderer.js +634 -0
  35. package/L2_Handling/display/inputs/form_renderer.js +583 -0
  36. package/L2_Handling/display/inputs/input_renderer.js +658 -0
  37. package/L2_Handling/display/inputs/inputs.js +12 -0
  38. package/L2_Handling/display/navigation/menu_renderer.js +206 -0
  39. package/L2_Handling/display/navigation/navigation.js +11 -0
  40. package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
  41. package/L2_Handling/display/orchestration/orchestration.js +11 -0
  42. package/L2_Handling/display/orchestration/renderer.js +430 -0
  43. package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
  44. package/L2_Handling/display/outputs/alert_renderer.js +161 -0
  45. package/L2_Handling/display/outputs/audio_renderer.js +94 -0
  46. package/L2_Handling/display/outputs/card_renderer.js +229 -0
  47. package/L2_Handling/display/outputs/code_renderer.js +66 -0
  48. package/L2_Handling/display/outputs/dl_renderer.js +131 -0
  49. package/L2_Handling/display/outputs/header_renderer.js +162 -0
  50. package/L2_Handling/display/outputs/icon_renderer.js +107 -0
  51. package/L2_Handling/display/outputs/image_renderer.js +145 -0
  52. package/L2_Handling/display/outputs/list_renderer.js +190 -0
  53. package/L2_Handling/display/outputs/outputs.js +19 -0
  54. package/L2_Handling/display/outputs/table_renderer.js +765 -0
  55. package/L2_Handling/display/outputs/text_renderer.js +818 -0
  56. package/L2_Handling/display/outputs/typography_renderer.js +293 -0
  57. package/L2_Handling/display/outputs/video_renderer.js +116 -0
  58. package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
  59. package/L2_Handling/display/primitives/form_primitives.js +526 -0
  60. package/L2_Handling/display/primitives/generic_containers.js +109 -0
  61. package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
  62. package/L2_Handling/display/primitives/link_primitives.js +552 -0
  63. package/L2_Handling/display/primitives/lists_primitives.js +262 -0
  64. package/L2_Handling/display/primitives/media_primitives.js +383 -0
  65. package/L2_Handling/display/primitives/primitives.js +19 -0
  66. package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
  67. package/L2_Handling/display/primitives/table_primitives.js +528 -0
  68. package/L2_Handling/display/primitives/typography_primitives.js +175 -0
  69. package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
  70. package/L2_Handling/display/specialized/specialized.js +10 -0
  71. package/L2_Handling/hooks/hooks.js +9 -0
  72. package/L2_Handling/hooks/menu_integration.js +57 -0
  73. package/L2_Handling/hooks/widget_hook_manager.js +292 -0
  74. package/L2_Handling/message/message.js +8 -0
  75. package/L2_Handling/message/message_handler.js +701 -0
  76. package/L2_Handling/navigation/navigation.js +8 -0
  77. package/L2_Handling/navigation/navigation_manager.js +403 -0
  78. package/L2_Handling/zhooks/features/cache_live.js +287 -0
  79. package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
  80. package/L2_Handling/zhooks/zhooks_manager.js +65 -0
  81. package/L2_Handling/zvaf/zvaf.js +8 -0
  82. package/L2_Handling/zvaf/zvaf_manager.js +334 -0
  83. package/L3_Abstraction/L3_Abstraction.js +12 -0
  84. package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
  85. package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
  86. package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
  87. package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
  88. package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
  89. package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
  90. package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
  91. package/L3_Abstraction/renderer/renderer.js +1 -0
  92. package/L3_Abstraction/session/session.js +1 -0
  93. package/L4_Orchestration/L4_Orchestration.js +11 -0
  94. package/L4_Orchestration/client/client.js +1 -0
  95. package/L4_Orchestration/facade/facade.js +9 -0
  96. package/L4_Orchestration/facade/manager_registry.js +118 -0
  97. package/L4_Orchestration/facade/renderer_registry.js +274 -0
  98. package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
  99. package/L4_Orchestration/lifecycle/initializer.js +135 -0
  100. package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
  101. package/L4_Orchestration/rendering/facade.js +94 -0
  102. package/L4_Orchestration/rendering/rendering.js +7 -0
  103. package/LICENSE +21 -0
  104. package/README.md +82 -0
  105. package/bifrost_client.js +204 -0
  106. package/bifrost_core.js +1686 -0
  107. package/docs/ARCHITECTURE.md +111 -0
  108. package/docs/PROTOCOL.md +106 -0
  109. package/docs/RENDERERS.md +101 -0
  110. package/docs/SECURITY.md +92 -0
  111. package/package.json +24 -0
  112. package/syntax/prism-zconfig.js +41 -0
  113. package/syntax/prism-zenv.js +69 -0
  114. package/syntax/prism-zolo-theme.css +288 -0
  115. package/syntax/prism-zolo.js +380 -0
  116. package/syntax/prism-zschema.js +38 -0
  117. package/syntax/prism-zspark.js +25 -0
  118. package/syntax/prism-zui.js +68 -0
  119. package/zSys/accessibility/accessibility.js +10 -0
  120. package/zSys/accessibility/emoji_accessibility.js +173 -0
  121. package/zSys/dom/block_utils.js +122 -0
  122. package/zSys/dom/container_utils.js +370 -0
  123. package/zSys/dom/dom.js +13 -0
  124. package/zSys/dom/dom_utils.js +328 -0
  125. package/zSys/dom/encoding_utils.js +117 -0
  126. package/zSys/dom/style_utils.js +71 -0
  127. package/zSys/errors/error_display.js +299 -0
  128. package/zSys/errors/errors.js +10 -0
  129. package/zSys/theme/color_utils.js +274 -0
  130. package/zSys/theme/dark_mode_utils.js +272 -0
  131. package/zSys/theme/size_utils.js +256 -0
  132. package/zSys/theme/spacing_utils.js +405 -0
  133. package/zSys/theme/theme.js +14 -0
  134. package/zSys/theme/zbase.css +1735 -0
  135. package/zSys/theme/zbase_inject.js +161 -0
  136. package/zSys/theme/ztheme_utils.js +305 -0
  137. package/zSys/validation/error_boundary.js +201 -0
  138. package/zSys/validation/validation.js +11 -0
  139. package/zSys/validation/validation_utils.js +238 -0
  140. package/zSys/zSys.js +14 -0
@@ -0,0 +1,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
+