tekivex-ui 2.0.2 → 2.2.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 (102) hide show
  1. package/dist/index.cjs +12 -12
  2. package/dist/index.d.ts +7 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2839 -2538
  5. package/dist/src/components/TkxAppBar.d.ts +12 -0
  6. package/dist/src/components/TkxAppBar.d.ts.map +1 -0
  7. package/dist/src/components/TkxAutocomplete.d.ts +25 -0
  8. package/dist/src/components/TkxAutocomplete.d.ts.map +1 -0
  9. package/dist/src/components/TkxBottomNav.d.ts +15 -0
  10. package/dist/src/components/TkxBottomNav.d.ts.map +1 -0
  11. package/dist/src/components/TkxBreadcrumb.d.ts +17 -0
  12. package/dist/src/components/TkxBreadcrumb.d.ts.map +1 -0
  13. package/dist/src/components/TkxCard.d.ts.map +1 -1
  14. package/dist/src/components/TkxConfigProvider.d.ts +100 -0
  15. package/dist/src/components/TkxConfigProvider.d.ts.map +1 -0
  16. package/dist/src/components/TkxDataGrid.d.ts +33 -0
  17. package/dist/src/components/TkxDataGrid.d.ts.map +1 -0
  18. package/dist/src/components/TkxEmpty.d.ts +870 -0
  19. package/dist/src/components/TkxEmpty.d.ts.map +1 -0
  20. package/dist/src/components/TkxForm.d.ts +54 -0
  21. package/dist/src/components/TkxForm.d.ts.map +1 -0
  22. package/dist/src/components/TkxLayout.d.ts +87 -0
  23. package/dist/src/components/TkxLayout.d.ts.map +1 -0
  24. package/dist/src/components/TkxMasonry.d.ts +12 -0
  25. package/dist/src/components/TkxMasonry.d.ts.map +1 -0
  26. package/dist/src/components/TkxModal.d.ts.map +1 -1
  27. package/dist/src/components/TkxPopover.d.ts +15 -0
  28. package/dist/src/components/TkxPopover.d.ts.map +1 -0
  29. package/dist/src/components/TkxRichTextDisplay.d.ts +19 -0
  30. package/dist/src/components/TkxRichTextDisplay.d.ts.map +1 -0
  31. package/dist/src/components/TkxSelect.d.ts.map +1 -1
  32. package/dist/src/components/TkxSlider.d.ts.map +1 -1
  33. package/dist/src/components/TkxSnackbar.d.ts +16 -0
  34. package/dist/src/components/TkxSnackbar.d.ts.map +1 -0
  35. package/dist/src/components/TkxSpeedDial.d.ts +18 -0
  36. package/dist/src/components/TkxSpeedDial.d.ts.map +1 -0
  37. package/dist/src/components/TkxSpin.d.ts +15 -0
  38. package/dist/src/components/TkxSpin.d.ts.map +1 -0
  39. package/dist/src/components/TkxStatistic.d.ts +1746 -0
  40. package/dist/src/components/TkxStatistic.d.ts.map +1 -0
  41. package/dist/src/components/TkxToolbar.d.ts +22 -0
  42. package/dist/src/components/TkxToolbar.d.ts.map +1 -0
  43. package/dist/src/components/TkxTooltip.d.ts.map +1 -1
  44. package/dist/src/components/TkxTransferList.d.ts +20 -0
  45. package/dist/src/components/TkxTransferList.d.ts.map +1 -0
  46. package/dist/src/components/TkxTreeView.d.ts +23 -0
  47. package/dist/src/components/TkxTreeView.d.ts.map +1 -0
  48. package/dist/src/components/TkxTypography.d.ts +2614 -0
  49. package/dist/src/components/TkxTypography.d.ts.map +1 -0
  50. package/dist/src/components/index.d.ts +20 -0
  51. package/dist/src/components/index.d.ts.map +1 -1
  52. package/dist/src/i18n/I18nProvider.d.ts +10 -0
  53. package/dist/src/i18n/I18nProvider.d.ts.map +1 -0
  54. package/dist/src/i18n/index.d.ts +50 -0
  55. package/dist/src/i18n/index.d.ts.map +1 -0
  56. package/dist/src/themes/index.d.ts +108 -0
  57. package/dist/src/themes/index.d.ts.map +1 -1
  58. package/package.json +2 -2
  59. package/src/components/TkxAppBar.tsx +260 -0
  60. package/src/components/TkxAutocomplete.tsx +453 -0
  61. package/src/components/TkxBottomNav.tsx +239 -0
  62. package/src/components/TkxBreadcrumb.tsx +322 -0
  63. package/src/components/TkxCard.tsx +3 -1
  64. package/src/components/TkxConfigProvider.tsx +458 -0
  65. package/src/components/TkxDataGrid.tsx +540 -0
  66. package/src/components/TkxEmpty.tsx +219 -0
  67. package/src/components/TkxForm.tsx +607 -0
  68. package/src/components/TkxLayout.tsx +647 -0
  69. package/src/components/TkxMasonry.tsx +176 -0
  70. package/src/components/TkxModal.tsx +4 -3
  71. package/src/components/TkxPopover.tsx +295 -0
  72. package/src/components/TkxRichTextDisplay.tsx +394 -0
  73. package/src/components/TkxSelect.tsx +2 -0
  74. package/src/components/TkxSlider.tsx +1 -0
  75. package/src/components/TkxSnackbar.tsx +314 -0
  76. package/src/components/TkxSpeedDial.tsx +395 -0
  77. package/src/components/TkxSpin.tsx +261 -0
  78. package/src/components/TkxStatistic.tsx +261 -0
  79. package/src/components/TkxToolbar.tsx +260 -0
  80. package/src/components/TkxTooltip.tsx +3 -2
  81. package/src/components/TkxTransferList.tsx +487 -0
  82. package/src/components/TkxTreeView.tsx +421 -0
  83. package/src/components/TkxTypography.tsx +263 -0
  84. package/src/components/index.ts +20 -0
  85. package/src/hooks/index.ts +1 -1
  86. package/src/i18n/I18nProvider.tsx +36 -0
  87. package/src/i18n/index.ts +237 -0
  88. package/src/themes/index.ts +149 -0
  89. package/dist/tests/TkxButton.test.d.ts +0 -2
  90. package/dist/tests/TkxButton.test.d.ts.map +0 -1
  91. package/dist/tests/a11y/components.test.d.ts +0 -2
  92. package/dist/tests/a11y/components.test.d.ts.map +0 -1
  93. package/dist/tests/quantum.test.d.ts +0 -2
  94. package/dist/tests/quantum.test.d.ts.map +0 -1
  95. package/dist/tests/security.test.d.ts +0 -2
  96. package/dist/tests/security.test.d.ts.map +0 -1
  97. package/dist/tests/setup.d.ts +0 -2
  98. package/dist/tests/setup.d.ts.map +0 -1
  99. package/dist/tests/tkx.test.d.ts +0 -2
  100. package/dist/tests/tkx.test.d.ts.map +0 -1
  101. package/dist/tests/wcag.test.d.ts +0 -2
  102. package/dist/tests/wcag.test.d.ts.map +0 -1
