@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,797 @@
1
+ /**
2
+ * L3_Abstraction/orchestrator/input_event_handler.js
3
+ *
4
+ * Input Event Handler - Input Request Rendering
5
+ *
6
+ * Handles complex input request events:
7
+ * - read_string / read_password: Text/password inputs with validation
8
+ * - read_bool: Boolean checkboxes with conditional rendering
9
+ * - selection: Dropdown/radio/checkbox selection controls
10
+ *
11
+ * Extracted from zdisplay_orchestrator.js (Phase 9, Task 2.7)
12
+ *
13
+ * @module orchestrator/input_event_handler
14
+ * @layer L3 (Abstraction)
15
+ */
16
+
17
+ import { TYPOGRAPHY } from '../../L1_Foundation/constants/bifrost_constants.js';
18
+ import { convertStyleToString } from '../../zSys/dom/style_utils.js';
19
+
20
+ /**
21
+ * Append a red required marker (*) to a label/legend when the field is required.
22
+ * SSOT — styled by .zRequired in zbase.css. aria-hidden since `required` already
23
+ * exposes the constraint to assistive tech via the input element.
24
+ */
25
+ function appendRequiredMark(labelEl, required) {
26
+ if (!required || !labelEl) return;
27
+ const star = document.createElement('span');
28
+ star.className = 'zRequired';
29
+ star.textContent = ' *';
30
+ star.setAttribute('aria-hidden', 'true');
31
+ labelEl.appendChild(star);
32
+ }
33
+
34
+ export class InputEventHandler {
35
+ constructor(client, logger) {
36
+ this.client = client;
37
+ this.logger = logger;
38
+ }
39
+
40
+ /**
41
+ * Handle read_string and read_password events
42
+ * @param {string} event - Event type ('read_string' or 'read_password')
43
+ * @param {Object} eventData - Event data
44
+ * @param {HTMLElement} parentElement - Parent element for context detection
45
+ * @returns {HTMLElement} - Input element or wrapper
46
+ */
47
+ async handleTextInput(event, eventData, parentElement) {
48
+ const { createLabel, createInput, createTextarea } = await import('../../L2_Handling/display/primitives/form_primitives.js');
49
+
50
+ const inputType = event === 'read_password' ? 'password' : (eventData.type || 'text');
51
+ const prompt = eventData.prompt || '';
52
+ const placeholder = eventData.placeholder || '';
53
+ const required = eventData.required || false;
54
+ const defaultValue = eventData.default || '';
55
+ const disabled = eventData.disabled || false;
56
+ const readonly = eventData.readonly || false;
57
+ const multiple = eventData.multiple || false;
58
+ const title = eventData.title || '';
59
+ const datalist = eventData.datalist || null;
60
+
61
+ // Conditional rendering support (if parameter from zWizard)
62
+ const condition = eventData.if || null;
63
+ if (condition) {
64
+ this.logger.log(`[InputEventHandler] Found 'if' condition in read_string event: "${condition}"`);
65
+ }
66
+
67
+ // Support zId (universal), _zId (Bifrost-only), and _id (legacy)
68
+ const inputId = eventData.zId || eventData._zId || eventData._id || `input_${Math.random().toString(36).substr(2, 9)}`;
69
+
70
+ // Generate datalist ID if datalist exists
71
+ const datalistId = datalist ? `${inputId}_datalist` : null;
72
+
73
+ // Support aria-describedby for accessibility (link to help text)
74
+ const ariaDescribedBy = eventData.aria_described_by || eventData.ariaDescribedBy || eventData['aria-describedby'];
75
+
76
+ // Detect if we're inside a zInputGroup context (parent or ancestor has zInputGroup class)
77
+ let isInsideInputGroup = false;
78
+ let checkParent = parentElement;
79
+ while (checkParent && checkParent !== document.body) {
80
+ if (checkParent.classList && checkParent.classList.contains('zInputGroup')) {
81
+ isInsideInputGroup = true;
82
+ break;
83
+ }
84
+ checkParent = checkParent.parentElement;
85
+ }
86
+
87
+ // Create wrapper div only if prompt exists AND not inside input group
88
+ // Otherwise return input directly to avoid double-nesting in grid layouts or input groups
89
+ let wrapper = null;
90
+ let wrapperClasses = null; // Track _zClass for wrapper (not input element)
91
+
92
+ // Create label if prompt exists (connected to input via for/id)
93
+ if (prompt && !isInsideInputGroup) {
94
+ wrapper = document.createElement('div');
95
+
96
+ // Apply _zClass to wrapper (not input element) to avoid double-nesting
97
+ // When wrapper exists, _zClass applies to the wrapper container
98
+ if (eventData._zClass) {
99
+ const classes = eventData._zClass.split(' ').filter(c => c.trim());
100
+ wrapper.classList.add(...classes);
101
+ wrapperClasses = eventData._zClass;
102
+ this.logger.log(`[InputEventHandler] Applied _zClass to wrapper: ${eventData._zClass}`);
103
+ }
104
+
105
+ // SSOT — every prompt label is a .zLabel (styled by zbase.css), regardless
106
+ // of _zClass. No magic-string branch → consistent label across all fields.
107
+ const label = createLabel(inputId, { class: 'zLabel' });
108
+ label.textContent = prompt;
109
+ appendRequiredMark(label, required);
110
+ wrapper.appendChild(label);
111
+ // Add line break after label (semantic HTML pattern)
112
+ wrapper.appendChild(document.createElement('br'));
113
+ }
114
+
115
+ // Check if we'll have prefix/suffix (which creates zInputGroup)
116
+ // Helper to format prefix/suffix values (defined early for class determination)
117
+ const formatAffix = (value) => {
118
+ if (!value && value !== 0) return '';
119
+ if (typeof value === 'string') return value;
120
+ if (typeof value === 'boolean') return String(value);
121
+ if (typeof value === 'number') {
122
+ if (value >= 0 && value < 1) {
123
+ return value.toFixed(2).replace(/^0/, '') || '0';
124
+ }
125
+ return String(value);
126
+ }
127
+ return String(value);
128
+ };
129
+
130
+ const prefix = formatAffix(eventData.prefix);
131
+ const suffix = formatAffix(eventData.suffix);
132
+ const hasInputGroup = !!(prefix || suffix);
133
+
134
+ // Build input classes:
135
+ // - If inside zInputGroup, use 'zInput' (required by CSS: .zInputGroup > .zInput)
136
+ // - If wrapper exists, use default 'zForm-control' (wrapper has _zClass)
137
+ // - Otherwise use _zClass or default 'zForm-control'
138
+ let inputClasses;
139
+ if (isInsideInputGroup) {
140
+ // If inside zInputGroup, use 'zInput' class (required by CSS: .zInputGroup > .zInput)
141
+ inputClasses = 'zInput';
142
+ } else if (hasInputGroup) {
143
+ // Input groups require 'zInput' class for proper flex styling
144
+ inputClasses = 'zInput';
145
+ } else if (wrapperClasses) {
146
+ // Wrapper has _zClass, input gets default styling
147
+ inputClasses = 'zForm-control';
148
+ } else {
149
+ // No wrapper, use _zClass if provided, otherwise default
150
+ inputClasses = eventData._zClass || 'zForm-control';
151
+ }
152
+
153
+ // Handle textarea vs input
154
+ let inputElement;
155
+ if (inputType === 'textarea') {
156
+ // Multi-line textarea
157
+ const rows = eventData.rows || 3;
158
+ const textareaAttrs = {
159
+ id: inputId,
160
+ placeholder: placeholder,
161
+ required: required,
162
+ rows: rows,
163
+ class: inputClasses
164
+ };
165
+
166
+ if (ariaDescribedBy) {
167
+ textareaAttrs['aria-describedby'] = ariaDescribedBy;
168
+ }
169
+
170
+ if (disabled) {
171
+ textareaAttrs.disabled = true;
172
+ }
173
+
174
+ if (readonly) {
175
+ textareaAttrs.readonly = true;
176
+ }
177
+
178
+ if (title) {
179
+ textareaAttrs.title = title;
180
+ }
181
+
182
+ inputElement = createTextarea(textareaAttrs);
183
+ inputElement.textContent = defaultValue; // Use textContent for textarea, not value
184
+ } else {
185
+ // Single-line input
186
+ const inputAttrs = {
187
+ id: inputId,
188
+ placeholder: placeholder,
189
+ required: required,
190
+ value: defaultValue,
191
+ class: inputClasses
192
+ };
193
+
194
+ // Add list attribute if datalist exists
195
+ if (datalistId) {
196
+ inputAttrs.list = datalistId;
197
+ }
198
+
199
+ if (ariaDescribedBy) {
200
+ inputAttrs['aria-describedby'] = ariaDescribedBy;
201
+ }
202
+
203
+ if (disabled) {
204
+ inputAttrs.disabled = true;
205
+ }
206
+
207
+ if (readonly) {
208
+ inputAttrs.readonly = true;
209
+ }
210
+
211
+ if (multiple) {
212
+ inputAttrs.multiple = true;
213
+ }
214
+
215
+ if (title) {
216
+ inputAttrs.title = title;
217
+ }
218
+
219
+ // Field name (needed for multipart field + stable selectors) and file accept filter.
220
+ if (eventData.name) {
221
+ inputAttrs.name = eventData.name;
222
+ }
223
+ if (inputType === 'file' && eventData.accept) {
224
+ inputAttrs.accept = eventData.accept;
225
+ }
226
+
227
+ inputElement = createInput(inputType, inputAttrs);
228
+ }
229
+
230
+ // Handle input groups (prefix/suffix pattern) - Terminal-first design
231
+ // Note: prefix and suffix were already determined above for class selection
232
+ if (hasInputGroup) {
233
+ // Create .zInputGroup wrapper for prefix/suffix pattern
234
+ const inputGroup = document.createElement('div');
235
+ inputGroup.classList.add('zInputGroup');
236
+
237
+ // Add prefix text before input
238
+ // Position-specific class (zInputGroup-prefix) lets users restyle the prefix
239
+ // alone (color/font) without touching the input or the suffix.
240
+ if (prefix) {
241
+ const prefixSpan = document.createElement('span');
242
+ prefixSpan.classList.add('zInputGroup-text', 'zInputGroup-prefix');
243
+ prefixSpan.textContent = prefix;
244
+ inputGroup.appendChild(prefixSpan);
245
+ }
246
+
247
+ // Add input element
248
+ inputGroup.appendChild(inputElement);
249
+
250
+ // Add suffix text after input
251
+ // Position-specific class (zInputGroup-suffix) lets users restyle the suffix
252
+ // alone (color/font) without touching the input or the prefix.
253
+ if (suffix) {
254
+ const suffixSpan = document.createElement('span');
255
+ suffixSpan.classList.add('zInputGroup-text', 'zInputGroup-suffix');
256
+ suffixSpan.textContent = suffix;
257
+ inputGroup.appendChild(suffixSpan);
258
+ }
259
+
260
+ // Replace inputElement with the input group
261
+ inputElement = inputGroup;
262
+
263
+ this.logger.log(`[InputEventHandler] Created input group with prefix='${prefix}', suffix='${suffix}'`);
264
+ }
265
+
266
+ // If wrapper exists (has prompt), append input to wrapper and return wrapper
267
+ // Otherwise return input/textarea directly to avoid double-nesting in grid layouts
268
+ let element;
269
+ if (wrapper) {
270
+ wrapper.appendChild(inputElement);
271
+
272
+ // Add datalist element if datalist exists
273
+ if (datalist && Array.isArray(datalist)) {
274
+ const datalistElement = document.createElement('datalist');
275
+ datalistElement.id = datalistId;
276
+
277
+ datalist.forEach(optionValue => {
278
+ const option = document.createElement('option');
279
+ option.value = optionValue;
280
+ datalistElement.appendChild(option);
281
+ });
282
+
283
+ wrapper.appendChild(datalistElement);
284
+ }
285
+
286
+ // Apply _zStyle to wrapper if present (when wrapper exists, styles go on wrapper)
287
+ if (eventData._zStyle) {
288
+ const cssString = convertStyleToString(eventData._zStyle, this.logger);
289
+ if (cssString) {
290
+ wrapper.setAttribute('style', cssString);
291
+ }
292
+ }
293
+
294
+ // Handle conditional rendering (if parameter from zWizard)
295
+ if (condition) {
296
+ wrapper.setAttribute('data-zif', condition);
297
+ wrapper.style.display = 'none'; // Initially hidden
298
+ this.logger.log(`[InputEventHandler] Input with condition: ${condition} (initially hidden)`);
299
+ }
300
+
301
+ element = wrapper;
302
+ } else {
303
+ // When returning input/textarea directly, apply _zStyle if present
304
+ // This allows inline styles for grid layout adjustments (e.g., padding-top)
305
+ if (eventData._zStyle) {
306
+ const cssString = convertStyleToString(eventData._zStyle, this.logger);
307
+ if (cssString) {
308
+ inputElement.setAttribute('style', cssString);
309
+ }
310
+ }
311
+
312
+ // If datalist exists but no wrapper, create minimal wrapper for datalist
313
+ if (datalist && Array.isArray(datalist)) {
314
+ const container = document.createElement('div');
315
+ container.appendChild(inputElement);
316
+
317
+ const datalistElement = document.createElement('datalist');
318
+ datalistElement.id = datalistId;
319
+
320
+ datalist.forEach(optionValue => {
321
+ const option = document.createElement('option');
322
+ option.value = optionValue;
323
+ datalistElement.appendChild(option);
324
+ });
325
+
326
+ container.appendChild(datalistElement);
327
+
328
+ // Handle conditional rendering
329
+ if (condition) {
330
+ container.setAttribute('data-zif', condition);
331
+ container.style.display = 'none'; // Initially hidden
332
+ this.logger.log(`[InputEventHandler] Input with condition: ${condition} (initially hidden)`);
333
+ }
334
+
335
+ element = container;
336
+ } else {
337
+ // Handle conditional rendering for bare input
338
+ if (condition) {
339
+ inputElement.setAttribute('data-zif', condition);
340
+ inputElement.style.display = 'none'; // Initially hidden
341
+ this.logger.log(`[InputEventHandler] Input with condition: ${condition} (initially hidden)`);
342
+ }
343
+
344
+ element = inputElement;
345
+ }
346
+ }
347
+
348
+ // Declarative zAPI upload: a file input whose block carried onChange.zAPI is
349
+ // stamped server-side with zapi_url/zapi_method. Bind change → multipart POST
350
+ // → swap the nearest avatar img. (No zFunc ever reaches the client.)
351
+ if (inputType === 'file' && eventData.zapi_url) {
352
+ const fileInput = (element.tagName === 'INPUT' && element.type === 'file')
353
+ ? element
354
+ : element.querySelector('input[type=file]');
355
+ if (fileInput) {
356
+ this._bindZapiUpload(fileInput, eventData);
357
+ }
358
+ }
359
+
360
+ this.logger.log(`[InputEventHandler] Rendered ${event} ${inputType} (id=${inputId}, aria-describedby=${ariaDescribedBy || 'none'}, condition=${condition || 'none'})`);
361
+ return element;
362
+ }
363
+
364
+ /**
365
+ * Bind a declarative zAPI file upload to a file input.
366
+ * On change: POST the selected file as multipart to the server-stamped endpoint,
367
+ * then on a {ok, data.url} envelope swap the nearest avatar image.
368
+ * @param {HTMLInputElement} fileInput
369
+ * @param {Object} eventData - carries zapi_url, zapi_method, zapi_field/name
370
+ */
371
+ _bindZapiUpload(fileInput, eventData) {
372
+ const url = eventData.zapi_url;
373
+ const method = (eventData.zapi_method || 'POST').toUpperCase();
374
+ const field = eventData.zapi_field || eventData.name || 'file';
375
+
376
+ fileInput.addEventListener('change', async () => {
377
+ const file = fileInput.files && fileInput.files[0];
378
+ if (!file) return;
379
+
380
+ const form = new FormData();
381
+ form.append(field, file, file.name);
382
+
383
+ try {
384
+ this.logger.log(`[InputEventHandler] zAPI upload → ${method} ${url} (${file.name}, ${file.size}B)`);
385
+ const resp = await fetch(url, { method, body: form, credentials: 'same-origin' });
386
+ let json = null;
387
+ try { json = await resp.json(); } catch (_) { /* non-JSON */ }
388
+
389
+ if (resp.ok && json && json.ok && json.data && json.data.url) {
390
+ this._swapAvatar(fileInput, json.data.url);
391
+ this.logger.log(`[InputEventHandler] zAPI upload ok → ${json.data.url}`);
392
+ } else {
393
+ const err = (json && json.error) || `HTTP ${resp.status}`;
394
+ this.logger.error(`[InputEventHandler] zAPI upload failed: ${err}`);
395
+ }
396
+ } catch (e) {
397
+ this.logger.error(`[InputEventHandler] zAPI upload error: ${e}`);
398
+ }
399
+ });
400
+ }
401
+
402
+ /**
403
+ * Swap the nearest avatar image to the freshly uploaded URL.
404
+ * Prefers an `img.acct-avatar` in the input's ancestry, then falls back globally.
405
+ * @param {HTMLInputElement} fileInput
406
+ * @param {string} url - already cache-busted server-side (?v=...)
407
+ */
408
+ _swapAvatar(fileInput, url) {
409
+ let img = null;
410
+ let anc = fileInput.parentElement;
411
+ while (anc && anc !== document.body) {
412
+ const candidate = anc.querySelector('img.acct-avatar');
413
+ if (candidate) { img = candidate; break; }
414
+ anc = anc.parentElement;
415
+ }
416
+ if (!img) img = document.querySelector('img.acct-avatar');
417
+ if (img) img.src = url;
418
+ }
419
+
420
+ /**
421
+ * Handle read_bool event (checkbox)
422
+ * @param {string} event - Event type ('read_bool')
423
+ * @param {Object} eventData - Event data
424
+ * @param {HTMLElement} parentElement - Parent element for context detection
425
+ * @returns {HTMLElement} - Checkbox element or wrapper
426
+ */
427
+ async handleBoolInput(event, eventData, parentElement) {
428
+ const { createDiv } = await import('../../L2_Handling/display/primitives/generic_containers.js');
429
+ const { createLabel, createInput } = await import('../../L2_Handling/display/primitives/form_primitives.js');
430
+
431
+ const prompt = eventData.prompt || eventData.label || '';
432
+ const checked = eventData.checked || false;
433
+ const required = eventData.required || false;
434
+ const disabled = eventData.disabled || false;
435
+
436
+ // Build checkbox classes from _zClass (defaults to zForm-check-input)
437
+ const checkboxClasses = eventData._zClass || 'zForm-check-input';
438
+
439
+ // Support zId (universal), _zId (Bifrost-only), and _id (legacy)
440
+ const checkboxId = eventData.zId || eventData._zId || eventData._id || `checkbox_${Math.random().toString(36).substr(2, 9)}`;
441
+
442
+ // Detect if we're inside a zInputGroup context (parent has zInputGroup-text class)
443
+ const isInsideInputGroup = parentElement && parentElement.classList && parentElement.classList.contains('zInputGroup-text');
444
+
445
+ // Create checkbox input (type='checkbox')
446
+ const checkbox = createInput('checkbox', {
447
+ checked: checked,
448
+ required: required,
449
+ disabled: disabled,
450
+ class: checkboxClasses,
451
+ id: checkboxId
452
+ });
453
+
454
+ // Store zCross flag on checkbox element (defaults to false if not set)
455
+ // zCross: true = terminal-first behavior (conditional rendering)
456
+ // zCross: false = HTML-like behavior (always visible)
457
+ const zCross = eventData.zCross !== undefined ? eventData.zCross : false;
458
+ checkbox.setAttribute('data-zcross', zCross.toString());
459
+
460
+ let element;
461
+ // If inside input group, render checkbox directly without wrapper
462
+ if (isInsideInputGroup) {
463
+ element = checkbox;
464
+ this.logger.log(`[InputEventHandler] Rendered ${event} checkbox (input-group mode, no wrapper): (id=${checkboxId}, checked=${checked})`);
465
+ } else {
466
+ // Normal mode: Create form check container (Bootstrap-style checkbox)
467
+ const formCheck = createDiv({ class: disabled ? 'zForm-check zForm-check-disabled zmb-2' : 'zForm-check zmb-2' });
468
+
469
+ // Create label for checkbox (wraps around or uses 'for' attribute)
470
+ if (prompt) {
471
+ const label = createLabel(checkboxId, { class: 'zForm-check-label' });
472
+ label.textContent = prompt;
473
+
474
+ // Add checkbox first, then label (Bootstrap convention)
475
+ formCheck.appendChild(checkbox);
476
+ formCheck.appendChild(label);
477
+ } else {
478
+ // No label, just the checkbox
479
+ formCheck.appendChild(checkbox);
480
+ }
481
+
482
+ element = formCheck;
483
+ this.logger.log(`[InputEventHandler] Rendered ${event} checkbox: ${prompt} (id=${checkboxId}, checked=${checked})`);
484
+ }
485
+
486
+ return element;
487
+ }
488
+
489
+ /**
490
+ * Handle selection event (dropdown, radio, checkbox)
491
+ * @param {string} event - Event type ('selection')
492
+ * @param {Object} eventData - Event data
493
+ * @param {HTMLElement} parentElement - Parent element for context detection
494
+ * @returns {HTMLElement} - Selection element or wrapper
495
+ */
496
+ /**
497
+ * Parse option string for inline modifiers like [disabled] or [default]
498
+ * @private
499
+ */
500
+ _parseOptionString(optionString) {
501
+ let cleanLabel = optionString;
502
+ let isDisabled = false;
503
+ let isDefault = false;
504
+
505
+ // Check for [disabled] suffix
506
+ const disabledMatch = optionString.match(/^(.*?)\s*\[disabled\]\s*$/i);
507
+ if (disabledMatch) {
508
+ cleanLabel = disabledMatch[1].trim();
509
+ isDisabled = true;
510
+ }
511
+
512
+ // Check for [default] suffix
513
+ const defaultMatch = cleanLabel.match(/^(.*?)\s*\[default\]\s*$/i);
514
+ if (defaultMatch) {
515
+ cleanLabel = defaultMatch[1].trim();
516
+ isDefault = true;
517
+ }
518
+
519
+ return { cleanLabel, isDisabled, isDefault };
520
+ }
521
+
522
+ async handleSelection(event, eventData, parentElement) {
523
+ const { createLabel, createInput } = await import('../../L2_Handling/display/primitives/form_primitives.js');
524
+
525
+ const prompt = eventData.prompt || '';
526
+ const options = eventData.options || [];
527
+ const multi = eventData.multi || false;
528
+ let defaultValue = eventData.default || null;
529
+ const disabled = eventData.disabled || false;
530
+ const required = eventData.required || false;
531
+ // `type` (interactive WS path) or `widget_type` (declarative — the zSelect
532
+ // expander renames type→widget_type). Accept both; default to dropdown.
533
+ const type = eventData.type || eventData.widget_type || 'dropdown';
534
+
535
+ // Auto-detect default from [default] suffix if no explicit default provided
536
+ if (defaultValue === null && options.length > 0) {
537
+ for (const opt of options) {
538
+ if (typeof opt === 'string') {
539
+ const parsed = this._parseOptionString(opt);
540
+ if (parsed.isDefault) {
541
+ defaultValue = parsed.cleanLabel;
542
+ break;
543
+ }
544
+ }
545
+ }
546
+ }
547
+
548
+ // Build classes from _zClass
549
+ const elementClasses = eventData._zClass || '';
550
+
551
+ // Support zId (universal), _zId (Bifrost-only), and _id (legacy)
552
+ const baseId = eventData.zId || eventData._zId || eventData._id || `select_${Math.random().toString(36).substr(2, 9)}`;
553
+
554
+ // Support aria-label for accessibility
555
+ const ariaLabel = eventData['_aria-label'] || eventData.ariaLabel || eventData['aria-label'];
556
+
557
+ // Detect if we're inside a zInputGroup context (for compact rendering)
558
+ const isInsideInputGroup = parentElement && parentElement.classList && parentElement.classList.contains('zInputGroup-text');
559
+
560
+ let element;
561
+ // Render based on type
562
+ if (type === 'radio' || (type === 'checkbox' && multi)) {
563
+ // Radio button group or checkbox group
564
+ const inputType = type === 'radio' ? 'radio' : 'checkbox';
565
+ const groupName = baseId; // Use baseId as group name for radio buttons
566
+
567
+ // CHUNK MODE: When inside zInputGroup-text, render raw radios without labels/wrappers
568
+ if (isInsideInputGroup) {
569
+ // Just render the first radio input directly (for single-option-per-group pattern)
570
+ // Or all radios stacked if multiple options
571
+ const firstOptionValue = options[0];
572
+ const optionVal = typeof firstOptionValue === 'string' ? firstOptionValue : (firstOptionValue.value || firstOptionValue.label || '');
573
+
574
+ const input = createInput(inputType, {
575
+ id: `${baseId}_0`,
576
+ name: groupName,
577
+ value: optionVal,
578
+ disabled: disabled,
579
+ required: required,
580
+ class: elementClasses || 'zCheck-input'
581
+ });
582
+
583
+ // Set checked state
584
+ if (defaultValue !== null && (optionVal === defaultValue || (Array.isArray(defaultValue) && defaultValue.includes(optionVal)))) {
585
+ input.checked = true;
586
+ }
587
+
588
+ element = input;
589
+ this.logger.log(`[InputEventHandler] Rendered ${event} ${inputType} (input-group mode, no wrapper): (id=${baseId}, value=${optionVal})`);
590
+ } else {
591
+ // NORMAL MODE: Standard radio/checkbox group with labels
592
+ // Create container
593
+ const container = document.createElement('div');
594
+ container.className = elementClasses ? `${elementClasses} zForm-check-group` : 'zForm-check-group';
595
+ if (eventData._zStyle) {
596
+ const cssString = convertStyleToString(eventData._zStyle, this.logger);
597
+ if (cssString) {
598
+ container.setAttribute('style', cssString);
599
+ }
600
+ }
601
+
602
+ // Create prompt label if exists (fieldset legend style)
603
+ if (prompt) {
604
+ const promptLabel = document.createElement('div');
605
+ promptLabel.textContent = prompt;
606
+ promptLabel.className = 'zLabel';
607
+ appendRequiredMark(promptLabel, required);
608
+ container.appendChild(promptLabel);
609
+ }
610
+
611
+ // Create radio/checkbox inputs for each option
612
+ options.forEach((optionValue, index) => {
613
+ const optionId = `${baseId}_${index}`;
614
+
615
+ // Parse option string for modifiers or extract from object
616
+ let optionLabel, optionVal, optionDisabled, optionIsDefault;
617
+ if (typeof optionValue === 'string') {
618
+ const parsed = this._parseOptionString(optionValue);
619
+ optionLabel = parsed.cleanLabel;
620
+ optionVal = parsed.cleanLabel;
621
+ optionDisabled = parsed.isDisabled;
622
+ optionIsDefault = parsed.isDefault;
623
+ } else {
624
+ optionLabel = optionValue.label || optionValue.value || '';
625
+ optionVal = optionValue.value || optionValue.label || '';
626
+ optionDisabled = optionValue.disabled || false;
627
+ optionIsDefault = false;
628
+ }
629
+
630
+ // Create wrapper div for input + label (canonical .zForm-check row)
631
+ const rowDisabled = disabled || optionDisabled;
632
+ const optionWrapper = document.createElement('div');
633
+ optionWrapper.className = rowDisabled ? 'zForm-check zForm-check-disabled zmb-2' : 'zForm-check zmb-2';
634
+
635
+ // Create input with per-option disabled state
636
+ const input = createInput(inputType, {
637
+ id: optionId,
638
+ name: groupName,
639
+ value: optionVal,
640
+ disabled: rowDisabled, // Component-level OR per-option disabled
641
+ required: required && index === 0, // Only first input has required
642
+ class: 'zForm-check-input'
643
+ });
644
+
645
+ // Set checked state based on default value
646
+ if (defaultValue !== null) {
647
+ if (multi && Array.isArray(defaultValue)) {
648
+ // Multi-select (checkbox): check if option is in default array
649
+ if (defaultValue.includes(optionVal) || defaultValue.includes(optionLabel)) {
650
+ input.checked = true;
651
+ }
652
+ } else {
653
+ // Single-select (radio): check if option matches default
654
+ if (optionVal === defaultValue || optionLabel === defaultValue) {
655
+ input.checked = true;
656
+ }
657
+ }
658
+ }
659
+
660
+ // Create label
661
+ const label = createLabel(optionId, { class: 'zForm-check-label' });
662
+ label.textContent = optionLabel;
663
+
664
+ // Assemble option
665
+ optionWrapper.appendChild(input);
666
+ optionWrapper.appendChild(label);
667
+ container.appendChild(optionWrapper);
668
+ });
669
+
670
+ element = container;
671
+ this.logger.log(`[InputEventHandler] Rendered ${event} ${inputType} group (id=${baseId}, options=${options.length})`);
672
+ }
673
+ } else {
674
+ // Dropdown select (default behavior)
675
+ let wrapper = null;
676
+
677
+ // Create label if prompt exists
678
+ if (prompt) {
679
+ wrapper = document.createElement('div');
680
+ // Use zLabel class for styled selects
681
+ const labelClass = elementClasses.includes('zSelect') ? 'zLabel' : '';
682
+ const labelAttrs = labelClass ? { class: labelClass } : {};
683
+ const label = createLabel(baseId, labelAttrs);
684
+ label.textContent = prompt;
685
+ appendRequiredMark(label, required);
686
+ wrapper.appendChild(label);
687
+ // Add line break after label (semantic HTML pattern)
688
+ wrapper.appendChild(document.createElement('br'));
689
+ }
690
+
691
+ // Create select element
692
+ const selectElement = document.createElement('select');
693
+ selectElement.id = baseId;
694
+
695
+ // Always carry the canonical .zSelect base so every dropdown / multi-select
696
+ // gets themed — even when _zClass overrides it (merge + de-dupe).
697
+ const selectClassList = elementClasses ? elementClasses.split(/\s+/).filter(Boolean) : [];
698
+ if (!selectClassList.includes('zSelect')) selectClassList.unshift('zSelect');
699
+ selectElement.className = selectClassList.join(' ');
700
+
701
+ if (disabled) {
702
+ selectElement.disabled = true;
703
+ }
704
+
705
+ if (required) {
706
+ selectElement.required = true;
707
+ }
708
+
709
+ if (multi) {
710
+ selectElement.multiple = true;
711
+ }
712
+
713
+ // Support size attribute (number of visible options)
714
+ const size = eventData.size || null;
715
+ if (size !== null) {
716
+ selectElement.size = size;
717
+ }
718
+
719
+ if (ariaLabel) {
720
+ selectElement.setAttribute('aria-label', ariaLabel);
721
+ }
722
+
723
+ // Add autocomplete="off" to prevent browser from remembering selections
724
+ selectElement.setAttribute('autocomplete', 'off');
725
+
726
+ // Apply inline styles if no wrapper (to avoid nesting issues)
727
+ if (!wrapper && eventData._zStyle) {
728
+ const cssString = convertStyleToString(eventData._zStyle, this.logger);
729
+ if (cssString) {
730
+ selectElement.setAttribute('style', cssString);
731
+ }
732
+ }
733
+
734
+ // Create option elements
735
+ options.forEach((optionValue, index) => {
736
+ const optionElement = document.createElement('option');
737
+
738
+ // Parse option string for modifiers or extract from object
739
+ let optionLabel, optionVal, optionDisabled;
740
+ if (typeof optionValue === 'string') {
741
+ const parsed = this._parseOptionString(optionValue);
742
+ optionLabel = parsed.cleanLabel;
743
+ optionVal = parsed.cleanLabel;
744
+ optionDisabled = parsed.isDisabled;
745
+ } else {
746
+ optionLabel = optionValue.label || optionValue.value || '';
747
+ optionVal = optionValue.value || optionValue.label || '';
748
+ optionDisabled = optionValue.disabled || false;
749
+ }
750
+
751
+ optionElement.textContent = optionLabel;
752
+ optionElement.value = optionVal;
753
+
754
+ // Set disabled state (per-option)
755
+ if (optionDisabled) {
756
+ optionElement.disabled = true;
757
+ }
758
+
759
+ // Set selected state based on default value
760
+ if (defaultValue !== null) {
761
+ if (multi && Array.isArray(defaultValue)) {
762
+ // Multi-select: check if option is in default array
763
+ if (defaultValue.includes(optionVal)) {
764
+ optionElement.selected = true;
765
+ }
766
+ } else {
767
+ // Single-select: check if option matches default
768
+ if (optionVal === defaultValue || optionLabel === defaultValue) {
769
+ optionElement.selected = true;
770
+ }
771
+ }
772
+ }
773
+
774
+ selectElement.appendChild(optionElement);
775
+ });
776
+
777
+ // Assemble final element
778
+ if (wrapper) {
779
+ wrapper.appendChild(selectElement);
780
+ // Apply wrapper styles if specified
781
+ if (eventData._zStyle) {
782
+ const cssString = convertStyleToString(eventData._zStyle, this.logger);
783
+ if (cssString) {
784
+ wrapper.setAttribute('style', cssString);
785
+ }
786
+ }
787
+ element = wrapper;
788
+ } else {
789
+ element = selectElement;
790
+ }
791
+
792
+ this.logger.log(`[InputEventHandler] Rendered ${event} select (id=${baseId}, options=${options.length}, multi=${multi})`);
793
+ }
794
+
795
+ return element;
796
+ }
797
+ }