@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,1686 @@
1
+ /**
2
+ *
3
+ * BifrostClient - Production JavaScript Client for zBifrost
4
+ *
5
+ *
6
+ * A production-ready WebSocket client for zCLI's zBifrost bridge.
7
+ * Modular architecture with lazy loading and automatic zTheme integration.
8
+ *
9
+ * @version 1.6.7
10
+ * @author Gal Nachshon
11
+ * @license MIT
12
+ *
13
+ *
14
+ * Quick Start
15
+ *
16
+ *
17
+ * // Swiper-Style Elegance (One declaration, everything happens automatically):
18
+ * const client = new BifrostClient('ws://localhost:8765', {
19
+ * autoConnect: true, // Auto-connect on instantiation
20
+ * zTheme: true, // Enable zTheme CSS & rendering
21
+ * // targetElement: 'zVaF', // Optional: default is 'zVaF' (zView and Function)
22
+ * autoRequest: 'show_hello',// Auto-send on connect
23
+ * onConnected: (info) => this.logger.log('Connected!', info)
24
+ * });
25
+ *
26
+ * // Traditional (More control):
27
+ * const client = new BifrostClient('ws://localhost:8765', {
28
+ * zTheme: true,
29
+ * hooks: {
30
+ * onConnected: (info) => this.logger.log('Connected!'),
31
+ * onDisconnected: (reason) => this.logger.log('Disconnected:', reason),
32
+ * onMessage: (msg) => this.logger.log('Message:', msg),
33
+ * onError: (error) => this.logger.error('Error:', error)
34
+ * }
35
+ * });
36
+ * await client.connect();
37
+ * client.send({event: 'my_event'});
38
+ * const users = await client.read('users');
39
+ *
40
+ *
41
+ * Lazy Loading Architecture
42
+ *
43
+ *
44
+ * Modules are loaded dynamically only when needed:
45
+ * - Logger/Hooks: Loaded immediately (lightweight)
46
+ * - Connection: Loaded on connect() via WebSocketConnection
47
+ * - MessageHandler: Loaded on connect()
48
+ * - Renderers: Loaded via RendererRegistry (16 renderer types)
49
+ * - Managers: Loaded via ManagerRegistry (cache, zvaf, navigation, hooks)
50
+ * - ThemeLoader: Loaded on connect() if zTheme enabled
51
+ *
52
+ * Benefits:
53
+ * - CDN-friendly (no import resolution at load time)
54
+ * - Progressive loading (only load what you use)
55
+ * - Registry-based module loading (centralized, maintainable)
56
+ * - Stays modular (source files remain separate)
57
+ *
58
+ * Refactoring History:
59
+ * - Phase 9: Deep architectural realignment (L1-L4 + zSys layers)
60
+ * - Task 0 (Pre-NPM): Decomposition and registry consolidation
61
+ * - Step 1.2: Created RendererRegistry (16 renderers → 1 registry)
62
+ * - Step 1.3: Created ManagerRegistry (4 managers → 1 registry)
63
+ * - Step 1.5: Extracted WebSocketConnection to L1_Foundation
64
+ * - Result: 1587 LOC → 1442 LOC (-145 LOC, 9.1% reduction)
65
+ *
66
+ *
67
+ * TODO: Future Build System (v2.0+)
68
+ *
69
+ *
70
+ * Current: Hybrid UMD + ES modules (main file UMD, lazy-loaded modules ESM)
71
+ * - Main file: UMD wrapper allows plain <script> tag usage
72
+ * - Sub-modules: ES modules loaded via dynamic import()
73
+ * - Works in modern browsers (2017+) with ES module support
74
+ *
75
+ * Future: Add bundled UMD build for maximum compatibility
76
+ * - Goal: Support older browsers without ES module support
77
+ * - Implementation: Use Rollup/esbuild to create dist/bifrost.umd.js
78
+ * - Bundle all modules into single file with UMD wrapper
79
+ * - Trade-off: Larger file size, no lazy loading, but works everywhere
80
+ *
81
+ * Distribution strategy (v2.0+):
82
+ * - src/bifrost_client.js: Current hybrid approach (default, recommended)
83
+ * - dist/bifrost.esm.js: Pure ES module build (for modern bundlers)
84
+ * - dist/bifrost.umd.js: Fully bundled UMD (for legacy browsers)
85
+ *
86
+ * Package.json exports (v2.0+):
87
+ * {
88
+ * "main": "dist/bifrost.umd.js", // CommonJS/legacy default
89
+ * "module": "dist/bifrost.esm.js", // ES module for bundlers
90
+ * "browser": "src/bifrost_client.js", // Browser CDN (current hybrid)
91
+ * "exports": {
92
+ * ".": {
93
+ * "import": "./dist/bifrost.esm.js",
94
+ * "require": "./dist/bifrost.umd.js",
95
+ * "browser": "./src/bifrost_client.js"
96
+ * }
97
+ * }
98
+ * }
99
+ *
100
+ */
101
+
102
+ // bifrost_core.js — ES module. Loaded dynamically by bifrost_client.js bootstrap.
103
+ // Server controls which version is loaded via connection_info.bifrost_core_url (Phase 3B).
104
+
105
+ // Base URL derived from this module's own URL (works for dynamic import())
106
+ const BASE_URL = new URL('.', import.meta.url).href;
107
+
108
+ /**
109
+ * BifrostCore - Full WebSocket client, loaded dynamically by BifrostBootstrap.
110
+ * Exported as ES module so the bootstrap can import() it at runtime.
111
+ */
112
+ class BifrostCore {
113
+ /**
114
+ * Construct WebSocket URL from backend config or validate provided URL
115
+ * @private
116
+ */
117
+ _constructWebSocketURL(url) {
118
+ // Read zUI config from page FIRST (server-injected WebSocket SSL config)
119
+ let zuiConfig = {};
120
+ if (typeof document !== 'undefined' && !url) {
121
+ const zuiConfigEl = document.getElementById('zui-config');
122
+ if (zuiConfigEl) {
123
+ try {
124
+ zuiConfig = JSON.parse(zuiConfigEl.textContent);
125
+ } catch (e) {
126
+ // Store for logging after logger init
127
+ this._zuiConfigParseError = e;
128
+ }
129
+ }
130
+ }
131
+
132
+ // Auto-construct WebSocket URL from backend config (respects .zEnv SSL settings)
133
+ if (!url) {
134
+ const wsConfig = zuiConfig.websocket || {};
135
+ const protocol = wsConfig.ssl_enabled ? 'wss:' : 'ws:';
136
+ const wsHost = wsConfig.host || '127.0.0.1';
137
+ const wsPort = wsConfig.port || 8765;
138
+ url = `${protocol}//${wsHost}:${wsPort}`;
139
+ // Store for logging after logger init
140
+ this._autoConstructedUrl = { url, ssl: wsConfig.ssl_enabled };
141
+ }
142
+
143
+ // Validate URL
144
+ if (typeof url !== 'string' || url.trim() === '') {
145
+ throw new Error('BifrostClient: URL must be a non-empty string');
146
+ }
147
+ if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
148
+ throw new Error('BifrostClient: URL must start with ws:// or wss://');
149
+ }
150
+
151
+ return { url, zuiConfig };
152
+ }
153
+
154
+ /**
155
+ * Parse zUI config from page and merge with options
156
+ * @private
157
+ */
158
+ _parseZUIConfig(options, zuiConfigEarly, reconnectDelay, timeout) {
159
+ // Auto-read zUI config from page (server-injected zSession values)
160
+ let zuiConfig = {};
161
+ if (typeof document !== 'undefined') {
162
+ const zuiConfigEl = document.getElementById('zui-config');
163
+ if (zuiConfigEl) {
164
+ try {
165
+ zuiConfig = JSON.parse(zuiConfigEl.textContent);
166
+ if (this.logger) {
167
+ this.logger.debug('Auto-loaded zUI config from page', zuiConfig);
168
+ }
169
+ } catch (e) {
170
+ if (this.logger) {
171
+ this.logger.warn('Failed to parse zui-config:', e);
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ // Determine autoRequest based on zui-config
178
+ let autoRequest = options.autoRequest || null;
179
+
180
+ // If zBlock is specified (from zui-config or options), auto-generate walker execution request
181
+ const zBlock = options.zBlock || zuiConfig.zBlock || null;
182
+ const zVaFile = options.zVaFile || zuiConfig.zVaFile || null;
183
+ const zVaFolder = options.zVaFolder || zuiConfig.zVaFolder || null;
184
+
185
+ if (zBlock && !autoRequest) {
186
+ autoRequest = {
187
+ event: 'execute_walker',
188
+ zBlock: zBlock,
189
+ zVaFile: zVaFile,
190
+ zVaFolder: zVaFolder
191
+ };
192
+ // Store for logging after logger init
193
+ this._autoGeneratedRequest = autoRequest;
194
+ }
195
+
196
+ const parsedOptions = {
197
+ autoConnect: options.autoConnect || false,
198
+ zIcons: options.zIcons || false,
199
+ targetElement: options.targetElement || 'zVaF',
200
+ autoRequest: autoRequest,
201
+ autoReconnect: options.autoReconnect !== false,
202
+ reconnectDelay: reconnectDelay,
203
+ timeout: timeout,
204
+ debug: options.debug || false,
205
+ token: options.token || null,
206
+ hooks: options.hooks || {},
207
+ zVaFile: zVaFile,
208
+ zVaFolder: zVaFolder,
209
+ zBlock: zBlock,
210
+ title: options.title || zuiConfig.title || null,
211
+ brand: options.brand || zuiConfig.brand || null,
212
+ // zHooks: declarative, data-only feature toggles (see L2_Handling/zhooks).
213
+ // Opt-in capabilities the client ships — never code. Merge config over zui-config.
214
+ zHooks: { ...(zuiConfig.zHooks || {}), ...(options.zHooks || {}) }
215
+ };
216
+
217
+ return { zuiConfig, options: parsedOptions };
218
+ }
219
+
220
+ /**
221
+ * Create a new BifrostClient instance
222
+ * @param {string} url - WebSocket server URL (e.g., 'ws://localhost:8765')
223
+ * @param {Object} options - Configuration options
224
+ * @param {boolean} options.autoConnect - Auto-connect on instantiation (default: false)
225
+ * @param {boolean} options.zTheme - Load zTheme CSS + JS from CDN (default: false)
226
+ * @param {string} options.zThemeCDN - CDN base URL for zTheme (default: jsdelivr ZoloAi/zTheme)
227
+ * Note: Bootstrap Icons and Prism.js are ALWAYS loaded automatically (unchangeable defaults)
228
+ * @param {string} options.targetElement - Target DOM selector for rendering (default: 'zVaF')
229
+ * @param {string|Object} options.autoRequest - Auto-send request on connect (event name or full request object)
230
+ * @param {boolean} options.autoReconnect - Auto-reconnect on disconnect (default: true)
231
+ * @param {number} options.reconnectDelay - Delay between reconnect attempts in ms (default: 3000 = TIMEOUTS.RECONNECT_DELAY)
232
+ * @param {number} options.timeout - Request timeout in ms (default: 30000 = TIMEOUTS.REQUEST_TIMEOUT)
233
+ * @param {boolean} options.debug - Enable debug logging (default: false)
234
+ * @param {string} options.token - Authentication token (optional)
235
+ * @param {Object} options.hooks - Event hooks for customization
236
+ */
237
+ constructor(url, options = {}) {
238
+ // Parse zUI config and construct WebSocket URL if needed
239
+ const { url: finalUrl, zuiConfig: zuiConfigEarly } = this._constructWebSocketURL(url);
240
+ this.url = finalUrl;
241
+
242
+ // Validate and set options (using bifrost_constants defaults)
243
+ // NOTE: UMD module limitation - cannot use top-level imports
244
+ // These constants mirror bifrost_constants.js TIMEOUTS (SSOT)
245
+ const RECONNECT_DELAY_DEFAULT = 3000; // TIMEOUTS.RECONNECT_DELAY
246
+ const REQUEST_TIMEOUT_DEFAULT = 30000; // TIMEOUTS.REQUEST_TIMEOUT
247
+
248
+ const reconnectDelay = options.reconnectDelay || RECONNECT_DELAY_DEFAULT;
249
+ const timeout = options.timeout || REQUEST_TIMEOUT_DEFAULT;
250
+
251
+ if (typeof reconnectDelay !== 'number' || reconnectDelay <= 0) {
252
+ throw new Error('BifrostClient: reconnectDelay must be a positive number');
253
+ }
254
+ if (typeof timeout !== 'number' || timeout <= 0) {
255
+ throw new Error('BifrostClient: timeout must be a positive number');
256
+ }
257
+
258
+ // Parse zUI config and build options
259
+ const { zuiConfig, options: parsedOptions } = this._parseZUIConfig(options, zuiConfigEarly, reconnectDelay, timeout);
260
+ this.zuiConfig = zuiConfig;
261
+ this.options = parsedOptions;
262
+
263
+ // Module cache (lazy loaded)
264
+ this._modules = {};
265
+ this._baseUrl = BASE_URL;
266
+ this.renderingFacade = null; // Phase 5.1: Lazy-loaded rendering facade
267
+ this.initializer = null; // Phase 5.2: Lazy-loaded initializer
268
+ this.assetLoader = null; // Phase 5.4: Lazy-loaded asset loader
269
+ this.rendererRegistry = null; // Task 0.2: Lazy-loaded renderer registry
270
+ this.managerRegistry = null; // Task 0.3: Lazy-loaded manager registry
271
+
272
+ // Pre-initialize lightweight modules synchronously (MUST BE FIRST - initializes logger)
273
+ this._initLightweightModules();
274
+
275
+ // Log early bootstrap info now that logger is ready
276
+ if (this._zuiConfigParseError) {
277
+ this.logger.warn('Failed to parse zui-config:', this._zuiConfigParseError);
278
+ }
279
+ if (this._autoConstructedUrl) {
280
+ this.logger.info('Auto-constructed WebSocket URL: %s (SSL: %s)', this._autoConstructedUrl.url, this._autoConstructedUrl.ssl);
281
+ }
282
+ if (this._autoGeneratedRequest) {
283
+ this.logger.info('Auto-generated walker request from zui-config', this._autoGeneratedRequest);
284
+ }
285
+
286
+ // v1.6.0: Initialize cache system (async, must complete before connect)
287
+ this.cache = null;
288
+ this.session = null;
289
+ this.storage = null;
290
+ this._cacheReady = this._initCacheSystem().then(() => {
291
+ // Register hooks after cache is initialized
292
+ this._registerCacheHooks();
293
+ this.logger.debug('[Cache] Ready for connection');
294
+ }).catch(err => {
295
+ this.logger.error('[Cache] Initialization failed:', err);
296
+ // Non-fatal: allow connection without cache
297
+ });
298
+
299
+ // Debug: confirm which declarative UI options were actually received
300
+ this.logger.debug('Init options:', {
301
+ targetElement: this.options.targetElement,
302
+ zVaFile: this.options.zVaFile,
303
+ zVaFolder: this.options.zVaFolder,
304
+ zBlock: this.options.zBlock
305
+ });
306
+
307
+ // Inject structural baseline CSS + expose window.zTheme (always-on, no flag)
308
+ // Store promise so connect() waits for CSS before first render (prevents
309
+ // race condition where UI renders before zbase.css is fetched from CDN).
310
+ this._zbaseReady = this._injectZBase();
311
+
312
+ // Bootstrap Icons are ALWAYS loaded (unchangeable default for zBifrost)
313
+ // Phase 2: Extracted to src/bootstrap/cdn_loader.js
314
+ this._loadBootstrapIcons();
315
+
316
+ // Prism.js is lazy-loaded on first code/zTerminal render — not on connect
317
+ this._prismLoaded = false;
318
+
319
+ // _zScripts: load immediately (same timing as Prism) so intercept plugins
320
+ // are active before any user interaction. asset_loader.js dedup guard
321
+ // prevents double-injection if widget_hook_manager's fallback also fires.
322
+ this._loadZScripts();
323
+ this._zScriptsLoaded = true;
324
+
325
+ // v1.6.0: Initialize zVaF elements (now synchronous - elements exist in HTML)
326
+ // Just populate content, don't create structure
327
+ this._initZVaFElements();
328
+
329
+ // zHooks: activate declared opt-in features (data flags, never code).
330
+ // Non-blocking; each feature is responsible for waiting on the connection.
331
+ this._initZHooks();
332
+
333
+ // Walker mode: all pages use execute_walker (server-side rendering via WebSocket)
334
+ if (this.options.autoRequest && this.options.autoRequest.event === 'execute_walker') {
335
+ this.logger.debug('Walker mode detected');
336
+ }
337
+
338
+ // Auto-connect if requested (Swiper-style elegance!)
339
+ // v1.6.0: Wait for cache initialization before connecting (zVaF elements are now sync)
340
+ if (this.options.autoConnect) {
341
+ this._cacheReady.finally(() => {
342
+ this.logger.debug('[Cache] Ready, connecting...');
343
+ // Await CSS injection so zbase.css is applied before the first render
344
+ (this._zbaseReady || Promise.resolve()).then(() => {
345
+ this.connect().catch(err => {
346
+ this.logger.error('Auto-connect failed:', err);
347
+ this.hooks.call('onError', { type: 'autoconnect_failed', error: err });
348
+ });
349
+ });
350
+ });
351
+ }
352
+
353
+ // Part 2: Browser lifecycle awareness - cleanup on page unload
354
+ // Track if we're doing client-side navigation (to avoid false page_unload events)
355
+ this._isClientSideNav = false;
356
+
357
+ window.addEventListener('beforeunload', (_e) => {
358
+ // Only send page_unload if this is a real page unload (not client-side nav)
359
+ if (this._isClientSideNav) {
360
+ this.logger.debug('[Lifecycle] Client-side nav detected, skipping page_unload');
361
+ this._isClientSideNav = false;
362
+ return;
363
+ }
364
+
365
+ this.logger.debug('[Lifecycle] Page unloading, notifying backend');
366
+ // Send cleanup notification (best effort - may not complete if page closes quickly)
367
+ if (this.connection && this.connection.isConnected()) {
368
+ try {
369
+ this.connection.send(JSON.stringify({
370
+ event: 'page_unload',
371
+ reason: 'navigation',
372
+ timestamp: Date.now()
373
+ }));
374
+ } catch (err) {
375
+ // Ignore errors during unload (connection might already be closing)
376
+ this.logger.warn('Could not send page_unload message:', err);
377
+ }
378
+ }
379
+ });
380
+ }
381
+
382
+ /**
383
+ * Activate declared zHooks — opt-in, data-only client features.
384
+ *
385
+ * zHooks are feature toggles (booleans), not callbacks. The manager imports
386
+ * each enabled feature module and calls its activate(client). Failures are
387
+ * isolated per-feature and never block boot.
388
+ */
389
+ _initZHooks() {
390
+ const config = this.options.zHooks;
391
+ if (!config || !Object.keys(config).length) return;
392
+ import(`${BASE_URL}L2_Handling/zhooks/zhooks_manager.js`)
393
+ .then(({ activateZHooks }) => activateZHooks(this, config, BASE_URL))
394
+ .catch(err => this.logger.error('[zHooks] Manager load failed:', err));
395
+ }
396
+
397
+ /**
398
+ * Initialize lightweight modules that don't require imports
399
+ *
400
+ * NOTE: Phase 2 Refactor - Logger/Hooks extracted to bootstrap/ directory
401
+ * Kept inline here due to UMD module limitations (cannot import ES modules at top level)
402
+ * See: src/bootstrap/bootstrap_logger.js and src/bootstrap/bootstrap_hooks.js
403
+ */
404
+ _initLightweightModules() {
405
+ // Determine log level based on deployment environment
406
+ const logLevel = this.zuiConfig?.deployment === 'Production' ? 'WARN' : 'INFO';
407
+
408
+ // Bootstrap Logger (inline due to UMD constraints)
409
+ // Extracted version: src/bootstrap/bootstrap_logger.js
410
+ this.logger = {
411
+ levels: { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 },
412
+ level: logLevel === 'DEBUG' ? 0 : logLevel === 'INFO' ? 1 : logLevel === 'WARN' ? 2 : 3,
413
+ context: 'Bifrost',
414
+ _interpolate: (message, args) => {
415
+ if (args.length === 0) return message;
416
+
417
+ // Support Python-style %s interpolation
418
+ if (message.includes('%s')) {
419
+ let result = message;
420
+ args.forEach(arg => {
421
+ result = result.replace('%s', String(arg));
422
+ });
423
+ return result;
424
+ }
425
+
426
+ return message;
427
+ },
428
+ _formatMessage: (level, message, args = []) => {
429
+ const interpolated = this.logger._interpolate(message, args);
430
+
431
+ // ANSI color codes for browser console
432
+ const colors = {
433
+ debug: '\x1b[90m', // Gray for DEBUG
434
+ info: '\x1b[34m', // Blue for INFO
435
+ warn: '\x1b[33m', // Yellow for WARN
436
+ error: '\x1b[91m', // Bright red for ERROR
437
+ message: '\x1b[38;2;255;251;203m', // Cream #fffbcb for message text
438
+ bold: '\x1b[1m', // Bold
439
+ reset: '\x1b[0m'
440
+ };
441
+
442
+ const levelColor = colors[level.toLowerCase()] || colors.info;
443
+
444
+ return `${colors.bold}${levelColor}[${level}]${colors.reset}: ${colors.message}${interpolated}${colors.reset}`;
445
+ },
446
+ debug: (message, ...args) => {
447
+ if (this.logger.level <= this.logger.levels.DEBUG) {
448
+ const formatted = this.logger._formatMessage('DEBUG', message, args);
449
+ console.debug(formatted, ...args.filter(arg => typeof arg === 'object'));
450
+ }
451
+ },
452
+ info: (message, ...args) => {
453
+ if (this.logger.level <= this.logger.levels.INFO) {
454
+ const formatted = this.logger._formatMessage('INFO', message, args);
455
+ console.info(formatted, ...args.filter(arg => typeof arg === 'object'));
456
+ }
457
+ },
458
+ log: (message, ...args) => {
459
+ if (this.logger.level <= this.logger.levels.INFO) {
460
+ const formatted = this.logger._formatMessage('INFO', message, args);
461
+ console.log(formatted, ...args.filter(arg => typeof arg === 'object'));
462
+ }
463
+ },
464
+ error: (message, ...args) => {
465
+ // Always show errors, regardless of debug mode
466
+ const formatted = this.logger._formatMessage('ERROR', message, args);
467
+ console.error(formatted, ...args.filter(arg => typeof arg === 'object'));
468
+
469
+ // Also show in ErrorDisplay for user-facing errors (if initialized)
470
+ if (this.errorDisplay && this.options.showErrors !== false) {
471
+ // Extract error object if present in args
472
+ const errorObj = args.find(arg => arg instanceof Error);
473
+ this.errorDisplay.show({
474
+ title: 'Error',
475
+ message: message,
476
+ error: errorObj || new Error(message),
477
+ timestamp: new Date().toISOString()
478
+ });
479
+ }
480
+ },
481
+ warn: (message, ...args) => {
482
+ if (this.logger.level <= this.logger.levels.WARN) {
483
+ const formatted = this.logger._formatMessage('WARN', message, args);
484
+ console.warn(formatted, ...args.filter(arg => typeof arg === 'object'));
485
+ }
486
+ },
487
+ setLevel: (level) => {
488
+ const levelMap = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
489
+ this.logger.level = levelMap[level] || levelMap.INFO;
490
+ },
491
+ enable: () => {
492
+ this.logger.level = this.logger.levels.DEBUG;
493
+ },
494
+ disable: () => {
495
+ this.logger.level = this.logger.levels.ERROR;
496
+ },
497
+ isEnabled: () => {
498
+ return this.logger.level <= this.logger.levels.INFO;
499
+ }
500
+ };
501
+
502
+ // Bootstrap Hooks (inline due to UMD constraints)
503
+ // Extracted version: src/bootstrap/bootstrap_hooks.js
504
+ this.hooks = {
505
+ hooks: this.options.hooks || {},
506
+ errorHandler: null, // Set by _initErrorDisplay()
507
+ call: (hookName, ...args) => {
508
+ const hook = this.hooks.hooks[hookName];
509
+ this.logger.debug(`[Hooks] Calling hook: ${hookName}`);
510
+ if (typeof hook === 'function') {
511
+ try {
512
+ return hook(...args);
513
+ } catch (error) {
514
+ // Log to console
515
+ this.logger.error(`Error in ${hookName} hook:`, error);
516
+
517
+ // Log via logger
518
+ this.logger.error(`Error in ${hookName} hook:`, error);
519
+
520
+ // Display in UI if error handler is set
521
+ if (this.hooks.errorHandler) {
522
+ try {
523
+ this.hooks.errorHandler({
524
+ type: 'hook_error',
525
+ hookName,
526
+ error,
527
+ message: error.message,
528
+ stack: error.stack
529
+ });
530
+ } catch (displayError) {
531
+ this.logger.error('Error handler itself failed:', displayError);
532
+ }
533
+ }
534
+
535
+ // Call onError hook if it exists and isn't the one that failed
536
+ if (hookName !== 'onError' && this.hooks.hooks.onError) {
537
+ try {
538
+ this.hooks.hooks.onError(error);
539
+ } catch (onErrorError) {
540
+ this.logger.error('onError hook failed:', onErrorError);
541
+ }
542
+ }
543
+ }
544
+ }
545
+ },
546
+ has: (hookName) => {
547
+ return typeof this.hooks.hooks[hookName] === 'function';
548
+ },
549
+ register: (hookName, fn) => {
550
+ if (typeof fn === 'function') {
551
+ this.hooks.hooks[hookName] = fn;
552
+ this.logger.debug(`[Hooks] Registered hook: ${hookName}`);
553
+ } else {
554
+ this.logger.error(`[Hooks] [ERROR] Failed to register hook ${hookName}: not a function`);
555
+ }
556
+ },
557
+ unregister: (hookName) => {
558
+ delete this.hooks.hooks[hookName];
559
+ },
560
+ list: () => Object.keys(this.hooks.hooks),
561
+
562
+ // Dark mode utilities
563
+ initBuiltInHooks: () => {
564
+ // Initialize dark mode from localStorage
565
+ const savedTheme = localStorage.getItem('zTheme-mode');
566
+ if (savedTheme === 'dark') {
567
+ this.hooks._applyDarkMode(true);
568
+ }
569
+ },
570
+
571
+ _applyDarkMode: async (isDark) => {
572
+ // Use dark mode utility (Layer 2) - eliminates 100+ lines of duplicate code
573
+ const { applyDarkModeClasses } = await import(`${BASE_URL}utils/dark_mode_utils.js`);
574
+ applyDarkModeClasses(isDark, {
575
+ contentArea: this._zVaFElement,
576
+ logger: this.logger
577
+ });
578
+ },
579
+
580
+ addDarkModeToggle: async (navElement) => {
581
+ // Use DarkModeToggle widget (extracted for modularity)
582
+ const { DarkModeToggle } = await import(`${BASE_URL}widgets/dark_mode_toggle.js`);
583
+ const darkModeWidget = new DarkModeToggle(this.logger);
584
+
585
+ // Create toggle with theme change callback
586
+ darkModeWidget.create(navElement, (newTheme) => {
587
+ // Apply theme
588
+ this.hooks._applyDarkMode(newTheme === 'dark');
589
+
590
+ // Call onThemeChange hook if registered
591
+ this.hooks.call('onThemeChange', newTheme);
592
+ });
593
+ }
594
+ };
595
+
596
+ // Initialize built-in hooks (dark mode)
597
+ this.hooks.initBuiltInHooks();
598
+
599
+ // Register default widget hooks (Week 4.2)
600
+ this._registerDefaultWidgetHooks();
601
+
602
+ this.logger.info('BifrostClient initialized', { url: this.url, options: this.options });
603
+ }
604
+
605
+ /**
606
+ * Register default hooks for widget events
607
+ *
608
+ * Registers hooks for progress bars, spinners, and swipers.
609
+ * These hooks use the new modular renderer architecture.
610
+ */
611
+ // Phase 5.2: Hook registration delegated to Initializer
612
+ _registerDefaultWidgetHooks() {
613
+ // Kept for backward compatibility - no-op
614
+ }
615
+
616
+ async _registerCacheHooks() {
617
+ await this._ensureInitializer();
618
+ return this.initializer.registerCacheHooks();
619
+ }
620
+
621
+ async _disableForms() {
622
+ await this._ensureInitializer();
623
+ return this.initializer.disableForms();
624
+ }
625
+
626
+ async _enableForms() {
627
+ await this._ensureInitializer();
628
+ return this.initializer.enableForms();
629
+ }
630
+
631
+ /**
632
+ * Initialize cache system (v1.6.0)
633
+ * Loads StorageManager, SessionManager, and CacheOrchestrator
634
+ * @private
635
+ */
636
+ async _initCacheSystem() {
637
+ await this._ensureCacheManager();
638
+ return this.cacheManager.initCacheSystem();
639
+ }
640
+
641
+ /**
642
+ * Dynamically load a script (v1.6.0)
643
+ * @private
644
+ */
645
+ async _loadScript(src) {
646
+ await this._ensureCacheManager();
647
+ return this.cacheManager.loadScript(src);
648
+ }
649
+
650
+ /**
651
+ * Initialize zVaF elements (connection badges, dynamic content)
652
+ *
653
+ * DECLARATIVE APPROACH: The zVaF element is an empty canvas.
654
+ * This method populates it entirely, including connection badge and content area.
655
+ */
656
+ /**
657
+ * Inject structural baseline CSS + expose window.zTheme (always-on).
658
+ * Replaces the old opt-in zTheme CDN loader.
659
+ * @private
660
+ */
661
+ async _injectZBase() {
662
+ try {
663
+ const { injectZBase } = await import(`${BASE_URL}zSys/theme/zbase_inject.js`);
664
+ await injectZBase(BASE_URL);
665
+ } catch (err) {
666
+ this.logger?.warn('[BifrostClient] zbase_inject failed:', err.message);
667
+ }
668
+ }
669
+
670
+ /**
671
+ * Load _zScripts from YAML metadata (plugin scripts)
672
+ * @private
673
+ */
674
+ async _loadZScripts() {
675
+ await this._ensureAssetLoader();
676
+ return this.assetLoader.loadZScripts();
677
+ }
678
+
679
+ /**
680
+ * Load Bootstrap Icons from CDN (ALWAYS loaded, unchangeable default)
681
+ * Phase 2: Extracted to src/bootstrap/cdn_loader.js
682
+ * @private
683
+ */
684
+ async _loadBootstrapIcons() {
685
+ const { loadBootstrapIcons } = await import(`${BASE_URL}L1_Foundation/bootstrap/cdn_loader.js`);
686
+ await loadBootstrapIcons(undefined, this.logger);
687
+ }
688
+
689
+ /**
690
+ * Load Prism.js from CDN for syntax highlighting.
691
+ * Lazy — called on first code/json/zTerminal render, not on connect().
692
+ * Guard: caller sets this._prismLoaded = true before calling.
693
+ * @private
694
+ */
695
+ async _loadPrismJS() {
696
+ await this._ensureAssetLoader();
697
+ return this.assetLoader.loadPrismJS();
698
+ }
699
+
700
+ /**
701
+ * Load custom .zolo language definitions for Prism.js
702
+ * Phase 5.4: Delegated to AssetLoader
703
+ * @private
704
+ */
705
+ async _loadPrismZolo() {
706
+ await this._ensureAssetLoader();
707
+ return this.assetLoader.loadPrismZolo();
708
+ }
709
+
710
+ /**
711
+ * Load and render declarative UI from zVaFile (client-side YAML parsing)
712
+ * @private
713
+ */
714
+ /**
715
+ * Render a zVaF block declaratively (convert YAML to DOM)
716
+ * @private
717
+ */
718
+ // Phase 5.1: Rendering methods delegated to RenderingFacade
719
+ async _renderBlock(blockData) {
720
+ await this._ensureRenderingFacade();
721
+ return this.renderingFacade.renderBlock(blockData);
722
+ }
723
+
724
+ async _renderChunkProgressive(message) {
725
+ await this._ensureRenderingFacade();
726
+ return this.renderingFacade.renderChunkProgressive(message);
727
+ }
728
+
729
+ async _renderItems(data, parentElement) {
730
+ await this._ensureRenderingFacade();
731
+ return this.renderingFacade.renderItems(data, parentElement);
732
+ }
733
+
734
+ async _createContainer(zKey, metadata) {
735
+ await this._ensureRenderingFacade();
736
+ return this.renderingFacade.createContainer(zKey, metadata);
737
+ }
738
+
739
+ async _renderMetaNavBarHTML(items) {
740
+ await this._ensureRenderingFacade();
741
+ return this.renderingFacade.renderMetaNavBarHTML(items);
742
+ }
743
+
744
+ async _renderNavBar(items, parentElement) {
745
+ await this._ensureRenderingFacade();
746
+ return this.renderingFacade.renderNavBar(items, parentElement);
747
+ }
748
+
749
+ async _renderZDisplayEvent(eventData) {
750
+ await this._ensureRenderingFacade();
751
+ return this.renderingFacade.renderZDisplayEvent(eventData);
752
+ }
753
+
754
+ /**
755
+ * Initialize zVaF-specific DOM elements (connection badges, etc.)
756
+ * @private
757
+ */
758
+ /**
759
+ * Initialize zVaF elements (v1.6.0: Simplified - elements exist in HTML, just populate)
760
+ *
761
+ * HTML structure (declared in zVaF.html):
762
+ * <zBifrostBadge></zBifrostBadge> ← Dynamic, always fresh
763
+ * <zNavBar></zNavBar> ← Dynamic, RBAC-aware
764
+ * <zVaF>...</zVaF> ← Cacheable content area
765
+ */
766
+ // Phase 5.2: Initialization methods delegated to Initializer
767
+ async _initZVaFElements() {
768
+ await this._ensureInitializer();
769
+ return this.initializer.initZVaFElements();
770
+ }
771
+
772
+ async _populateConnectionBadge() {
773
+ await this._ensureInitializer();
774
+ return this.initializer.populateConnectionBadge();
775
+ }
776
+
777
+ async _updateBadgeState(state) {
778
+ await this._ensureInitializer();
779
+ return this.initializer.updateBadgeState(state);
780
+ }
781
+
782
+ async _updateRenderState(opts) {
783
+ await this._ensureInitializer();
784
+ return this.initializer.updateRenderState(opts);
785
+ }
786
+
787
+ async _populateNavBar() {
788
+ await this._ensureInitializer();
789
+ return this.initializer.populateNavBar();
790
+ }
791
+
792
+ async _fetchAndPopulateNavBar(navHtmlFromServer = null) {
793
+ await this._ensureInitializer();
794
+ return this.initializer.fetchAndPopulateNavBar(navHtmlFromServer);
795
+ }
796
+
797
+ async _enableClientSideNavigation() {
798
+ await this._ensureInitializer();
799
+ return this.initializer.enableClientSideNavigation();
800
+ }
801
+
802
+ async _navigateToRoute(routePath, options = {}) {
803
+ await this._ensureInitializer();
804
+ return this.initializer.navigateToRoute(routePath, options);
805
+ }
806
+
807
+ async _snapshotCurrentPage() {
808
+ await this._ensureInitializer();
809
+ return this.initializer.snapshotCurrentPage();
810
+ }
811
+
812
+
813
+ /**
814
+ * Lazy load a module
815
+ * @param {string} moduleName - Name of the module (connection, message_handler, renderer, zdisplay_renderer)
816
+ * @returns {Promise<any>}
817
+ */
818
+ /**
819
+ * Ensure RenderingFacade is initialized (Phase 5.1)
820
+ * @private
821
+ */
822
+ async _ensureRenderingFacade() {
823
+ if (!this.renderingFacade) {
824
+ const { RenderingFacade } = await import(`${BASE_URL}L4_Orchestration/rendering/facade.js`);
825
+ this.renderingFacade = new RenderingFacade(this);
826
+ }
827
+ return this.renderingFacade;
828
+ }
829
+
830
+ /**
831
+ * Ensure Initializer is loaded (Phase 5.2)
832
+ * @private
833
+ */
834
+ async _ensureInitializer() {
835
+ if (!this.initializer) {
836
+ const { Initializer } = await import(`${BASE_URL}L4_Orchestration/lifecycle/initializer.js`);
837
+ this.initializer = new Initializer(this);
838
+ }
839
+ return this.initializer;
840
+ }
841
+
842
+ async _ensureAssetLoader() {
843
+ if (!this.assetLoader) {
844
+ const { AssetLoader } = await import(`${BASE_URL}L4_Orchestration/lifecycle/asset_loader.js`);
845
+ this.assetLoader = new AssetLoader(this);
846
+ }
847
+ return this.assetLoader;
848
+ }
849
+
850
+ /**
851
+ * Ensure RendererRegistry is loaded (Task 0.2)
852
+ * @private
853
+ */
854
+ async _ensureRendererRegistry() {
855
+ if (!this.rendererRegistry) {
856
+ const { RendererRegistry } = await import(`${BASE_URL}L4_Orchestration/facade/renderer_registry.js`);
857
+ this.rendererRegistry = new RendererRegistry(this);
858
+ }
859
+ return this.rendererRegistry;
860
+ }
861
+
862
+ /**
863
+ * Ensure ManagerRegistry is loaded (Task 0.3)
864
+ * @private
865
+ */
866
+ async _ensureManagerRegistry() {
867
+ if (!this.managerRegistry) {
868
+ const { ManagerRegistry } = await import(`${BASE_URL}L4_Orchestration/facade/manager_registry.js`);
869
+ this.managerRegistry = new ManagerRegistry(this);
870
+ }
871
+ return this.managerRegistry;
872
+ }
873
+
874
+ async _loadModule(moduleName) {
875
+ if (this._modules[moduleName]) {
876
+ return this._modules[moduleName];
877
+ }
878
+
879
+ // Phase 9: Use L1 Foundation MODULE_REGISTRY for SSOT module paths
880
+ // Lazy-load the registry (ES module import in UMD context)
881
+ if (!this._moduleRegistry) {
882
+ const registryModule = await import(`${BASE_URL}L1_Foundation/bootstrap/module_registry.js`);
883
+ this._moduleRegistry = registryModule.MODULE_REGISTRY;
884
+ this._getModulePath = registryModule.getModulePath;
885
+ }
886
+
887
+ // Get module path from registry
888
+ const modulePath = this._getModulePath(moduleName);
889
+ if (!modulePath) {
890
+ this.logger.error(`Module not found in registry: ${moduleName}`);
891
+ throw new Error(`Unknown BifrostClient module: ${moduleName}`);
892
+ }
893
+
894
+ const fullPath = `${this._baseUrl}${modulePath}`;
895
+ this.logger.debug(`Loading module: ${moduleName} from ${fullPath}`);
896
+
897
+ try {
898
+ const module = await import(fullPath);
899
+ this._modules[moduleName] = module;
900
+ return module;
901
+ } catch (error) {
902
+ this.logger.error(`Failed to load module ${moduleName}:`, error);
903
+ throw new Error(`Failed to load BifrostClient module: ${moduleName}`);
904
+ }
905
+ }
906
+
907
+ // ==========================================
908
+ // Module Loaders (_ensure* methods)
909
+ // Phase 5.3: Organized by category for clarity
910
+ // ==========================================
911
+
912
+ // Connection & Message Handling
913
+ async _ensureConnection() {
914
+ if (!this.connection) {
915
+ const { WebSocketConnection } = await import(`${BASE_URL}L1_Foundation/connection/websocket_connection.js`);
916
+ this.connection = new WebSocketConnection(this.url, this.logger, this.hooks, this.options);
917
+ }
918
+ return this.connection;
919
+ }
920
+
921
+ async _ensureMessageHandler() {
922
+ if (!this.messageHandler) {
923
+ const { MessageHandler } = await this._loadModule('message_handler');
924
+ this.messageHandler = new MessageHandler(this.logger, this.hooks, this);
925
+ this.messageHandler.setTimeout(this.options.timeout);
926
+ }
927
+ return this.messageHandler;
928
+ }
929
+
930
+ // Renderers - Core
931
+ async _ensureRenderer() {
932
+ if (!this.renderer) {
933
+ const { Renderer } = await this._loadModule('renderer');
934
+ this.renderer = new Renderer(this.logger);
935
+ }
936
+ return this.renderer;
937
+ }
938
+
939
+ async _ensureNavigationRenderer() {
940
+ const registry = await this._ensureRendererRegistry();
941
+ this.navigationRenderer = await registry.ensureRenderer('navigation');
942
+ return this.navigationRenderer;
943
+ }
944
+
945
+ // Renderers - Outputs (Typography, Text, Images, Icons, etc.)
946
+ // Consolidated via RendererRegistry (Task 0.2)
947
+ async _ensureTypographyRenderer() {
948
+ const registry = await this._ensureRendererRegistry();
949
+ this.typographyRenderer = await registry.ensureRenderer('typography');
950
+ return this.typographyRenderer;
951
+ }
952
+
953
+ async _ensureTextRenderer() {
954
+ const registry = await this._ensureRendererRegistry();
955
+ this.textRenderer = await registry.ensureRenderer('text');
956
+ return this.textRenderer;
957
+ }
958
+
959
+ async _ensureCodeRenderer() {
960
+ const registry = await this._ensureRendererRegistry();
961
+ this.codeRenderer = await registry.ensureRenderer('code');
962
+ return this.codeRenderer;
963
+ }
964
+
965
+ async _ensureCardRenderer() {
966
+ const registry = await this._ensureRendererRegistry();
967
+ this.cardRenderer = await registry.ensureRenderer('card');
968
+ return this.cardRenderer;
969
+ }
970
+
971
+ async _ensureButtonRenderer() {
972
+ const registry = await this._ensureRendererRegistry();
973
+ this.buttonRenderer = await registry.ensureRenderer('button');
974
+ return this.buttonRenderer;
975
+ }
976
+
977
+ async _ensureTableRenderer() {
978
+ const registry = await this._ensureRendererRegistry();
979
+ this.tableRenderer = await registry.ensureRenderer('table');
980
+ return this.tableRenderer;
981
+ }
982
+
983
+ async _ensureListRenderer() {
984
+ const registry = await this._ensureRendererRegistry();
985
+ this.listRenderer = await registry.ensureRenderer('list');
986
+ return this.listRenderer;
987
+ }
988
+
989
+ async _ensureImageRenderer() {
990
+ const registry = await this._ensureRendererRegistry();
991
+ this.imageRenderer = await registry.ensureRenderer('image');
992
+ return this.imageRenderer;
993
+ }
994
+
995
+ async _ensureVideoRenderer() {
996
+ const registry = await this._ensureRendererRegistry();
997
+ this.videoRenderer = await registry.ensureRenderer('video');
998
+ return this.videoRenderer;
999
+ }
1000
+
1001
+ async _ensureAudioRenderer() {
1002
+ const registry = await this._ensureRendererRegistry();
1003
+ this.audioRenderer = await registry.ensureRenderer('audio');
1004
+ return this.audioRenderer;
1005
+ }
1006
+
1007
+ async _ensureIconRenderer() {
1008
+ const registry = await this._ensureRendererRegistry();
1009
+ this.iconRenderer = await registry.ensureRenderer('icon');
1010
+ return this.iconRenderer;
1011
+ }
1012
+
1013
+ // Renderers - Composite (Dashboard, Swiper, etc.)
1014
+ // Consolidated via RendererRegistry (Task 0.2)
1015
+ async _ensureDashboardRenderer() {
1016
+ const registry = await this._ensureRendererRegistry();
1017
+ this.dashboardRenderer = await registry.ensureRenderer('dashboard');
1018
+ return this.dashboardRenderer;
1019
+ }
1020
+
1021
+ async _ensureSwiperRenderer() {
1022
+ const registry = await this._ensureRendererRegistry();
1023
+ this.swiperRenderer = await registry.ensureRenderer('swiper');
1024
+ return this.swiperRenderer;
1025
+ }
1026
+
1027
+ // Renderers - Feedback (Spinner, ProgressBar)
1028
+ // Consolidated via RendererRegistry (Task 0.2)
1029
+ async _ensureSpinnerRenderer() {
1030
+ const registry = await this._ensureRendererRegistry();
1031
+ this.spinnerRenderer = await registry.ensureRenderer('spinner');
1032
+ return this.spinnerRenderer;
1033
+ }
1034
+
1035
+ async _ensureProgressBarRenderer() {
1036
+ const registry = await this._ensureRendererRegistry();
1037
+ this.progressBarRenderer = await registry.ensureRenderer('progressBar');
1038
+ return this.progressBarRenderer;
1039
+ }
1040
+
1041
+ // Orchestrators
1042
+ async _ensureZDisplayOrchestrator() {
1043
+ if (!this.zDisplayOrchestrator) {
1044
+ const { ZDisplayOrchestrator } = await import(`${BASE_URL}L2_Handling/display/orchestration/zdisplay_orchestrator.js`);
1045
+ this.zDisplayOrchestrator = new ZDisplayOrchestrator(this);
1046
+ this.logger.debug('ZDisplayOrchestrator loaded');
1047
+ }
1048
+ return this.zDisplayOrchestrator;
1049
+ }
1050
+
1051
+ // Renderers - Specialized (Wizard, Terminal, Form, Menu)
1052
+ async _ensureWizardConditionalRenderer() {
1053
+ if (!this.wizardConditionalRenderer) {
1054
+ const { WizardConditionalRenderer } = await import(`${BASE_URL}L2_Handling/display/composite/wizard_conditional_renderer.js`);
1055
+ this.wizardConditionalRenderer = new WizardConditionalRenderer(this.logger);
1056
+ this.logger.debug('WizardConditionalRenderer loaded');
1057
+ }
1058
+ return this.wizardConditionalRenderer;
1059
+ }
1060
+
1061
+ async _ensureTerminalRenderer() {
1062
+ const registry = await this._ensureRendererRegistry();
1063
+ this.terminalRenderer = await registry.ensureRenderer('terminal');
1064
+ return this.terminalRenderer;
1065
+ }
1066
+
1067
+ // Managers (Cache, ZVaF, Navigation, Hooks)
1068
+ // Consolidated via ManagerRegistry (Task 0.3)
1069
+ async _ensureCacheManager() {
1070
+ const registry = await this._ensureManagerRegistry();
1071
+ this.cacheManager = await registry.ensureManager('cache');
1072
+ return this.cacheManager;
1073
+ }
1074
+
1075
+ async _ensureZVaFManager() {
1076
+ const registry = await this._ensureManagerRegistry();
1077
+ this.zvafManager = await registry.ensureManager('zvaf');
1078
+ return this.zvafManager;
1079
+ }
1080
+
1081
+ async _ensureNavigationManager() {
1082
+ const registry = await this._ensureManagerRegistry();
1083
+ this.navigationManager = await registry.ensureManager('navigation');
1084
+ return this.navigationManager;
1085
+ }
1086
+
1087
+ async _ensureWidgetHookManager() {
1088
+ const registry = await this._ensureManagerRegistry();
1089
+ this.widgetHookManager = await registry.ensureManager('widgetHook');
1090
+ return this.widgetHookManager;
1091
+ }
1092
+
1093
+ async _ensureDLRenderer() {
1094
+ const registry = await this._ensureRendererRegistry();
1095
+ this.dlRenderer = await registry.ensureRenderer('dl');
1096
+ return this.dlRenderer;
1097
+ }
1098
+
1099
+ async _ensureFormRenderer() {
1100
+ const registry = await this._ensureRendererRegistry();
1101
+ this.formRenderer = await registry.ensureRenderer('form');
1102
+ return this.formRenderer;
1103
+ }
1104
+
1105
+ async _ensureMenuRenderer() {
1106
+ const registry = await this._ensureRendererRegistry();
1107
+ this.menuRenderer = await registry.ensureRenderer('menu');
1108
+ return this.menuRenderer;
1109
+ }
1110
+
1111
+
1112
+ //
1113
+ // Connection Management
1114
+ //
1115
+
1116
+ // Error Display
1117
+ async _ensureErrorDisplay() {
1118
+ if (this.options.showErrors === false) {
1119
+ return;
1120
+ } // Allow disabling
1121
+
1122
+ if (!this.errorDisplay) {
1123
+ const { ErrorDisplay } = await this._loadModule('error_display');
1124
+ this.errorDisplay = new ErrorDisplay({
1125
+ position: this.options.errorPosition || 'top-right',
1126
+ maxErrors: this.options.maxErrors || 5,
1127
+ autoDismiss: this.options.autoDismiss || 10000 // TIMEOUTS.AUTO_DISMISS (UMD limitation, can't import)
1128
+ });
1129
+
1130
+ // Set error handler for hooks
1131
+ this.hooks.errorHandler = (errorInfo) => {
1132
+ this.errorDisplay.show(errorInfo);
1133
+ };
1134
+
1135
+ this.logger.debug('[ErrorDisplay] Initialized');
1136
+ }
1137
+ return this.errorDisplay;
1138
+ }
1139
+
1140
+ /**
1141
+ * Check if running on file:// protocol (which doesn't support ES6 module imports)
1142
+ * @private
1143
+ */
1144
+ _isFileProtocol() {
1145
+ return typeof window !== 'undefined' && window.location.protocol === 'file:';
1146
+ }
1147
+
1148
+ /**
1149
+ * Connect to the WebSocket server
1150
+ * @returns {Promise<void>}
1151
+ */
1152
+ async connect() {
1153
+ // Skip module loading for file:// protocol (ES6 imports not supported)
1154
+ const isFileProtocol = this._isFileProtocol();
1155
+
1156
+ if (isFileProtocol) {
1157
+ this.logger.warn('[file://] Skipping module loading (use HTTP server)');
1158
+ this.logger.debug('[file://] Error display and auto-rendering disabled');
1159
+ }
1160
+
1161
+ // Initialize error display (for visual error boundaries) - skip on file://
1162
+ if (!isFileProtocol && this.options.showErrors !== false) {
1163
+ try {
1164
+ await this._ensureErrorDisplay();
1165
+ } catch (error) {
1166
+ this.logger.warn('Error display failed to load:', error.message);
1167
+ }
1168
+ }
1169
+
1170
+ // Initialize widget hook manager (for auto-rendering zDisplay events) - skip on file://
1171
+ if (!isFileProtocol) {
1172
+ try {
1173
+ await this._ensureWidgetHookManager();
1174
+ await this.widgetHookManager.registerAllWidgetHooks();
1175
+ } catch (error) {
1176
+ this.logger.warn('Widget hooks failed to load:', error.message);
1177
+ }
1178
+ }
1179
+
1180
+ // Load theme BEFORE connecting to prevent FOUC (Flash of Unstyled Content)
1181
+ // Load required modules
1182
+ await this._ensureConnection();
1183
+ await this._ensureMessageHandler();
1184
+
1185
+ await this.connection.connect();
1186
+
1187
+ // Set up message handler
1188
+ this.connection.onMessage((event) => {
1189
+ this.messageHandler.handleMessage(event.data);
1190
+ });
1191
+
1192
+ // Auto-send request if specified (Swiper-style elegance!)
1193
+ if (this.options.autoRequest) {
1194
+ const request = typeof this.options.autoRequest === 'string'
1195
+ ? { event: this.options.autoRequest }
1196
+ : this.options.autoRequest;
1197
+
1198
+ // For execute_walker requests, use fire-and-forget (chunks come asynchronously)
1199
+ // For other requests, use send() to wait for response
1200
+ if (request.event === 'execute_walker') {
1201
+ this.logger.debug('Auto-sending walker request', request);
1202
+ this.connection.send(JSON.stringify(request));
1203
+ } else {
1204
+ this.logger.debug('Auto-sending request', request);
1205
+ this.send(request);
1206
+ }
1207
+ }
1208
+ }
1209
+
1210
+ /**
1211
+ * Disconnect from the server
1212
+ */
1213
+ disconnect() {
1214
+ if (this.connection) {
1215
+ this.connection.disconnect();
1216
+ }
1217
+ }
1218
+
1219
+ /**
1220
+ * Check if connected
1221
+ * @returns {boolean}
1222
+ */
1223
+ isConnected() {
1224
+ return this.connection ? this.connection.isConnected() : false;
1225
+ }
1226
+
1227
+ //
1228
+ // Message Sending
1229
+ //
1230
+
1231
+ /**
1232
+ * Send a message and wait for response
1233
+ * @param {Object} payload - Message payload
1234
+ * @param {number} timeout - Custom timeout (optional)
1235
+ * @returns {Promise<any>}
1236
+ */
1237
+ async send(payload, timeout = null) {
1238
+ if (!this.isConnected()) {
1239
+ throw new Error('Not connected to server. Call connect() first.');
1240
+ }
1241
+
1242
+ await this._ensureMessageHandler();
1243
+
1244
+ return this.messageHandler.send(
1245
+ payload,
1246
+ (msg) => this.connection.send(msg),
1247
+ timeout
1248
+ );
1249
+ }
1250
+
1251
+ /**
1252
+ * Send input response to server
1253
+ * @param {string} requestId - Request ID from input event
1254
+ * @param {any} value - Input value
1255
+ */
1256
+ sendInputResponse(requestId, value) {
1257
+ if (!this.isConnected()) {
1258
+ this.logger.log('[ERROR] Cannot send input response: not connected');
1259
+ return;
1260
+ }
1261
+
1262
+ // Use new event protocol
1263
+ const message = JSON.stringify({
1264
+ event: 'input_response',
1265
+ requestId: requestId,
1266
+ value: value
1267
+ });
1268
+
1269
+ this.connection.send(message);
1270
+ }
1271
+
1272
+ //
1273
+ // CRUD Operations
1274
+ //
1275
+
1276
+ /**
1277
+ * Create a new record
1278
+ * @param {string} model - Table/model name
1279
+ * @param {Object} data - Field values
1280
+ * @returns {Promise<Object>}
1281
+ */
1282
+ async create(model, data) {
1283
+ return this.send({
1284
+ event: 'dispatch',
1285
+ zKey: `^Create ${model}`,
1286
+ model: model,
1287
+ data: data
1288
+ });
1289
+ }
1290
+
1291
+ /**
1292
+ * Read records
1293
+ * @param {string} model - Table/model name
1294
+ * @param {Object} filters - WHERE conditions (optional)
1295
+ * @param {Object} options - Additional options (fields, order_by, limit, offset)
1296
+ * @returns {Promise<Array>}
1297
+ */
1298
+ async read(model, filters = null, options = {}) {
1299
+ const payload = {
1300
+ event: 'dispatch',
1301
+ zKey: `^List ${model}`,
1302
+ model: model
1303
+ };
1304
+
1305
+ if (filters) {
1306
+ payload.where = filters;
1307
+ }
1308
+ if (options.fields) {
1309
+ payload.fields = options.fields;
1310
+ }
1311
+ if (options.order_by) {
1312
+ payload.order_by = options.order_by;
1313
+ }
1314
+ if (options.limit !== undefined) {
1315
+ payload.limit = options.limit;
1316
+ }
1317
+ if (options.offset !== undefined) {
1318
+ payload.offset = options.offset;
1319
+ }
1320
+
1321
+ return this.send(payload);
1322
+ }
1323
+
1324
+ /**
1325
+ * Update record(s)
1326
+ * @param {string} model - Table/model name
1327
+ * @param {Object|number} filters - WHERE conditions or ID
1328
+ * @param {Object} data - Fields to update
1329
+ * @returns {Promise<Object>}
1330
+ */
1331
+ async update(model, filters, data) {
1332
+ if (typeof filters === 'number') {
1333
+ filters = { id: filters };
1334
+ }
1335
+
1336
+ return this.send({
1337
+ event: 'dispatch',
1338
+ zKey: `^Update ${model}`,
1339
+ model: model,
1340
+ where: filters,
1341
+ data: data
1342
+ });
1343
+ }
1344
+
1345
+ /**
1346
+ * Delete record(s)
1347
+ * @param {string} model - Table/model name
1348
+ * @param {Object|number} filters - WHERE conditions or ID
1349
+ * @returns {Promise<Object>}
1350
+ */
1351
+ async delete(model, filters) {
1352
+ if (typeof filters === 'number') {
1353
+ filters = { id: filters };
1354
+ }
1355
+
1356
+ return this.send({
1357
+ event: 'dispatch',
1358
+ zKey: `^Delete ${model}`,
1359
+ model: model,
1360
+ where: filters
1361
+ });
1362
+ }
1363
+
1364
+ //
1365
+ // zCLI Operations
1366
+ //
1367
+
1368
+ /**
1369
+ * Execute a zFunc command
1370
+ * @param {string} command - zFunc command string
1371
+ * @returns {Promise<any>}
1372
+ */
1373
+ async zFunc(command) {
1374
+ return this.send({
1375
+ zKey: 'zFunc',
1376
+ zHorizontal: command
1377
+ });
1378
+ }
1379
+
1380
+ /**
1381
+ * SSOT: resolve the top-level section key a click originated FROM.
1382
+ * Walks up the DOM from the clicked element collecting [data-zkey] ancestors
1383
+ * and returns the OUTERMOST one within the zVaF root — i.e. the departing
1384
+ * block's top-level key (e.g. "ZDelta_Section"). Every nav verb (zLink,
1385
+ * zDelta, …) feeds this single origin into its walker request so the server
1386
+ * records the click-crumb verb-agnostically (mirrors zCLI on_continue, timed
1387
+ * to the click). Returns null when there is no section ancestor (navbar/root).
1388
+ * @param {HTMLElement} el - the clicked element
1389
+ * @returns {string|null}
1390
+ */
1391
+ navOriginKey(el) {
1392
+ const root = this._zVaFElement || null;
1393
+ let top = null;
1394
+ let node = el;
1395
+ while (node && node !== root) {
1396
+ if (node.getAttribute && typeof node.hasAttribute === 'function' && node.hasAttribute('data-zkey')) {
1397
+ top = node.getAttribute('data-zkey');
1398
+ }
1399
+ node = node.parentElement;
1400
+ }
1401
+ return top;
1402
+ }
1403
+
1404
+ /**
1405
+ * Navigate to a zLink path.
1406
+ * Resolves @.UI.* paths to Bifrost routes client-side (no backend walker needed).
1407
+ * @param {string} path - zLink navigation path (@.UI.Folder.zUI.File.Block or raw URL)
1408
+ * @param {string|null} originKey - SSOT click-origin section key (from navOriginKey)
1409
+ * @returns {Promise<any>}
1410
+ */
1411
+ async zLink(path, originKey = null) {
1412
+ const url = this._zLinkPathToUrl(path);
1413
+ if (url) {
1414
+ this.logger.log(`[zLink] Navigating to: ${url} (from: ${path})`);
1415
+ return this._navigateToRoute(url, { zOrigin: originKey });
1416
+ }
1417
+ // Fallback: unknown path format — pass to backend
1418
+ return this.send({
1419
+ event: 'dispatch',
1420
+ zKey: 'zLink',
1421
+ zHorizontal: `zLink(${path})`
1422
+ });
1423
+ }
1424
+
1425
+ /**
1426
+ * Intra-file block hop — re-execute a different block from the same file.
1427
+ * Same route, same zVaFile/zVaFolder, different zBlock streamed into the container.
1428
+ * Mirrors CLI zDelta semantics client-side.
1429
+ * @param {string} blockName - target block name ($ prefix already stripped)
1430
+ * @param {string|null} originKey - SSOT click-origin section key (from navOriginKey)
1431
+ */
1432
+ async zDelta(blockName, originKey = null) {
1433
+ const zVaFile = this.zuiConfig?.zVaFile;
1434
+ const zVaFolder = this.zuiConfig?.zVaFolder;
1435
+ if (!zVaFile || !zVaFolder) {
1436
+ this.logger.warn(`[zDelta] No zVaFile/zVaFolder in zuiConfig — cannot hop to block: ${blockName}`);
1437
+ return;
1438
+ }
1439
+ // Track current block for zBack before hopping
1440
+ this._prevBlock = this._currentBlock || this.options.zBlock || null;
1441
+ this._currentBlock = blockName;
1442
+ this.logger.log(`[zDelta] Block hop: ${zVaFile} → ${blockName} (prev: ${this._prevBlock}, origin: ${originKey})`);
1443
+ // execute_walker is fire-and-forget: the server streams chunks, it never
1444
+ // sends a single _requestId-correlated response. Using send() here would
1445
+ // leak a never-resolving callback into messageHandler.callbacks, which then
1446
+ // trips the "response without _requestId" guard on the next real response.
1447
+ // zOrigin carries the SSOT click-crumb so the server records where the hop
1448
+ // launched from (verb-agnostic — same field zLink uses).
1449
+ const payload = { event: 'execute_walker', zBlock: blockName, zVaFile, zVaFolder };
1450
+ if (originKey) payload.zOrigin = originKey;
1451
+ this._sendWalker(payload);
1452
+ }
1453
+
1454
+ /**
1455
+ * zDelegate (inline) — render a dotted target section ($Block.Section) IN PLACE
1456
+ * within the carrier's parent key container, routeless and AJAX-like. Unlike
1457
+ * zDelta (which swaps the whole zVaF panel + is crumb-aware), this keeps the
1458
+ * surrounding panel intact and injects a Back affordance to restore the carrier.
1459
+ * Mirrors CLI "descend into a sub-block, then bounce back" — the dual-mode SSOT
1460
+ * intent of zDelegate-to-a-KEY. The server already resolves the dotted path to
1461
+ * just that section's fragment; we only redirect WHERE the fragment lands.
1462
+ * @param {string} targetPath - e.g. "$Edit_Profile.Change_Photo"
1463
+ * @param {HTMLElement} carrierEl - the clicked carrier button
1464
+ */
1465
+ async zDelegateInline(targetPath, carrierEl) {
1466
+ const base = String(targetPath).replace(/^[$^~]+/, '').trim();
1467
+ const zVaFile = this.zuiConfig?.zVaFile;
1468
+ const zVaFolder = this.zuiConfig?.zVaFolder;
1469
+ if (!zVaFile || !zVaFolder) {
1470
+ this.logger.warn(`[zDelegateInline] No zVaFile/zVaFolder — cannot delegate: ${base}`);
1471
+ return;
1472
+ }
1473
+ // Host = the carrier's parent KEY container (e.g. the avatar's #Avatar). The
1474
+ // section renders within it, the same way descending into a sub-block reads
1475
+ // in CLI. Fall back to the zVaF root if the carrier is unparented.
1476
+ const host = (carrierEl && carrierEl.parentElement)
1477
+ ? (carrierEl.parentElement.closest('[data-zkey]') || carrierEl.parentElement)
1478
+ : this._zVaFElement;
1479
+ if (!host) {
1480
+ this.logger.warn('[zDelegateInline] No host container — falling back to zDelta');
1481
+ return this.zDelta(base);
1482
+ }
1483
+ // Save the LIVE child nodes (not innerHTML) so restoring the carrier keeps its
1484
+ // event listeners intact. Clearing the host detaches them but the refs survive.
1485
+ // SSOT render-target primitive: zDelegate is the render-target + restore
1486
+ // (once) variant. zDash sets the bare target (no restore). One resolver in
1487
+ // the orchestrator consumes whichever producer set client._renderTarget.
1488
+ this._renderTarget = {
1489
+ el: host,
1490
+ mode: 'replace',
1491
+ restoreNodes: Array.from(host.childNodes),
1492
+ target: base,
1493
+ once: true,
1494
+ };
1495
+ this.logger.log(`[zDelegateInline] ${zVaFile} → ${base} (host: ${host.id || host.getAttribute?.('data-zkey') || 'parent'})`);
1496
+ // Fire-and-forget (chunks stream back); see zDelta note on the _requestId guard.
1497
+ this._sendWalker({
1498
+ event: 'execute_walker',
1499
+ zBlock: base,
1500
+ zVaFile,
1501
+ zVaFolder,
1502
+ _zDelegateInline: true, // hint for future first-class server-side wiring
1503
+ });
1504
+ }
1505
+
1506
+ /**
1507
+ * Navigate back to the previous block (intra-file).
1508
+ * Mirrors CLI zBack semantics — pops one level from the block navigation history.
1509
+ * Falls back to browser history.back() if no block history is available.
1510
+ */
1511
+ async zBack() {
1512
+ const prevBlock = this._prevBlock;
1513
+ if (!prevBlock) {
1514
+ // Cross-file back (e.g. arrived via zLink): there is no same-file sibling
1515
+ // to hop to, so step out through the browser. Flag back-intent so the
1516
+ // popstate handler re-navigates with zBack:true — the server then consumes
1517
+ // the origin section recorded on the parent scope (Step 1) and returns it
1518
+ // as a zPsi anchor, mirroring zCLI's start_key resume. SSOT: the section
1519
+ // lives in zCrumbs; the browser only supplies the destination URL.
1520
+ this.logger.log('[zBack] No same-file prev block — crumb-driven step-out via history');
1521
+ this._pendingZBack = true;
1522
+ window.history.back();
1523
+ return;
1524
+ }
1525
+ const zVaFile = this.zuiConfig?.zVaFile;
1526
+ const zVaFolder = this.zuiConfig?.zVaFolder;
1527
+ if (!zVaFile || !zVaFolder) {
1528
+ this.logger.warn('[zBack] No zVaFile/zVaFolder — falling back to browser history');
1529
+ window.history.back();
1530
+ return;
1531
+ }
1532
+ this.logger.log(`[zBack] Returning to block: ${prevBlock}`);
1533
+ this._currentBlock = prevBlock;
1534
+ this._prevBlock = null;
1535
+ // Fire-and-forget (chunks stream back); see zDelta note on the _requestId guard.
1536
+ // zBack:true flags back-intent so the server can read the crumb's origin
1537
+ // section off the (now-active) parent scope and return it as a zPsi anchor on
1538
+ // walker_complete — letting us land on the section the user came FROM, not the
1539
+ // page top. SSOT: the section lives in zCrumbs, the client never derives it.
1540
+ this._sendWalker({ event: 'execute_walker', zBlock: prevBlock, zVaFile, zVaFolder, zBack: true });
1541
+ }
1542
+
1543
+ /**
1544
+ * Fire-and-forget walker send. execute_walker responses are streamed as chunks
1545
+ * (handled in MessageHandler before response correlation), never a single
1546
+ * _requestId-correlated reply — so we must NOT register a send() callback or it
1547
+ * leaks forever and trips the "response without _requestId" guard. Mirrors the
1548
+ * raw-send pattern already used by autoRequest and ClientNavigationManager.
1549
+ * Attaches the session id (matching MessageHandler.send) so server-side session
1550
+ * sync still works for routeless hops.
1551
+ * @param {Object} payload - execute_walker payload
1552
+ */
1553
+ _sendWalker(payload) {
1554
+ if (!this.isConnected()) {
1555
+ this.logger.warn('[_sendWalker] Not connected — dropping walker request', payload);
1556
+ return;
1557
+ }
1558
+ try {
1559
+ const sid = this.messageHandler?._getSessionIdFromCookie?.();
1560
+ if (sid) payload._sessionId = sid;
1561
+ } catch (_) { /* cookie read is best-effort */ }
1562
+ this.connection.send(JSON.stringify(payload));
1563
+ }
1564
+
1565
+ /**
1566
+ * Convert a zLink path to a Bifrost URL route.
1567
+ * Pattern: @.UI.<Folders>.zUI.<PageName>.<Block> → /<Folders>/<PageName>
1568
+ * @param {string} path
1569
+ * @returns {string|null}
1570
+ */
1571
+ _zLinkPathToUrl(path) {
1572
+ if (!path.startsWith('@.UI.')) return null;
1573
+ const inner = path.slice(5); // strip '@.UI.'
1574
+ const parts = inner.split('.');
1575
+ const zUIIdx = parts.indexOf('zUI');
1576
+ if (zUIIdx === -1) {
1577
+ // Folder-only path: @.UI.zProducts.zOS → /zProducts/zOS
1578
+ return '/' + parts.join('/');
1579
+ }
1580
+ const folderParts = parts.slice(0, zUIIdx); // e.g. ['Demos']
1581
+ const pageNamePart = parts[zUIIdx + 1] || null; // e.g. 'zNavigation_demo'
1582
+ if (!pageNamePart) return null;
1583
+ return '/' + [...folderParts, pageNamePart].join('/');
1584
+ }
1585
+
1586
+ /**
1587
+ * Execute a zOpen command
1588
+ * @param {string} command - zOpen command string
1589
+ * @returns {Promise<any>}
1590
+ */
1591
+ async zOpen(command) {
1592
+ return this.send({
1593
+ zKey: 'zOpen',
1594
+ zHorizontal: `zOpen(${command})`
1595
+ });
1596
+ }
1597
+
1598
+
1599
+ //
1600
+ // Auto-Rendering Methods (Using zTheme)
1601
+ //
1602
+
1603
+ /**
1604
+ * Render data as a table with zTheme styling
1605
+ * @param {Array} data - Array of objects to render
1606
+ * @param {string|HTMLElement} container - Container selector or element
1607
+ * @param {Object} options - Rendering options
1608
+ */
1609
+ async renderTable(data, container, options = {}) {
1610
+ await this._ensureRenderer();
1611
+ this.renderer.renderTable(data, container, options);
1612
+ }
1613
+
1614
+ /**
1615
+ * Render a menu with buttons
1616
+ * @param {Array} items - Array of menu items {label, action, icon, variant}
1617
+ * @param {string|HTMLElement} container - Container selector or element
1618
+ */
1619
+ async renderMenu(items, container) {
1620
+ await this._ensureRenderer();
1621
+ this.renderer.renderMenu(items, container);
1622
+ }
1623
+
1624
+ /**
1625
+ * Render a form with zTheme styling
1626
+ * @param {Array} fields - Array of field definitions
1627
+ * @param {string|HTMLElement} container - Container selector or element
1628
+ * @param {Function} onSubmit - Submit handler
1629
+ */
1630
+ async renderForm(fields, container, onSubmit) {
1631
+ await this._ensureRenderer();
1632
+ this.renderer.renderForm(fields, container, onSubmit);
1633
+ }
1634
+
1635
+ /**
1636
+ * Render a message/alert
1637
+ * @param {string} text - Message text
1638
+ * @param {string} type - Message type (success, error, warning, info)
1639
+ * @param {string|HTMLElement} container - Container selector or element
1640
+ * @param {number} duration - Auto-hide duration in ms (default: 5000 = TIMEOUTS.AUTO_DISMISS_SHORT)
1641
+ */
1642
+ async renderMessage(text, type = 'info', container, duration = 5000) { // TIMEOUTS.AUTO_DISMISS_SHORT
1643
+ await this._ensureRenderer();
1644
+ this.renderer.renderMessage(text, type, container, duration);
1645
+ }
1646
+
1647
+ //
1648
+ // Dashboard Rendering (zDash Event)
1649
+ //
1650
+
1651
+ // NOTE: Dashboard rendering has been extracted to dashboard_renderer.js
1652
+ // The onZDash hook now uses the DashboardRenderer class (see _ensureDashboardRenderer)
1653
+ // Legacy methods below are kept for backward compatibility but should not be used directly
1654
+
1655
+ //
1656
+ // Hook Management
1657
+ //
1658
+
1659
+ /**
1660
+ * Register a new hook at runtime
1661
+ * @param {string} hookName - Name of the hook
1662
+ * @param {Function} fn - Hook function
1663
+ */
1664
+ registerHook(hookName, fn) {
1665
+ this.hooks.register(hookName, fn);
1666
+ }
1667
+
1668
+ /**
1669
+ * Unregister a hook
1670
+ * @param {string} hookName - Name of the hook
1671
+ */
1672
+ unregisterHook(hookName) {
1673
+ this.hooks.unregister(hookName);
1674
+ }
1675
+
1676
+ /**
1677
+ * List all registered hooks
1678
+ * @returns {Array<string>}
1679
+ */
1680
+ listHooks() {
1681
+ return this.hooks.list();
1682
+ }
1683
+ }
1684
+
1685
+ // ES module export — bootstrap does: const { BifrostCore } = await import(bifrost_core_url)
1686
+ export { BifrostCore };