tekivex-ui 2.2.0 → 2.3.0

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 (47) hide show
  1. package/dist/index.cjs +75 -14
  2. package/dist/index.d.ts +32 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +10957 -4936
  5. package/dist/src/components/TkxAffix.d.ts +10 -0
  6. package/dist/src/components/TkxAffix.d.ts.map +1 -0
  7. package/dist/src/components/TkxAnchor.d.ts +14 -0
  8. package/dist/src/components/TkxAnchor.d.ts.map +1 -0
  9. package/dist/src/components/TkxCascader.d.ts +16 -0
  10. package/dist/src/components/TkxCascader.d.ts.map +1 -0
  11. package/dist/src/components/TkxDataGrid.d.ts +5 -1
  12. package/dist/src/components/TkxDataGrid.d.ts.map +1 -1
  13. package/dist/src/components/TkxList.d.ts +24 -0
  14. package/dist/src/components/TkxList.d.ts.map +1 -0
  15. package/dist/src/components/TkxMentions.d.ts +15 -0
  16. package/dist/src/components/TkxMentions.d.ts.map +1 -0
  17. package/dist/src/components/TkxQRCode.d.ts +11 -0
  18. package/dist/src/components/TkxQRCode.d.ts.map +1 -0
  19. package/dist/src/components/TkxResult.d.ts +11 -0
  20. package/dist/src/components/TkxResult.d.ts.map +1 -0
  21. package/dist/src/components/TkxSegmented.d.ts +16 -0
  22. package/dist/src/components/TkxSegmented.d.ts.map +1 -0
  23. package/dist/src/components/TkxSelect.d.ts +5 -1
  24. package/dist/src/components/TkxSelect.d.ts.map +1 -1
  25. package/dist/src/components/TkxTour.d.ts +15 -0
  26. package/dist/src/components/TkxTour.d.ts.map +1 -0
  27. package/dist/src/components/TkxWatermark.d.ts +12 -0
  28. package/dist/src/components/TkxWatermark.d.ts.map +1 -0
  29. package/dist/src/components/index.d.ts +10 -0
  30. package/dist/src/components/index.d.ts.map +1 -1
  31. package/dist/src/engine/tkx.d.ts +38 -0
  32. package/dist/src/engine/tkx.d.ts.map +1 -1
  33. package/package.json +1 -1
  34. package/src/components/TkxAffix.tsx +138 -0
  35. package/src/components/TkxAnchor.tsx +193 -0
  36. package/src/components/TkxCascader.tsx +276 -0
  37. package/src/components/TkxDataGrid.tsx +65 -2
  38. package/src/components/TkxList.tsx +242 -0
  39. package/src/components/TkxMentions.tsx +210 -0
  40. package/src/components/TkxQRCode.tsx +174 -0
  41. package/src/components/TkxResult.tsx +128 -0
  42. package/src/components/TkxSegmented.tsx +169 -0
  43. package/src/components/TkxSelect.tsx +209 -6
  44. package/src/components/TkxTour.tsx +239 -0
  45. package/src/components/TkxWatermark.tsx +143 -0
  46. package/src/components/index.ts +10 -0
  47. package/src/engine/tkx.ts +62 -0
