tekivex-ui 2.0.2 → 2.1.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 (78) hide show
  1. package/dist/index.cjs +12 -12
  2. package/dist/index.d.ts +5 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2207 -2018
  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/TkxDataGrid.d.ts +33 -0
  15. package/dist/src/components/TkxDataGrid.d.ts.map +1 -0
  16. package/dist/src/components/TkxMasonry.d.ts +12 -0
  17. package/dist/src/components/TkxMasonry.d.ts.map +1 -0
  18. package/dist/src/components/TkxModal.d.ts.map +1 -1
  19. package/dist/src/components/TkxPopover.d.ts +15 -0
  20. package/dist/src/components/TkxPopover.d.ts.map +1 -0
  21. package/dist/src/components/TkxRichTextDisplay.d.ts +19 -0
  22. package/dist/src/components/TkxRichTextDisplay.d.ts.map +1 -0
  23. package/dist/src/components/TkxSelect.d.ts.map +1 -1
  24. package/dist/src/components/TkxSlider.d.ts.map +1 -1
  25. package/dist/src/components/TkxSnackbar.d.ts +16 -0
  26. package/dist/src/components/TkxSnackbar.d.ts.map +1 -0
  27. package/dist/src/components/TkxSpeedDial.d.ts +18 -0
  28. package/dist/src/components/TkxSpeedDial.d.ts.map +1 -0
  29. package/dist/src/components/TkxToolbar.d.ts +22 -0
  30. package/dist/src/components/TkxToolbar.d.ts.map +1 -0
  31. package/dist/src/components/TkxTooltip.d.ts.map +1 -1
  32. package/dist/src/components/TkxTransferList.d.ts +20 -0
  33. package/dist/src/components/TkxTransferList.d.ts.map +1 -0
  34. package/dist/src/components/TkxTreeView.d.ts +23 -0
  35. package/dist/src/components/TkxTreeView.d.ts.map +1 -0
  36. package/dist/src/components/index.d.ts +13 -0
  37. package/dist/src/components/index.d.ts.map +1 -1
  38. package/dist/src/i18n/I18nProvider.d.ts +10 -0
  39. package/dist/src/i18n/I18nProvider.d.ts.map +1 -0
  40. package/dist/src/i18n/index.d.ts +50 -0
  41. package/dist/src/i18n/index.d.ts.map +1 -0
  42. package/package.json +2 -2
  43. package/src/components/TkxAppBar.tsx +260 -0
  44. package/src/components/TkxAutocomplete.tsx +453 -0
  45. package/src/components/TkxBottomNav.tsx +239 -0
  46. package/src/components/TkxBreadcrumb.tsx +322 -0
  47. package/src/components/TkxCard.tsx +3 -1
  48. package/src/components/TkxDataGrid.tsx +540 -0
  49. package/src/components/TkxMasonry.tsx +176 -0
  50. package/src/components/TkxModal.tsx +4 -3
  51. package/src/components/TkxPopover.tsx +295 -0
  52. package/src/components/TkxRichTextDisplay.tsx +394 -0
  53. package/src/components/TkxSelect.tsx +2 -0
  54. package/src/components/TkxSlider.tsx +1 -0
  55. package/src/components/TkxSnackbar.tsx +314 -0
  56. package/src/components/TkxSpeedDial.tsx +395 -0
  57. package/src/components/TkxToolbar.tsx +260 -0
  58. package/src/components/TkxTooltip.tsx +3 -2
  59. package/src/components/TkxTransferList.tsx +487 -0
  60. package/src/components/TkxTreeView.tsx +421 -0
  61. package/src/components/index.ts +13 -0
  62. package/src/hooks/index.ts +1 -1
  63. package/src/i18n/I18nProvider.tsx +36 -0
  64. package/src/i18n/index.ts +237 -0
  65. package/dist/tests/TkxButton.test.d.ts +0 -2
  66. package/dist/tests/TkxButton.test.d.ts.map +0 -1
  67. package/dist/tests/a11y/components.test.d.ts +0 -2
  68. package/dist/tests/a11y/components.test.d.ts.map +0 -1
  69. package/dist/tests/quantum.test.d.ts +0 -2
  70. package/dist/tests/quantum.test.d.ts.map +0 -1
  71. package/dist/tests/security.test.d.ts +0 -2
  72. package/dist/tests/security.test.d.ts.map +0 -1
  73. package/dist/tests/setup.d.ts +0 -2
  74. package/dist/tests/setup.d.ts.map +0 -1
  75. package/dist/tests/tkx.test.d.ts +0 -2
  76. package/dist/tests/tkx.test.d.ts.map +0 -1
  77. package/dist/tests/wcag.test.d.ts +0 -2
  78. package/dist/tests/wcag.test.d.ts.map +0 -1
