@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,703 @@
1
+ /**
2
+ *
3
+ * Navigation Renderer - zTheme Navigation Components
4
+ *
5
+ *
6
+ * Renders navigation components aligned with zTheme:
7
+ * - zNav (navigation bars)
8
+ * - zNavbar (top navigation)
9
+ * - zBreadcrumb (breadcrumb trails)
10
+ * - zTabs (tabbed navigation)
11
+ * - zPagination (page navigation)
12
+ * - Sidebar navigation
13
+ * - Dropdown menus
14
+ *
15
+ * REFACTORED: Uses Layer 0 primitives
16
+ *
17
+ * @module rendering/navigation_renderer
18
+ * @layer 3
19
+ * @pattern Strategy (navigation components)
20
+ *
21
+ * @see https://github.com/ZoloAi/zTheme - zTheme Navigation
22
+ */
23
+
24
+ // ─────────────────────────────────────────────────────────────────
25
+ // Imports
26
+ // ─────────────────────────────────────────────────────────────────
27
+
28
+ // Layer 2: Utilities
29
+ import { withErrorBoundary } from '../../../zSys/validation/error_boundary.js';
30
+
31
+ // Layer 0: Primitives
32
+ import { createNav } from '../primitives/document_structure_primitives.js';
33
+ import { createList, createListItem } from '../primitives/lists_primitives.js';
34
+ import { createLink, createButton } from '../primitives/interactive_primitives.js';
35
+ import { createDiv, createSpan } from '../primitives/generic_containers.js';
36
+ import { renderLink } from '../primitives/link_primitives.js';
37
+
38
+ export class NavigationRenderer {
39
+ constructor(logger = null, client = null) {
40
+ this.logger = logger || console;
41
+ this.client = client; // NEW: Store client for link rendering
42
+
43
+ // Wrap renderNavBar method with error boundary
44
+ // Note: We wrap it after the class is fully initialized
45
+ const proto = Object.getPrototypeOf(this);
46
+ if (proto.renderNavBar) {
47
+ const originalRenderNavBar = proto.renderNavBar.bind(this);
48
+ this.renderNavBar = withErrorBoundary(originalRenderNavBar, {
49
+ component: 'NavigationRenderer.renderNavBar',
50
+ logger: this.logger
51
+ });
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Render a navigation bar from menu items (zTheme zNavbar component)
57
+ * @param {Array<string|Object>} items - Array of navigation items (strings or {label, href})
58
+ * @param {Object} options - Rendering options
59
+ * @returns {HTMLElement} - Navigation element with zNavbar classes
60
+ * @see https://github.com/ZoloAi/zTheme/blob/main/src/css/zNavbar.css
61
+ */
62
+ renderNavBar(items, options = {}) {
63
+ if (!Array.isArray(items) || items.length === 0) {
64
+ this.logger.warn('[NavigationRenderer] No items provided for navbar');
65
+ return null;
66
+ }
67
+
68
+ const {
69
+ className = 'zcli-navbar-meta',
70
+ theme = 'light',
71
+ activeIndex = null,
72
+ href = '#',
73
+ brand = null
74
+ } = options;
75
+
76
+ // Generate unique ID for collapse target
77
+ const collapseId = `navbar-collapse-${Math.random().toString(36).substr(2, 9)}`;
78
+
79
+ // Create nav container with zNavbar component classes (using primitive)
80
+ const nav = createNav({
81
+ class: `zNavbar zNavbar-${theme} ${className}`,
82
+ role: 'navigation'
83
+ });
84
+
85
+ // Add brand/logo if provided (using primitive)
86
+ if (brand) {
87
+ const brandLink = createLink('/', { class: 'zNavbar-brand' });
88
+ brandLink.textContent = brand;
89
+ nav.appendChild(brandLink);
90
+ }
91
+
92
+ // Create mobile hamburger toggle button (using primitive)
93
+ const toggleButton = createButton('button', {
94
+ class: 'zNavbar-toggler',
95
+ 'data-bs-toggle': 'collapse',
96
+ 'data-bs-target': `#${collapseId}`,
97
+ 'aria-controls': collapseId,
98
+ 'aria-expanded': 'false',
99
+ 'aria-label': 'Toggle navigation'
100
+ });
101
+
102
+ // Add Bootstrap Icon (hamburger menu)
103
+ toggleButton.innerHTML = `
104
+ <i class="bi bi-list" style="font-size: 1.5rem;"></i>
105
+ `;
106
+ nav.appendChild(toggleButton);
107
+
108
+ // Create navbar collapse wrapper (using primitive)
109
+ const collapseDiv = createDiv({
110
+ class: 'zNavbar-collapse',
111
+ id: collapseId
112
+ });
113
+
114
+ // FIX: Add manual toggle handler (zTheme doesn't include Bootstrap JS)
115
+ toggleButton.addEventListener('click', (e) => {
116
+ e.preventDefault();
117
+ const isExpanded = toggleButton.getAttribute('aria-expanded') === 'true';
118
+
119
+ // Toggle aria state
120
+ toggleButton.setAttribute('aria-expanded', !isExpanded);
121
+
122
+ // Toggle visibility (try both 'show' and 'zShow' for compatibility)
123
+ if (isExpanded) {
124
+ collapseDiv.classList.remove('show', 'zShow');
125
+ this.logger.debug('[NavigationRenderer] Navbar collapsed');
126
+ } else {
127
+ collapseDiv.classList.add('show', 'zShow');
128
+ this.logger.debug('[NavigationRenderer] Navbar expanded');
129
+ }
130
+ });
131
+ this.logger.debug('[NavigationRenderer] Hamburger toggle attached to:', collapseId);
132
+
133
+ // Create navigation list (using primitive)
134
+ const ul = createList(false, { class: 'zNavbar-nav' });
135
+
136
+ // REFACTORED: Use link_primitives.js for ALL navigation links
137
+ // This ensures consistent behavior between navbar and content links
138
+ items.forEach((item, index) => {
139
+ const li = createListItem({ class: 'zNav-item' });
140
+
141
+ // Check if this is a hierarchical item with zSub
142
+ if (typeof item === 'object' && item !== null && !item.label && !item.href) {
143
+ // Dict format: {"zProducts": {"zSub": ["zCLI", "zBifrost", ...]}}
144
+ const itemName = Object.keys(item)[0];
145
+ const itemData = item[itemName];
146
+
147
+ if (itemData && typeof itemData === 'object' && itemData.zSub && Array.isArray(itemData.zSub)) {
148
+ // This is a hierarchical menu item - render using zTheme's zDropdown component
149
+ li.classList.add('zDropdown'); // zTheme dropdown container
150
+
151
+ const parentLabel = itemName.replace(/^[$^~]+/, '');
152
+ const parentHref = this._convertDeltaLinkToHref(itemName);
153
+
154
+ // Create dropdown toggle link (zTheme adds caret automatically via ::after)
155
+ const dropdownLink = createLink(parentHref, {
156
+ class: `zNav-link zDropdown-toggle${activeIndex === index ? ' active' : ''}`,
157
+ 'data-toggle': 'dropdown',
158
+ 'aria-haspopup': 'true',
159
+ 'aria-expanded': 'false'
160
+ });
161
+ dropdownLink.textContent = parentLabel;
162
+
163
+ // Create dropdown menu using zTheme classes
164
+ const dropdownMenu = createDiv({ class: 'zDropdown-menu' });
165
+
166
+ // Add sub-items using zTheme's zDropdown-item class
167
+ itemData.zSub.forEach(subItem => {
168
+ const subHref = `${parentHref}/${subItem}`;
169
+ const subLink = createLink(subHref, { class: 'zDropdown-item' });
170
+ subLink.textContent = subItem;
171
+
172
+ // Add click handler for internal navigation
173
+ subLink.addEventListener('click', (e) => {
174
+ e.preventDefault();
175
+ // Close dropdown after selection
176
+ dropdownMenu.classList.remove('zShow');
177
+ dropdownLink.setAttribute('aria-expanded', 'false');
178
+
179
+ if (this.client && this.client.navigationManager) {
180
+ this.client.navigationManager.navigateToRoute(subHref);
181
+ } else {
182
+ window.location.href = subHref;
183
+ }
184
+ });
185
+
186
+ dropdownMenu.appendChild(subLink);
187
+ });
188
+
189
+ // Toggle dropdown on click (zTheme pattern)
190
+ dropdownLink.addEventListener('click', (e) => {
191
+ e.preventDefault();
192
+ const isOpen = dropdownMenu.classList.contains('zShow');
193
+
194
+ // Close all other dropdowns
195
+ document.querySelectorAll('.zDropdown-menu.zShow').forEach(menu => {
196
+ if (menu !== dropdownMenu) {
197
+ menu.classList.remove('zShow');
198
+ const toggle = menu.previousElementSibling;
199
+ if (toggle) toggle.setAttribute('aria-expanded', 'false');
200
+ }
201
+ });
202
+
203
+ // Toggle this dropdown
204
+ if (isOpen) {
205
+ dropdownMenu.classList.remove('zShow');
206
+ dropdownLink.setAttribute('aria-expanded', 'false');
207
+ } else {
208
+ dropdownMenu.classList.add('zShow');
209
+ dropdownLink.setAttribute('aria-expanded', 'true');
210
+ }
211
+ });
212
+
213
+ li.appendChild(dropdownLink);
214
+ li.appendChild(dropdownMenu);
215
+ ul.appendChild(li);
216
+
217
+ this.logger.debug(`[NavigationRenderer] Created dropdown for ${parentLabel}`);
218
+ return; // Continue to next item
219
+ }
220
+ }
221
+
222
+ // Handle simple item as string or object {label, href}
223
+ let itemLabel, itemHref, originalItem;
224
+ if (typeof item === 'string') {
225
+ // Strip navigation prefixes for clean display
226
+ // $ (delta link), ^ (bounce-back), ~ (anchor)
227
+ // Example: "$^zLogin" → "zLogin"
228
+ originalItem = item;
229
+ itemLabel = item.replace(/^[$^~]+/, '');
230
+ // Convert delta links ($zBlock) to web routes (/zBlock)
231
+ itemHref = this._convertDeltaLinkToHref(item);
232
+ } else if (typeof item === 'object' && item !== null) {
233
+ originalItem = item.label || item.text || '';
234
+ itemLabel = originalItem.replace(/^[$^~]+/, '');
235
+ itemHref = item.href || this._convertDeltaLinkToHref(itemLabel);
236
+ } else {
237
+ originalItem = String(item);
238
+ itemLabel = originalItem;
239
+ itemHref = href;
240
+ }
241
+
242
+ // Detect link type for renderLink primitive
243
+ const linkType = this._detectLinkType(itemHref, originalItem);
244
+
245
+ // Prepare link data for renderLink primitive
246
+ const linkData = {
247
+ label: itemLabel,
248
+ href: itemHref,
249
+ target: '_self',
250
+ link_type: linkType,
251
+ _zClass: `zNav-link${activeIndex === index ? ' active' : ''}`,
252
+ color: '',
253
+ window: {}
254
+ };
255
+
256
+ this.logger.debug('[NavigationRenderer] Creating navbar link:', linkData.label);
257
+
258
+ // Use renderLink primitive (now returns element directly)
259
+ const link = renderLink(linkData, null, this.client, this.logger);
260
+
261
+ if (link) {
262
+ li.appendChild(link);
263
+ } else {
264
+ this.logger.error('[NavigationRenderer] [ERROR] renderLink returned no link element');
265
+ }
266
+
267
+ ul.appendChild(li);
268
+ });
269
+
270
+ // Assemble: ul -> collapseDiv -> nav
271
+ collapseDiv.appendChild(ul);
272
+ nav.appendChild(collapseDiv);
273
+
274
+ // Close dropdowns when clicking outside (zTheme pattern)
275
+ const closeDropdowns = (e) => {
276
+ if (!nav.contains(e.target)) {
277
+ nav.querySelectorAll('.zDropdown-menu.zShow').forEach(menu => {
278
+ menu.classList.remove('zShow');
279
+ const toggle = menu.previousElementSibling;
280
+ if (toggle) {
281
+ toggle.setAttribute('aria-expanded', 'false');
282
+ }
283
+ });
284
+ }
285
+ };
286
+
287
+ // Use setTimeout to avoid immediate triggering
288
+ setTimeout(() => {
289
+ document.addEventListener('click', closeDropdowns);
290
+ }, 100);
291
+
292
+ this.logger.info('[NavigationRenderer] Rendered navbar (%s items)', items.length);
293
+
294
+ return nav;
295
+ }
296
+
297
+ /**
298
+ * Convert delta link notation ($zBlock) to web route (/zBlock)
299
+ *
300
+ * Delta links ($) are used in YAML for intra-file navigation.
301
+ * Navigation modifiers (^, ~) are stripped for clean URLs.
302
+ * In Bifrost mode, these are converted to web routes for proper navigation.
303
+ *
304
+ * @param {string} item - Item text (may contain $^zBlock notation with modifiers)
305
+ * @returns {string} - Web-friendly href
306
+ * @private
307
+ */
308
+ _convertDeltaLinkToHref(item) {
309
+ if (typeof item !== 'string') {
310
+ return '#';
311
+ }
312
+
313
+ // Strip all navigation prefixes: $ (delta), ^ (bounce-back), ~ (anchor)
314
+ // Example: "$^zLogin" → "zLogin" → "/zLogin"
315
+ const cleanBlock = item.replace(/^[$^~]+/, '');
316
+
317
+ // Check if original item had $ (delta link) or other navigation prefixes
318
+ if (item !== cleanBlock) {
319
+ // Had navigation prefixes - convert to web route
320
+ return `/${cleanBlock}`;
321
+ }
322
+
323
+ // Default: use item as-is (for explicit /path or # links)
324
+ return item.startsWith('/') || item.startsWith('#') ? item : `/${item}`;
325
+ }
326
+
327
+ /**
328
+ * Detect link type from href and original item.
329
+ *
330
+ * This mirrors the logic in link_primitives.js to ensure consistent
331
+ * link type detection across navbar and content links.
332
+ *
333
+ * @param {string} href - Converted href (e.g., "/zBlock")
334
+ * @param {string} originalItem - Original item with prefixes (e.g., "$zBlock")
335
+ * @returns {string} - Link type: 'delta', 'zpath', 'external', 'anchor', 'placeholder'
336
+ * @private
337
+ */
338
+ _detectLinkType(href, originalItem) {
339
+ // Check original item for navigation prefixes
340
+ if (originalItem && typeof originalItem === 'string') {
341
+ // Delta link ($) - internal navigation
342
+ if (originalItem.startsWith('$') || originalItem.includes('$')) {
343
+ return 'delta';
344
+ }
345
+ // zPath (@) - absolute path navigation
346
+ if (originalItem.startsWith('@')) {
347
+ return 'zpath';
348
+ }
349
+ }
350
+
351
+ // Check href for external URLs
352
+ if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('www.')) {
353
+ return 'external';
354
+ }
355
+
356
+ // Check for anchor links
357
+ if (href.startsWith('#') && href !== '#') {
358
+ return 'anchor';
359
+ }
360
+
361
+ // Placeholder link
362
+ if (!href || href === '#') {
363
+ return 'placeholder';
364
+ }
365
+
366
+ // Default: treat as delta (internal navigation)
367
+ return 'delta';
368
+ }
369
+
370
+ /**
371
+ * Render breadcrumb navigation (zTheme-styled)
372
+ * @param {Array<string>} trail - Breadcrumb trail items
373
+ * @param {Object} options - Rendering options
374
+ * @returns {HTMLElement} - Breadcrumb element with zTheme classes
375
+ */
376
+ renderBreadcrumb(trail, options = {}) {
377
+ if (!Array.isArray(trail) || trail.length === 0) {
378
+ return null;
379
+ }
380
+
381
+ const {
382
+ separator = '>',
383
+ className = 'zcli-breadcrumb'
384
+ } = options;
385
+
386
+ const nav = createNav({
387
+ class: `${className} zmb-3`,
388
+ 'aria-label': 'breadcrumb'
389
+ });
390
+
391
+ const ol = createList(true, {
392
+ class: 'zD-flex zFlex-row zFlex-items-center zGap-2'
393
+ });
394
+ ol.style.listStyle = 'none';
395
+ ol.style.padding = '0';
396
+ ol.style.margin = '0';
397
+
398
+ trail.forEach((item, index) => {
399
+ const li = createListItem({ class: 'breadcrumb-item' });
400
+
401
+ if (index === trail.length - 1) {
402
+ // Last item (current page) - use muted text, bold weight (using primitive)
403
+ const span = createSpan({
404
+ class: 'zText-muted zFw-bold',
405
+ 'aria-current': 'page'
406
+ });
407
+ span.textContent = item;
408
+ li.appendChild(span);
409
+ } else {
410
+ // Link to parent pages - use primary color (using primitive)
411
+ const a = createLink('#', { class: 'zText-primary zText-decoration-none' });
412
+ a.textContent = item;
413
+ li.appendChild(a);
414
+ }
415
+
416
+ ol.appendChild(li);
417
+
418
+ // Add separator (except after last item) (using primitive)
419
+ if (index < trail.length - 1) {
420
+ const sep = createSpan({ class: 'breadcrumb-separator zText-muted' });
421
+ sep.textContent = ` ${separator} `;
422
+ ol.appendChild(sep);
423
+ }
424
+ });
425
+
426
+ nav.appendChild(ol);
427
+ return nav;
428
+ }
429
+
430
+ /**
431
+ * Render breadcrumbs from zCrumbs display event (handles multiple trails)
432
+ * Uses zTheme breadcrumb structure: nav > ol.zBreadcrumb > li.zBreadcrumb-item
433
+ *
434
+ * Single trail: Returns <nav> directly (no wrapper div) - semantic HTML like zH1
435
+ * Multi-trail: Returns container div with multiple navs + scope labels
436
+ *
437
+ * @param {Object} eventData - Event data from backend zCrumbs event
438
+ * @returns {HTMLElement|null} - nav element (single trail) or container div (multi-trail)
439
+ * @see zOS/zTheme/Manual/ztheme-breadcrumb.html
440
+ */
441
+ /**
442
+ * Derive display labels from an array of zPaths using minimum-depth uniqueness.
443
+ * Mirrors the Python _derive_zpath_labels logic for Bifrost-side label rendering.
444
+ * @param {string[]} paths - Array of zPath strings (may include #N suffixes)
445
+ * @returns {string[]} - Unique display labels at minimum consistent depth
446
+ */
447
+ _deriveZpathLabels(paths) {
448
+ if (!paths || paths.length === 0) return [];
449
+ // resolve_zpath_references converts @.UI.* zPaths to HTTP routes before the chunk is
450
+ // sent to Bifrost (e.g. @.UI.zProducts.zUI.zOS.zOS → /zProducts/zOS).
451
+ // URL paths have no '.' separators, so the dot-split algorithm yields empty strings.
452
+ // Detect URL-format paths and derive labels from the last non-empty path segment instead.
453
+ if (paths[0] && paths[0].startsWith('/')) {
454
+ return paths.map(p => {
455
+ const segs = p.split('/').filter(Boolean);
456
+ return segs.length > 0 ? segs[segs.length - 1] : p;
457
+ });
458
+ }
459
+ // zPath strings (@.UI.* not yet resolved): minimum-depth uniqueness algorithm
460
+ const stripped = paths.map(p => p.split('#')[0]);
461
+ const parts = stripped.map(p => p.split('.'));
462
+ const maxDepth = Math.max(...parts.map(p => p.length));
463
+ for (let depth = 2; depth < maxDepth; depth++) {
464
+ const labels = parts.map(p => p.slice(-depth).join('.'));
465
+ if (new Set(labels).size === labels.length) return labels;
466
+ }
467
+ return parts.map(p => p.slice(1).join('.'));
468
+ }
469
+
470
+ /**
471
+ * Derive structure trail from the client's current page context.
472
+ * Reads zVaFolder + zVaFile from bifrostClient.zuiConfig — the runtime SSOT.
473
+ * @returns {string[]} - Folder segments + file label (e.g. ['zProducts','zOS','Events','zNavigation'])
474
+ */
475
+ _deriveStructureTrail() {
476
+ const folder = this.client?.zuiConfig?.zVaFolder || '';
477
+ const file = this.client?.zuiConfig?.zVaFile || '';
478
+ if (!folder && !file) return [];
479
+ // "@.UI.zProducts.zOS.Events" → strip root prefix → "UI.zProducts.zOS.Events" → split → drop mount root
480
+ const folderParts = folder.replace(/^[@~]\./, '').split('.');
481
+ // Keep only z-prefixed segments (named sections like zProducts, zOS).
482
+ // Plain organizational folders (Events, UI, etc.) are file-system details, not nav levels.
483
+ const segments = folderParts.slice(1).filter(p => p && p.startsWith('z'));
484
+ const fileLabel = file.startsWith('zUI.') ? file.slice(4) : file;
485
+ return [...segments, ...(fileLabel ? [fileLabel] : [])];
486
+ }
487
+
488
+ _formatLabel(item) {
489
+ return String(item).replace(/_/g, ' ');
490
+ }
491
+
492
+ /**
493
+ * Render breadcrumbs from a zCrumbs display event.
494
+ *
495
+ * Bifrost receives the raw expander display_data: {event, show, header, trail?, parent?}
496
+ * — the Python zCrumbs() method does NOT run in Bifrost mode (chunks are pre-built).
497
+ * All derivation (labels, structure trail) must happen here in JS.
498
+ *
499
+ * Modes:
500
+ * manual — trail[] of zPaths in eventData; derive labels; ancestors are zLink clickable
501
+ * structure — derive trail from client.zuiConfig.zVaFolder/zVaFile; ancestors display-only
502
+ * session — crumbs from separate try_gui_event payload (crumbs key) if present; else empty
503
+ * static — legacy: parent dot-path in eventData; display-only ancestors
504
+ *
505
+ * @param {Object} eventData - Raw event from backend: {event, show, trail?, parent?, crumbs?}
506
+ * @returns {HTMLElement|null}
507
+ */
508
+ renderBreadcrumbs(eventData) {
509
+ this.logger.debug('[NavigationRenderer] renderBreadcrumbs called', eventData);
510
+
511
+ const show = eventData.show || 'session';
512
+ const zMenu = eventData.zMenu === true || eventData.zMenu === 'true';
513
+ let displayLabels = [];
514
+ let navPaths = null; // zPaths for manual mode
515
+ let structureSegs = null; // raw URL segments for structure mode
516
+
517
+ if (show === 'manual') {
518
+ // trail: array of zPaths injected by the expander
519
+ const rawPaths = (eventData.trail || []).map(p => String(p).trim().replace(/^["']|["']$/g, ''));
520
+ if (!rawPaths.length) return null;
521
+ displayLabels = this._deriveZpathLabels(rawPaths);
522
+ navPaths = rawPaths;
523
+ this.logger.debug('[NavigationRenderer] manual mode, labels:', displayLabels);
524
+
525
+ } else if (show === 'structure') {
526
+ // Derive from current page context — raw segments double as URL path parts
527
+ structureSegs = this._deriveStructureTrail();
528
+ if (!structureSegs.length) return null;
529
+ displayLabels = structureSegs;
530
+ this.logger.debug('[NavigationRenderer] structure mode, zMenu=%s, trail:', zMenu, displayLabels);
531
+
532
+ } else if (show === 'session') {
533
+ // Session crumbs come via a separate try_gui_event payload (crumbs key)
534
+ // if the Python backend sent them; otherwise return null (no history yet)
535
+ const crumbsData = eventData.crumbs || {};
536
+ const trails = crumbsData.trails || {};
537
+ const visibleTrails = Object.entries(trails).filter(([k]) => !k.startsWith('_'));
538
+ if (!visibleTrails.length) {
539
+ this.logger.debug('[NavigationRenderer] session mode: no crumbs data, skipping');
540
+ return null;
541
+ }
542
+ displayLabels = visibleTrails[0][1];
543
+ this.logger.debug('[NavigationRenderer] session mode, labels:', displayLabels);
544
+
545
+ } else if (show === 'static') {
546
+ // Legacy: parent dot-path → display-only label trail
547
+ const parent = eventData.parent || '';
548
+ if (!parent) return null;
549
+ displayLabels = parent.split('.');
550
+ this.logger.debug('[NavigationRenderer] static (legacy) mode, labels:', displayLabels);
551
+ }
552
+
553
+ if (!displayLabels || !displayLabels.length) return null;
554
+
555
+ // Build nav > ol.zBreadcrumb
556
+ const nav = createNav({ 'aria-label': `${show} breadcrumb`, class: 'zmb-3' });
557
+ const ol = createList(true, { class: 'zBreadcrumb' });
558
+
559
+ displayLabels.forEach((label, index) => {
560
+ const isLast = index === displayLabels.length - 1;
561
+ const li = createListItem({
562
+ class: isLast ? 'zBreadcrumb-item zActive' : 'zBreadcrumb-item'
563
+ });
564
+
565
+ if (isLast) {
566
+ li.setAttribute('aria-current', 'page');
567
+ li.textContent = this._formatLabel(label);
568
+ } else if (show === 'manual' && navPaths && navPaths[index]) {
569
+ // manual ancestors: zMenu: true → clickable zLink; zMenu: false → disabled link
570
+ const zPath = navPaths[index];
571
+ if (zMenu) {
572
+ const a = createLink('#', {});
573
+ a.textContent = this._formatLabel(label);
574
+ a.onclick = async (e) => {
575
+ e.preventDefault();
576
+ this.logger.log(`[Breadcrumbs] zLink → ${zPath}`);
577
+ if (this.client) {
578
+ try {
579
+ if (typeof this.client.zLink === 'function') {
580
+ await this.client.zLink(zPath);
581
+ } else if (this.client.navigationManager) {
582
+ const url = this.client._zLinkPathToUrl?.(zPath) || zPath;
583
+ await this.client.navigationManager.navigateToRoute(url);
584
+ }
585
+ } catch (err) {
586
+ this.logger.error('[Breadcrumbs] zLink failed:', err);
587
+ }
588
+ }
589
+ };
590
+ li.appendChild(a);
591
+ } else {
592
+ // disabled link — same visual as zMenu:true but non-clickable
593
+ const a = createLink('#', {
594
+ 'aria-disabled': 'true',
595
+ tabindex: '-1'
596
+ });
597
+ a.style.pointerEvents = 'none';
598
+ a.style.cursor = 'default';
599
+ a.textContent = this._formatLabel(label);
600
+ li.appendChild(a);
601
+ }
602
+ } else if (show === 'structure' && structureSegs) {
603
+ // structure ancestors: cumulative URL path from folder segments
604
+ const href = '/' + structureSegs.slice(0, index + 1).join('/');
605
+ if (zMenu) {
606
+ // zMenu: true → real navigable link
607
+ const a = createLink(href, {});
608
+ a.textContent = this._formatLabel(label);
609
+ a.onclick = (e) => {
610
+ e.preventDefault();
611
+ if (this.client?.navigationManager) {
612
+ this.client.navigationManager.navigateToRoute(href);
613
+ } else {
614
+ window.location.href = href;
615
+ }
616
+ };
617
+ li.appendChild(a);
618
+ } else {
619
+ // zMenu: false (default) → disabled link (same visual, non-clickable)
620
+ const a = createLink(href, {
621
+ 'aria-disabled': 'true',
622
+ tabindex: '-1'
623
+ });
624
+ a.style.pointerEvents = 'none';
625
+ a.style.cursor = 'default';
626
+ a.textContent = this._formatLabel(label);
627
+ li.appendChild(a);
628
+ }
629
+ } else {
630
+ // session / static ancestors: display-only span
631
+ const span = createSpan({ class: 'zText-muted' });
632
+ span.textContent = this._formatLabel(label);
633
+ li.appendChild(span);
634
+ }
635
+
636
+ ol.appendChild(li);
637
+ });
638
+
639
+ nav.appendChild(ol);
640
+ this.logger.debug('[NavigationRenderer] Rendered breadcrumbs (%s mode, %s items)', show, displayLabels.length);
641
+ return nav;
642
+ }
643
+
644
+ /**
645
+ * Render vertical sidebar navigation (zTheme-styled)
646
+ * @param {Array<string>} items - Navigation item labels
647
+ * @param {Object} options - Rendering options
648
+ * @returns {HTMLElement} - Sidebar nav element with zTheme classes
649
+ */
650
+ renderSidebarNav(items, options = {}) {
651
+ if (!Array.isArray(items) || items.length === 0) {
652
+ return null;
653
+ }
654
+
655
+ const {
656
+ className = 'zcli-sidebar-nav',
657
+ activeIndex = null
658
+ } = options;
659
+
660
+ // Sidebar container with zTheme utilities (using primitive)
661
+ const nav = createNav({ class: `${className} zBg-light zP-3 zRounded` });
662
+ nav.style.width = '200px';
663
+
664
+ const ul = createList(false, { class: 'zD-flex zFlex-column zGap-2' });
665
+ ul.style.listStyle = 'none';
666
+ ul.style.padding = '0';
667
+ ul.style.margin = '0';
668
+
669
+ items.forEach((item, index) => {
670
+ const li = createListItem({ class: 'sidebar-item' });
671
+
672
+ const a = createLink('#');
673
+ a.textContent = item;
674
+
675
+ // zTheme sidebar link: padding, display block, rounded
676
+ a.className = 'sidebar-link zText-dark zText-decoration-none zP-2 zD-block zRounded';
677
+
678
+ // Active state with zTheme classes
679
+ if (activeIndex === index) {
680
+ a.classList.add('zBg-primary', 'zText-white', 'zFw-bold');
681
+ }
682
+
683
+ // Hover effect via zTheme classes
684
+ a.addEventListener('mouseenter', () => {
685
+ if (activeIndex !== index) {
686
+ a.classList.add('zBg-white');
687
+ }
688
+ });
689
+ a.addEventListener('mouseleave', () => {
690
+ if (activeIndex !== index) {
691
+ a.classList.remove('zBg-white');
692
+ }
693
+ });
694
+
695
+ li.appendChild(a);
696
+ ul.appendChild(li);
697
+ });
698
+
699
+ nav.appendChild(ul);
700
+ return nav;
701
+ }
702
+ }
703
+