@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,8 @@
1
+ /**
2
+ * L2_Handling/navigation - Client-Side Routing
3
+ *
4
+ * Client-side routing, URL parsing, history management, walker request generation.
5
+ * Depends on: L1_Foundation (Logger, Constants)
6
+ */
7
+
8
+ export { NavigationManager } from './navigation_manager.js';
@@ -0,0 +1,403 @@
1
+ /**
2
+ * NavigationManager - Handles client-side navigation (SPA-style routing)
3
+ *
4
+ * Responsibilities:
5
+ * - Enable client-side navigation (intercept clicks on navbar + body links)
6
+ * - Handle browser back/forward buttons (popstate)
7
+ * - Navigate to routes via WebSocket (no page reload)
8
+ * - Snapshot each visited page into the TrailStore (offline-browse engine)
9
+ * - Replay a cached page on Back/forward and when the socket is down, so zOS
10
+ * feels like a regular HTML site even with a flaky/absent connection
11
+ * - Fetch route configuration from backend
12
+ * - Update browser URL without reload
13
+ *
14
+ * Offline-browse model (mirrors what a normal MPA gets free from bfcache):
15
+ * - Forward to a NEW page → server round-trip (route-config + execute_walker)
16
+ * - Back/forward to a SEEN page → replay the frozen paint from the trail
17
+ * - Socket down + SEEN target → replay from trail; NEW target → graceful notice
18
+ * A replayed page is stale render output, never authority (the server revalidates
19
+ * on the next forward navigation).
20
+ */
21
+
22
+ export class NavigationManager {
23
+ constructor(client) {
24
+ this.client = client;
25
+ this.logger = client.logger;
26
+ }
27
+
28
+ /**
29
+ * Enable client-side navigation (SPA-style routing)
30
+ *
31
+ * Individual links (rendered via link_primitives.js) wire their own click
32
+ * handlers and stopPropagation. For pages REPLAYED from the trail (raw HTML,
33
+ * no live handlers) we add ONE delegated click handler on the zVaF container:
34
+ * fresh links never reach it (they stopPropagation), restored links bubble up
35
+ * and get routed through the SPA. This is what keeps a replayed page browsable.
36
+ */
37
+ enableClientSideNavigation() {
38
+ if (typeof document === 'undefined') {
39
+ return;
40
+ }
41
+
42
+ // Remove legacy global handler if it exists (cleanup from old implementation)
43
+ if (this.client._navClickHandler) {
44
+ document.removeEventListener('click', this.client._navClickHandler, true);
45
+ this.client._navClickHandler = null;
46
+ this.logger.debug('[ClientNav] Removed legacy global click handler');
47
+ }
48
+
49
+ this._wireRestoredLinkDelegation();
50
+
51
+ this.logger.info('[ClientNav] Client-side navigation enabled');
52
+
53
+ // Handle browser back/forward buttons
54
+ if (!this.client._popstateHandler) {
55
+ this.client._popstateHandler = async (_e) => {
56
+ this.logger.debug('[ClientNav] Browser back/forward detected');
57
+ const path = window.location.pathname;
58
+
59
+ // An in-page zBack button routes a cross-file step-out through
60
+ // history.back() and flags intent — carry it so the server consumes the
61
+ // crumb's origin section (zPsi), mirroring zCLI's start_key resume. A
62
+ // plain browser Back/Fwd leaves the flag unset and renders from the top.
63
+ const zBack = !!this.client._pendingZBack;
64
+ this.client._pendingZBack = false;
65
+
66
+ // Freeze the page we're leaving, then try to replay the target's frozen
67
+ // paint (bfcache parity — Back is instant and works offline). Only fall
68
+ // through to a server nav if we never cached this page.
69
+ await this.snapshotCurrentPage();
70
+ if (await this._replayFromTrail(path, { zBack })) {
71
+ return;
72
+ }
73
+ await this.navigateToRoute(path, { skipHistory: true, zBack });
74
+ };
75
+
76
+ window.addEventListener('popstate', this.client._popstateHandler);
77
+ }
78
+
79
+ this.logger.debug('[ClientNav] Enabled');
80
+ }
81
+
82
+ /**
83
+ * Attach the single delegated click handler that routes links on replayed
84
+ * (handler-less) pages. Idempotent — wired once per zVaF element.
85
+ * @private
86
+ */
87
+ _wireRestoredLinkDelegation() {
88
+ const el = this.client._zVaFElement;
89
+ if (!el || this.client._zVaFLinkDelegationWired) {
90
+ return;
91
+ }
92
+ el.addEventListener('click', (e) => {
93
+ // A live link handler (fresh render) already handled + stopped this.
94
+ if (e.defaultPrevented) {
95
+ return;
96
+ }
97
+ const a = e.target && e.target.closest ? e.target.closest('a') : null;
98
+ if (!a || !el.contains(a)) {
99
+ return;
100
+ }
101
+ if (a.target === '_blank') {
102
+ return; // let new-tab links behave natively
103
+ }
104
+ const href = a.getAttribute('href');
105
+ // Internal, same-origin path only (e.g. /zProducts/zOS/...).
106
+ if (!href || !href.startsWith('/') || href.startsWith('//')) {
107
+ return;
108
+ }
109
+ e.preventDefault();
110
+ this.logger.debug('[ClientNav] Restored-link click → SPA nav:', href);
111
+ this.navigateToRoute(href);
112
+ }, false);
113
+ this.client._zVaFLinkDelegationWired = true;
114
+ this.logger.debug('[ClientNav] Restored-link delegation wired');
115
+ }
116
+
117
+ /**
118
+ * Apply a destination route's navbar on SPA navigation (SSOT: server-resolved).
119
+ *
120
+ * @param {Object} routeConfig - Parsed /api/route-config payload
121
+ * @private
122
+ */
123
+ _applyRouteNavBar(routeConfig) {
124
+ const el = this.client._zNavBarElement;
125
+ if (!el) return;
126
+
127
+ const navHtml = routeConfig && routeConfig.nav_html;
128
+ const navItems = routeConfig && routeConfig.navbar;
129
+ const wantsNavbar = !!(navHtml || (Array.isArray(navItems) && navItems.length));
130
+
131
+ // Keep zuiConfig.zMeta in sync with the destination so a later reconnect's
132
+ // per-page opt-out guard reads THIS page's zNavBar.
133
+ if (this.client.zuiConfig && routeConfig && routeConfig.zMeta) {
134
+ this.client.zuiConfig.zMeta = routeConfig.zMeta;
135
+ }
136
+
137
+ if (!wantsNavbar) {
138
+ el.style.display = 'none';
139
+ el.innerHTML = '';
140
+ this.logger.debug('[ClientNav] Destination has no navbar — hiding chrome');
141
+ return;
142
+ }
143
+
144
+ el.style.display = '';
145
+ if (this.client.zuiConfig) {
146
+ this.client.zuiConfig.nav_html = navHtml || this.client.zuiConfig.nav_html;
147
+ this.client.zuiConfig.zNavBar = navItems || this.client.zuiConfig.zNavBar;
148
+ }
149
+ if (navHtml && typeof this.client._fetchAndPopulateNavBar === 'function') {
150
+ this.client._fetchAndPopulateNavBar(navHtml).catch((err) =>
151
+ this.logger.error('[ClientNav] navbar populate failed:', err)
152
+ );
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Snapshot the page currently on screen into the trail (bfcache-style freeze).
158
+ * Stores the rendered HTML + the route's resolved config so a later replay can
159
+ * restore both the content and its navbar without the server.
160
+ * @returns {Promise<boolean>} true if a snapshot was written
161
+ */
162
+ async snapshotCurrentPage() {
163
+ const client = this.client;
164
+ if (!client.cache || typeof document === 'undefined') {
165
+ return false;
166
+ }
167
+ const el = client._zVaFElement;
168
+ if (!el) {
169
+ return false;
170
+ }
171
+ const html = el.innerHTML;
172
+ // Never freeze a transient loading/placeholder state.
173
+ if (!html || html.indexOf('Loading...') !== -1) {
174
+ return false;
175
+ }
176
+ const path = client._currentPath || window.location.pathname;
177
+ const routeConfig = client._currentRouteConfig || null;
178
+ try {
179
+ await client.cache.set(path, {
180
+ html,
181
+ routeConfig,
182
+ title: document.title,
183
+ ts: Date.now()
184
+ });
185
+ this.logger.debug('[ClientNav] Snapshotted page into trail: %s', path);
186
+ return true;
187
+ } catch (err) {
188
+ this.logger.debug('[ClientNav] Snapshot skipped:', err && err.message);
189
+ return false;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Replay a page from the trail (no server round-trip).
195
+ * @param {string} routePath - target path
196
+ * @param {Object} opts - { zBack }
197
+ * @returns {Promise<boolean>} true if the page was replayed
198
+ * @private
199
+ */
200
+ async _replayFromTrail(routePath, { zBack = false } = {}) {
201
+ const client = this.client;
202
+ if (!client.cache) {
203
+ return false;
204
+ }
205
+ let entry;
206
+ try {
207
+ entry = await client.cache.get(routePath);
208
+ } catch (err) {
209
+ return false;
210
+ }
211
+ if (!entry || !entry.html) {
212
+ return false;
213
+ }
214
+ const el = client._zVaFElement;
215
+ if (!el) {
216
+ return false;
217
+ }
218
+
219
+ el.innerHTML = entry.html;
220
+ if (entry.title) {
221
+ document.title = entry.title;
222
+ }
223
+ if (entry.routeConfig) {
224
+ this._applyRouteNavBar(entry.routeConfig);
225
+ }
226
+ client._currentPath = routePath;
227
+ client._currentRouteConfig = entry.routeConfig || null;
228
+ // We served a page — cancel any pending offline retry.
229
+ client._pendingOfflineNav = null;
230
+
231
+ // Re-enable nav so the delegated handler + popstate stay live.
232
+ if (typeof client._enableClientSideNavigation === 'function') {
233
+ await client._enableClientSideNavigation();
234
+ }
235
+
236
+ // A page replayed while the socket is down is stale render output — lock its
237
+ // forms (the disconnect handler ran before this content existed).
238
+ if (!this._isSocketConnected() && client.cacheManager && typeof client.cacheManager.disableForms === 'function') {
239
+ client.cacheManager.disableForms();
240
+ }
241
+
242
+ // Don't yank to the top on a crumb-driven back.
243
+ if (!zBack) {
244
+ window.scrollTo({ top: 0, behavior: 'auto' });
245
+ }
246
+
247
+ this.logger.info('[ClientNav] Replayed from trail: %s', routePath);
248
+ return true;
249
+ }
250
+
251
+ /**
252
+ * Render a graceful offline notice when a NEVER-seen page is requested while
253
+ * the connection is down (a normal site shows the browser dino here).
254
+ * @private
255
+ */
256
+ _showOfflineNotice(routePath) {
257
+ if (this.client._zVaFElement) {
258
+ this.client._zVaFElement.innerHTML = `<div class="zAlert zAlert-warning zmt-4">
259
+ <strong>You're offline</strong><br>
260
+ <small>"${routePath}" hasn't been opened yet, so it isn't available offline. Reconnecting…</small>
261
+ </div>`;
262
+ }
263
+ // Remember the intent so the reconnect handler can fulfill it automatically.
264
+ this.client._pendingOfflineNav = routePath;
265
+ this.logger.warn('[ClientNav] Offline + uncached target: %s', routePath);
266
+ }
267
+
268
+ /** @private */
269
+ _isSocketConnected() {
270
+ const conn = this.client.connection;
271
+ if (conn && typeof conn.isConnected === 'function') {
272
+ return conn.isConnected();
273
+ }
274
+ // Unknown → assume connected (preserve legacy behavior).
275
+ return true;
276
+ }
277
+
278
+ /**
279
+ * Navigate to a route via WebSocket (client-side navigation)
280
+ * @param {string} routePath - Path to navigate to (e.g., '/zAbout', '/zAccount')
281
+ * @param {Object} options - Navigation options
282
+ */
283
+ async navigateToRoute(routePath, options = {}) {
284
+ const { skipHistory = false, navbar = false, zOrigin = null, zBack = false } = options;
285
+
286
+ this.client._isClientSideNav = true;
287
+
288
+ try {
289
+ this.logger.info('[ClientNav] Navigating to: %s', routePath);
290
+
291
+ // Freeze the page we're leaving before we touch the DOM (bfcache-style).
292
+ await this.snapshotCurrentPage();
293
+
294
+ // 2B: Python owns route resolution — ask the server for walker params.
295
+ // If the server is unreachable, fall back to the trail (offline-browse).
296
+ let routeConfig;
297
+ try {
298
+ const res = await fetch(`/api/route-config?path=${encodeURIComponent(routePath)}`);
299
+ if (!res.ok) {
300
+ throw new Error(`Route not found: ${routePath} (${res.status})`);
301
+ }
302
+ routeConfig = await res.json();
303
+ } catch (netErr) {
304
+ this.logger.warn('[ClientNav] route-config unreachable (%s) — trying trail', netErr && netErr.message);
305
+ if (await this._replayFromTrail(routePath, { zBack })) {
306
+ return;
307
+ }
308
+ this._showOfflineNotice(routePath);
309
+ return;
310
+ }
311
+
312
+ const { zBlock, zVaFile, zVaFolder, zMeta } = routeConfig;
313
+ this.logger.debug('[ClientNav] Route config', { zVaFile, zVaFolder, zBlock });
314
+
315
+ // Per-page navbar (SSOT: server-resolved via route-config).
316
+ this._applyRouteNavBar(routeConfig);
317
+
318
+ // If the socket is down, the walker request can't be served — replay the
319
+ // frozen paint instead of stalling on the timeout.
320
+ if (!this._isSocketConnected()) {
321
+ this.logger.warn('[ClientNav] Socket down — trying trail for %s', routePath);
322
+ if (await this._replayFromTrail(routePath, { zBack })) {
323
+ return;
324
+ }
325
+ this._showOfflineNotice(routePath);
326
+ return;
327
+ }
328
+
329
+ // Inject page-specific zBrush CSS (not loaded by full-page <head> on SPA nav)
330
+ const brushes = zMeta?.zBrush
331
+ ? (Array.isArray(zMeta.zBrush) ? zMeta.zBrush : [zMeta.zBrush])
332
+ : [];
333
+ brushes.forEach(brush => {
334
+ const href = `/styles/${brush}.css`;
335
+ if (!document.querySelector(`link[href="${href}"]`)) {
336
+ const link = document.createElement('link');
337
+ link.rel = 'stylesheet';
338
+ link.href = href;
339
+ document.head.appendChild(link);
340
+ this.logger.debug('[ClientNav] Injected zBrush CSS:', href);
341
+ }
342
+ });
343
+
344
+ // Track the destination so the NEXT snapshot (on leave) is stamped right.
345
+ this.client._currentPath = routePath;
346
+ this.client._currentRouteConfig = routeConfig;
347
+ // A live server nav supersedes any pending offline retry.
348
+ this.client._pendingOfflineNav = null;
349
+
350
+ // Clear current content and show loading state
351
+ if (this.client._zVaFElement) {
352
+ this.client._zVaFElement.innerHTML = '<div class="zText-center zp-4">Loading...</div>';
353
+ }
354
+
355
+ // Send walker execution request via WebSocket.
356
+ const walkerRequest = { event: 'execute_walker', zBlock, zVaFile, zVaFolder };
357
+ if (navbar) walkerRequest.navbar = true;
358
+ if (zOrigin) walkerRequest.zOrigin = zOrigin;
359
+ if (zBack) walkerRequest.zBack = true;
360
+ this.logger.debug('[ClientNav] Sending walker request', walkerRequest);
361
+ this.client.connection.send(JSON.stringify(walkerRequest));
362
+
363
+ // Timeout if backend doesn't respond
364
+ this.client._navigationTimeout = setTimeout(() => {
365
+ if (this.client._zVaFElement?.innerHTML.includes('Loading...')) {
366
+ this.logger.warn('[ClientNav] Walker request timeout - no chunks received after 10s');
367
+ this.client._zVaFElement.innerHTML = `<div class="zAlert zAlert-warning zmt-4">
368
+ <strong>Loading Timeout:</strong> Backend did not respond.<br>
369
+ <small>Check terminal logs for errors.</small></div>`;
370
+ }
371
+ }, 10000);
372
+
373
+ // Don't yank to the top on a crumb-driven back.
374
+ if (!zBack) window.scrollTo({ top: 0, behavior: 'smooth' });
375
+
376
+ // Update browser URL (skip for popstate — URL already correct)
377
+ if (!skipHistory) {
378
+ const newUrl = routePath.startsWith('/') ? routePath : `/${routePath}`;
379
+ history.pushState({ route: routePath }, '', newUrl);
380
+ }
381
+
382
+ this.logger.debug('[ClientNav] Navigation complete');
383
+ } catch (error) {
384
+ this.logger.error('[ClientNav] [ERROR] Navigation failed:', error);
385
+
386
+ this.client._isClientSideNav = false;
387
+
388
+ if (this.client._zVaFElement) {
389
+ this.client._zVaFElement.innerHTML = `
390
+ <div class="zAlert zAlert-danger zmt-4">
391
+ <strong>Navigation Error:</strong> ${error.message}
392
+ </div>
393
+ `;
394
+ }
395
+ } finally {
396
+ setTimeout(() => {
397
+ this.client._isClientSideNav = false;
398
+ }, 100);
399
+ }
400
+ }
401
+ }
402
+
403
+ export default NavigationManager;