@@ -0,0 +1,487 @@
1
+ import {
2
+ useState,
3
+ useMemo,
4
+ useCallback,
5
+ useId,
6
+ type CSSProperties,
7
+ type KeyboardEvent,
8
+ } from 'react';
9
+ import { useTheme } from '../themes';
10
+ import { sanitizeString } from '../engine/security';
11
+ import { useReducedMotion } from '../hooks';
12
+ import { tkx } from '../engine/tkx';
13
+
14
+ // ── Types ─────────────────────────────────────────────────────────────────────
15
+
16
+ export interface TransferItem {
17
+ value: string;
18
+ label: string;
19
+ disabled?: boolean;
20
+ }
21
+
22
+ export interface TkxTransferListProps {
23
+ sourceItems: TransferItem[];
24
+ targetItems: TransferItem[];
25
+ onTransfer: (source: TransferItem[], target: TransferItem[]) => void;
26
+ sourceTitle?: string;
27
+ targetTitle?: string;
28
+ searchable?: boolean;
29
+ height?: number;
30
+ className?: string;
31
+ style?: CSSProperties;
32
+ }
33
+
34
+ // ── Search icon ─────────────────────────────────────────────────────────────
35
+
36
+ function SearchIcon({ color }: { color: string }) {
37
+ return (
38
+ <svg
39
+ width="14"
40
+ height="14"
41
+ viewBox="0 0 14 14"
42
+ fill="none"
43
+ aria-hidden="true"
44
+ >
45
+ <circle cx="6" cy="6" r="4.5" stroke={color} strokeWidth="1.5" />
46
+ <path d="M9.5 9.5L13 13" stroke={color} strokeWidth="1.5" strokeLinecap="round" />
47
+ </svg>
48
+ );
49
+ }
50
+
51
+ // ── Checkbox ────────────────────────────────────────────────────────────────
52
+
53
+ function Checkbox({
54
+ checked,
55
+ primaryColor,
56
+ borderColor,
57
+ }: {
58
+ checked: boolean;
59
+ primaryColor: string;
60
+ borderColor: string;
61
+ }) {
62
+ return (
63
+ <span
64
+ aria-hidden="true"
65
+ className={tkx('inline-flex items-center justify-center flex-shrink-0')}
66
+ style={{
67
+ width: 16,
68
+ height: 16,
69
+ borderRadius: 3,
70
+ border: `1.5px solid ${checked ? primaryColor : borderColor}`,
71
+ backgroundColor: checked ? primaryColor : 'transparent',
72
+ transition: 'all 100ms ease',
73
+ }}
74
+ >
75
+ {checked && (
76
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
77
+ <path
78
+ d="M2 5l2.5 2.5L8 3"
79
+ stroke="#fff"
80
+ strokeWidth="1.5"
81
+ strokeLinecap="round"
82
+ strokeLinejoin="round"
83
+ />
84
+ </svg>
85
+ )}
86
+ </span>
87
+ );
88
+ }
89
+
90
+ // ── Arrow icons ─────────────────────────────────────────────────────────────
91
+
92
+ function ArrowRightIcon({ color }: { color: string }) {
93
+ return (
94
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
95
+ <path d="M3 8h10M9 4l4 4-4 4" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
96
+ </svg>
97
+ );
98
+ }
99
+
100
+ function ArrowLeftIcon({ color }: { color: string }) {
101
+ return (
102
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
103
+ <path d="M13 8H3M7 4l-4 4 4 4" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
104
+ </svg>
105
+ );
106
+ }
107
+
108
+ // ── Internal list panel ─────────────────────────────────────────────────────
109
+
110
+ interface ListPanelProps {
111
+ title: string;
112
+ items: TransferItem[];
113
+ selected: Set<string>;
114
+ onToggle: (value: string) => void;
115
+ onToggleAll: () => void;
116
+ search: string;
117
+ onSearchChange: (s: string) => void;
118
+ searchable: boolean;
119
+ height: number;
120
+ listId: string;
121
+ }
122
+
123
+ function ListPanel({
124
+ title,
125
+ items,
126
+ selected,
127
+ onToggle,
128
+ onToggleAll,
129
+ search,
130
+ onSearchChange,
131
+ searchable,
132
+ height,
133
+ listId,
134
+ }: ListPanelProps) {
135
+ const theme = useTheme();
136
+ const reducedMotion = useReducedMotion();
137
+
138
+ const safeTitle = sanitizeString(title);
139
+
140
+ const filtered = useMemo(() => {
141
+ if (!search) return items;
142
+ const q = search.toLowerCase();
143
+ return items.filter((item) => item.label.toLowerCase().includes(q));
144
+ }, [items, search]);
145
+
146
+ const enabledItems = filtered.filter((i) => !i.disabled);
147
+ const allSelected = enabledItems.length > 0 && enabledItems.every((i) => selected.has(i.value));
148
+ const someSelected = enabledItems.some((i) => selected.has(i.value));
149
+
150
+ const handleKeyDown = (e: KeyboardEvent<HTMLElement>, value: string) => {
151
+ if (e.key === 'Enter' || e.key === ' ') {
152
+ e.preventDefault();
153
+ onToggle(value);
154
+ }
155
+ };
156
+
157
+ return (
158
+ <div
159
+ className={tkx('flex flex-col rounded-lg overflow-hidden flex-1 min-w-0')}
160
+ style={{
161
+ border: `1px solid ${theme.border}`,
162
+ backgroundColor: theme.surface,
163
+ }}
164
+ >
165
+ {/* Header */}
166
+ <div
167
+ className={tkx('flex items-center gap-2 px-3 py-2 border-b')}
168
+ style={{ borderColor: theme.border, backgroundColor: theme.surfaceAlt }}
169
+ >
170
+ <button
171
+ type="button"
172
+ aria-label={allSelected ? `Deselect all ${safeTitle}` : `Select all ${safeTitle}`}
173
+ onClick={onToggleAll}
174
+ className={tkx('border-none bg-transparent cursor-pointer p-0 outline-none focus-visible:ring-2')}
175
+ >
176
+ <Checkbox
177
+ checked={allSelected}
178
+ primaryColor={theme.primary}
179
+ borderColor={theme.border}
180
+ />
181
+ </button>
182
+ <span
183
+ className={tkx('text-sm font-medium flex-1 truncate')}
184
+ style={{ color: theme.text }}
185
+ >
186
+ {safeTitle}
187
+ </span>
188
+ <span
189
+ className={tkx('text-xs')}
190
+ style={{ color: theme.textMuted }}
191
+ >
192
+ {selected.size}/{items.length}
193
+ </span>
194
+ </div>
195
+
196
+ {/* Search */}
197
+ {searchable && (
198
+ <div
199
+ className={tkx('flex items-center gap-1.5 px-3 py-1.5 border-b')}
200
+ style={{ borderColor: theme.border }}
201
+ >
202
+ <SearchIcon color={theme.textMuted} />
203
+ <input
204
+ type="text"
205
+ aria-label={`Search ${safeTitle}`}
206
+ value={search}
207
+ onChange={(e) => onSearchChange(e.target.value)}
208
+ placeholder="Search..."
209
+ className={tkx('flex-1 bg-transparent border-none outline-none text-xs')}
210
+ style={{ color: theme.text }}
211
+ />
212
+ </div>
213
+ )}
214
+
215
+ {/* Items list */}
216
+ <ul
217
+ id={listId}
218
+ role="listbox"
219
+ aria-label={safeTitle}
220
+ aria-multiselectable="true"
221
+ className={tkx('list-none m-0 p-1 overflow-y-auto')}
222
+ style={{ height, minHeight: 100 }}
223
+ >
224
+ {filtered.length === 0 ? (
225
+ <li
226
+ className={tkx('text-xs text-center py-4')}
227
+ style={{ color: theme.textMuted }}
228
+ aria-disabled="true"
229
+ >
230
+ No items
231
+ </li>
232
+ ) : (
233
+ filtered.map((item) => {
234
+ const isChecked = selected.has(item.value);
235
+ const safeLabel = sanitizeString(item.label);
236
+
237
+ return (
238
+ <li
239
+ key={item.value}
240
+ role="option"
241
+ aria-selected={isChecked}
242
+ aria-disabled={item.disabled || undefined}
243
+ tabIndex={0}
244
+ onClick={() => !item.disabled && onToggle(item.value)}
245
+ onKeyDown={(e) => !item.disabled && handleKeyDown(e, item.value)}
246
+ className={tkx(
247
+ 'flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer text-sm',
248
+ 'outline-none focus-visible:ring-2 focus-visible:ring-offset-1',
249
+ item.disabled ? 'opacity-50 cursor-not-allowed' : '',
250
+ )}
251
+ style={{
252
+ color: item.disabled ? theme.textMuted : theme.text,
253
+ backgroundColor: isChecked ? `${theme.primary}10` : 'transparent',
254
+ transition: reducedMotion ? 'none' : 'background-color 100ms ease',
255
+ }}
256
+ onMouseEnter={(e) => {
257
+ if (!item.disabled && !isChecked) {
258
+ (e.currentTarget as HTMLElement).style.backgroundColor = theme.surfaceAlt;
259
+ }
260
+ }}
261
+ onMouseLeave={(e) => {
262
+ (e.currentTarget as HTMLElement).style.backgroundColor = isChecked
263
+ ? `${theme.primary}10`
264
+ : 'transparent';
265
+ }}
266
+ >
267
+ <Checkbox
268
+ checked={isChecked}
269
+ primaryColor={theme.primary}
270
+ borderColor={theme.border}
271
+ />
272
+ <span className={tkx('truncate')}>{safeLabel}</span>
273
+ </li>
274
+ );
275
+ })
276
+ )}
277
+ </ul>
278
+ </div>
279
+ );
280
+ }
281
+
282
+ // ── Main Component ──────────────────────────────────────────────────────────
283
+
284
+ export function TkxTransferList({
285
+ sourceItems,
286
+ targetItems,
287
+ onTransfer,
288
+ sourceTitle = 'Available',
289
+ targetTitle = 'Selected',
290
+ searchable = false,
291
+ height = 300,
292
+ className,
293
+ style,
294
+ }: TkxTransferListProps) {
295
+ const theme = useTheme();
296
+ const reducedMotion = useReducedMotion();
297
+ const sourceListId = useId();
298
+ const targetListId = useId();
299
+
300
+ const [sourceSelected, setSourceSelected] = useState<Set<string>>(new Set());
301
+ const [targetSelected, setTargetSelected] = useState<Set<string>>(new Set());
302
+ const [sourceSearch, setSourceSearch] = useState('');
303
+ const [targetSearch, setTargetSearch] = useState('');
304
+
305
+ const toggleSourceItem = useCallback((value: string) => {
306
+ setSourceSelected((prev) => {
307
+ const next = new Set(prev);
308
+ if (next.has(value)) next.delete(value);
309
+ else next.add(value);
310
+ return next;
311
+ });
312
+ }, []);
313
+
314
+ const toggleTargetItem = useCallback((value: string) => {
315
+ setTargetSelected((prev) => {
316
+ const next = new Set(prev);
317
+ if (next.has(value)) next.delete(value);
318
+ else next.add(value);
319
+ return next;
320
+ });
321
+ }, []);
322
+
323
+ const toggleAllSource = useCallback(() => {
324
+ const enabledValues = sourceItems.filter((i) => !i.disabled).map((i) => i.value);
325
+ const allSelected = enabledValues.every((v) => sourceSelected.has(v));
326
+ if (allSelected) {
327
+ setSourceSelected(new Set());
328
+ } else {
329
+ setSourceSelected(new Set(enabledValues));
330
+ }
331
+ }, [sourceItems, sourceSelected]);
332
+
333
+ const toggleAllTarget = useCallback(() => {
334
+ const enabledValues = targetItems.filter((i) => !i.disabled).map((i) => i.value);
335
+ const allSelected = enabledValues.every((v) => targetSelected.has(v));
336
+ if (allSelected) {
337
+ setTargetSelected(new Set());
338
+ } else {
339
+ setTargetSelected(new Set(enabledValues));
340
+ }
341
+ }, [targetItems, targetSelected]);
342
+
343
+ // Transfer selected items to target
344
+ const moveToTarget = useCallback(() => {
345
+ if (sourceSelected.size === 0) return;
346
+ const toMove = sourceItems.filter((i) => sourceSelected.has(i.value) && !i.disabled);
347
+ const remainingSource = sourceItems.filter((i) => !sourceSelected.has(i.value) || i.disabled);
348
+ const newTarget = [...targetItems, ...toMove];
349
+ onTransfer(remainingSource, newTarget);
350
+ setSourceSelected(new Set());
351
+ }, [sourceItems, targetItems, sourceSelected, onTransfer]);
352
+
353
+ // Transfer selected items back to source
354
+ const moveToSource = useCallback(() => {
355
+ if (targetSelected.size === 0) return;
356
+ const toMove = targetItems.filter((i) => targetSelected.has(i.value) && !i.disabled);
357
+ const remainingTarget = targetItems.filter((i) => !targetSelected.has(i.value) || i.disabled);
358
+ const newSource = [...sourceItems, ...toMove];
359
+ onTransfer(newSource, remainingTarget);
360
+ setTargetSelected(new Set());
361
+ }, [sourceItems, targetItems, targetSelected, onTransfer]);
362
+
363
+ const transferButtonStyle = (disabled: boolean): CSSProperties => ({
364
+ display: 'inline-flex',
365
+ alignItems: 'center',
366
+ justifyContent: 'center',
367
+ width: 36,
368
+ height: 36,
369
+ borderRadius: 8,
370
+ border: `1px solid ${theme.border}`,
371
+ backgroundColor: disabled ? theme.surface : theme.surfaceAlt,
372
+ cursor: disabled ? 'not-allowed' : 'pointer',
373
+ opacity: disabled ? 0.5 : 1,
374
+ transition: reducedMotion ? 'none' : 'all 100ms ease',
375
+ outline: 'none',
376
+ });
377
+
378
+ return (
379
+ <div
380
+ className={tkx(
381
+ 'flex gap-3 font-sans',
382
+ // Responsive: stack vertically on narrow containers
383
+ 'flex-col sm:flex-row',
384
+ className ?? '',
385
+ )}
386
+ style={{
387
+ color: theme.text,
388
+ ...style,
389
+ }}
390
+ >
391
+ {/* Source list */}
392
+ <ListPanel
393
+ title={sourceTitle}
394
+ items={sourceItems}
395
+ selected={sourceSelected}
396
+ onToggle={toggleSourceItem}
397
+ onToggleAll={toggleAllSource}
398
+ search={sourceSearch}
399
+ onSearchChange={setSourceSearch}
400
+ searchable={searchable}
401
+ height={height}
402
+ listId={sourceListId}
403
+ />
404
+
405
+ {/* Transfer buttons */}
406
+ <div
407
+ className={tkx(
408
+ 'flex items-center justify-center gap-2',
409
+ 'flex-row sm:flex-col',
410
+ )}
411
+ style={{ padding: '8px 0' }}
412
+ >
413
+ <button
414
+ type="button"
415
+ aria-label="Move selected items to target list"
416
+ disabled={sourceSelected.size === 0}
417
+ onClick={moveToTarget}
418
+ onKeyDown={(e) => {
419
+ if (e.key === 'Enter' || e.key === ' ') {
420
+ e.preventDefault();
421
+ moveToTarget();
422
+ }
423
+ }}
424
+ className={tkx('focus-visible:ring-2 focus-visible:ring-offset-1')}
425
+ style={transferButtonStyle(sourceSelected.size === 0)}
426
+ onMouseEnter={(e) => {
427
+ if (sourceSelected.size > 0) {
428
+ (e.currentTarget as HTMLElement).style.backgroundColor = theme.primary;
429
+ (e.currentTarget as HTMLElement).style.borderColor = theme.primary;
430
+ }
431
+ }}
432
+ onMouseLeave={(e) => {
433
+ (e.currentTarget as HTMLElement).style.backgroundColor =
434
+ sourceSelected.size === 0 ? theme.surface : theme.surfaceAlt;
435
+ (e.currentTarget as HTMLElement).style.borderColor = theme.border;
436
+ }}
437
+ >
438
+ <ArrowRightIcon color={theme.text} />
439
+ </button>
440
+
441
+ <button
442
+ type="button"
443
+ aria-label="Move selected items to source list"
444
+ disabled={targetSelected.size === 0}
445
+ onClick={moveToSource}
446
+ onKeyDown={(e) => {
447
+ if (e.key === 'Enter' || e.key === ' ') {
448
+ e.preventDefault();
449
+ moveToSource();
450
+ }
451
+ }}
452
+ className={tkx('focus-visible:ring-2 focus-visible:ring-offset-1')}
453
+ style={transferButtonStyle(targetSelected.size === 0)}
454
+ onMouseEnter={(e) => {
455
+ if (targetSelected.size > 0) {
456
+ (e.currentTarget as HTMLElement).style.backgroundColor = theme.primary;
457
+ (e.currentTarget as HTMLElement).style.borderColor = theme.primary;
458
+ }
459
+ }}
460
+ onMouseLeave={(e) => {
461
+ (e.currentTarget as HTMLElement).style.backgroundColor =
462
+ targetSelected.size === 0 ? theme.surface : theme.surfaceAlt;
463
+ (e.currentTarget as HTMLElement).style.borderColor = theme.border;
464
+ }}
465
+ >
466
+ <ArrowLeftIcon color={theme.text} />
467
+ </button>
468
+ </div>
469
+
470
+ {/* Target list */}
471
+ <ListPanel
472
+ title={targetTitle}
473
+ items={targetItems}
474
+ selected={targetSelected}
475
+ onToggle={toggleTargetItem}
476
+ onToggleAll={toggleAllTarget}
477
+ search={targetSearch}
478
+ onSearchChange={setTargetSearch}
479
+ searchable={searchable}
480
+ height={height}
481
+ listId={targetListId}
482
+ />
483
+ </div>
484
+ );
485
+ }
486
+
487
+ export default TkxTransferList;