@@ -0,0 +1,421 @@
1
+ import {
2
+ useState,
3
+ useCallback,
4
+ useRef,
5
+ useId,
6
+ type ReactNode,
7
+ type CSSProperties,
8
+ type KeyboardEvent,
9
+ } from 'react';
10
+ import { useTheme } from '../themes';
11
+ import { sanitizeString } from '../engine/security';
12
+ import { useReducedMotion } from '../hooks';
13
+ import { tkx } from '../engine/tkx';
14
+
15
+ // ── Types ─────────────────────────────────────────────────────────────────────
16
+
17
+ export interface TreeNode {
18
+ id: string;
19
+ label: string;
20
+ icon?: ReactNode;
21
+ children?: TreeNode[];
22
+ disabled?: boolean;
23
+ }
24
+
25
+ export interface TkxTreeViewProps {
26
+ data: TreeNode[];
27
+ selected?: string[];
28
+ onSelect?: (ids: string[]) => void;
29
+ expanded?: string[];
30
+ onExpand?: (ids: string[]) => void;
31
+ multiSelect?: boolean;
32
+ showCheckboxes?: boolean;
33
+ showLines?: boolean;
34
+ className?: string;
35
+ style?: CSSProperties;
36
+ }
37
+
38
+ // ── Helpers ─────────────────────────────────────────────────────────────────
39
+
40
+ function flattenVisible(
41
+ nodes: TreeNode[],
42
+ expandedSet: Set<string>,
43
+ ): { node: TreeNode; depth: number }[] {
44
+ const result: { node: TreeNode; depth: number }[] = [];
45
+ function walk(items: TreeNode[], depth: number) {
46
+ for (const item of items) {
47
+ result.push({ node: item, depth });
48
+ if (item.children?.length && expandedSet.has(item.id)) {
49
+ walk(item.children, depth + 1);
50
+ }
51
+ }
52
+ }
53
+ walk(nodes, 0);
54
+ return result;
55
+ }
56
+
57
+ function getAllIds(nodes: TreeNode[]): string[] {
58
+ const ids: string[] = [];
59
+ function walk(items: TreeNode[]) {
60
+ for (const item of items) {
61
+ ids.push(item.id);
62
+ if (item.children) walk(item.children);
63
+ }
64
+ }
65
+ walk(nodes);
66
+ return ids;
67
+ }
68
+
69
+ // ── Chevron icon ────────────────────────────────────────────────────────────
70
+
71
+ function ChevronIcon({
72
+ expanded,
73
+ color,
74
+ reducedMotion,
75
+ }: {
76
+ expanded: boolean;
77
+ color: string;
78
+ reducedMotion: boolean;
79
+ }) {
80
+ return (
81
+ <svg
82
+ width="16"
83
+ height="16"
84
+ viewBox="0 0 16 16"
85
+ fill="none"
86
+ aria-hidden="true"
87
+ style={{
88
+ transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)',
89
+ transition: reducedMotion ? 'none' : 'transform 150ms ease',
90
+ flexShrink: 0,
91
+ }}
92
+ >
93
+ <path
94
+ d="M6 3l5 5-5 5"
95
+ stroke={color}
96
+ strokeWidth="1.5"
97
+ strokeLinecap="round"
98
+ strokeLinejoin="round"
99
+ />
100
+ </svg>
101
+ );
102
+ }
103
+
104
+ // ── Checkbox ────────────────────────────────────────────────────────────────
105
+
106
+ function TreeCheckbox({
107
+ checked,
108
+ color,
109
+ borderColor,
110
+ }: {
111
+ checked: boolean;
112
+ color: string;
113
+ borderColor: string;
114
+ }) {
115
+ return (
116
+ <span
117
+ role="presentation"
118
+ aria-hidden="true"
119
+ className={tkx('inline-flex items-center justify-center flex-shrink-0')}
120
+ style={{
121
+ width: 16,
122
+ height: 16,
123
+ borderRadius: 3,
124
+ border: `1.5px solid ${checked ? color : borderColor}`,
125
+ backgroundColor: checked ? color : 'transparent',
126
+ transition: 'all 100ms ease',
127
+ }}
128
+ >
129
+ {checked && (
130
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
131
+ <path
132
+ d="M2 5l2.5 2.5L8 3"
133
+ stroke="#fff"
134
+ strokeWidth="1.5"
135
+ strokeLinecap="round"
136
+ strokeLinejoin="round"
137
+ />
138
+ </svg>
139
+ )}
140
+ </span>
141
+ );
142
+ }
143
+
144
+ // ── Component ───────────────────────────────────────────────────────────────
145
+
146
+ const INDENT_PX = 24;
147
+
148
+ export function TkxTreeView({
149
+ data,
150
+ selected: controlledSelected,
151
+ onSelect,
152
+ expanded: controlledExpanded,
153
+ onExpand,
154
+ multiSelect = false,
155
+ showCheckboxes = false,
156
+ showLines = false,
157
+ className,
158
+ style,
159
+ }: TkxTreeViewProps) {
160
+ const theme = useTheme();
161
+ const reducedMotion = useReducedMotion();
162
+ const treeId = useId();
163
+
164
+ // Internal state fallbacks
165
+ const [internalSelected, setInternalSelected] = useState<string[]>([]);
166
+ const [internalExpanded, setInternalExpanded] = useState<string[]>([]);
167
+
168
+ const selectedSet = new Set(controlledSelected ?? internalSelected);
169
+ const expandedSet = new Set(controlledExpanded ?? internalExpanded);
170
+
171
+ const setExpanded = useCallback(
172
+ (ids: string[]) => {
173
+ if (controlledExpanded === undefined) setInternalExpanded(ids);
174
+ onExpand?.(ids);
175
+ },
176
+ [controlledExpanded, onExpand],
177
+ );
178
+
179
+ const setSelected = useCallback(
180
+ (ids: string[]) => {
181
+ if (controlledSelected === undefined) setInternalSelected(ids);
182
+ onSelect?.(ids);
183
+ },
184
+ [controlledSelected, onSelect],
185
+ );
186
+
187
+ const toggleExpand = useCallback(
188
+ (id: string) => {
189
+ const arr = Array.from(expandedSet);
190
+ if (expandedSet.has(id)) {
191
+ setExpanded(arr.filter((x) => x !== id));
192
+ } else {
193
+ setExpanded([...arr, id]);
194
+ }
195
+ },
196
+ [expandedSet, setExpanded],
197
+ );
198
+
199
+ const toggleSelect = useCallback(
200
+ (id: string) => {
201
+ if (multiSelect) {
202
+ const arr = Array.from(selectedSet);
203
+ if (selectedSet.has(id)) {
204
+ setSelected(arr.filter((x) => x !== id));
205
+ } else {
206
+ setSelected([...arr, id]);
207
+ }
208
+ } else {
209
+ setSelected(selectedSet.has(id) ? [] : [id]);
210
+ }
211
+ },
212
+ [multiSelect, selectedSet, setSelected],
213
+ );
214
+
215
+ const flatItems = flattenVisible(data, expandedSet);
216
+ const focusedIndexRef = useRef(0);
217
+ const itemRefs = useRef<Map<number, HTMLElement>>(new Map());
218
+
219
+ const focusItem = (index: number) => {
220
+ focusedIndexRef.current = index;
221
+ itemRefs.current.get(index)?.focus();
222
+ };
223
+
224
+ const findNodeById = (id: string, nodes: TreeNode[]): TreeNode | undefined => {
225
+ for (const n of nodes) {
226
+ if (n.id === id) return n;
227
+ if (n.children) {
228
+ const found = findNodeById(id, n.children);
229
+ if (found) return found;
230
+ }
231
+ }
232
+ return undefined;
233
+ };
234
+
235
+ const handleKeyDown = (e: KeyboardEvent<HTMLElement>, index: number) => {
236
+ const item = flatItems[index];
237
+ if (!item) return;
238
+ const { node } = item;
239
+ const hasChildren = (node.children?.length ?? 0) > 0;
240
+
241
+ switch (e.key) {
242
+ case 'ArrowDown': {
243
+ e.preventDefault();
244
+ if (index < flatItems.length - 1) focusItem(index + 1);
245
+ break;
246
+ }
247
+ case 'ArrowUp': {
248
+ e.preventDefault();
249
+ if (index > 0) focusItem(index - 1);
250
+ break;
251
+ }
252
+ case 'ArrowRight': {
253
+ e.preventDefault();
254
+ if (hasChildren && !expandedSet.has(node.id)) {
255
+ toggleExpand(node.id);
256
+ } else if (hasChildren && expandedSet.has(node.id)) {
257
+ // Move to first child
258
+ if (index < flatItems.length - 1) focusItem(index + 1);
259
+ }
260
+ break;
261
+ }
262
+ case 'ArrowLeft': {
263
+ e.preventDefault();
264
+ if (hasChildren && expandedSet.has(node.id)) {
265
+ toggleExpand(node.id);
266
+ } else {
267
+ // Move to parent: find the item at depth - 1
268
+ for (let i = index - 1; i >= 0; i--) {
269
+ if (flatItems[i].depth < item.depth) {
270
+ focusItem(i);
271
+ break;
272
+ }
273
+ }
274
+ }
275
+ break;
276
+ }
277
+ case 'Enter':
278
+ case ' ': {
279
+ e.preventDefault();
280
+ if (!node.disabled) toggleSelect(node.id);
281
+ break;
282
+ }
283
+ case 'Home': {
284
+ e.preventDefault();
285
+ focusItem(0);
286
+ break;
287
+ }
288
+ case 'End': {
289
+ e.preventDefault();
290
+ focusItem(flatItems.length - 1);
291
+ break;
292
+ }
293
+ }
294
+ };
295
+
296
+ return (
297
+ <ul
298
+ role="tree"
299
+ aria-label="Tree view"
300
+ aria-multiselectable={multiSelect || undefined}
301
+ className={tkx('list-none m-0 p-0 font-sans select-none', className ?? '')}
302
+ style={{
303
+ color: theme.text,
304
+ ...style,
305
+ }}
306
+ >
307
+ {flatItems.map(({ node, depth }, index) => {
308
+ const hasChildren = (node.children?.length ?? 0) > 0;
309
+ const isExpanded = expandedSet.has(node.id);
310
+ const isSelected = selectedSet.has(node.id);
311
+ const safeLabel = sanitizeString(node.label);
312
+
313
+ return (
314
+ <li
315
+ key={node.id}
316
+ role="treeitem"
317
+ aria-selected={isSelected}
318
+ aria-expanded={hasChildren ? isExpanded : undefined}
319
+ aria-disabled={node.disabled || undefined}
320
+ aria-level={depth + 1}
321
+ aria-setsize={flatItems.filter((f) => f.depth === depth).length}
322
+ tabIndex={index === 0 ? 0 : -1}
323
+ ref={(el) => {
324
+ if (el) itemRefs.current.set(index, el);
325
+ else itemRefs.current.delete(index);
326
+ }}
327
+ onKeyDown={(e) => handleKeyDown(e, index)}
328
+ onClick={() => {
329
+ if (node.disabled) return;
330
+ if (hasChildren) toggleExpand(node.id);
331
+ toggleSelect(node.id);
332
+ }}
333
+ className={tkx(
334
+ 'relative flex items-center gap-1.5 py-1 px-2 rounded-md cursor-pointer',
335
+ 'outline-none focus-visible:ring-2 focus-visible:ring-offset-1',
336
+ node.disabled ? 'opacity-50 cursor-not-allowed' : '',
337
+ )}
338
+ style={{
339
+ paddingLeft: depth * INDENT_PX + 8,
340
+ backgroundColor: isSelected ? `${theme.primary}18` : 'transparent',
341
+ transition: reducedMotion ? 'none' : 'background-color 100ms ease',
342
+ minHeight: 32,
343
+ }}
344
+ onMouseEnter={(e) => {
345
+ if (!isSelected && !node.disabled) {
346
+ (e.currentTarget as HTMLElement).style.backgroundColor = theme.surfaceAlt;
347
+ }
348
+ }}
349
+ onMouseLeave={(e) => {
350
+ (e.currentTarget as HTMLElement).style.backgroundColor = isSelected
351
+ ? `${theme.primary}18`
352
+ : 'transparent';
353
+ }}
354
+ >
355
+ {/* Connecting lines */}
356
+ {showLines && depth > 0 && (
357
+ <span
358
+ aria-hidden="true"
359
+ style={{
360
+ position: 'absolute',
361
+ left: (depth - 1) * INDENT_PX + 16,
362
+ top: 0,
363
+ bottom: 0,
364
+ width: 1,
365
+ backgroundColor: theme.border,
366
+ }}
367
+ />
368
+ )}
369
+
370
+ {/* Expand/collapse chevron */}
371
+ {hasChildren ? (
372
+ <span
373
+ role="presentation"
374
+ onClick={(e) => {
375
+ e.stopPropagation();
376
+ toggleExpand(node.id);
377
+ }}
378
+ className={tkx('inline-flex items-center justify-center cursor-pointer')}
379
+ style={{ width: 20, height: 20 }}
380
+ >
381
+ <ChevronIcon
382
+ expanded={isExpanded}
383
+ color={theme.textMuted}
384
+ reducedMotion={reducedMotion}
385
+ />
386
+ </span>
387
+ ) : (
388
+ <span style={{ width: 20, height: 20 }} aria-hidden="true" />
389
+ )}
390
+
391
+ {/* Checkbox */}
392
+ {showCheckboxes && (
393
+ <TreeCheckbox
394
+ checked={isSelected}
395
+ color={theme.primary}
396
+ borderColor={theme.border}
397
+ />
398
+ )}
399
+
400
+ {/* Icon */}
401
+ {node.icon && (
402
+ <span aria-hidden="true" className={tkx('flex-shrink-0')}>
403
+ {node.icon}
404
+ </span>
405
+ )}
406
+
407
+ {/* Label */}
408
+ <span
409
+ className={tkx('text-sm truncate')}
410
+ style={{ color: node.disabled ? theme.textMuted : theme.text }}
411
+ >
412
+ {safeLabel}
413
+ </span>
414
+ </li>
415
+ );
416
+ })}
417
+ </ul>
418
+ );
419
+ }
420
+
421
+ export default TkxTreeView;
@@ -38,3 +38,16 @@ export * from './TkxNumberInput';
38
38
  export * from './TkxOTP';
