@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,698 @@
1
+ /**
2
+ * L3_Abstraction/orchestrator/group_renderer.js
3
+ *
4
+ * Grouped Rendering for _zGroup Metadata
5
+ *
6
+ * Handles grouped rendering contexts where multiple items are rendered
7
+ * into a single container with group-specific styling:
8
+ * - input-group: Radio buttons with conditional inputs, checkboxes
9
+ * - list-group: Lists with interactive items
10
+ *
11
+ * Includes complex logic for:
12
+ * - Radio zSelect splitting (each option gets its own zInputGroup)
13
+ * - Conditional input matching (if conditions paired with radio options)
14
+ * - Position-based and condition-based pairing
15
+ * - Group-specific styling application
16
+ *
17
+ * Extracted from zdisplay_orchestrator.js (Phase 4.4b)
18
+ */
19
+
20
+ // Layer 0: Primitives
21
+ import { createSemanticElement } from '../../L2_Handling/display/primitives/semantic_element_primitive.js';
22
+ import { convertStyleToString } from '../../zSys/dom/style_utils.js';
23
+
24
+ /**
25
+ * GroupRenderer - Handles _zGroup rendering contexts
26
+ */
27
+ export class GroupRenderer {
28
+ constructor(client, logger, orchestrator, metadataProcessor) {
29
+ this.client = client;
30
+ this.logger = logger;
31
+ this.orchestrator = orchestrator;
32
+ this.metadataProcessor = metadataProcessor;
33
+ }
34
+
35
+ /**
36
+ * Check if data should be rendered as a group
37
+ * @param {Object} metadata - Metadata object
38
+ * @returns {boolean} True if should render as group
39
+ */
40
+ shouldRenderAsGroup(metadata) {
41
+ return !!(metadata._zGroup || this.metadataProcessor.isInputGroupContext(metadata));
42
+ }
43
+
44
+ /**
45
+ * Render items as a grouped container
46
+ * @param {Object} data - Data object with items to group
47
+ * @param {Object} metadata - Metadata with _zGroup
48
+ * @param {HTMLElement} parentElement - Parent to append group to
49
+ * @param {string} currentPath - Current path for recursion
50
+ */
51
+ async renderGroupedItems(data, metadata, parentElement, currentPath = '') {
52
+ // If _zClass contains zInputGroup but no _zGroup, treat as input-group
53
+ if (isInputGroupContext && !metadata._zGroup) {
54
+ metadata._zGroup = 'input-group';
55
+ }
56
+ this.logger.log(`[ZDisplayOrchestrator] _zGroup detected: "${metadata._zGroup}" - rendering as grouped container`);
57
+ this.logger.log(` _zGroup detected: "${metadata._zGroup}"`);
58
+
59
+ // Create group container with zTheme classes based on _zGroup type
60
+ const groupContainer = document.createElement('div');
61
+ groupContainer.setAttribute('data-zgroup', metadata._zGroup);
62
+
63
+ // Apply zTheme container class based on group type
64
+ if (metadata._zGroup === 'list-group') {
65
+ groupContainer.classList.add('zList-group');
66
+ this.logger.debug('Applied zTheme class: zList-group');
67
+ } else if (metadata._zGroup === 'input-group') {
68
+ groupContainer.classList.add('zInputGroup');
69
+ this.logger.debug('Applied zTheme class: zInputGroup');
70
+ }
71
+
72
+ // Apply additional _zClass styling if provided (from YAML)
73
+ if (metadata._zClass) {
74
+ const classes = metadata._zClass.split(' ').filter(c => c.trim());
75
+ if (classes.length > 0) {
76
+ groupContainer.classList.add(...classes);
77
+ this.logger.log(` Applied additional _zClass: ${metadata._zClass}`);
78
+ }
79
+ }
80
+
81
+ // Add prefix label for input-group if _zGroupLabel is provided
82
+ if (metadata._zGroup === 'input-group' && metadata._zGroupLabel) {
83
+ const labelSpan = document.createElement('span');
84
+ labelSpan.classList.add('zInputGroup-text');
85
+ labelSpan.textContent = metadata._zGroupLabel;
86
+ groupContainer.appendChild(labelSpan);
87
+ this.logger.log(` Added input group label: ${metadata._zGroupLabel}`);
88
+ }
89
+
90
+ // DEBUG: Log what we're about to iterate
91
+ if (metadata._zGroup === 'input-group') {
92
+ this.logger.debug('[INPUT-GROUP] Data keys:', Object.keys(data));
93
+ this.logger.debug('[INPUT-GROUP] Full data:', JSON.stringify(data, null, 2));
94
+ }
95
+
96
+ // Track matched conditional inputs (for radio zSelect splitting)
97
+ const matchedInputKeys = new Set();
98
+
99
+ // Iterate through all non-metadata children and render into group
100
+ for (const [key, value] of Object.entries(data)) {
101
+ // Skip ONLY metadata keys (not organizational containers like _Visual_Progression)
102
+ // Delegated to MetadataProcessor (Phase 4.4a)
103
+ if (this.metadataProcessor.isMetadataKey(key)) {
104
+ continue;
105
+ }
106
+
107
+ // Skip if this input was already matched and rendered by radio zSelect splitting
108
+ if (matchedInputKeys.has(key)) {
109
+ this.logger.log(` Skipping already-matched conditional input: ${key}`);
110
+ continue;
111
+ }
112
+
113
+ this.logger.log(` Rendering grouped item: ${key}`);
114
+
115
+ // DEBUG for input-group
116
+ if (metadata._zGroup === 'input-group') {
117
+ this.logger.log(`[INPUT-GROUP] Processing child: ${key}`);
118
+ this.logger.log(`[INPUT-GROUP] Value type: ${Array.isArray(value) ? 'Array' : typeof value}`);
119
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
120
+ this.logger.log(`[INPUT-GROUP] Value keys: ${Object.keys(value).join(', ')}`);
121
+ if (value.event) {
122
+ this.logger.log(`[INPUT-GROUP] Event: ${value.event}, type: ${value.type}`);
123
+ }
124
+ if (value.zSelect) {
125
+ this.logger.log(`[INPUT-GROUP] zSelect found, type: ${value.zSelect.type}`);
126
+ }
127
+ }
128
+ }
129
+
130
+ // Handle list/array values (zDisplay events)
131
+ if (Array.isArray(value)) {
132
+ for (const item of value) {
133
+ if (item && item.zDisplay) {
134
+ // SEPARATION OF CONCERNS: Render element without group context
135
+ const element = await this.renderZDisplayEvent(item.zDisplay, groupContainer);
136
+ if (element) {
137
+ // Apply group-specific styling AFTER rendering
138
+ this._applyGroupStyling(element, metadata._zGroup, item.zDisplay);
139
+ groupContainer.appendChild(element);
140
+ }
141
+ }
142
+ }
143
+ } else if (value && value.zDisplay && value.zDisplay.event === 'selection' && value.zDisplay.type === 'radio' && metadata._zGroup === 'input-group') {
144
+ // Backend sent expanded zSelect as value.zDisplay: run same radio+input split logic
145
+ const sel = value.zDisplay;
146
+ const options = sel.options || [];
147
+ const zCross = sel.zCross !== undefined ? sel.zCross : false;
148
+ const groupName = sel.zId || sel._zId || sel._id || `radio_${Math.random().toString(36).substr(2, 9)}`;
149
+ const conditionalInputs = [];
150
+ for (const [childKey, childValue] of Object.entries(data)) {
151
+ if (childValue && childValue.zInput && childValue.zInput.if) {
152
+ conditionalInputs.push({ key: childKey, value: childValue, condition: (childValue.zInput.if || '').replace(/#.*$/gm, '').trim(), payload: childValue.zInput });
153
+ } else if (childValue && childValue.zDisplay && childValue.zDisplay.event === 'read_string' && childValue.zDisplay.if) {
154
+ conditionalInputs.push({ key: childKey, value: childValue, condition: (childValue.zDisplay.if || '').replace(/#.*$/gm, '').trim(), payload: childValue.zDisplay });
155
+ }
156
+ }
157
+ const parseSuffixN = (s) => {
158
+ if (s == null) return null;
159
+ if (typeof s === 'number' && s > 0) return s;
160
+ const m = String(s).trim().match(/^\+(\d+)$/);
161
+ return m ? parseInt(m[1], 10) : null;
162
+ };
163
+ const suffixN = parseSuffixN(sel.suffix);
164
+
165
+ // Helper function to parse option string
166
+ const parseOptionString = (optionString) => {
167
+ let cleanLabel = optionString;
168
+ let isDisabled = false;
169
+
170
+ const disabledMatch = optionString.match(/^(.*?)\s*\[disabled\]\s*$/i);
171
+ if (disabledMatch) {
172
+ cleanLabel = disabledMatch[1].trim();
173
+ isDisabled = true;
174
+ }
175
+
176
+ const defaultMatch = cleanLabel.match(/^(.*?)\s*\[default\]\s*$/i);
177
+ if (defaultMatch) {
178
+ cleanLabel = defaultMatch[1].trim();
179
+ }
180
+
181
+ return { cleanLabel, isDisabled };
182
+ };
183
+
184
+ for (let i = 0; i < options.length; i++) {
185
+ let optionValue, optionLabel, optionDisabled;
186
+
187
+ if (typeof options[i] === 'string') {
188
+ const parsed = parseOptionString(options[i]);
189
+ optionValue = parsed.cleanLabel;
190
+ optionLabel = parsed.cleanLabel;
191
+ optionDisabled = parsed.isDisabled;
192
+ } else {
193
+ optionValue = options[i].value || options[i].label || '';
194
+ optionLabel = options[i].label || options[i].value || '';
195
+ optionDisabled = options[i].disabled || false;
196
+ }
197
+ let matchingInput;
198
+ if (suffixN != null && i < suffixN && i < conditionalInputs.length) {
199
+ matchingInput = conditionalInputs[i];
200
+ } else {
201
+ matchingInput = conditionalInputs.find(input => {
202
+ const condition = (input.condition || '').trim();
203
+ const valueMatch = condition.match(/==\s*['"]?([^'"\s]+)['"]?\s*(?:#|$)/);
204
+ if (valueMatch && valueMatch[1].trim() === optionValue) return true;
205
+ return [`== '${optionValue}'`, `== "${optionValue}"`, `=='${optionValue}'`, `=="${optionValue}"`, `== ${optionValue}`, `==${optionValue}`].some(p => condition.includes(p));
206
+ });
207
+ }
208
+ const inputGroupDiv = document.createElement('div');
209
+ inputGroupDiv.classList.add('zInputGroup');
210
+ if (sel._zClass) {
211
+ const classes = sel._zClass.split(' ').filter(c => c.trim() && c !== 'zCheck-input');
212
+ if (classes.length) inputGroupDiv.classList.add(...classes);
213
+ }
214
+ const textWrapper = document.createElement('div');
215
+ textWrapper.classList.add('zInputGroup-text');
216
+ const { createInput } = await import('../primitives/form_primitives.js');
217
+ const radioInput = createInput('radio', {
218
+ id: `${groupName}_${i}`,
219
+ name: groupName,
220
+ value: optionValue,
221
+ class: 'zCheck-input',
222
+ 'aria-label': optionLabel,
223
+ disabled: optionDisabled
224
+ });
225
+ if (sel.default === optionValue) radioInput.checked = true;
226
+ radioInput.setAttribute('data-zcross', zCross.toString());
227
+ textWrapper.appendChild(radioInput);
228
+ inputGroupDiv.appendChild(textWrapper);
229
+ if (matchingInput) {
230
+ matchedInputKeys.add(matchingInput.key);
231
+ const payload = matchingInput.payload || matchingInput.value.zInput || matchingInput.value.zDisplay || {};
232
+ const inputEventData = { event: 'read_string', ...payload };
233
+ if (matchingInput.value.zCross !== undefined) inputEventData.zCross = matchingInput.value.zCross;
234
+ delete inputEventData.prompt;
235
+ const inputElement = await this.renderZDisplayEvent(inputEventData, inputGroupDiv);
236
+ if (inputElement) {
237
+ if (inputElement.tagName === 'INPUT' || inputElement.tagName === 'TEXTAREA') {
238
+ inputGroupDiv.appendChild(inputElement);
239
+ } else {
240
+ const actualInput = inputElement.querySelector('input, textarea');
241
+ inputGroupDiv.appendChild(actualInput || inputElement);
242
+ }
243
+ }
244
+ }
245
+ groupContainer.appendChild(inputGroupDiv);
246
+ }
247
+ continue;
248
+ } else if (value && value.zDisplay) {
249
+ // Handle direct zDisplay event
250
+ // SEPARATION OF CONCERNS: Render element without group context
251
+ const element = await this.renderZDisplayEvent(value.zDisplay, groupContainer);
252
+ if (element) {
253
+ // Apply group-specific styling AFTER rendering
254
+ this._applyGroupStyling(element, metadata._zGroup, value.zDisplay);
255
+ groupContainer.appendChild(element);
256
+ }
257
+ } else if (value && typeof value === 'object') {
258
+ // Check if this is already a zDisplay event object
259
+ if (value.event) {
260
+ // This is a zDisplay event that was already expanded by the backend
261
+ this.logger.log(` Found pre-expanded zDisplay event '${value.event}' in grouped item: ${key}`);
262
+
263
+ // Special handling for radio selection events in input-group: split into separate zInputGroup containers
264
+ this.logger.log(` Checking for radio selection: key=${key}, event=${value.event}, type=${value.type}, _zGroup=${metadata._zGroup}`);
265
+ if (value.event === 'selection' && metadata._zGroup === 'input-group' && value.type === 'radio') {
266
+ this.logger.log(` Radio selection detected! Processing ${value.options?.length || 0} options`);
267
+ // Radio buttons in input-group: create separate zInputGroup for each option
268
+ const options = value.options || [];
269
+ const zCross = value.zCross !== undefined ? value.zCross : false;
270
+ const groupName = value.zId || value._zId || value._id || `radio_${Math.random().toString(36).substr(2, 9)}`;
271
+
272
+ // Find all conditional zInput elements (shorthand or expanded zDisplay) in declaration order
273
+ const conditionalInputs = [];
274
+ for (const [childKey, childValue] of Object.entries(data)) {
275
+ if (childValue && childValue.zInput && childValue.zInput.if) {
276
+ conditionalInputs.push({ key: childKey, value: childValue, condition: (childValue.zInput.if || '').replace(/#.*$/gm, '').trim(), payload: childValue.zInput });
277
+ this.logger.log(` Found conditional input: ${childKey} with condition: "${childValue.zInput.if}"`);
278
+ } else if (childValue && childValue.zDisplay && childValue.zDisplay.event === 'read_string' && childValue.zDisplay.if) {
279
+ conditionalInputs.push({ key: childKey, value: childValue, condition: (childValue.zDisplay.if || '').replace(/#.*$/gm, '').trim(), payload: childValue.zDisplay });
280
+ this.logger.log(` Found conditional input (zDisplay): ${childKey} with condition: "${childValue.zDisplay.if}"`);
281
+ }
282
+ }
283
+ this.logger.log(` Found ${conditionalInputs.length} conditional input(s) total`);
284
+ // Parse suffix +N (e.g. "+3" => pair with next N inputs by position)
285
+ const parseSuffixN = (s) => {
286
+ if (s == null) return null;
287
+ if (typeof s === 'number' && s > 0) return s;
288
+ const m = String(s).trim().match(/^\+(\d+)$/);
289
+ return m ? parseInt(m[1], 10) : null;
290
+ };
291
+ const suffixN = parseSuffixN(value.suffix);
292
+ if (suffixN != null) this.logger.log(` suffix: ${value.suffix} → suffixN=${suffixN} (position-based pairing)`);
293
+
294
+ // Create a separate zInputGroup for each radio option
295
+ for (let i = 0; i < options.length; i++) {
296
+ const optionValue = typeof options[i] === 'string' ? options[i] : (options[i].value || options[i].label || '');
297
+ const optionLabel = typeof options[i] === 'string' ? options[i] : (options[i].label || options[i].value || '');
298
+
299
+ // Pair by position when suffix +N is set; else match by condition
300
+ let matchingInput;
301
+ if (suffixN != null && i < suffixN && i < conditionalInputs.length) {
302
+ matchingInput = conditionalInputs[i];
303
+ this.logger.log(` Position-based pair: option[${i}] "${optionValue}" → ${matchingInput.key}`);
304
+ } else {
305
+ matchingInput = conditionalInputs.find(input => {
306
+ const condition = (input.condition || '').trim();
307
+ const valueMatch = condition.match(/==\s*['"]?([^'"\s]+)['"]?\s*(?:#|$)/);
308
+ if (valueMatch && valueMatch[1].trim() === optionValue) return true;
309
+ const patterns = [
310
+ `== '${optionValue}'`,
311
+ `== "${optionValue}"`,
312
+ `=='${optionValue}'`,
313
+ `=="${optionValue}"`,
314
+ `== ${optionValue}`,
315
+ `==${optionValue}`
316
+ ];
317
+ return patterns.some(pattern => condition.includes(pattern));
318
+ });
319
+ }
320
+ if (!suffixN) this.logger.log(` Looking for input matching option "${optionValue}": ${matchingInput ? `Found: ${matchingInput.key}` : 'Not found'}`);
321
+
322
+ // Create zInputGroup container for this radio + input pair
323
+ const inputGroupDiv = document.createElement('div');
324
+ inputGroupDiv.classList.add('zInputGroup');
325
+ if (value._zClass) {
326
+ const classes = value._zClass.split(' ').filter(c => c.trim() && c !== 'zCheck-input');
327
+ if (classes.length > 0) {
328
+ inputGroupDiv.classList.add(...classes);
329
+ }
330
+ }
331
+
332
+ // Create zInputGroup-text wrapper for radio
333
+ const textWrapper = document.createElement('div');
334
+ textWrapper.classList.add('zInputGroup-text');
335
+
336
+ // Create radio input
337
+ const { createInput } = await import('../primitives/form_primitives.js');
338
+ const radioInput = createInput('radio', {
339
+ id: `${groupName}_${i}`,
340
+ name: groupName,
341
+ value: optionValue,
342
+ class: 'zCheck-input',
343
+ 'aria-label': optionLabel
344
+ });
345
+
346
+ // Set checked state if default matches
347
+ if (value.default === optionValue) {
348
+ radioInput.checked = true;
349
+ }
350
+
351
+ // Store zCross flag on radio element (for conditional rendering)
352
+ radioInput.setAttribute('data-zcross', zCross.toString());
353
+
354
+ textWrapper.appendChild(radioInput);
355
+ inputGroupDiv.appendChild(textWrapper);
356
+
357
+ // Add matching conditional input if found
358
+ if (matchingInput) {
359
+ // Mark this input as matched so we don't process it again
360
+ matchedInputKeys.add(matchingInput.key);
361
+ // Support both shorthand (zInput) and expanded (zDisplay) payloads
362
+ const payload = matchingInput.payload || matchingInput.value.zInput || matchingInput.value.zDisplay || {};
363
+ const inputEventData = { event: 'read_string', ...payload };
364
+ if (matchingInput.value.zCross !== undefined) {
365
+ inputEventData.zCross = matchingInput.value.zCross;
366
+ }
367
+ {
368
+ // Remove prompt to prevent wrapper div creation
369
+ delete inputEventData.prompt;
370
+ // Render input directly into the zInputGroup (no wrapper needed)
371
+ // Pass inputGroupDiv as parent so it detects input-group context
372
+ const inputElement = await this.renderZDisplayEvent(inputEventData, inputGroupDiv);
373
+ if (inputElement) {
374
+ // If renderZDisplayEvent returns a wrapper, extract the actual input
375
+ // Otherwise use the element directly
376
+ if (inputElement.tagName === 'INPUT' || inputElement.tagName === 'TEXTAREA') {
377
+ inputGroupDiv.appendChild(inputElement);
378
+ } else {
379
+ // Wrapper returned - find the input inside and move it
380
+ const actualInput = inputElement.querySelector('input, textarea');
381
+ if (actualInput) {
382
+ inputGroupDiv.appendChild(actualInput);
383
+ } else {
384
+ inputGroupDiv.appendChild(inputElement);
385
+ }
386
+ }
387
+ }
388
+ }
389
+ }
390
+
391
+ groupContainer.appendChild(inputGroupDiv);
392
+ this.logger.log(` Created zInputGroup for radio option: ${optionValue}`);
393
+ }
394
+
395
+ // Skip normal rendering for this zSelect
396
+ continue;
397
+ }
398
+
399
+ // Special handling for checkboxes in input-group: wrap in zInputGroup-text
400
+ if (value.event === 'read_bool' && metadata._zGroup === 'input-group') {
401
+ const wrapperDiv = document.createElement('div');
402
+ wrapperDiv.classList.add('zInputGroup-text');
403
+ wrapperDiv.setAttribute('data-zkey', key);
404
+
405
+ // Render checkbox inside wrapper, passing wrapper as parent so it detects input-group context
406
+ const checkboxElement = await this.renderZDisplayEvent(value, wrapperDiv);
407
+ if (checkboxElement) {
408
+ wrapperDiv.appendChild(checkboxElement);
409
+ }
410
+ groupContainer.appendChild(wrapperDiv);
411
+ this.logger.log(` Wrapped read_bool checkbox in zInputGroup-text for input-group`);
412
+ } else {
413
+ // Normal rendering for other events
414
+ const element = await this.renderZDisplayEvent(value, groupContainer);
415
+ if (element) {
416
+ groupContainer.appendChild(element);
417
+ }
418
+ }
419
+ } else {
420
+ // Check for shorthand keys (zInput, zButton, etc.) that need expansion
421
+ const shorthandKeys = ['zInput', 'zButton', 'zCheckbox', 'zSelect', 'zText', 'zMD', 'zH1', 'zH2', 'zH3', 'zH4', 'zH5', 'zH6', 'zURL', 'zImage'];
422
+ const foundShorthand = shorthandKeys.find(sk => value[sk]);
423
+
424
+ if (foundShorthand) {
425
+ // This is a shorthand that needs to be rendered as a zDisplay event
426
+ this.logger.debug(`Found shorthand '%s' in grouped item: %s`, foundShorthand, key);
427
+
428
+ // Special handling for radio zSelect in input-group: split into separate zInputGroup containers
429
+ this.logger.debug(`Checking radio zSelect shorthand: %s`, foundShorthand);
430
+ if (foundShorthand === 'zSelect' && metadata._zGroup === 'input-group' && value.zSelect && value.zSelect.type === 'radio') {
431
+ this.logger.debug(`Radio zSelect shorthand detected (%s options)`, value.zSelect.options?.length || 0);
432
+ // Radio buttons in input-group: create separate zInputGroup for each option
433
+ const options = value.zSelect.options || [];
434
+ const zCross = value.zCross !== undefined ? value.zCross : false;
435
+ const groupName = value.zSelect.zId || value.zSelect._zId || value.zSelect._id || `radio_${Math.random().toString(36).substr(2, 9)}`;
436
+
437
+ // Find all conditional zInput elements (shorthand or expanded zDisplay) in declaration order
438
+ const conditionalInputs = [];
439
+ for (const [childKey, childValue] of Object.entries(data)) {
440
+ if (childValue && childValue.zInput && childValue.zInput.if) {
441
+ conditionalInputs.push({ key: childKey, value: childValue, condition: (childValue.zInput.if || '').replace(/#.*$/gm, '').trim(), payload: childValue.zInput });
442
+ } else if (childValue && childValue.zDisplay && childValue.zDisplay.event === 'read_string' && childValue.zDisplay.if) {
443
+ conditionalInputs.push({ key: childKey, value: childValue, condition: (childValue.zDisplay.if || '').replace(/#.*$/gm, '').trim(), payload: childValue.zDisplay });
444
+ }
445
+ }
446
+ // Parse suffix +N (e.g. "+3" => pair with next N inputs by position)
447
+ const parseSuffixN = (s) => {
448
+ if (s == null) return null;
449
+ if (typeof s === 'number' && s > 0) return s;
450
+ const m = String(s).trim().match(/^\+(\d+)$/);
451
+ return m ? parseInt(m[1], 10) : null;
452
+ };
453
+ const suffixN = parseSuffixN(value.zSelect.suffix);
454
+ this.logger.log(` suffix: ${value.zSelect.suffix} → suffixN=${suffixN}, conditionalInputs=${conditionalInputs.length}`);
455
+
456
+ // Create a separate zInputGroup for each radio option
457
+ for (let i = 0; i < options.length; i++) {
458
+ const optionValue = typeof options[i] === 'string' ? options[i] : (options[i].value || options[i].label || '');
459
+ const optionLabel = typeof options[i] === 'string' ? options[i] : (options[i].label || options[i].value || '');
460
+
461
+ // Pair by position when suffix +N is set; else match by condition
462
+ let matchingInput;
463
+ if (suffixN != null && i < suffixN && i < conditionalInputs.length) {
464
+ matchingInput = conditionalInputs[i];
465
+ this.logger.log(` Position-based pair: option[${i}] "${optionValue}" → ${matchingInput.key}`);
466
+ } else {
467
+ matchingInput = conditionalInputs.find(input => {
468
+ const condition = (input.condition || '').trim();
469
+ const valueMatch = condition.match(/==\s*['"]?([^'"\s]+)['"]?\s*(?:#|$)/);
470
+ if (valueMatch && valueMatch[1].trim() === optionValue) return true;
471
+ const patterns = [
472
+ `== '${optionValue}'`,
473
+ `== "${optionValue}"`,
474
+ `=='${optionValue}'`,
475
+ `=="${optionValue}"`,
476
+ `== ${optionValue}`,
477
+ `==${optionValue}`
478
+ ];
479
+ return patterns.some(pattern => condition.includes(pattern));
480
+ });
481
+ }
482
+
483
+ // Create zInputGroup container for this radio + input pair
484
+ const inputGroupDiv = document.createElement('div');
485
+ inputGroupDiv.classList.add('zInputGroup');
486
+ if (value.zSelect._zClass) {
487
+ const classes = value.zSelect._zClass.split(' ').filter(c => c.trim() && c !== 'zCheck-input');
488
+ if (classes.length > 0) {
489
+ inputGroupDiv.classList.add(...classes);
490
+ }
491
+ }
492
+
493
+ // Create zInputGroup-text wrapper for radio
494
+ const textWrapper = document.createElement('div');
495
+ textWrapper.classList.add('zInputGroup-text');
496
+
497
+ // Create radio input
498
+ const { createInput } = await import('../primitives/form_primitives.js');
499
+ const radioInput = createInput('radio', {
500
+ id: `${groupName}_${i}`,
501
+ name: groupName,
502
+ value: optionValue,
503
+ class: 'zCheck-input',
504
+ 'aria-label': optionLabel
505
+ });
506
+
507
+ // Set checked state if default matches
508
+ if (value.zSelect.default === optionValue) {
509
+ radioInput.checked = true;
510
+ }
511
+
512
+ // Store zCross flag on radio element (for conditional rendering)
513
+ radioInput.setAttribute('data-zcross', zCross.toString());
514
+
515
+ textWrapper.appendChild(radioInput);
516
+ inputGroupDiv.appendChild(textWrapper);
517
+
518
+ // Add matching conditional input if found
519
+ if (matchingInput) {
520
+ // Mark this input as matched so we don't process it again
521
+ matchedInputKeys.add(matchingInput.key);
522
+ // Support both shorthand (zInput) and expanded (zDisplay) payloads from backend
523
+ const payload = matchingInput.payload || matchingInput.value.zInput || matchingInput.value.zDisplay || {};
524
+ const inputEventData = { event: 'read_string', ...payload };
525
+ if (matchingInput.value.zCross !== undefined) {
526
+ inputEventData.zCross = matchingInput.value.zCross;
527
+ }
528
+ // Remove prompt to prevent wrapper div creation
529
+ delete inputEventData.prompt;
530
+ const inputElement = await this.renderZDisplayEvent(inputEventData, inputGroupDiv);
531
+ if (inputElement) {
532
+ // Extract actual input/textarea if renderZDisplayEvent returned a wrapper
533
+ if (inputElement.tagName === 'INPUT' || inputElement.tagName === 'TEXTAREA') {
534
+ inputGroupDiv.appendChild(inputElement);
535
+ } else {
536
+ const actualInput = inputElement.querySelector('input, textarea');
537
+ if (actualInput) {
538
+ inputGroupDiv.appendChild(actualInput);
539
+ } else {
540
+ inputGroupDiv.appendChild(inputElement);
541
+ }
542
+ }
543
+ }
544
+ }
545
+
546
+ groupContainer.appendChild(inputGroupDiv);
547
+ this.logger.log(` Created zInputGroup for radio option: ${optionValue}`);
548
+ }
549
+
550
+ // Skip normal rendering for zSelect
551
+ continue;
552
+ }
553
+
554
+ // Special handling for checkboxes in input-group: wrap in zInputGroup-text
555
+ if (foundShorthand === 'zCheckbox' && metadata._zGroup === 'input-group') {
556
+ // Check if already wrapped in a container with zInputGroup-text
557
+ const hasInputGroupTextWrapper = value._zClass && value._zClass.includes('zInputGroup-text');
558
+
559
+ if (!hasInputGroupTextWrapper) {
560
+ // Wrap checkbox in zInputGroup-text container
561
+ const wrapperDiv = document.createElement('div');
562
+ wrapperDiv.classList.add('zInputGroup-text');
563
+ wrapperDiv.setAttribute('data-zkey', key);
564
+
565
+ // Expand zCheckbox to read_bool event and render directly into wrapper
566
+ // Pass zCross from parent value if present
567
+ const eventData = { event: 'read_bool', ...value.zCheckbox };
568
+ if (value.zCross !== undefined) {
569
+ eventData.zCross = value.zCross;
570
+ }
571
+ const checkboxElement = await this.renderZDisplayEvent(eventData, wrapperDiv);
572
+ if (checkboxElement) {
573
+ wrapperDiv.appendChild(checkboxElement);
574
+ }
575
+ groupContainer.appendChild(wrapperDiv);
576
+ this.logger.log(` Wrapped checkbox in zInputGroup-text for input-group`);
577
+ } else {
578
+ // Already wrapped, render normally
579
+ const element = await this.renderChunk({ [key]: value });
580
+ if (element && element.firstChild) {
581
+ const actualElement = element.firstChild;
582
+ groupContainer.appendChild(actualElement);
583
+ }
584
+ }
585
+ } else {
586
+ // Recursively render the entire structure for other shorthands
587
+ const element = await this.renderChunk({ [key]: value });
588
+ if (element && element.firstChild) {
589
+ // Extract the actual rendered element (skip wrapper if present)
590
+ const actualElement = element.firstChild;
591
+ groupContainer.appendChild(actualElement);
592
+ }
593
+ }
594
+ } else {
595
+ // Handle nested objects (recurse)
596
+ // DEBUG: Log organizational containers
597
+ if (key.startsWith('_')) {
598
+ this.logger.log(` [GROUP] Processing organizational container: ${key}`);
599
+ }
600
+
601
+ // Use centralized semantic element primitive (SSOT for _zHTML)
602
+ const elementType = value._zHTML || 'div';
603
+ const itemDiv = createSemanticElement(elementType, {}, this.logger);
604
+ itemDiv.setAttribute('data-zkey', key);
605
+
606
+ // Apply metadata to the organizational container
607
+ if (value._zClass) {
608
+ itemDiv.className = value._zClass;
609
+ }
610
+ if (value._zStyle) {
611
+ const cssString = convertStyleToString(value._zStyle, this.logger);
612
+ if (cssString) {
613
+ itemDiv.setAttribute('style', cssString);
614
+ }
615
+ }
616
+
617
+ await this.renderItems(value, itemDiv, keyPath);
618
+
619
+ if (itemDiv.children.length > 0) {
620
+ // OPTIMIZATION: Unwrap single-child containers with no styling
621
+ // This handles zText with semantic:div where the div itself has styling
622
+ if (itemDiv.children.length === 1 && !itemDiv.className) {
623
+ const child = itemDiv.children[0];
624
+ // Transfer data-zkey and id to the child
625
+ child.setAttribute('data-zkey', key);
626
+ if (!child.id) {
627
+ child.setAttribute('id', key);
628
+ }
629
+ groupContainer.appendChild(child);
630
+ } else {
631
+ groupContainer.appendChild(itemDiv);
632
+ }
633
+
634
+ if (key.startsWith('_')) {
635
+ this.logger.log(`[GROUP] Rendered organizational container ${key} with ${itemDiv.children.length} children`);
636
+ }
637
+ }
638
+ }
639
+ }
640
+ }
641
+ }
642
+
643
+ // Append group to parent
644
+ if (groupContainer.children.length > 0) {
645
+ parentElement.appendChild(groupContainer);
646
+ this.logger.log(`[ZDisplayOrchestrator] Grouped container rendered with ${groupContainer.children.length} items`);
647
+ this.logger.log(`Grouped container rendered with ${groupContainer.children.length} items`);
648
+ }
649
+
650
+ // Exit early - we've handled all children in the group
651
+ return;
652
+ }
653
+
654
+ /**
655
+ * Apply group-specific styling to an element (Terminal-first pattern)
656
+ * @param {HTMLElement} element - The rendered element
657
+ * @param {string} groupType - The type of group
658
+ * @param {Object} eventData - The original event data
659
+ * @private
660
+ */
661
+ _applyGroupStyling(element, groupType, eventData) {
662
+ if (!element || !groupType) {
663
+ return;
664
+ }
665
+
666
+ this.logger.log(`[_applyGroupStyling] Applying group styling: ${groupType}, color: ${eventData.color || 'none'}`);
667
+
668
+ // Apply group-specific zTheme classes based on group type and event type
669
+ switch (groupType) {
670
+ case 'list-group':
671
+ // For links, buttons, or any interactive element in a list-group
672
+ if (eventData.event === 'zURL' || eventData.event === 'button') {
673
+ element.classList.add('zList-group-item', 'zList-group-item-action');
674
+
675
+ // Terminal-first: Auto-infer color variant from YAML color parameter
676
+ if (eventData.color) {
677
+ const colorClass = `zList-group-item-${eventData.color.toLowerCase()}`;
678
+ element.classList.add(colorClass);
679
+ this.logger.log(`[_applyGroupStyling] Applied list-group color: ${colorClass}`);
680
+ }
681
+ }
682
+ break;
683
+
684
+ case 'button-group':
685
+ // For future: Button groups (horizontal button toolbar)
686
+ // element.classList.add('zBtn-group-item');
687
+ break;
688
+
689
+ case 'card-group':
690
+ // For future: Card groups (masonry/grid layout)
691
+ // element.classList.add('zCard-group-item');
692
+ break;
693
+
694
+ default:
695
+ this.logger.warn(`[_applyGroupStyling] Unknown group type: ${groupType}`);
696
+ }
697
+ }
698
+ }