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,322 @@
1
+ import {
2
+ useCallback,
3
+ type ReactNode,
4
+ type CSSProperties,
5
+ type KeyboardEvent,
6
+ } from 'react';
7
+ import { useTheme } from '../themes';
8
+ import { sanitizeString } from '../engine/security';
9
+ import { useReducedMotion } from '../hooks';
10
+ import { tkx } from '../engine/tkx';
11
+
12
+ // ── Types ─────────────────────────────────────────────────────────────────────
13
+
14
+ export interface BreadcrumbItem {
15
+ label: string;
16
+ href?: string;
17
+ icon?: ReactNode;
18
+ }
19
+
20
+ export interface TkxBreadcrumbProps {
21
+ items: BreadcrumbItem[];
22
+ separator?: ReactNode;
23
+ maxItems?: number;
24
+ onNavigate?: (item: BreadcrumbItem, index: number) => void;
25
+ className?: string;
26
+ style?: CSSProperties;
27
+ }
28
+
29
+ // ── Chevron separator icon ──────────────────────────────────────────────────
30
+
31
+ function DefaultSeparator({ color }: { color: string }) {
32
+ return (
33
+ <svg
34
+ width="16"
35
+ height="16"
36
+ viewBox="0 0 16 16"
37
+ fill="none"
38
+ aria-hidden="true"
39
+ style={{ flexShrink: 0 }}
40
+ >
41
+ <path
42
+ d="M6 3l5 5-5 5"
43
+ stroke={color}
44
+ strokeWidth="1.5"
45
+ strokeLinecap="round"
46
+ strokeLinejoin="round"
47
+ />
48
+ </svg>
49
+ );
50
+ }
51
+
52
+ // ── Ellipsis button ─────────────────────────────────────────────────────────
53
+
54
+ function EllipsisButton({
55
+ onClick,
56
+ bgColor,
57
+ hoverColor,
58
+ textColor,
59
+ }: {
60
+ onClick: () => void;
61
+ bgColor: string;
62
+ hoverColor: string;
63
+ textColor: string;
64
+ }) {
65
+ const handleKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
66
+ if (e.key === 'Enter' || e.key === ' ') {
67
+ e.preventDefault();
68
+ onClick();
69
+ }
70
+ };
71
+
72
+ return (
73
+ <button
74
+ type="button"
75
+ onClick={onClick}
76
+ onKeyDown={handleKeyDown}
77
+ aria-label="Show hidden breadcrumb items"
78
+ className={tkx(
79
+ 'inline-flex items-center justify-center rounded px-1.5 py-0.5',
80
+ 'text-sm font-medium cursor-pointer border-none outline-none',
81
+ 'focus-visible:ring-2 focus-visible:ring-offset-1',
82
+ )}
83
+ style={{
84
+ backgroundColor: bgColor,
85
+ color: textColor,
86
+ transition: 'background-color 150ms ease',
87
+ }}
88
+ onMouseEnter={(e) => {
89
+ (e.currentTarget as HTMLElement).style.backgroundColor = hoverColor;
90
+ }}
91
+ onMouseLeave={(e) => {
92
+ (e.currentTarget as HTMLElement).style.backgroundColor = bgColor;
93
+ }}
94
+ >
95
+ &hellip;
96
+ </button>
97
+ );
98
+ }
99
+
100
+ // ── Component ───────────────────────────────────────────────────────────────
101
+
102
+ export function TkxBreadcrumb({
103
+ items,
104
+ separator,
105
+ maxItems,
106
+ onNavigate,
107
+ className,
108
+ style,
109
+ }: TkxBreadcrumbProps) {
110
+ const theme = useTheme();
111
+ const reducedMotion = useReducedMotion();
112
+
113
+ // Collapse logic: show first, last, and ellipsis when items exceed maxItems
114
+ const shouldCollapse = maxItems !== undefined && maxItems > 1 && items.length > maxItems;
115
+
116
+ const getVisibleItems = useCallback((): { item: BreadcrumbItem; originalIndex: number }[] => {
117
+ if (!shouldCollapse || !maxItems) {
118
+ return items.map((item, i) => ({ item, originalIndex: i }));
119
+ }
120
+
121
+ const headCount = Math.ceil((maxItems - 1) / 2);
122
+ const tailCount = maxItems - 1 - headCount;
123
+
124
+ const head = items.slice(0, headCount).map((item, i) => ({
125
+ item,
126
+ originalIndex: i,
127
+ }));
128
+
129
+ const tail = items.slice(items.length - tailCount).map((item, i) => ({
130
+ item,
131
+ originalIndex: items.length - tailCount + i,
132
+ }));
133
+
134
+ return head.concat(tail);
135
+ }, [items, maxItems, shouldCollapse]);
136
+
137
+ const visibleItems = getVisibleItems();
138
+
139
+ // Where to insert the ellipsis
140
+ const headCount = shouldCollapse && maxItems ? Math.ceil((maxItems - 1) / 2) : -1;
141
+
142
+ const handleClick = (item: BreadcrumbItem, index: number) => {
143
+ onNavigate?.(item, index);
144
+ };
145
+
146
+ const handleKeyDown = (e: KeyboardEvent<HTMLElement>, item: BreadcrumbItem, index: number) => {
147
+ if (e.key === 'Enter' || e.key === ' ') {
148
+ e.preventDefault();
149
+ handleClick(item, index);
150
+ }
151
+ };
152
+
153
+ const transitionStyle: CSSProperties = reducedMotion
154
+ ? {}
155
+ : { transition: 'color 150ms ease, background-color 150ms ease' };
156
+
157
+ const renderSeparator = (key: string) => (
158
+ <li
159
+ key={key}
160
+ role="presentation"
161
+ aria-hidden="true"
162
+ className={tkx('flex items-center mx-1.5')}
163
+ >
164
+ {separator ?? <DefaultSeparator color={theme.textMuted} />}
165
+ </li>
166
+ );
167
+
168
+ const renderItem = (
169
+ item: BreadcrumbItem,
170
+ originalIndex: number,
171
+ isLast: boolean,
172
+ ) => {
173
+ const safeLabel = sanitizeString(item.label);
174
+
175
+ if (isLast) {
176
+ return (
177
+ <li key={`item-${originalIndex}`}>
178
+ <span
179
+ aria-current="page"
180
+ className={tkx('inline-flex items-center gap-1.5 text-sm font-medium')}
181
+ style={{ color: theme.text }}
182
+ >
183
+ {item.icon && (
184
+ <span aria-hidden="true" className={tkx('flex-shrink-0')}>
185
+ {item.icon}
186
+ </span>
187
+ )}
188
+ {safeLabel}
189
+ </span>
190
+ </li>
191
+ );
192
+ }
193
+
194
+ const isLink = !!item.href;
195
+
196
+ const commonStyle: CSSProperties = {
197
+ color: theme.textMuted,
198
+ textDecoration: 'none',
199
+ ...transitionStyle,
200
+ };
201
+
202
+ const hoverHandlers = {
203
+ onMouseEnter: (e: React.MouseEvent<HTMLElement>) => {
204
+ (e.currentTarget as HTMLElement).style.color = theme.primary;
205
+ },
206
+ onMouseLeave: (e: React.MouseEvent<HTMLElement>) => {
207
+ (e.currentTarget as HTMLElement).style.color = theme.textMuted;
208
+ },
209
+ };
210
+
211
+ const content = (
212
+ <>
213
+ {item.icon && (
214
+ <span aria-hidden="true" className={tkx('flex-shrink-0')}>
215
+ {item.icon}
216
+ </span>
217
+ )}
218
+ {safeLabel}
219
+ </>
220
+ );
221
+
222
+ if (isLink) {
223
+ return (
224
+ <li key={`item-${originalIndex}`}>
225
+ <a
226
+ href={item.href}
227
+ onClick={(e) => {
228
+ if (onNavigate) {
229
+ e.preventDefault();
230
+ handleClick(item, originalIndex);
231
+ }
232
+ }}
233
+ className={tkx(
234
+ 'inline-flex items-center gap-1.5 text-sm rounded',
235
+ 'outline-none focus-visible:ring-2 focus-visible:ring-offset-1',
236
+ )}
237
+ style={{
238
+ ...commonStyle,
239
+ focusRingColor: theme.primary,
240
+ } as CSSProperties}
241
+ {...hoverHandlers}
242
+ >
243
+ {content}
244
+ </a>
245
+ </li>
246
+ );
247
+ }
248
+
249
+ return (
250
+ <li key={`item-${originalIndex}`}>
251
+ <button
252
+ type="button"
253
+ onClick={() => handleClick(item, originalIndex)}
254
+ onKeyDown={(e) => handleKeyDown(e, item, originalIndex)}
255
+ className={tkx(
256
+ 'inline-flex items-center gap-1.5 text-sm rounded',
257
+ 'border-none bg-transparent cursor-pointer p-0',
258
+ 'outline-none focus-visible:ring-2 focus-visible:ring-offset-1',
259
+ )}
260
+ style={commonStyle}
261
+ {...hoverHandlers}
262
+ >
263
+ {content}
264
+ </button>
265
+ </li>
266
+ );
267
+ };
268
+
269
+ // Build the rendered list
270
+ const rendered: ReactNode[] = [];
271
+ let ellipsisInserted = false;
272
+
273
+ for (let vi = 0; vi < visibleItems.length; vi++) {
274
+ const { item, originalIndex } = visibleItems[vi];
275
+ const isLast = originalIndex === items.length - 1;
276
+
277
+ // Insert ellipsis after headCount items
278
+ if (shouldCollapse && !ellipsisInserted && vi === headCount) {
279
+ if (rendered.length > 0) {
280
+ rendered.push(renderSeparator(`sep-ellipsis-before`));
281
+ }
282
+ rendered.push(
283
+ <li key="ellipsis" role="presentation">
284
+ <EllipsisButton
285
+ onClick={() => {
286
+ // Re-render without collapse -- parent can control maxItems
287
+ }}
288
+ bgColor={theme.surfaceAlt}
289
+ hoverColor={theme.surface}
290
+ textColor={theme.textMuted}
291
+ />
292
+ </li>,
293
+ );
294
+ ellipsisInserted = true;
295
+ }
296
+
297
+ if (rendered.length > 0 && rendered[rendered.length - 1] !== null) {
298
+ rendered.push(renderSeparator(`sep-${originalIndex}`));
299
+ }
300
+
301
+ rendered.push(renderItem(item, originalIndex, isLast));
302
+ }
303
+
304
+ return (
305
+ <nav
306
+ aria-label="Breadcrumb"
307
+ className={tkx('font-sans', className ?? '')}
308
+ style={{
309
+ ...style,
310
+ }}
311
+ >
312
+ <ol
313
+ role="list"
314
+ className={tkx('flex items-center flex-wrap list-none m-0 p-0')}
315
+ >
316
+ {rendered}
317
+ </ol>
318
+ </nav>
319
+ );
320
+ }
321
+
322
+ export default TkxBreadcrumb;
@@ -1,5 +1,6 @@
1
1
  import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
