@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,552 @@
1
+ /**
2
+ *
3
+ * Link Primitives - Semantic Link Rendering with Target Support
4
+ *
5
+ *
6
+ * Renders semantic HTML links with proper href, target, and security
7
+ * attributes for zDisplay link events.
8
+ *
9
+ * @module rendering/link_primitives
10
+ * @layer 1.0 (Event-aware primitive renderer)
11
+ * @pattern Factory + Event Handler Pattern
12
+ *
13
+ * Philosophy:
14
+ * - Semantic HTML (use <a> for navigation, not <button>)
15
+ * - Security-first (auto-add rel="noopener noreferrer" for _blank)
16
+ * - Target support (_blank, _self, _parent, _top, custom window.open())
17
+ * - Mode-aware (handles internal vs external vs anchor links)
18
+ *
19
+ * Link Types:
20
+ * - Internal Delta: $zAbout → Client-side routing
21
+ * - Internal zPath: @.UI.zUI.zAbout → Client-side routing
22
+ * - External: https://example.com → Native browser navigation
23
+ * - Anchor: #section → Smooth scroll to element
24
+ * - Placeholder: # → No navigation (styled text)
25
+ *
26
+ * Target Behavior:
27
+ * - _self: Navigate in current tab (default)
28
+ * - _blank: Open in new tab/window (auto-add security)
29
+ * - _parent: Navigate parent frame
30
+ * - _top: Navigate top-level frame
31
+ * - Custom window: Use window.open() with features
32
+ *
33
+ * Security:
34
+ * - External _blank links: Auto-add rel="noopener noreferrer"
35
+ * - Prevents window.opener exploitation (Tabnabbing attack)
36
+ * - User can override via explicit rel parameter
37
+ *
38
+ * Dependencies:
39
+ * - utils/dom_utils.js (createElement, setAttributes)
40
+ * - bifrost_client.js (for client-side navigation)
41
+ *
42
+ * Exports:
43
+ * - renderLink(linkData, container, client, logger) → HTMLAnchorElement
44
+ *
45
+ * Example:
46
+ * ```javascript
47
+ * import { renderLink } from './link_primitives.js';
48
+ *
49
+ * // Internal link
50
+ * renderLink({
51
+ * label: 'About',
52
+ * href: '$zAbout',
53
+ * target: '_self'
54
+ * }, container, bifrostClient);
55
+ *
56
+ * // External link (new tab)
57
+ * renderLink({
58
+ * label: 'GitHub',
59
+ * href: 'https://github.com',
60
+ * target: '_blank',
61
+ * _zClass: 'zBtn zBtn-primary'
62
+ * }, container, bifrostClient);
63
+ * ```
64
+ */
65
+
66
+ // ─────────────────────────────────────────────────────────────────
67
+ // Imports
68
+ // ─────────────────────────────────────────────────────────────────
69
+
70
+ // Layer 2: Utilities
71
+ import { createElement } from '../../../zSys/dom/dom_utils.js';
72
+
73
+ // Link type constants (must match backend ZLinkResolver.classify_href)
74
+ const LINK_TYPE_INTERNAL_DELTA = 'delta';
75
+ const LINK_TYPE_INTERNAL_ZPATH = 'zpath';
76
+ const LINK_TYPE_EXTERNAL = 'external';
77
+ const LINK_TYPE_ANCHOR = 'anchor';
78
+ const LINK_TYPE_PLACEHOLDER = 'placeholder';
79
+
80
+ // Named exports for shared use by text_renderer.js and other consumers
81
+ export { LINK_TYPE_INTERNAL_DELTA, LINK_TYPE_INTERNAL_ZPATH, LINK_TYPE_EXTERNAL, LINK_TYPE_ANCHOR, LINK_TYPE_PLACEHOLDER };
82
+
83
+ // Target constants
84
+ const TARGET_BLANK = '_blank';
85
+ const TARGET_SELF = '_self';
86
+
87
+ //
88
+ // Helper: Fallback Link Type Detection (Frontend Safety)
89
+ //
90
+
91
+ /**
92
+ * Detect link type from href when backend doesn't provide it.
93
+ *
94
+ * This is a fallback mechanism to ensure robust link rendering even
95
+ * when the backend omits the link_type field. It mirrors the backend's
96
+ * detection logic in display_event_links.py.
97
+ *
98
+ * @param {string} href - Link destination
99
+ * @returns {string} Detected link type constant
100
+ * @private
101
+ */
102
+ export function detectLinkType(href) { return _detectLinkTypeFromHref(href); }
103
+
104
+ function _detectLinkTypeFromHref(href) {
105
+ if (!href || href === '#') {
106
+ return LINK_TYPE_PLACEHOLDER;
107
+ }
108
+
109
+ // External URLs (http, https, www)
110
+ if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('www.')) {
111
+ return LINK_TYPE_EXTERNAL;
112
+ }
113
+
114
+ // Anchor links (#section)
115
+ if (href.startsWith('#') && href !== '#') {
116
+ return LINK_TYPE_ANCHOR;
117
+ }
118
+
119
+ // Internal delta links ($zBlock)
120
+ if (href.startsWith('$') || href.includes('$')) {
121
+ return LINK_TYPE_INTERNAL_DELTA;
122
+ }
123
+
124
+ // Internal zPath links (@.UI.zUI.zBlock)
125
+ if (href.startsWith('@')) {
126
+ return LINK_TYPE_INTERNAL_ZPATH;
127
+ }
128
+
129
+ // Default: treat web routes (/path) as internal delta
130
+ if (href.startsWith('/')) {
131
+ return LINK_TYPE_INTERNAL_DELTA;
132
+ }
133
+
134
+ // Fallback: placeholder
135
+ return LINK_TYPE_PLACEHOLDER;
136
+ }
137
+
138
+ //
139
+ // Main Link Renderer
140
+ //
141
+
142
+ /**
143
+ * Render a semantic link with mode-aware behavior and target support.
144
+ *
145
+ * Handles internal navigation (client-side routing), external links
146
+ * (native browser), anchor links (smooth scroll), and placeholder links
147
+ * (styled text only).
148
+ *
149
+ * Security: Auto-adds rel="noopener noreferrer" for external _blank links.
150
+ *
151
+ * @param {Object} linkData - Link configuration from backend
152
+ * @param {string} linkData.label - Link text to display
153
+ * @param {string} linkData.href - Link destination
154
+ * @param {string} linkData.target - Target behavior (_self, _blank, etc.)
155
+ * @param {string} linkData.link_type - Detected link type (delta, external, etc.)
156
+ * @param {string} [linkData.rel] - Link relationship (security)
157
+ * @param {string} [linkData._zClass] - CSS classes for styling
158
+ * @param {string} [linkData.color] - Color theme
159
+ * @param {Object} [linkData.window] - Window.open() features
160
+ * @param {HTMLElement} container - DOM element to append link to
161
+ * @param {Object} client - BifrostClient instance for navigation
162
+ *
163
+ * @example
164
+ * // Internal navigation
165
+ * renderLink({
166
+ * label: 'About',
167
+ * href: '$zAbout',
168
+ * target: '_self',
169
+ * link_type: 'delta'
170
+ * }, containerDiv, bifrostClient);
171
+ *
172
+ * @example
173
+ * // External link with new tab
174
+ * renderLink({
175
+ * label: 'Documentation',
176
+ * href: 'https://docs.example.com',
177
+ * target: '_blank',
178
+ * link_type: 'external',
179
+ * _zClass: 'zBtn zBtn-primary'
180
+ * }, containerDiv, bifrostClient);
181
+ *
182
+ * @example
183
+ * // Anchor link (smooth scroll)
184
+ * renderLink({
185
+ * label: 'Features',
186
+ * href: '#features',
187
+ * target: '_self',
188
+ * link_type: 'anchor'
189
+ * }, containerDiv, bifrostClient);
190
+ */
191
+ export function renderLink(linkData, container, client, logger = console) {
192
+ const {
193
+ label,
194
+ href,
195
+ target = TARGET_SELF,
196
+ link_type,
197
+ rel = '',
198
+ _zClass = '',
199
+ color = '',
200
+ window: windowFeatures = {},
201
+ disabled = false,
202
+ } = linkData;
203
+
204
+ let detectedLinkType = link_type;
205
+ if (!detectedLinkType || detectedLinkType === 'undefined') {
206
+ logger.warn('[LinkPrimitives] link_type missing, detecting from href:', href);
207
+ detectedLinkType = _detectLinkTypeFromHref(href);
208
+ logger.debug('[LinkPrimitives] Detected link type:', detectedLinkType, 'for href:', href);
209
+ }
210
+
211
+ // Create semantic <a> element
212
+ const link = createElement('a');
213
+ link.textContent = label;
214
+
215
+ // TERMINAL-FIRST PATTERN: color is the source of truth
216
+ // Smart class inference based on context:
217
+ // - If _zClass has 'zBtn' → add zBtn-{color}
218
+ // - If plain link (no zBtn) → add zText-{color}
219
+ // - Group styling handled by orchestrator
220
+
221
+ let hasButtonClass = false;
222
+ if (_zClass) {
223
+ const classes = _zClass.split(' ').filter(c => c.trim());
224
+ if (classes.length > 0) {
225
+ link.classList.add(...classes);
226
+ hasButtonClass = classes.some(c => c === 'zBtn' || c.startsWith('zBtn-'));
227
+ }
228
+ }
229
+
230
+ if (color) {
231
+ const colorLower = color.toLowerCase();
232
+ if (hasButtonClass) {
233
+ const colorClass = `zBtn-${colorLower}`;
234
+ if (!link.classList.contains(colorClass)) {
235
+ link.classList.add(colorClass);
236
+ }
237
+ } else {
238
+ const colorClass = `zText-${colorLower}`;
239
+ link.classList.add(colorClass);
240
+ }
241
+ }
242
+
243
+ // RBAC disabled — render non-clickable element (Python gate fired)
244
+ if (disabled) {
245
+ link.setAttribute('aria-disabled', 'true');
246
+ link.style.pointerEvents = 'none';
247
+ link.style.opacity = '0.4';
248
+ link.style.cursor = 'not-allowed';
249
+ logger.debug('[LinkPrimitives] Link disabled (RBAC denied by server):', href);
250
+ if (container) container.appendChild(link);
251
+ return link;
252
+ }
253
+
254
+ // Handle different link types using DETECTED type (fallback-safe)
255
+ logger.debug('[LinkPrimitives] Setting up link type:', detectedLinkType, 'href:', href, 'hasClient:', !!client);
256
+ switch (detectedLinkType) {
257
+ case LINK_TYPE_INTERNAL_DELTA:
258
+ case LINK_TYPE_INTERNAL_ZPATH:
259
+ case 'internal_delta': // Backend sends with underscore prefix
260
+ case 'internal_zpath': // Backend sends with underscore prefix
261
+ _setupInternalLink(link, href, target, windowFeatures, client, logger);
262
+ break;
263
+
264
+ case LINK_TYPE_EXTERNAL:
265
+ _setupExternalLink(link, href, target, rel, windowFeatures, logger);
266
+ break;
267
+
268
+ case LINK_TYPE_ANCHOR:
269
+ _setupAnchorLink(link, href, logger);
270
+ break;
271
+
272
+ case LINK_TYPE_PLACEHOLDER:
273
+ _setupPlaceholderLink(link);
274
+ break;
275
+
276
+ default:
277
+ logger.warn('[LinkPrimitives] Unknown link type after detection:', detectedLinkType);
278
+ _setupPlaceholderLink(link);
279
+ }
280
+
281
+ // Append to container if provided (legacy), otherwise return element
282
+ if (container) {
283
+ container.appendChild(link);
284
+ }
285
+ return link; // Return link element for direct use
286
+ }
287
+
288
+ //
289
+ // zPath to URL Conversion
290
+ //
291
+
292
+ /**
293
+ * Convert zPath to URL path for client-side routing.
294
+ *
295
+ * Example conversions:
296
+ * - @.UI.zProducts.zTheme.zUI.zGrid.zGrid_Details → /zProducts/zTheme/zGrid
297
+ * - @.UI.zAbout.zAbout_Details → /zAbout
298
+ * - $zBlock → $zBlock (delta links pass through)
299
+ * - /regular/path → /regular/path (web paths pass through)
300
+ *
301
+ * @private
302
+ * @param {string} href - zPath or regular path
303
+ * @returns {string} URL path for navigation
304
+ */
305
+ export function convertZPathToURL(href) { return _convertZPathToURL(href); }
306
+
307
+ function _convertZPathToURL(href) {
308
+ // Pass through delta links ($) and web paths (/)
309
+ if (!href.startsWith('@')) {
310
+ return href;
311
+ }
312
+
313
+ // Parse zPath: @.UI.zProducts.zTheme.zUI.zGrid.zGrid_Details
314
+ // 1. Remove @.UI. prefix
315
+ // 2. Split remaining path by dots
316
+ // 3. Remove zUI (file prefix marker)
317
+ // 4. Remove final block name (ends with _Details or _Section)
318
+ // 5. Convert to /path/format
319
+
320
+ let path = href.replace(/^@\.UI\./, ''); // Remove @.UI.
321
+ const parts = path.split('.');
322
+
323
+ // Filter out zUI markers and block names (typically last segment with _)
324
+ const pathParts = parts.filter((part, index) => {
325
+ // Keep non-zUI parts
326
+ if (part === 'zUI') return false;
327
+ // Remove last segment if it looks like a block name (has underscore or ends in Details/Section)
328
+ if (index === parts.length - 1 && (part.includes('_') || part.endsWith('Details') || part.endsWith('Section'))) {
329
+ return false;
330
+ }
331
+ return true;
332
+ });
333
+
334
+ // Convert to /path format
335
+ return '/' + pathParts.join('/');
336
+ }
337
+
338
+ //
339
+ // Internal Link Setup (Client-Side Routing)
340
+ //
341
+
342
+ /**
343
+ * Setup internal link for client-side routing.
344
+ *
345
+ * Prevents default browser navigation and uses BifrostClient.navigate()
346
+ * for SPA-style routing. Supports opening in new tab via window.open().
347
+ *
348
+ * @private
349
+ * @param {HTMLAnchorElement} link - Link element to configure
350
+ * @param {string} href - Internal path (delta or zPath)
351
+ * @param {string} target - Target behavior
352
+ * @param {Object} windowFeatures - Custom window features
353
+ * @param {Object} client - BifrostClient instance
354
+ * @param {Object} logger - Logger instance
355
+ */
356
+ function _setupInternalLink(link, href, target, windowFeatures, client, logger) {
357
+ // Convert zPath to URL path if needed
358
+ const navigationPath = _convertZPathToURL(href);
359
+
360
+ logger.debug('[LinkPrimitives] _setupInternalLink called:', { href, navigationPath, hasClient: !!client, hasNavigateMethod: !!(client && client._navigateToRoute) });
361
+
362
+ // CRITICAL: Set href to the actual path for proper browser behavior
363
+ // This allows middle-click, right-click "Open in new tab", and accessibility
364
+ link.href = navigationPath;
365
+
366
+ // Internal link setup (silent)
367
+
368
+ link.addEventListener('click', (e) => {
369
+ e.preventDefault();
370
+ e.stopPropagation();
371
+ logger.debug('[LinkPrimitives] Link clicked:', navigationPath);
372
+
373
+ if (target === TARGET_BLANK) {
374
+ // Open in new tab/window using window.open()
375
+ const newWindow = _openInNewWindow(navigationPath, windowFeatures, client, logger);
376
+ if (newWindow) {
377
+ logger.debug(`[LinkPrimitives] Opened ${navigationPath} in new tab`);
378
+ }
379
+ } else {
380
+ // Navigate in current tab via client-side routing
381
+ if (client && typeof client._navigateToRoute === 'function') {
382
+ logger.debug('[LinkPrimitives] Calling client._navigateToRoute:', navigationPath);
383
+ client._navigateToRoute(navigationPath);
384
+ } else {
385
+ logger.error('[LinkPrimitives] [ERROR] BifrostClient._navigateToRoute() not available:', {
386
+ hasClient: !!client,
387
+ clientType: client ? client.constructor.name : 'none',
388
+ clientKeys: client ? Object.keys(client).filter(k => k.includes('nav')) : []
389
+ });
390
+ }
391
+ }
392
+ });
393
+ }
394
+
395
+ //
396
+ // External Link Setup (Native Browser Navigation)
397
+ //
398
+
399
+ /**
400
+ * Setup external link with proper security and target attributes.
401
+ *
402
+ * Auto-adds rel="noopener noreferrer" for _blank to prevent window.opener
403
+ * exploitation (Tabnabbing attack).
404
+ *
405
+ * @private
406
+ * @param {HTMLAnchorElement} link - Link element to configure
407
+ * @param {string} href - External URL
408
+ * @param {string} target - Target behavior
409
+ * @param {string} rel - Link relationship
410
+ * @param {Object} windowFeatures - Custom window features
411
+ * @param {Object} logger - Logger instance
412
+ */
413
+ function _setupExternalLink(link, href, target, rel, windowFeatures, logger) {
414
+ link.href = href;
415
+ link.target = target;
416
+
417
+ // External link setup (silent)
418
+
419
+ // Security: Auto-add rel="noopener noreferrer" for _blank
420
+ if (target === TARGET_BLANK && !rel) {
421
+ link.rel = 'noopener noreferrer';
422
+ } else if (rel) {
423
+ link.rel = rel;
424
+ }
425
+
426
+ // Custom window features (if specified)
427
+ if (target === TARGET_BLANK && Object.keys(windowFeatures).length > 0) {
428
+ link.addEventListener('click', (e) => {
429
+ e.preventDefault();
430
+ logger.debug('[LinkPrimitives] External link clicked (custom window):', href);
431
+ _openInNewWindow(href, windowFeatures, null, logger);
432
+ });
433
+ } else {
434
+ // Add click log for standard external links too
435
+ link.addEventListener('click', (_e) => {
436
+ logger.debug('[LinkPrimitives] External link clicked (native):', href);
437
+ // No preventDefault - let browser handle normally
438
+ });
439
+ }
440
+ }
441
+
442
+ //
443
+ // Anchor Link Setup (Smooth Scroll)
444
+ //
445
+
446
+ /**
447
+ * Setup anchor link for smooth scrolling to target element.
448
+ *
449
+ * Uses scrollIntoView with smooth behavior for better UX.
450
+ * Warns if target element not found.
451
+ *
452
+ * @private
453
+ * @param {HTMLAnchorElement} link - Link element to configure
454
+ * @param {string} href - Anchor hash (e.g., "#features")
455
+ * @param {Object} logger - Logger instance
456
+ */
457
+ function _setupAnchorLink(link, href, logger) {
458
+ link.href = href;
459
+ link.addEventListener('click', (e) => {
460
+ e.preventDefault();
461
+ const targetElement = document.querySelector(href);
462
+ if (targetElement) {
463
+ targetElement.scrollIntoView({ behavior: 'smooth' });
464
+ } else {
465
+ logger.warn(`[LinkPrimitives] Anchor target not found: ${href}`);
466
+ }
467
+ });
468
+ }
469
+
470
+ //
471
+ // Placeholder Link Setup (No Navigation)
472
+ //
473
+
474
+ /**
475
+ * Setup placeholder link (no navigation action).
476
+ *
477
+ * Used for design/mock purposes or "coming soon" links.
478
+ * Prevents default click behavior.
479
+ *
480
+ * @private
481
+ * @param {HTMLAnchorElement} link - Link element to configure
482
+ */
483
+ function _setupPlaceholderLink(link) {
484
+ link.href = '#';
485
+
486
+ // Placeholder link setup (silent)
487
+
488
+ link.addEventListener('click', (e) => {
489
+ e.preventDefault();
490
+ // No action - just styled text
491
+ });
492
+ }
493
+
494
+ //
495
+ // Window.open() Helper
496
+ //
497
+
498
+ /**
499
+ * Open URL in new window with custom features.
500
+ *
501
+ * Centers the window on screen and applies custom width, height, and
502
+ * window features (menubar, toolbar, etc.).
503
+ *
504
+ * @private
505
+ * @param {string} url - URL to open
506
+ * @param {Object} features - Window features
507
+ * @param {number} [features.width=800] - Window width
508
+ * @param {number} [features.height=600] - Window height
509
+ * @param {string} [features.features] - Custom window.open() features string
510
+ * @param {Object} client - BifrostClient (for internal URLs)
511
+ * @param {Object} logger - Logger instance
512
+ * @returns {Window|null} New window reference or null if blocked
513
+ */
514
+ function _openInNewWindow(url, features = {}, client = null, logger = console) {
515
+ const { width = 800, height = 600, features: customFeatures = '' } = features;
516
+
517
+ // Calculate center position
518
+ const left = (screen.width - width) / 2;
519
+ const top = (screen.height - height) / 2;
520
+
521
+ // Build features string
522
+ const featuresStr = customFeatures ||
523
+ `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
524
+
525
+ // For internal URLs with client, construct full URL
526
+ let fullUrl = url;
527
+ if (client && (url.startsWith('$') || url.startsWith('@'))) {
528
+ // Convert zPath to URL path first
529
+ const navigationPath = _convertZPathToURL(url);
530
+ // Then construct full URL (same origin)
531
+ fullUrl = `${window.location.origin}${navigationPath}`;
532
+ logger.debug('[LinkPrimitives] Opening internal link in new window:', {
533
+ original: url,
534
+ converted: navigationPath,
535
+ fullUrl
536
+ });
537
+ }
538
+
539
+ // Open new window
540
+ const newWindow = window.open(fullUrl, '_blank', featuresStr);
541
+
542
+ if (newWindow) {
543
+ newWindow.focus();
544
+ return newWindow;
545
+ } else {
546
+ logger.error('[LinkPrimitives] Popup blocked or failed to open');
547
+ return null;
548
+ }
549
+ }
550
+
551
+ export default { renderLink };
552
+