tekivex-ui 2.5.2 → 2.5.4

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 (85) hide show
  1. package/dist/TkxForm-DSMDo2Ol.js +388 -0
  2. package/dist/TkxForm-DwV_n-eE.cjs +12 -0
  3. package/dist/charts.cjs +1 -1
  4. package/dist/charts.js +324 -301
  5. package/dist/headless.cjs +1 -1
  6. package/dist/headless.js +396 -195
  7. package/dist/i18n.cjs +1 -0
  8. package/dist/i18n.js +757 -0
  9. package/dist/index.cjs +27 -22
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4994 -5089
  13. package/dist/quantum.cjs +91 -0
  14. package/dist/quantum.js +2312 -0
  15. package/dist/realtime.cjs +25 -0
  16. package/dist/realtime.js +944 -0
  17. package/dist/security-C369oOWH.cjs +1 -0
  18. package/dist/security-CzAmaMqa.js +97 -0
  19. package/dist/src/charts/TkxDonutChart.d.ts.map +1 -1
  20. package/dist/src/charts/TkxPieChart.d.ts.map +1 -1
  21. package/dist/src/components/TkxDataGrid.d.ts +9 -1
  22. package/dist/src/components/TkxDataGrid.d.ts.map +1 -1
  23. package/dist/src/components/TkxDropdown.d.ts +41 -0
  24. package/dist/src/components/TkxDropdown.d.ts.map +1 -0
  25. package/dist/src/components/TkxLiveFeed.d.ts +891 -0
  26. package/dist/src/components/TkxLiveFeed.d.ts.map +1 -0
  27. package/dist/src/components/TkxLiveLog.d.ts +892 -0
  28. package/dist/src/components/TkxLiveLog.d.ts.map +1 -0
  29. package/dist/src/components/TkxLiveMetrics.d.ts +24 -0
  30. package/dist/src/components/TkxLiveMetrics.d.ts.map +1 -0
  31. package/dist/src/components/TkxPlayground.d.ts +15 -0
  32. package/dist/src/components/TkxPlayground.d.ts.map +1 -0
  33. package/dist/src/components/TkxQuantumForm.d.ts +20 -0
  34. package/dist/src/components/TkxQuantumForm.d.ts.map +1 -0
  35. package/dist/src/components/TkxRealTimeChart.d.ts +27 -0
  36. package/dist/src/components/TkxRealTimeChart.d.ts.map +1 -0
  37. package/dist/src/components/TkxTable.d.ts +11 -1
  38. package/dist/src/components/TkxTable.d.ts.map +1 -1
  39. package/dist/src/components/TkxThemeBuilder.d.ts +9 -0
  40. package/dist/src/components/TkxThemeBuilder.d.ts.map +1 -0
  41. package/dist/src/components/index.d.ts +1 -0
  42. package/dist/src/components/index.d.ts.map +1 -1
  43. package/dist/src/engine/index.d.ts +2 -0
  44. package/dist/src/engine/index.d.ts.map +1 -1
  45. package/dist/src/engine/quantum-ai.d.ts +201 -0
  46. package/dist/src/engine/quantum-ai.d.ts.map +1 -0
  47. package/dist/src/engine/tkx.d.ts.map +1 -1
  48. package/dist/src/headless/index.d.ts +6 -0
  49. package/dist/src/headless/index.d.ts.map +1 -1
  50. package/dist/src/headless/useInfiniteQuery.d.ts +35 -0
  51. package/dist/src/headless/useInfiniteQuery.d.ts.map +1 -0
  52. package/dist/src/headless/useSSE.d.ts +36 -0
  53. package/dist/src/headless/useSSE.d.ts.map +1 -0
  54. package/dist/src/headless/useWebSocket.d.ts +33 -0
  55. package/dist/src/headless/useWebSocket.d.ts.map +1 -0
  56. package/dist/src/quantum/index.d.ts +9 -0
  57. package/dist/src/quantum/index.d.ts.map +1 -0
  58. package/dist/src/realtime/index.d.ts +9 -0
  59. package/dist/src/realtime/index.d.ts.map +1 -0
  60. package/dist/{TkxForm-BWK4LqY3.js → tkx-BJ10JtMv.js} +372 -721
  61. package/dist/tkx-BeNyC0w7.cjs +4 -0
  62. package/package.json +16 -1
  63. package/src/charts/TkxDonutChart.tsx +15 -2
  64. package/src/charts/TkxPieChart.tsx +17 -2
  65. package/src/components/TkxDataGrid.tsx +72 -0
  66. package/src/components/TkxDropdown.tsx +802 -0
  67. package/src/components/TkxLiveFeed.tsx +329 -0
  68. package/src/components/TkxLiveLog.tsx +470 -0
  69. package/src/components/TkxLiveMetrics.tsx +291 -0
  70. package/src/components/TkxPlayground.tsx +736 -0
  71. package/src/components/TkxQuantumForm.tsx +592 -0
  72. package/src/components/TkxRealTimeChart.tsx +381 -0
  73. package/src/components/TkxTable.tsx +351 -77
  74. package/src/components/TkxThemeBuilder.tsx +685 -0
  75. package/src/components/index.ts +4 -0
  76. package/src/engine/index.ts +2 -0
  77. package/src/engine/quantum-ai.ts +1045 -0
  78. package/src/engine/tkx.ts +95 -4
  79. package/src/headless/index.ts +6 -0
  80. package/src/headless/useInfiniteQuery.ts +153 -0
  81. package/src/headless/useSSE.ts +165 -0
  82. package/src/headless/useWebSocket.ts +229 -0
  83. package/src/quantum/index.ts +39 -0
  84. package/src/realtime/index.ts +21 -0
  85. package/dist/TkxForm-ljQjX7KD.cjs +0 -15