2
2
  import { useTheme } from '../themes';
3
+ import { useReducedMotion } from '../hooks';
3
4
  import { tkx, cx } from '../engine/tkx';
4
5
 
5
6
  export type CardVariant = 'default' | 'glass' | 'quantum' | 'elevated' | 'outlined';
@@ -20,6 +21,7 @@ const PADDING_CLASS: Record<CardPadding, string> = {
20
21
  export const TkxCard = forwardRef<HTMLElement, TkxCardProps>(
21
22
  ({ variant = 'default', isHoverable, isClickable, padding = 'md', as, children, className, style, ...rest }, ref) => {
22
23
  const theme = useTheme();
24
+ const reducedMotion = useReducedMotion();
23
25
  const Tag = (as ?? (isClickable ? 'button' : 'div')) as 'div';
24
26
 
25
27
  const variantStyle: React.CSSProperties = {
@@ -33,7 +35,7 @@ export const TkxCard = forwardRef<HTMLElement, TkxCardProps>(
33
35
  const base = tkx(
34
36
  'rounded-xl w-full text-left block font-sans focus-visible:focus-ring',
35
37
  PADDING_CLASS[padding],
36
- (isHoverable || isClickable) && 'transition-transform duration-150 hover:scale-[1.01]',
38
+ (isHoverable || isClickable) && !reducedMotion && 'transition-transform duration-150 hover:scale-[1.01]',
37
39
  isClickable ? 'cursor-pointer' : 'cursor-default',
38
40
  );
39
41