@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,765 @@
1
+ /**
2
+ *
3
+ * Table Renderer - Data Tables with Pagination
4
+ *
5
+ *
6
+ * Renders zTable events from zCLI backend (AdvancedData subsystem).
7
+ * Supports semantic HTML tables with zTheme styling, pagination metadata,
8
+ * and both array and object row formats.
9
+ *
10
+ * @module rendering/table_renderer
11
+ * @layer 3
12
+ * @pattern Strategy (single event type)
13
+ *
14
+ * Philosophy:
15
+ * - "Terminal first" - tables are fundamental data display primitives
16
+ * - Pure rendering (no client-side pagination/sorting - that's backend's job)
17
+ * - Semantic HTML (table/thead/tbody/tr/th/td tags)
18
+ * - Backend sends already-paginated data (we just render it)
19
+ * - Uses Layer 2 utilities exclusively (no inline logic)
20
+ *
21
+ * Dependencies:
22
+ * - Layer 0: bifrost_constants.js
23
+ * - Layer 2: dom_utils.js
24
+ *
25
+ * Exports:
26
+ * - TableRenderer: Class for rendering zTable events
27
+ *
28
+ * Example:
29
+ * ```javascript
30
+ * import { TableRenderer } from './table_renderer.js';
31
+ *
32
+ * const renderer = new TableRenderer(logger);
33
+ * renderer.render({
34
+ * title: 'Users',
35
+ * columns: ['id', 'name', 'email'],
36
+ * rows: [
37
+ * [1, 'Alice', 'alice@example.com'],
38
+ * [2, 'Bob', 'bob@example.com']
39
+ * ]
40
+ * }, 'zVaF');
41
+ * ```
42
+ */
43
+
44
+ // ─────────────────────────────────────────────────────────────────
45
+ // Imports
46
+ // ─────────────────────────────────────────────────────────────────
47
+
48
+ // Layer 2: Utilities
49
+ import { TYPOGRAPHY } from '../../../L1_Foundation/constants/bifrost_constants.js';
50
+ import { createElement, setAttributes } from '../../../zSys/dom/dom_utils.js';
51
+ import { withErrorBoundary } from '../../../zSys/validation/error_boundary.js';
52
+
53
+ // Layer 0: Primitives
54
+ import {
55
+ createTable,
56
+ createThead,
57
+ createTbody,
58
+ createTr,
59
+ createTh,
60
+ createTd
61
+ } from '../primitives/table_primitives.js';
62
+ import { createDiv, createSpan } from '../primitives/generic_containers.js';
63
+ import { createButton } from '../primitives/interactive_primitives.js';
64
+ import { createInput } from '../primitives/form_primitives.js';
65
+ import { getBackgroundClass, getTextColorClass } from '../../../zSys/theme/color_utils.js';
66
+ import { getPaddingClass, getMarginClass, getGapClass } from '../../../zSys/theme/spacing_utils.js';
67
+ import { TextRenderer } from '../outputs/text_renderer.js';
68
+
69
+ //
70
+ // Table Renderer Class
71
+ //
72
+
73
+ /**
74
+ * TableRenderer - Renders data tables with pagination metadata
75
+ *
76
+ * Handles the 'zTable' zDisplay event from AdvancedData subsystem.
77
+ * Creates semantic HTML tables (table/thead/tbody) with zTheme styling.
78
+ *
79
+ * Backend sends already-paginated data, so this renderer just displays it.
80
+ * No client-side pagination/sorting logic (that's backend's responsibility).
81
+ */
82
+ export class TableRenderer {
83
+ /**
84
+ * Create a TableRenderer instance
85
+ * @param {Object} logger - Logger instance for debugging
86
+ */
87
+ constructor(logger) {
88
+ this.logger = logger || console;
89
+ this.logger.debug('[TableRenderer] Initialized');
90
+
91
+ // Initialize TextRenderer for markdown parsing in cells (DRY - reuse zMD logic)
92
+ this.textRenderer = new TextRenderer(this.logger);
93
+
94
+ // Wrap render method with error boundary
95
+ const originalRender = this.render.bind(this);
96
+ this.render = withErrorBoundary(originalRender, {
97
+ component: 'TableRenderer',
98
+ logger: this.logger
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Render a zTable event
104
+ *
105
+ * @param {Object} data - Table event data
106
+ * @param {string} data.title - Table title (optional)
107
+ * @param {Array<string>} data.columns - Column names
108
+ * @param {Array<Array|Object>} data.rows - Table rows (arrays or objects)
109
+ * @param {number} [data.limit] - Pagination limit (metadata only, rows already sliced)
110
+ * @param {number} [data.offset=0] - Pagination offset (metadata only)
111
+ * @param {boolean} [data.show_header=true] - Whether to show column headers
112
+ * @param {number} [data.indent=0] - Indentation level
113
+ * @param {string} [data.class] - Custom CSS class (optional)
114
+ * @param {string} zone - Target DOM element ID
115
+ * @returns {HTMLElement|null} Created table container or null if failed
116
+ *
117
+ * @example
118
+ * // Array rows
119
+ * renderer.render({
120
+ * title: 'Users',
121
+ * columns: ['id', 'name'],
122
+ * rows: [[1, 'Alice'], [2, 'Bob']]
123
+ * }, 'zVaF');
124
+ *
125
+ * @example
126
+ * // Object rows (typical from SQL queries)
127
+ * renderer.render({
128
+ * title: 'Users (showing 1-10 of 127)',
129
+ * columns: ['id', 'username', 'email'],
130
+ * rows: [
131
+ * {id: 1, username: 'alice', email: 'alice@example.com'},
132
+ * {id: 2, username: 'bob', email: 'bob@example.com'}
133
+ * ],
134
+ * limit: 10,
135
+ * offset: 0
136
+ * }, 'zVaF');
137
+ */
138
+ render(data, zone) {
139
+ const {
140
+ title,
141
+ caption,
142
+ columns = [],
143
+ rows: allRows = [], // Backend sends ALL rows (we slice them)
144
+ limit,
145
+ offset = 0,
146
+ show_header = true,
147
+ zPages = false, // Enable navigation controls (First/Prev/Next/Last)
148
+ indent = 0,
149
+ class: classAttr,
150
+ _zClass, // Support both 'class' and '_zClass' from .zolo files
151
+ _zColumn, // Column-level classes: { colName: 'class1 class2' }
152
+ _zRows, // Row-pattern classes: { odd, even, first, last }
153
+ _tableInstanceId, // Unique DOM target ID for in-place navigation replacement
154
+ } = data;
155
+
156
+ // Use _zClass if provided, fallback to class attribute
157
+ const customClass = _zClass || classAttr;
158
+
159
+ // Get target container (optional for orchestrator pattern)
160
+ let container = null;
161
+ if (zone) {
162
+ container = document.getElementById(zone);
163
+ if (!container) {
164
+ this.logger.error(`[TableRenderer] [ERROR] Zone not found: ${zone}`);
165
+ // Continue anyway - return element for orchestrator to append
166
+ }
167
+ }
168
+
169
+ // Validate columns
170
+ if (columns.length === 0) {
171
+ this.logger.warn('[TableRenderer] [WARN] No columns provided');
172
+ // Still render empty table (semantic HTML)
173
+ }
174
+
175
+ //
176
+ // CLIENT-SIDE PAGINATION: Slice rows based on limit/offset
177
+ //
178
+ let rows = allRows;
179
+ let hasMore = false;
180
+ let moreCount = 0;
181
+
182
+ if (limit !== null && limit !== undefined && limit > 0) {
183
+ // Slice rows: from offset to offset+limit
184
+ rows = allRows.slice(offset, offset + limit);
185
+ hasMore = (offset + limit) < allRows.length;
186
+ moreCount = allRows.length - (offset + limit);
187
+ }
188
+
189
+ // Create outer container for title + table + footer
190
+ const wrapper = createElement('div', ['zTable-container']);
191
+ // _zClass/_zStyle are applied to the inner <table> below (zTable + modifiers),
192
+ // so the orchestrator's central metadata pass must skip this wrapper frame.
193
+ wrapper.__zMetaScoped = true;
194
+
195
+ // Mark interactive tables with a unique instance ID so navigation re-renders replace in-place.
196
+ // _tableInstanceId is preferred (survives navigation round-trips); generate one on first render.
197
+ const instanceId = _tableInstanceId || `${title || 'table'}_${Math.random().toString(36).substr(2, 9)}`;
198
+ if (zPages && limit && limit > 0) {
199
+ wrapper.setAttribute('data-table-id', instanceId);
200
+ wrapper.setAttribute('data-interactive', 'true');
201
+ }
202
+
203
+ // Apply indent to wrapper (if specified)
204
+ const wrapperAttributes = {};
205
+ if (indent > 0) {
206
+ wrapperAttributes.style = `margin-left: ${indent}rem;`;
207
+ }
208
+ if (Object.keys(wrapperAttributes).length > 0) {
209
+ setAttributes(wrapper, wrapperAttributes);
210
+ }
211
+
212
+ // Render title with pagination info (if provided)
213
+ if (title) {
214
+ const titleElement = this._renderTitle(title, rows.length, allRows.length, limit, offset);
215
+ wrapper.appendChild(titleElement);
216
+ }
217
+
218
+ // Render caption as a description line UNDER the title (not a full-width row
219
+ // inside the table). Reads like a subtitle: what the table shows / source / date.
220
+ if (caption) {
221
+ const captionElement = createElement('p', ['zMuted', 'zMb-3']);
222
+ captionElement.style.fontSize = '0.875rem';
223
+ captionElement.textContent = this._decodeUnicodeEscapes(caption);
224
+ wrapper.appendChild(captionElement);
225
+ }
226
+
227
+ // Create responsive table wrapper (zTheme class)
228
+ const tableWrapper = createElement('div', ['zTable-responsive']);
229
+
230
+ // zTable styling is BUILT IN — the event is always themed, no opt-in needed.
231
+ // Any _zClass / class from the .zolo is appended on top for extra modifiers
232
+ // (deduped so an explicit `_zClass: zTable` never doubles up).
233
+ const tableClasses = ['zTable'];
234
+ if (customClass) {
235
+ for (const c of String(customClass).split(/\s+/)) {
236
+ if (c && c !== 'zTable') tableClasses.push(c);
237
+ }
238
+ }
239
+
240
+ // Create table element (using Layer 0 primitive)
241
+ const table = createTable({ class: tableClasses.length > 0 ? tableClasses.join(' ') : undefined });
242
+
243
+ // Render table head (if show_header is true)
244
+ if (show_header && columns.length > 0) {
245
+ const thead = this._renderTableHead(columns, _zColumn);
246
+ table.appendChild(thead);
247
+ }
248
+
249
+ // Render table body
250
+ if (rows.length > 0) {
251
+ const tbody = this._renderTableBody(columns, rows, _zColumn, _zRows, offset, allRows.length);
252
+ table.appendChild(tbody);
253
+ } else {
254
+ // Empty table body (semantic HTML)
255
+ const tbody = createTbody();
256
+ table.appendChild(tbody);
257
+ this.logger.warn('[TableRenderer] [WARN] No rows to display');
258
+ }
259
+
260
+ // Append table to wrapper
261
+ tableWrapper.appendChild(table);
262
+ wrapper.appendChild(tableWrapper);
263
+
264
+ //
265
+ // PAGINATION FOOTER: Interactive navigation OR simple "... N more rows"
266
+ //
267
+ if (zPages && limit && limit > 0) {
268
+ // Interactive mode: Render navigation buttons (First/Prev/Next/Last/Jump)
269
+ this._renderNavigationControls(wrapper, {
270
+ title,
271
+ _tableInstanceId: instanceId,
272
+ columns,
273
+ rows: allRows,
274
+ limit,
275
+ offset,
276
+ totalRows: allRows.length,
277
+ zPages: true,
278
+ _zClass,
279
+ _zColumn,
280
+ _zRows,
281
+ });
282
+ } else if (hasMore && moreCount > 0) {
283
+ // Simple truncation: Show "... N more rows" footer
284
+ const footer = this._renderMoreRowsFooter(moreCount);
285
+ wrapper.appendChild(footer);
286
+ }
287
+
288
+ // Append wrapper to container (if zone was provided - legacy behavior)
289
+ // If no zone, just return element (orchestrator pattern)
290
+ if (container) {
291
+ container.appendChild(wrapper);
292
+ }
293
+
294
+ // Log success
295
+ const paginationInfo = limit ? ` (showing ${rows.length} of ${allRows.length} total)` : '';
296
+ this.logger.log(`[TableRenderer] Rendered table (${columns.length} cols, ${rows.length} rows${paginationInfo}, indent: ${indent})`);
297
+
298
+ return wrapper;
299
+ }
300
+
301
+ /**
302
+ * Render table title with optional pagination info
303
+ * @private
304
+ * @param {string} title - Table title
305
+ * @param {number} displayedRowCount - Number of rows actually displayed (after pagination)
306
+ * @param {number} totalRowCount - Total number of rows (before pagination)
307
+ * @param {number} limit - Pagination limit
308
+ * @param {number} offset - Pagination offset
309
+ * @returns {HTMLElement} Title element (h4)
310
+ */
311
+ _renderTitle(title, displayedRowCount, totalRowCount, limit, offset) {
312
+ const titleElement = createElement('h4');
313
+
314
+ // Show pagination range in title if limited
315
+ if (limit !== null && limit !== undefined && limit > 0 && totalRowCount > 0) {
316
+ const showingStart = offset + 1;
317
+ const showingEnd = Math.min(offset + displayedRowCount, totalRowCount);
318
+ const decodedTitle = this._decodeUnicodeEscapes(title);
319
+ titleElement.textContent = `${decodedTitle} (showing ${showingStart}-${showingEnd} of ${totalRowCount})`;
320
+ } else {
321
+ titleElement.textContent = this._decodeUnicodeEscapes(title);
322
+ }
323
+
324
+ // Apply zTheme styling
325
+ setAttributes(titleElement, {
326
+ class: 'zMb-3 zText-dark',
327
+ style: `font-weight: ${TYPOGRAPHY.FONT_WEIGHTS.MEDIUM};`
328
+ });
329
+
330
+ return titleElement;
331
+ }
332
+
333
+ /**
334
+ * Render table head (column headers)
335
+ * @private
336
+ * @param {Array<string>} columns - Column names
337
+ * @returns {HTMLElement} thead element
338
+ */
339
+ _renderTableHead(columns, _zColumn) {
340
+ const thead = createThead();
341
+ const headerRow = createTr();
342
+
343
+ columns.forEach(column => {
344
+ const th = createTh();
345
+ // Decode Unicode escapes in column names
346
+ th.textContent = this._decodeUnicodeEscapes(column); // XSS safe
347
+ const colClass = _zColumn?.[column];
348
+ if (colClass) th.className = colClass;
349
+ headerRow.appendChild(th);
350
+ });
351
+
352
+ thead.appendChild(headerRow);
353
+ return thead;
354
+ }
355
+
356
+ /**
357
+ * Render table body (data rows)
358
+ * @private
359
+ * @param {Array<string>} columns - Column names (for object row mapping)
360
+ * @param {Array<Array|Object>} rows - Table rows
361
+ * @returns {HTMLElement} tbody element
362
+ */
363
+ _renderTableBody(columns, rows, _zColumn, _zRows, offset = 0, totalRows = null) {
364
+ const tbody = createTbody();
365
+ const cellTracker = [];
366
+ const datasetLastIndex = totalRows !== null ? totalRows - 1 : null;
367
+
368
+ rows.forEach((row, rowIndex) => {
369
+ const tr = createTr();
370
+
371
+ // Apply _zRows pattern classes to <tr>
372
+ // first/last are dataset-absolute (not page-relative), odd/even use absolute index for
373
+ // consistent alternation across page boundaries.
374
+ if (_zRows) {
375
+ const trClasses = [];
376
+ const absoluteIndex = offset + rowIndex;
377
+ const isFirst = absoluteIndex === 0;
378
+ const isLast = datasetLastIndex !== null ? absoluteIndex === datasetLastIndex : false;
379
+ if (_zRows.first && isFirst) trClasses.push(_zRows.first);
380
+ if (_zRows.last && isLast) trClasses.push(_zRows.last);
381
+ if (!isFirst && !isLast) {
382
+ if (_zRows.odd && absoluteIndex % 2 === 0) trClasses.push(_zRows.odd);
383
+ if (_zRows.even && absoluteIndex % 2 === 1) trClasses.push(_zRows.even);
384
+ }
385
+ if (trClasses.length > 0) tr.className = trClasses.join(' ');
386
+ }
387
+
388
+ // Handle both array and object rows (zData sends objects from SQL queries)
389
+ if (Array.isArray(row)) {
390
+ // Array row: [val1, val2, val3]
391
+ row.forEach((value, colIndex) => {
392
+ const colClass = _zColumn?.[columns[colIndex]] || null;
393
+ const cellContent = this._formatCellValue(value);
394
+
395
+ if (cellContent === '^^' && rowIndex > 0 && cellTracker[rowIndex - 1]?.[colIndex]) {
396
+ const prevCell = cellTracker[rowIndex - 1][colIndex];
397
+ const currentRowspan = parseInt(prevCell.getAttribute('rowspan') || '1');
398
+ prevCell.setAttribute('rowspan', currentRowspan + 1);
399
+ cellTracker[rowIndex] = cellTracker[rowIndex] || [];
400
+ cellTracker[rowIndex][colIndex] = prevCell;
401
+ } else {
402
+ const td = createTd();
403
+ if (colClass) td.className = colClass;
404
+ td.innerHTML = this._parseCellMarkdown(cellContent);
405
+ tr.appendChild(td);
406
+ cellTracker[rowIndex] = cellTracker[rowIndex] || [];
407
+ cellTracker[rowIndex][colIndex] = td;
408
+ }
409
+ });
410
+ } else {
411
+ // Object row: {col1: val1, col2: val2, ...}
412
+ // Supports cell descriptor: {col: {val: value, _zClass: 'className'}}
413
+ columns.forEach((column, colIndex) => {
414
+ const raw = row[column];
415
+
416
+ // Cell descriptor: { val: ..., _zClass: '...' } — object rows only
417
+ const isCellDescriptor = raw !== null
418
+ && typeof raw === 'object'
419
+ && !Array.isArray(raw)
420
+ && 'val' in raw;
421
+
422
+ const cellValue = isCellDescriptor ? raw.val : raw;
423
+ const cellClass = isCellDescriptor ? (raw._zClass || null) : null;
424
+
425
+ // Column class is the BASE (alignment, tint…); a cell descriptor
426
+ // _zClass LAYERS on top so both apply (e.g. zText-end + zText-success).
427
+ // On a genuine conflict the later zTheme rule wins — not the cell.
428
+ const colClass = _zColumn?.[column] || null;
429
+ const combinedClass = [colClass, cellClass].filter(Boolean).join(' ') || null;
430
+
431
+ const cellContent = this._formatCellValue(cellValue);
432
+
433
+ if (cellContent === '^^' && rowIndex > 0 && cellTracker[rowIndex - 1]?.[colIndex]) {
434
+ const prevCell = cellTracker[rowIndex - 1][colIndex];
435
+ const currentRowspan = parseInt(prevCell.getAttribute('rowspan') || '1');
436
+ prevCell.setAttribute('rowspan', currentRowspan + 1);
437
+ cellTracker[rowIndex] = cellTracker[rowIndex] || [];
438
+ cellTracker[rowIndex][colIndex] = prevCell;
439
+ } else {
440
+ const td = createTd();
441
+ if (combinedClass) td.className = combinedClass;
442
+ td.innerHTML = this._parseCellMarkdown(cellContent);
443
+ tr.appendChild(td);
444
+ cellTracker[rowIndex] = cellTracker[rowIndex] || [];
445
+ cellTracker[rowIndex][colIndex] = td;
446
+ }
447
+ });
448
+ }
449
+
450
+ tbody.appendChild(tr);
451
+ });
452
+
453
+ return tbody;
454
+ }
455
+
456
+ /**
457
+ * Render "... N more rows" footer (shown when table is truncated)
458
+ * @private
459
+ * @param {number} moreCount - Number of additional rows not displayed
460
+ * @returns {HTMLElement} Footer element (p)
461
+ */
462
+ _renderMoreRowsFooter(moreCount) {
463
+ const footer = createElement('p', ['zText-info', 'zMt-2', 'zMs-3']);
464
+ footer.style.fontStyle = 'italic';
465
+ footer.style.fontSize = '0.875rem';
466
+ footer.textContent = `... ${moreCount} more rows`;
467
+
468
+ return footer;
469
+ }
470
+
471
+ /**
472
+ * Render interactive navigation controls for paginated tables
473
+ * Creates First/Previous/Next/Last buttons + Jump to page input
474
+ * Buttons send 'table_navigate' events back to server (Terminal first!)
475
+ *
476
+ * STYLIZED COMPOSITION: Using Layer 0 primitives + Layer 2 utilities
477
+ *
478
+ * @private
479
+ * @param {HTMLElement} container - Container to append controls to
480
+ * @param {Object} tableState - Table state (limit, offset, totalRows, etc.)
481
+ */
482
+ _renderNavigationControls(container, tableState) {
483
+ const { limit, offset, totalRows } = tableState;
484
+
485
+ // Calculate pagination metadata
486
+ const totalPages = Math.ceil(totalRows / limit);
487
+ const currentPage = Math.floor(offset / limit) + 1;
488
+ const canGoPrev = currentPage > 1;
489
+ const canGoNext = currentPage < totalPages;
490
+
491
+ //
492
+ // MODERN 2-ROW PAGINATION NAVIGATION (Primitives + Utilities)
493
+ // Row 1: Page Info (centered, full width)
494
+ // Row 2: Navigation Buttons (flexed, centered)
495
+ //
496
+
497
+ // Full-width wrapper (primitive + utilities)
498
+ const navWrapper = createDiv();
499
+ navWrapper.classList.add(
500
+ getMarginClass('top', 3),
501
+ getPaddingClass('all', 3),
502
+ getBackgroundClass('white'),
503
+ 'zBorder',
504
+ 'zRounded',
505
+ 'zShadow-sm'
506
+ );
507
+
508
+ // ROW 1: Page Info Container (centered with proper zTheme classes)
509
+ const pageInfoRow = createDiv();
510
+ pageInfoRow.classList.add(
511
+ 'zD-flex',
512
+ 'zFlex-center', // Correct zTheme centering class
513
+ 'zFlex-items-center', // Vertical alignment
514
+ getMarginClass('bottom', 3)
515
+ );
516
+
517
+ // Page info text (primitive + utilities)
518
+ const pageInfo = createSpan();
519
+ pageInfo.classList.add(getTextColorClass('muted'));
520
+ pageInfo.style.fontSize = '0.875rem';
521
+ pageInfo.style.fontWeight = TYPOGRAPHY.FONT_WEIGHTS.MEDIUM;
522
+ pageInfo.innerHTML = `<span class="zText-dark">Page ${currentPage}</span> of <span class="zText-dark">${totalPages}</span> <span class="zText-muted">(${totalRows} total rows)</span>`;
523
+
524
+ pageInfoRow.appendChild(pageInfo);
525
+ navWrapper.appendChild(pageInfoRow);
526
+
527
+ // ROW 2: Navigation Controls Container (centered with proper zTheme classes)
528
+ const navControlsRow = createDiv();
529
+ navControlsRow.classList.add(
530
+ 'zD-flex',
531
+ 'zFlex-center', // Correct zTheme centering class
532
+ 'zFlex-items-center', // Vertical alignment
533
+ 'zFlex-wrap', // Wrap on small screens
534
+ getGapClass(3)
535
+ );
536
+
537
+ //
538
+ // NAVIGATION BUTTONS (primitives + utilities)
539
+ //
540
+ const buttonGroup = createDiv();
541
+ buttonGroup.classList.add('zBtn-group', 'zBtn-group-sm');
542
+
543
+ // Helper to create navigation button (using primitives!)
544
+ const createNavButton = (label, command, enabled) => {
545
+ const btn = createButton('button');
546
+ btn.classList.add('zBtn', 'zBtn-sm');
547
+
548
+ if (enabled) {
549
+ btn.classList.add('zBtn-outline-primary');
550
+ btn.onclick = () => {
551
+ this.logger.log(`[TableRenderer] Navigation: ${command}`);
552
+ this._handleTableNavigation(command, tableState);
553
+ };
554
+ } else {
555
+ btn.classList.add('zBtn-outline-secondary');
556
+ btn.disabled = true;
557
+ }
558
+
559
+ btn.innerHTML = label; // Support icons
560
+ return btn;
561
+ };
562
+
563
+ // Navigation buttons (First/Previous/Next/Last) - Using Bootstrap Icons
564
+ buttonGroup.appendChild(createNavButton('<i class="bi bi-skip-start-fill"></i> First', 'f', canGoPrev));
565
+ buttonGroup.appendChild(createNavButton('<i class="bi bi-chevron-left"></i> Prev', 'p', canGoPrev));
566
+ buttonGroup.appendChild(createNavButton('Next <i class="bi bi-chevron-right"></i>', 'n', canGoNext));
567
+ buttonGroup.appendChild(createNavButton('Last <i class="bi bi-skip-end-fill"></i>', 'l', canGoNext));
568
+
569
+ navControlsRow.appendChild(buttonGroup);
570
+
571
+ //
572
+ // JUMP TO PAGE (primitives + utilities)
573
+ //
574
+ const jumpContainer = createDiv();
575
+ jumpContainer.classList.add(
576
+ 'zD-flex',
577
+ 'zAlign-items-center',
578
+ getGapClass(2)
579
+ );
580
+
581
+ const jumpLabel = createSpan();
582
+ jumpLabel.classList.add(getTextColorClass('muted'));
583
+ jumpLabel.textContent = 'Jump to:';
584
+ jumpContainer.appendChild(jumpLabel);
585
+
586
+ const jumpInput = createInput('number');
587
+ jumpInput.classList.add('zInput', 'zInput-sm');
588
+ jumpInput.setAttribute('min', '1');
589
+ jumpInput.setAttribute('max', totalPages.toString());
590
+ jumpInput.setAttribute('placeholder', '#');
591
+ jumpInput.style.width = '60px';
592
+ jumpInput.style.textAlign = 'center';
593
+
594
+ const jumpBtn = createButton('button');
595
+ jumpBtn.classList.add('zBtn', 'zBtn-sm', 'zBtn-primary');
596
+ jumpBtn.textContent = 'Go';
597
+ jumpBtn.onclick = () => {
598
+ const pageNum = parseInt(jumpInput.value);
599
+ if (pageNum >= 1 && pageNum <= totalPages) {
600
+ this.logger.log(`[TableRenderer] Jumping to page: ${pageNum}`);
601
+ this._handleTableNavigation(pageNum.toString(), tableState);
602
+ jumpInput.value = '';
603
+ } else {
604
+ this.logger.warn(`[TableRenderer] [WARN] Invalid page number: ${pageNum} (must be 1-${totalPages})`);
605
+ }
606
+ };
607
+
608
+ // Enter key on jump input
609
+ jumpInput.onkeypress = (e) => {
610
+ if (e.key === 'Enter') {
611
+ jumpBtn.click();
612
+ }
613
+ };
614
+
615
+ jumpContainer.appendChild(jumpInput);
616
+ jumpContainer.appendChild(jumpBtn);
617
+ navControlsRow.appendChild(jumpContainer);
618
+
619
+ // Append row 2 to wrapper
620
+ navWrapper.appendChild(navControlsRow);
621
+
622
+ // Append complete navigation to container
623
+ container.appendChild(navWrapper);
624
+ }
625
+
626
+ /**
627
+ * Handle table navigation (send command to server)
628
+ * In "Terminal first" philosophy, navigation updates happen server-side
629
+ * @private
630
+ * @param {string} command - Navigation command (first/prev/next/last/jump:N)
631
+ * @param {Object} tableState - Table state
632
+ */
633
+ _handleTableNavigation(command, tableState) {
634
+ this.logger.log(`[TableRenderer] Navigation: ${command}`);
635
+ if (this.client && this.client.connection) {
636
+ // Fire-and-forget via raw WebSocket — no _requestId, no timeout
637
+ this.client.connection.send(JSON.stringify({
638
+ event: 'table_navigate',
639
+ data: { command, ...tableState }
640
+ }));
641
+ } else {
642
+ this.logger.warn('[TableRenderer] No client reference — cannot send table_navigate');
643
+ }
644
+ }
645
+
646
+ /**
647
+ * Format cell value for display
648
+ * Handles null, undefined, objects, arrays, dates, numbers, strings
649
+ * @private
650
+ * @param {*} value - Cell value
651
+ * @returns {string} Formatted value
652
+ */
653
+ _formatCellValue(value) {
654
+ // Handle null/undefined
655
+ if (value === null || value === undefined) {
656
+ return '—'; // Em dash for empty values
657
+ }
658
+
659
+ // Handle dates (ISO strings or Date objects)
660
+ if (value instanceof Date) {
661
+ return value.toLocaleDateString();
662
+ }
663
+
664
+ // Handle date-like strings (ISO 8601 format)
665
+ if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value)) {
666
+ try {
667
+ const date = new Date(value);
668
+ if (!isNaN(date.getTime())) {
669
+ return date.toLocaleDateString();
670
+ }
671
+ } catch (e) {
672
+ // Fall through to default string handling
673
+ }
674
+ }
675
+
676
+ // Handle numbers
677
+ if (typeof value === 'number') {
678
+ // Format large numbers with commas
679
+ if (Math.abs(value) >= 1000) {
680
+ return value.toLocaleString();
681
+ }
682
+ return value.toString();
683
+ }
684
+
685
+ // Handle booleans
686
+ if (typeof value === 'boolean') {
687
+ return value ? '[ok]' : '';
688
+ }
689
+
690
+ // Handle objects/arrays (JSON stringify with truncation)
691
+ if (typeof value === 'object') {
692
+ const json = JSON.stringify(value);
693
+ if (json.length > 50) {
694
+ return `${json.substring(0, 47) }...`;
695
+ }
696
+ return json;
697
+ }
698
+
699
+ // Handle strings (decode Unicode escapes)
700
+ const str = String(value);
701
+
702
+ // Decode Unicode escapes (\UXXXX or U+XXXX format)
703
+ const decoded = this._decodeUnicodeEscapes(str);
704
+
705
+ // No truncation - let CSS handle overflow with text wrapping or ellipsis
706
+ // This ensures markdown links and formatted text aren't broken mid-parse
707
+ return decoded;
708
+ }
709
+
710
+ /**
711
+ * Decode Unicode escape sequences to actual characters
712
+ * Supports: \uXXXX (standard) and \UXXXXXXXX (extended) formats
713
+ *
714
+ * Note: Basic escape sequences (\n, \t, etc.) are handled by JSON.parse()
715
+ * automatically when receiving data from backend. We only need to decode
716
+ * custom Unicode formats that JSON doesn't handle.
717
+ *
718
+ * @param {string} text - Text containing Unicode escapes
719
+ * @returns {string} - Decoded text
720
+ * @private
721
+ */
722
+ _decodeUnicodeEscapes(text) {
723
+ if (!text || typeof text !== 'string') return text;
724
+
725
+ // Replace \uXXXX format (standard 4-digit Unicode escape)
726
+ text = text.replace(/\\u([0-9A-Fa-f]{4})/g, (match, hexCode) => {
727
+ return String.fromCodePoint(parseInt(hexCode, 16));
728
+ });
729
+
730
+ // Replace \UXXXXXXXX format (extended 4-8 digit for supplementary characters & emojis)
731
+ text = text.replace(/\\U([0-9A-Fa-f]{4,8})/g, (match, hexCode) => {
732
+ return String.fromCodePoint(parseInt(hexCode, 16));
733
+ });
734
+
735
+ return text;
736
+ }
737
+
738
+ /**
739
+ * Parse markdown and HTML in table cells
740
+ * Reuses TextRenderer._parseMarkdown() logic (DRY - same as zMD)
741
+ *
742
+ * Supports:
743
+ * - `code` -> <code>code</code>
744
+ * - **bold** -> <strong>bold</strong>
745
+ * - *italic* -> <em>italic</em>
746
+ * - HTML tags pass through (e.g., <h1>text</h1>)
747
+ *
748
+ * @param {string} text - Cell content with potential markdown or HTML
749
+ * @returns {string} - HTML string with markdown parsed and HTML preserved
750
+ * @private
751
+ */
752
+ _parseCellMarkdown(text) {
753
+ if (!text || typeof text !== 'string') return text;
754
+
755
+ // Reuse TextRenderer's markdown parser (DRY principle)
756
+ // This handles: `code`, **bold**, *italic*, [links](url), etc.
757
+ return this.textRenderer._parseMarkdown(text);
758
+ }
759
+ }
760
+
761
+ //
762
+ // Default Export
763
+ //
764
+ export default TableRenderer;
765
+