@@ -1,9 +1,18 @@
1
- import { useState, useMemo, type ReactNode } from 'react';
1
+ import {
2
+ useState,
3
+ useRef,
4
+ useEffect,
5
+ useCallback,
6
+ useMemo,
7
+ type ReactNode,
8
+ } from 'react';
2
9
  import { useTheme } from '../themes';
3
10
  import { sanitizeString } from '../engine/security';
4
11
  import { TkxSkeleton } from './TkxSkeleton';
5
12
  import { tkx } from '../engine/tkx';
6
13
 
14
+ // ── Types ─────────────────────────────────────────────────────────────────────
15
+
7
16
  export type SortDirection = 'asc' | 'desc' | 'none';
8
17
 
9
18
  export interface ColumnDef<T> {
@@ -25,27 +34,80 @@ export interface TkxTableProps<T extends Record<string, unknown>> {
25
34
  style?: React.CSSProperties;
26
35
  className?: string;
27
36
  striped?: boolean;
37
+ // Virtual scroll
38
+ maxHeight?: number | string;
39
+ rowHeight?: number;
40
+ virtualScroll?: boolean;
41
+ // Infinite scroll
42
+ onLoadMore?: () => void | Promise<void>;
43
+ hasMore?: boolean;
44
+ loadingMore?: boolean;
45
+ // Row interaction
46
+ onRowClick?: (row: T, index: number) => void;
47
+ selectedRows?: number[];
48
+ // Appearance
49
+ bordered?: boolean;
50
+ compact?: boolean;
28
51
  }
29
52
 
53
+ // ── Sort icon ─────────────────────────────────────────────────────────────────
54
+
30
55
  function SortIcon({ dir }: { dir: SortDirection }) {
31
56
  return (
32
- <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={tkx('shrink-0')}>
33
- {dir === 'asc' ? <path d="M7 14l5-5 5 5H7z" /> : dir === 'desc' ? <path d="M7 10l5 5 5-5H7z" /> : <path d="M7 10l5 5 5-5H7zM7 14l5-5 5 5H7z" opacity="0.4" />}
57
+ <svg
58
+ width="12"
59
+ height="12"
60
+ viewBox="0 0 24 24"
61
+ fill="currentColor"
62
+ aria-hidden="true"
63
+ className={tkx('shrink-0')}
64
+ >
65
+ {dir === 'asc' ? (
66
+ <path d="M7 14l5-5 5 5H7z" />
67
+ ) : dir === 'desc' ? (
68
+ <path d="M7 10l5 5 5-5H7z" />
69
+ ) : (
70
+ <path d="M7 10l5 5 5-5H7zM7 14l5-5 5 5H7z" opacity="0.4" />
71
+ )}
34
72
  </svg>
35
73
  );
36
74
  }
37
75
 
76
+ // ── Component ─────────────────────────────────────────────────────────────────
77
+
38
78
  export function TkxTable<T extends Record<string, unknown>>({
39
- columns, data, caption, sortable, stickyHeader, isLoading, emptyState, style, className, striped,
79
+ columns,
80
+ data,
81
+ caption,
82
+ sortable,
83
+ stickyHeader,
84
+ isLoading,
85
+ emptyState,
86
+ style,
87
+ className,
88
+ striped,
89
+ maxHeight,
90
+ rowHeight = 44,
91
+ virtualScroll,
92
+ onLoadMore,
93
+ hasMore,
94
+ loadingMore = false,
95
+ onRowClick,
96
+ selectedRows,
97
+ bordered,
98
+ compact,
40
99
  }: TkxTableProps<T>) {
41
100
  const theme = useTheme();
101
+
102
+ // ── Sort ───────────────────────────────────────────────────────────────
42
103
  const [sortKey, setSortKey] = useState<keyof T | null>(null);
43
104
  const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
44
105
 
45
106
  const sorted = useMemo(() => {
46
107
  if (!sortKey || !sortable) return data;
47
108
  return [...data].sort((a, b) => {
48
- const av = a[sortKey], bv = b[sortKey];
109
+ const av = a[sortKey];
110
+ const bv = b[sortKey];
49
111
  let cmp = 0;
50
112
  if (typeof av === 'string' && typeof bv === 'string') cmp = av.localeCompare(bv);
51
113
  else if (typeof av === 'number' && typeof bv === 'number') cmp = av - bv;
@@ -53,96 +115,308 @@ export function TkxTable<T extends Record<string, unknown>>({
53
115
  });
54
116
  }, [data, sortKey, sortDir, sortable]);
55
117
 
56
- function handleSort(key: keyof T) {
57
- if (sortKey === key) setSortDir((d) => d === 'asc' ? 'desc' : 'asc');
58
- else { setSortKey(key); setSortDir('asc'); }
59
- }
118
+ const handleSort = useCallback(
119
+ (key: keyof T) => {
120
+ if (sortKey === key) setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
121
+ else { setSortKey(key); setSortDir('asc'); }
122
+ },
123
+ [sortKey],
124
+ );
60
125
 
61
126
  function getDir(key: keyof T): SortDirection {
62
127
  return sortKey !== key ? 'none' : sortDir;
63
128
  }
64
129
 
130
+ // ── Virtual scroll ─────────────────────────────────────────────────────
131
+ const OVERSCAN = 5;
132
+ const isVirtual =
133
+ virtualScroll !== undefined
134
+ ? virtualScroll
135
+ : maxHeight !== undefined && sorted.length >= 50;
136
+
137
+ const scrollContainerRef = useRef<HTMLDivElement>(null);
138
+ const [scrollTop, setScrollTop] = useState(0);
139
+ const [containerHeight, setContainerHeight] = useState(400);
140
+
141
+ const handleScroll = useCallback(() => {
142
+ if (scrollContainerRef.current) {
143
+ setScrollTop(scrollContainerRef.current.scrollTop);
144
+ }
145
+ }, []);
146
+
147
+ useEffect(() => {
148
+ if (!isVirtual || !scrollContainerRef.current) return;
149
+ const el = scrollContainerRef.current;
150
+ setContainerHeight(el.clientHeight);
151
+ const ro = new ResizeObserver(() => setContainerHeight(el.clientHeight));
152
+ ro.observe(el);
153
+ return () => ro.disconnect();
154
+ }, [isVirtual]);
155
+
156
+ const startIndex = isVirtual
157
+ ? Math.max(0, Math.floor(scrollTop / rowHeight) - OVERSCAN)
158
+ : 0;
159
+ const endIndex = isVirtual
160
+ ? Math.min(sorted.length, Math.ceil((scrollTop + containerHeight) / rowHeight) + OVERSCAN)
161
+ : sorted.length;
162
+
163
+ const visibleRows = isVirtual ? sorted.slice(startIndex, endIndex) : sorted;
164
+
165
+ // ── Infinite scroll ────────────────────────────────────────────────────
166
+ const sentinelRef = useRef<HTMLDivElement>(null);
167
+ const loadingRef = useRef<boolean>(false);
168
+
169
+ // Reset debounce flag when data.length changes (new data arrived)
170
+ useEffect(() => {
171
+ loadingRef.current = false;
172
+ }, [data.length]);
173
+
174
+ useEffect(() => {
175
+ if (!onLoadMore) return;
176
+ const sentinel = sentinelRef.current;
177
+ if (!sentinel) return;
178
+
179
+ const observer = new IntersectionObserver(
180
+ (entries) => {
181
+ const entry = entries[0];
182
+ if (entry.isIntersecting && !loadingRef.current && hasMore !== false) {
183
+ loadingRef.current = true;
184
+ onLoadMore();
185
+ }
186
+ },
187
+ {
188
+ root: scrollContainerRef.current,
189
+ rootMargin: '0px 0px 200px 0px',
190
+ threshold: 0,
191
+ },
192
+ );
193
+
194
+ observer.observe(sentinel);
195
+ return () => observer.disconnect();
196
+ // eslint-disable-next-line react-hooks/exhaustive-deps
197
+ }, [onLoadMore, hasMore]);
198
+
199
+ // ── Styles ─────────────────────────────────────────────────────────────
200
+ const py = compact ? '4px' : '12px';
201
+ const px = compact ? '8px' : '16px';
202
+ const borderStyle = bordered ? `1px solid ${theme.border}` : 'none';
203
+ const selectedSet = useMemo(() => new Set(selectedRows ?? []), [selectedRows]);
204
+
65
205
  return (
66
206
  <div
67
- className={tkx('w-full overflow-x-auto rounded-lg', className ?? '')}
68
- style={{ border: `1px solid ${theme.border}`, ...style }}
207
+ className={tkx('w-full rounded-lg', className ?? '')}
208
+ style={{ border: `1px solid ${theme.border}`, overflow: 'hidden', ...style }}
69
209
  >
70
- <table
71
- className={tkx('w-full font-sans text-sm')}
72
- style={{ borderCollapse: 'collapse', color: theme.text }}
210
+ <div
211
+ ref={scrollContainerRef}
212
+ onScroll={isVirtual ? handleScroll : undefined}
213
+ style={{
214
+ overflowX: 'auto',
215
+ overflowY: maxHeight ? 'auto' : 'visible',
216
+ maxHeight: maxHeight ?? 'none',
217
+ }}
73
218
  >
74
- {caption && (
75
- <caption className={tkx('text-sm text-left p-2')} style={{ color: theme.textMuted }}>
76
- {sanitizeString(caption)}
77
- </caption>
78
- )}
219
+ <table
220
+ className={tkx('w-full font-sans text-sm')}
221
+ style={{ borderCollapse: 'collapse', color: theme.text }}
222
+ >
223
+ {caption && (
224
+ <caption
225
+ className={tkx('text-sm text-left p-2')}
226
+ style={{ color: theme.textMuted }}
227
+ >
228
+ {sanitizeString(caption)}
229
+ </caption>
230
+ )}
79
231
 
80
- <thead style={stickyHeader ? { position: 'sticky', top: 0, backgroundColor: theme.surfaceAlt, zIndex: 1 } : { backgroundColor: theme.surfaceAlt }}>
81
- <tr>
82
- {columns.map((col) => {
83
- const canSort = sortable && col.sortable !== false;
84
- const dir = getDir(col.key);
85
- return (
86
- <th
87
- key={String(col.key)}
88
- scope="col"
89
- aria-sort={canSort ? (dir === 'none' ? 'none' : dir === 'asc' ? 'ascending' : 'descending') : undefined}
90
- className={tkx('px-4 py-3 text-left font-semibold whitespace-nowrap', canSort ? 'cursor-pointer select-none focus-visible:focus-ring' : '')}
91
- style={{ color: theme.textMuted, borderBottom: `2px solid ${theme.border}`, width: col.width }}
92
- onClick={() => canSort && handleSort(col.key)}
93
- onKeyDown={(e) => { if (canSort && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); handleSort(col.key); } }}
94
- tabIndex={canSort ? 0 : undefined}
95
- >
96
- <span className={tkx('inline-flex items-center gap-1.5')}>
97
- {sanitizeString(col.header)}
98
- {canSort && <SortIcon dir={dir} />}
99
- </span>
100
- </th>
101
- );
102
- })}
103
- </tr>
104
- </thead>
105
-
106
- <tbody>
107
- {isLoading
108
- ? Array.from({ length: 5 }, (_, i) => (
232
+ {/* ── Header ─────────────────────────────────────────────── */}
233
+ <thead
234
+ style={
235
+ stickyHeader
236
+ ? { position: 'sticky', top: 0, backgroundColor: theme.surfaceAlt, zIndex: 1 }
237
+ : { backgroundColor: theme.surfaceAlt }
238
+ }
239
+ >
240
+ <tr>
241
+ {columns.map((col) => {
242
+ const canSort = sortable && col.sortable !== false;
243
+ const dir = getDir(col.key);
244
+ return (
245
+ <th
246
+ key={String(col.key)}
247
+ scope="col"
248
+ aria-sort={
249
+ canSort
250
+ ? dir === 'none'
251
+ ? 'none'
252
+ : dir === 'asc'
253
+ ? 'ascending'
254
+ : 'descending'
255
+ : undefined
256
+ }
257
+ className={tkx(
258
+ 'text-left font-semibold whitespace-nowrap',
259
+ canSort ? 'cursor-pointer select-none' : '',
260
+ )}
261
+ style={{
262
+ padding: `${py} ${px}`,
263
+ color: theme.textMuted,
264
+ borderBottom: `2px solid ${theme.border}`,
265
+ borderRight: borderStyle,
266
+ width: col.width,
267
+ }}
268
+ onClick={() => canSort && handleSort(col.key)}
269
+ onKeyDown={(e) => {
270
+ if (canSort && (e.key === 'Enter' || e.key === ' ')) {
271
+ e.preventDefault();
272
+ handleSort(col.key);
273
+ }
274
+ }}
275
+ tabIndex={canSort ? 0 : undefined}
276
+ >
277
+ <span className={tkx('inline-flex items-center gap-1.5')}>
278
+ {sanitizeString(col.header)}
279
+ {canSort && <SortIcon dir={dir} />}
280
+ </span>
281
+ </th>
282
+ );
283
+ })}
284
+ </tr>
285
+ </thead>
286
+
287
+ {/* ── Body ──────────────────────────────────────────────── */}
288
+ <tbody>
289
+ {isLoading ? (
290
+ Array.from({ length: 5 }, (_, i) => (
109
291
  <tr key={i}>
110
292
  {columns.map((col) => (
111
- <td key={String(col.key)} className={tkx('px-4 py-3')}>
293
+ <td key={String(col.key)} style={{ padding: `${py} ${px}` }}>
112
294
  <TkxSkeleton variant="text" height="16px" />
113
295
  </td>
114
296
  ))}
115
297
  </tr>
116
298
  ))
117
- : sorted.length === 0
118
- ? (
119
- <tr>
120
- <td colSpan={columns.length} className={tkx('px-4 py-8 text-center')} style={{ color: theme.textMuted }}>
121
- {emptyState ?? 'No data available'}
122
- </td>
123
- </tr>
124
- )
125
- : sorted.map((row, ri) => (
126
- <tr
127
- key={ri}
128
- style={{
129
- backgroundColor: striped && ri % 2 === 1 ? theme.surfaceAlt : 'transparent',
130
- borderBottom: `1px solid ${theme.border}`,
131
- }}
299
+ ) : sorted.length === 0 ? (
300
+ <tr>
301
+ <td
302
+ colSpan={columns.length}
303
+ className={tkx('text-center')}
304
+ style={{ padding: '32px 16px', color: theme.textMuted }}
132
305
  >
133
- {columns.map((col) => {
134
- const raw = row[col.key];
135
- const cell = col.render ? col.render(raw, row) : (typeof raw === 'string' ? sanitizeString(raw) : String(raw ?? ''));
136
- return (
137
- <td key={String(col.key)} className={tkx('px-4 py-3')} style={{ color: theme.text }}>
138
- {cell}
139
- </td>
140
- );
141
- })}
142
- </tr>
143
- ))}
144
- </tbody>
145
- </table>
306
+ {emptyState ?? 'No data available'}
307
+ </td>
308
+ </tr>
309
+ ) : (
310
+ <>
311
+ {/* Top spacer for virtual scroll */}
312
+ {isVirtual && startIndex > 0 && (
313
+ <tr aria-hidden="true" style={{ height: startIndex * rowHeight }}>
314
+ <td colSpan={columns.length} style={{ padding: 0, border: 'none' }} />
315
+ </tr>
316
+ )}
317
+
318
+ {visibleRows.map((row, i) => {
319
+ const rowIndex = startIndex + i;
320
+ const isSelected = selectedSet.has(rowIndex);
321
+ return (
322
+ <tr
323
+ key={rowIndex}
324
+ aria-rowindex={rowIndex + 1}
325
+ aria-selected={isSelected || undefined}
326
+ onClick={onRowClick ? () => onRowClick(row, rowIndex) : undefined}
327
+ onKeyDown={
328
+ onRowClick
329
+ ? (e) => {
330
+ if (e.key === 'Enter' || e.key === ' ') {
331
+ e.preventDefault();
332
+ onRowClick(row, rowIndex);
333
+ }
334
+ }
335
+ : undefined
336
+ }
337
+ tabIndex={onRowClick ? 0 : undefined}
338
+ className={tkx(onRowClick ? 'cursor-pointer' : '')}
339
+ style={{
340
+ backgroundColor: isSelected
341
+ ? `${theme.primary}22`
342
+ : striped && rowIndex % 2 === 1
343
+ ? theme.surfaceAlt
344
+ : 'transparent',
345
+ borderBottom: `1px solid ${theme.border}`,
346
+ ...(isVirtual ? { height: rowHeight, boxSizing: 'border-box' as const } : {}),
347
+ }}
348
+ >
349
+ {columns.map((col) => {
350
+ const raw = row[col.key];
351
+ const cell = col.render
352
+ ? col.render(raw, row)
353
+ : typeof raw === 'string'
354
+ ? sanitizeString(raw)
355
+ : String(raw ?? '');
356
+ return (
357
+ <td
358
+ key={String(col.key)}
359
+ style={{
360
+ padding: `${py} ${px}`,
361
+ color: theme.text,
362
+ borderRight: borderStyle,
363
+ }}
364
+ >
365
+ {cell}
366
+ </td>
367
+ );
368
+ })}
369
+ </tr>
370
+ );
371
+ })}
372
+
373
+ {/* Bottom spacer for virtual scroll */}
374
+ {isVirtual && endIndex < sorted.length && (
375
+ <tr
376
+ aria-hidden="true"
377
+ style={{ height: (sorted.length - endIndex) * rowHeight }}
378
+ >
379
+ <td colSpan={columns.length} style={{ padding: 0, border: 'none' }} />
380
+ </tr>
381
+ )}
382
+ </>
383
+ )}
384
+ </tbody>
385
+ </table>
386
+
387
+ {/* ── Infinite scroll sentinel ─────────────────────────────── */}
388
+ {(hasMore || loadingMore) && (
389
+ <div ref={sentinelRef} style={{ height: 1, width: '100%' }}>
390
+ {loadingMore && (
391
+ <table style={{ width: '100%', borderCollapse: 'collapse' }}>
392
+ <tbody>
393
+ {Array.from({ length: 3 }, (_, i) => (
394
+ <tr key={i}>
395
+ {columns.map((col) => (
396
+ <td key={String(col.key)} style={{ padding: '10px 16px' }}>
397
+ <TkxSkeleton variant="text" height="16px" />
398
+ </td>
399
+ ))}
400
+ </tr>
401
+ ))}
402
+ </tbody>
403
+ </table>
404
+ )}
405
+ </div>
406
+ )}
407
+ {!hasMore && onLoadMore && (
408
+ <div
409
+ style={{
410
+ padding: '16px',
411
+ textAlign: 'center',
412
+ fontSize: 13,
413
+ color: theme.textMuted,
414
+ }}
415
+ >
416
+ ✓ All rows loaded
417
+ </div>
418
+ )}
419
+ </div>
146
420
  </div>
147
421
  );
148
422
  }