39
39
  export * from './TkxCommand';
40
40
  export * from './TkxCarousel';
41
+ export * from './TkxBreadcrumb';
42
+ export * from './TkxPopover';
43
+ export * from './TkxAutocomplete';
44
+ export * from './TkxTreeView';
45
+ export * from './TkxToolbar';
46
+ export * from './TkxTransferList';
47
+ export * from './TkxSpeedDial';
48
+ export * from './TkxAppBar';
49
+ export * from './TkxBottomNav';
50
+ export * from './TkxSnackbar';
51
+ export * from './TkxDataGrid';
52
+ export * from './TkxMasonry';
53
+ export * from './TkxRichTextDisplay';
@@ -91,7 +91,7 @@ export function useEscapeKey(handler: () => void, active = true): void {
91
91
  handlerRef.current = handler;
92
92
 
93
93
  useEffect(() => {
94
- if (!active) return;
94
+ if (!active || typeof document === 'undefined') return;
95
95
  const listener = (e: KeyboardEvent) => {
96
96
  if (e.key === 'Escape') handlerRef.current();
97
97
  };
@@ -0,0 +1,36 @@
1
+ import { useMemo, type ReactNode } from 'react';
2
+ import { I18nContext, LOCALES, isRTL, enUS, type LocaleCode, type LocaleStrings, type Direction } from './index';
3
+
4
+ export interface I18nProviderProps {
5
+ locale?: LocaleCode;
6
+ direction?: Direction;
7
+ strings?: Partial<LocaleStrings>;
8
+ children: ReactNode;
9
+ }
10
+
11
+ export function I18nProvider({
12
+ locale = 'en-US',
13
+ direction,
14
+ strings: overrides,
15
+ children,
16
+ }: I18nProviderProps) {
17
+ const value = useMemo(() => {
18
+ const base = LOCALES[locale] ?? enUS;
19
+ const merged = overrides ? { ...base, ...overrides } : base;
20
+ const dir = direction ?? (isRTL(locale) ? 'rtl' : 'ltr');
21
+
22
+ return {
23
+ locale,
24
+ direction: dir,
25
+ strings: merged,
26
+ };
27
+ }, [locale, direction, overrides]);
28
+
29
+ return (
30
+ <I18nContext.Provider value={value}>
31
+ <div dir={value.direction} style={{ direction: value.direction }}>
32
+ {children}
33
+ </div>
34
+ </I18nContext.Provider>
35
+ );
36
+ }