@@ -0,0 +1,193 @@
1
+ import { useState, useEffect, useCallback, useRef } from 'react';
2
+ import { useTheme } from '../themes';
3
+ import { sanitizeString } from '../engine/security';
4
+ import { useReducedMotion } from '../hooks';
5
+ import { tkx } from '../engine/tkx';
6
+
7
+ // ── Types ────────────────────────────────────────────────────────────────────
8
+
9
+ export interface AnchorLink {
10
+ key: string;
11
+ href: string;
12
+ title: string;
13
+ children?: AnchorLink[];
14
+ }
15
+
16
+ export interface TkxAnchorProps {
17
+ items: AnchorLink[];
18
+ offsetTop?: number;
19
+ getCurrentAnchor?: (activeLink: string) => string;
20
+ onChange?: (currentLink: string) => void;
21
+ }
22
+
23
+ // ── Helpers ──────────────────────────────────────────────────────────────────
24
+
25
+ function flattenLinks(items: AnchorLink[]): AnchorLink[] {
26
+ const result: AnchorLink[] = [];
27
+ for (const item of items) {
28
+ result.push(item);
29
+ if (item.children) result.push(...flattenLinks(item.children));
30
+ }
31
+ return result;
32
+ }
33
+
34
+ function getAnchorId(href: string): string {
35
+ return href.startsWith('#') ? href.slice(1) : href;
36
+ }
37
+
38
+ // ── Link Item ────────────────────────────────────────────────────────────────
39
+
40
+ function AnchorItem({
41
+ item,
42
+ activeKey,
43
+ depth,
44
+ onClick,
45
+ theme,
46
+ reducedMotion,
47
+ }: {
48
+ item: AnchorLink;
49
+ activeKey: string;
50
+ depth: number;
51
+ onClick: (href: string) => void;
52
+ theme: ReturnType<typeof useTheme>;
53
+ reducedMotion: boolean;
54
+ }) {
55
+ const isActive = item.key === activeKey;
56
+ const safeTitle = sanitizeString(item.title);
57
+
58
+ return (
59
+ <li role="none" style={{ listStyle: 'none' }}>
60
+ <a
61
+ role="treeitem"
62
+ aria-current={isActive ? 'location' : undefined}
63
+ href={item.href}
64
+ onClick={(e) => {
65
+ e.preventDefault();
66
+ onClick(item.href);
67
+ }}
68
+ className={tkx('block text-sm py-1 no-underline')}
69
+ style={{
70
+ paddingLeft: 12 + depth * 16,
71
+ color: isActive ? theme.primary : theme.textMuted,
72
+ fontWeight: isActive ? 600 : 400,
73
+ borderLeft: `2px solid ${isActive ? theme.primary : 'transparent'}`,
74
+ transition: reducedMotion ? 'none' : 'color 0.15s ease, border-color 0.15s ease',
75
+ }}
76
+ >
77
+ {safeTitle}
78
+ </a>
79
+ {item.children && item.children.length > 0 && (
80
+ <ul role="group" style={{ margin: 0, padding: 0 }}>
81
+ {item.children.map((child) => (
82
+ <AnchorItem
83
+ key={child.key}
84
+ item={child}
85
+ activeKey={activeKey}
86
+ depth={depth + 1}
87
+ onClick={onClick}
88
+ theme={theme}
89
+ reducedMotion={reducedMotion}
90
+ />
91
+ ))}
92
+ </ul>
93
+ )}
94
+ </li>
95
+ );
96
+ }
97
+
98
+ // ── Component ────────────────────────────────────────────────────────────────
99
+
100
+ export function TkxAnchor({
101
+ items,
102
+ offsetTop = 0,
103
+ getCurrentAnchor,
104
+ onChange,
105
+ }: TkxAnchorProps) {
106
+ const theme = useTheme();
107
+ const reducedMotion = useReducedMotion();
108
+ const [activeKey, setActiveKey] = useState(items[0]?.key ?? '');
109
+ const isScrolling = useRef(false);
110
+
111
+ const flat = flattenLinks(items);
112
+
113
+ const updateActive = useCallback(() => {
114
+ if (isScrolling.current) return;
115
+
116
+ let current = flat[0]?.key ?? '';
117
+ for (const link of flat) {
118
+ const id = getAnchorId(link.href);
119
+ const el = document.getElementById(id);
120
+ if (el) {
121
+ const top = el.getBoundingClientRect().top;
122
+ if (top <= offsetTop + 10) {
123
+ current = link.key;
124
+ }
125
+ }
126
+ }
127
+
128
+ if (getCurrentAnchor) {
129
+ current = getCurrentAnchor(current);
130
+ }
131
+
132
+ if (current !== activeKey) {
133
+ setActiveKey(current);
134
+ onChange?.(current);
135
+ }
136
+ }, [flat, offsetTop, getCurrentAnchor, activeKey, onChange]);
137
+
138
+ useEffect(() => {
139
+ window.addEventListener('scroll', updateActive, { passive: true });
140
+ updateActive();
141
+ return () => window.removeEventListener('scroll', updateActive);
142
+ }, [updateActive]);
143
+
144
+ const handleClick = useCallback(
145
+ (href: string) => {
146
+ const id = getAnchorId(href);
147
+ const el = document.getElementById(id);
148
+ if (!el) return;
149
+
150
+ isScrolling.current = true;
151
+ const top = el.getBoundingClientRect().top + window.scrollY - offsetTop;
152
+ window.scrollTo({ top, behavior: reducedMotion ? 'auto' : 'smooth' });
153
+
154
+ // Find the link by href and set active
155
+ const link = flat.find((l) => l.href === href);
156
+ if (link) {
157
+ setActiveKey(link.key);
158
+ onChange?.(link.key);
159
+ }
160
+
161
+ setTimeout(() => {
162
+ isScrolling.current = false;
163
+ }, 800);
164
+ },
165
+ [flat, offsetTop, reducedMotion, onChange],
166
+ );
167
+
168
+ return (
169
+ <nav
170
+ role="tree"
171
+ aria-label="Anchor navigation"
172
+ className={tkx('font-sans')}
173
+ style={{
174
+ borderLeft: `1px solid ${theme.border}`,
175
+ padding: '4px 0',
176
+ }}
177
+ >
178
+ <ul role="group" style={{ margin: 0, padding: 0 }}>
179
+ {items.map((item) => (
180
+ <AnchorItem
181
+ key={item.key}
182
+ item={item}
183
+ activeKey={activeKey}
184
+ depth={0}
185
+ onClick={handleClick}
186
+ theme={theme}
187
+ reducedMotion={reducedMotion}
188
+ />
189
+ ))}
190
+ </ul>
191
+ </nav>
192
+ );
193
+ }
@@ -0,0 +1,276 @@
1
+ import { useState, useRef, useEffect, useCallback } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { useTheme } from '../themes';
4
+ import { sanitizeString } from '../engine/security';
5
+ import { useReducedMotion } from '../hooks';
6
+ import { tkx } from '../engine/tkx';
7
+
8
+ // ── Types ────────────────────────────────────────────────────────────────────
9
+
10
+ export interface CascaderOption {
11
+ value: string;
12
+ label: string;
13
+ children?: CascaderOption[];
14
+ disabled?: boolean;
15
+ }
16
+
17
+ export interface TkxCascaderProps {
18
+ options: CascaderOption[];
19
+ value?: string[];
20
+ onChange?: (value: string[], selectedOptions: CascaderOption[]) => void;
21
+ placeholder?: string;
22
+ label?: string;
23
+ multiple?: boolean;
24
+ }
25
+
26
+ // ── Helpers ──────────────────────────────────────────────────────────────────
27
+
28
+ function getColumns(
29
+ options: CascaderOption[],
30
+ selected: string[],
31
+ ): CascaderOption[][] {
32
+ const columns: CascaderOption[][] = [options];
33
+ let current = options;
34
+ for (const val of selected) {
35
+ const found = current.find((o) => o.value === val);
36
+ if (found?.children?.length) {
37
+ columns.push(found.children);
38
+ current = found.children;
39
+ } else {
40
+ break;
41
+ }
42
+ }
43
+ return columns;
44
+ }
45
+
46
+ function getSelectedOptions(
47
+ options: CascaderOption[],
48
+ values: string[],
49
+ ): CascaderOption[] {
50
+ const result: CascaderOption[] = [];
51
+ let current = options;
52
+ for (const val of values) {
53
+ const found = current.find((o) => o.value === val);
54
+ if (found) {
55
+ result.push(found);
56
+ current = found.children ?? [];
57
+ } else break;
58
+ }
59
+ return result;
60
+ }
61
+
62
+ function getLabelPath(options: CascaderOption[], values: string[]): string {
63
+ return getSelectedOptions(options, values)
64
+ .map((o) => sanitizeString(o.label))
65
+ .join(' / ');
66
+ }
67
+
68
+ // ── Component ────────────────────────────────────────────────────────────────
69
+
70
+ export function TkxCascader({
71
+ options,
72
+ value = [],
73
+ onChange,
74
+ placeholder = 'Select...',
75
+ label,
76
+ multiple = false,
77
+ }: TkxCascaderProps) {
78
+ const theme = useTheme();
79
+ const reducedMotion = useReducedMotion();
80
+ const triggerRef = useRef<HTMLButtonElement>(null);
81
+ const dropdownRef = useRef<HTMLDivElement>(null);
82
+ const [open, setOpen] = useState(false);
83
+ const [hoverPath, setHoverPath] = useState<string[]>(value);
84
+ const [dropdownPos, setDropdownPos] = useState({ top: 0, left: 0, width: 0 });
85
+
86
+ const safeLabel = label ? sanitizeString(label) : undefined;
87
+ const safePlaceholder = sanitizeString(placeholder);
88
+ const displayText = value.length > 0 ? getLabelPath(options, value) : '';
89
+
90
+ // Position dropdown
91
+ useEffect(() => {
92
+ if (!open || !triggerRef.current) return;
93
+ const rect = triggerRef.current.getBoundingClientRect();
94
+ setDropdownPos({
95
+ top: rect.bottom + 4 + window.scrollY,
96
+ left: rect.left + window.scrollX,
97
+ width: Math.max(rect.width, 200),
98
+ });
99
+ }, [open]);
100
+
101
+ // Close on outside click
102
+ useEffect(() => {
103
+ if (!open) return;
104
+ const handler = (e: MouseEvent) => {
105
+ if (
106
+ !triggerRef.current?.contains(e.target as Node) &&
107
+ !dropdownRef.current?.contains(e.target as Node)
108
+ ) {
109
+ setOpen(false);
110
+ }
111
+ };
112
+ document.addEventListener('mousedown', handler);
113
+ return () => document.removeEventListener('mousedown', handler);
114
+ }, [open]);
115
+
116
+ const handleSelect = useCallback(
117
+ (colIdx: number, opt: CascaderOption) => {
118
+ if (opt.disabled) return;
119
+ const newPath = [...hoverPath.slice(0, colIdx), opt.value];
120
+ setHoverPath(newPath);
121
+
122
+ // If leaf node (no children), commit selection
123
+ if (!opt.children?.length) {
124
+ const selected = getSelectedOptions(options, newPath);
125
+ onChange?.(newPath, selected);
126
+ if (!multiple) setOpen(false);
127
+ }
128
+ },
129
+ [hoverPath, options, onChange, multiple],
130
+ );
131
+
132
+ const handleKeyDown = useCallback(
133
+ (e: React.KeyboardEvent) => {
134
+ if (e.key === 'Escape') {
135
+ setOpen(false);
136
+ triggerRef.current?.focus();
137
+ } else if (e.key === 'Enter' || e.key === ' ') {
138
+ if (!open) {
139
+ e.preventDefault();
140
+ setOpen(true);
141
+ }
142
+ }
143
+ },
144
+ [open],
145
+ );
146
+
147
+ const columns = getColumns(options, hoverPath);
148
+
149
+ const dropdown = open
150
+ ? createPortal(
151
+ <div
152
+ ref={dropdownRef}
153
+ role="tree"
154
+ aria-label={safeLabel ?? 'Cascader options'}
155
+ className={tkx('flex rounded-lg border overflow-hidden')}
156
+ style={{
157
+ position: 'absolute',
158
+ zIndex: 9999,
159
+ top: dropdownPos.top,
160
+ left: dropdownPos.left,
161
+ backgroundColor: theme.surface,
162
+ borderColor: theme.border,
163
+ boxShadow: `0 4px 16px ${theme.bg}80`,
164
+ animation: reducedMotion ? 'none' : 'tkxFadeIn 0.15s ease',
165
+ }}
166
+ >
167
+ {columns.map((col, colIdx) => (
168
+ <ul
169
+ key={colIdx}
170
+ role="group"
171
+ className={tkx('m-0 p-0 overflow-auto')}
172
+ style={{
173
+ listStyle: 'none',
174
+ minWidth: 160,
175
+ maxHeight: 260,
176
+ borderRight: colIdx < columns.length - 1 ? `1px solid ${theme.border}` : 'none',
177
+ }}
178
+ >
179
+ {col.map((opt) => {
180
+ const isSelected = hoverPath[colIdx] === opt.value;
181
+ const safeOptLabel = sanitizeString(opt.label);
182
+ return (
183
+ <li
184
+ key={opt.value}
185
+ role="treeitem"
186
+ aria-selected={isSelected}
187
+ aria-disabled={opt.disabled || undefined}
188
+ aria-expanded={opt.children?.length ? isSelected : undefined}
189
+ className={tkx('flex items-center justify-between px-3 py-2 cursor-pointer text-sm')}
190
+ style={{
191
+ backgroundColor: isSelected ? theme.surfaceAlt : 'transparent',
192
+ color: opt.disabled ? theme.textMuted : theme.text,
193
+ opacity: opt.disabled ? 0.5 : 1,
194
+ cursor: opt.disabled ? 'not-allowed' : 'pointer',
195
+ }}
196
+ onClick={() => handleSelect(colIdx, opt)}
197
+ onMouseEnter={() => {
198
+ if (!opt.disabled) {
199
+ setHoverPath((prev) => [...prev.slice(0, colIdx), opt.value]);
200
+ }
201
+ }}
202
+ >
203
+ <span>{safeOptLabel}</span>
204
+ {opt.children && opt.children.length > 0 && (
205
+ <svg
206
+ width="12"
207
+ height="12"
208
+ viewBox="0 0 24 24"
209
+ fill="currentColor"
210
+ aria-hidden="true"
211
+ style={{ color: theme.textMuted, flexShrink: 0 }}
212
+ >
213
+ <path d="M10 6l6 6-6 6V6z" />
214
+ </svg>
215
+ )}
216
+ </li>
217
+ );
218
+ })}
219
+ </ul>
220
+ ))}
221
+ </div>,
222
+ document.body,
223
+ )
224
+ : null;
225
+
226
+ return (
227
+ <div className={tkx('relative font-sans')} onKeyDown={handleKeyDown}>
228
+ {safeLabel && (
229
+ <label
230
+ className={tkx('block text-sm font-medium mb-1')}
231
+ style={{ color: theme.text }}
232
+ >
233
+ {safeLabel}
234
+ </label>
235
+ )}
236
+
237
+ <button
238
+ ref={triggerRef}
239
+ type="button"
240
+ role="combobox"
241
+ aria-expanded={open}
242
+ aria-haspopup="tree"
243
+ aria-label={safeLabel ?? 'Cascader'}
244
+ className={tkx('w-full flex items-center justify-between rounded-lg border px-3 py-2 text-sm cursor-pointer')}
245
+ style={{
246
+ backgroundColor: theme.surface,
247
+ borderColor: open ? theme.primary : theme.border,
248
+ color: displayText ? theme.text : theme.textMuted,
249
+ outline: 'none',
250
+ minHeight: 38,
251
+ textAlign: 'left',
252
+ }}
253
+ onClick={() => setOpen((v) => !v)}
254
+ >
255
+ <span className={tkx('truncate')}>{displayText || safePlaceholder}</span>
256
+ <svg
257
+ width="16"
258
+ height="16"
259
+ viewBox="0 0 24 24"
260
+ fill="currentColor"
261
+ aria-hidden="true"
262
+ style={{
263
+ color: theme.textMuted,
264
+ transform: open ? 'rotate(180deg)' : 'rotate(0deg)',
265
+ transition: reducedMotion ? 'none' : 'transform 0.2s ease',
266
+ flexShrink: 0,
267
+ }}
268
+ >
269
+ <path d="M7 10l5 5 5-5H7z" />
270
+ </svg>
271
+ </button>
272
+
273
+ {dropdown}
274
+ </div>
275
+ );
276
+ }
@@ -4,6 +4,7 @@ import {
4
4
  useRef,
5
5
  useMemo,
6
6
  useId,
7
+ useEffect,
7
8
  type ReactNode,
8
9
  type CSSProperties,
9
10
  type MouseEvent as ReactMouseEvent,
@@ -46,6 +47,10 @@ export interface TkxDataGridProps<T = any> {
46
47
  compact?: boolean;
47
48
  maxHeight?: number | string;
48
49
  onRowClick?: (row: T) => void;
50
+ /** Enable virtual scrolling. Defaults to auto (enabled when data has 50+ rows and maxHeight is set). */
51
+ virtualScroll?: boolean;
52
+ /** Row height in pixels for virtual scrolling calculations. Default: 40 */
53
+ rowHeight?: number;
49
54
  }
50
55
 
51
56
  // ── Sort icon ─────────────────────────────────────────────────────────────────
@@ -156,11 +161,51 @@ export function TkxDataGrid<T = any>({
156
161
  compact = false,
157
162
  maxHeight,
158
163
  onRowClick,
164
+ virtualScroll,
165
+ rowHeight = 40,
159
166
  }: TkxDataGridProps<T>) {
160
167
  const theme = useTheme();
161
168
  const reduced = useReducedMotion();
162
169
  const gridId = useId();
163
170
 
171
+ // ── Virtual scroll ────────────────────────────────────────────────────
172
+
173
+ const OVERSCAN = 10;
174
+ const isVirtual =
175
+ virtualScroll !== undefined
176
+ ? virtualScroll
177
+ : maxHeight !== undefined && data.length >= 50;
178
+
179
+ const scrollContainerRef = useRef<HTMLDivElement>(null);
180
+ const [scrollTop, setScrollTop] = useState(0);
181
+ const [containerHeight, setContainerHeight] = useState(0);
182
+
183
+ const handleScroll = useCallback(() => {
184
+ if (!scrollContainerRef.current) return;
185
+ setScrollTop(scrollContainerRef.current.scrollTop);
186
+ }, []);
187
+
188
+ useEffect(() => {
189
+ if (!isVirtual || !scrollContainerRef.current) return;
190
+ const el = scrollContainerRef.current;
191
+ setContainerHeight(el.clientHeight);
192
+ const ro = new ResizeObserver(() => {
193
+ setContainerHeight(el.clientHeight);
194
+ });
195
+ ro.observe(el);
196
+ return () => ro.disconnect();
197
+ }, [isVirtual]);
198
+
199
+ const totalHeight = data.length * rowHeight;
200
+ const startIndex = isVirtual
201
+ ? Math.max(0, Math.floor(scrollTop / rowHeight) - OVERSCAN)
202
+ : 0;
203
+ const endIndex = isVirtual
204
+ ? Math.min(data.length, Math.ceil((scrollTop + containerHeight) / rowHeight) + OVERSCAN)
205
+ : data.length;
206
+ const visibleData = isVirtual ? data.slice(startIndex, endIndex) : data;
207
+ const offsetY = startIndex * rowHeight;
208
+
164
209
  // ── Sort state ──────────────────────────────────────────────────────────
165
210
 
166
211
  const [sortKey, setSortKey] = useState<string | null>(null);
@@ -281,6 +326,8 @@ export function TkxDataGrid<T = any>({
281
326
  }}
282
327
  >
283
328
  <div
329
+ ref={scrollContainerRef}
330
+ onScroll={isVirtual ? handleScroll : undefined}
284
331
  style={{
285
332
  maxHeight: maxHeight ?? 'none',
286
333
  overflowX: 'auto',
@@ -427,7 +474,15 @@ export function TkxDataGrid<T = any>({
427
474
  </td>
428
475
  </tr>
429
476
  ) : (
430
- data.map((row, rowIndex) => {
477
+ <>
478
+ {/* Top spacer for virtual scrolling */}
479
+ {isVirtual && offsetY > 0 && (
480
+ <tr aria-hidden="true">
481
+ <td colSpan={totalCols} style={{ height: offsetY, padding: 0, border: 'none' }} />
482
+ </tr>
483
+ )}
484
+ {visibleData.map((row, i) => {
485
+ const rowIndex = startIndex + i;
431
486
  const id = getRowId(row, rowKey);
432
487
  const isSelected = selectedSet.has(id);
433
488
  const isStriped = striped && rowIndex % 2 === 1;
@@ -448,6 +503,7 @@ export function TkxDataGrid<T = any>({
448
503
  transition: reduced
449
504
  ? 'none'
450
505
  : 'background-color 120ms ease',
506
+ ...(isVirtual ? { height: rowHeight, boxSizing: 'border-box' } : {}),
451
507
  }}
452
508
  onMouseEnter={
453
509
  onRowClick
@@ -529,7 +585,14 @@ export function TkxDataGrid<T = any>({
529
585
  })}
530
586
  </tr>
531
587
  );
532
- })
588
+ })}
589
+ {/* Bottom spacer for virtual scrolling */}
590
+ {isVirtual && endIndex < data.length && (
591
+ <tr aria-hidden="true">
592
+ <td colSpan={totalCols} style={{ height: (data.length - endIndex) * rowHeight, padding: 0, border: 'none' }} />
593
+ </tr>
594
+ )}
595
+ </>
533
596
  )}
534
597
  </tbody>
535
598
  )}