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.
- package/dist/index.cjs +12 -12
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2839 -2538
- package/dist/src/components/TkxAppBar.d.ts +12 -0
- package/dist/src/components/TkxAppBar.d.ts.map +1 -0
- package/dist/src/components/TkxAutocomplete.d.ts +25 -0
- package/dist/src/components/TkxAutocomplete.d.ts.map +1 -0
- package/dist/src/components/TkxBottomNav.d.ts +15 -0
- package/dist/src/components/TkxBottomNav.d.ts.map +1 -0
- package/dist/src/components/TkxBreadcrumb.d.ts +17 -0
- package/dist/src/components/TkxBreadcrumb.d.ts.map +1 -0
- package/dist/src/components/TkxCard.d.ts.map +1 -1
- package/dist/src/components/TkxConfigProvider.d.ts +100 -0
- package/dist/src/components/TkxConfigProvider.d.ts.map +1 -0
- package/dist/src/components/TkxDataGrid.d.ts +33 -0
- package/dist/src/components/TkxDataGrid.d.ts.map +1 -0
- package/dist/src/components/TkxEmpty.d.ts +870 -0
- package/dist/src/components/TkxEmpty.d.ts.map +1 -0
- package/dist/src/components/TkxForm.d.ts +54 -0
- package/dist/src/components/TkxForm.d.ts.map +1 -0
- package/dist/src/components/TkxLayout.d.ts +87 -0
- package/dist/src/components/TkxLayout.d.ts.map +1 -0
- package/dist/src/components/TkxMasonry.d.ts +12 -0
- package/dist/src/components/TkxMasonry.d.ts.map +1 -0
- package/dist/src/components/TkxModal.d.ts.map +1 -1
- package/dist/src/components/TkxPopover.d.ts +15 -0
- package/dist/src/components/TkxPopover.d.ts.map +1 -0
- package/dist/src/components/TkxRichTextDisplay.d.ts +19 -0
- package/dist/src/components/TkxRichTextDisplay.d.ts.map +1 -0
- package/dist/src/components/TkxSelect.d.ts.map +1 -1
- package/dist/src/components/TkxSlider.d.ts.map +1 -1
- package/dist/src/components/TkxSnackbar.d.ts +16 -0
- package/dist/src/components/TkxSnackbar.d.ts.map +1 -0
- package/dist/src/components/TkxSpeedDial.d.ts +18 -0
- package/dist/src/components/TkxSpeedDial.d.ts.map +1 -0
- package/dist/src/components/TkxSpin.d.ts +15 -0
- package/dist/src/components/TkxSpin.d.ts.map +1 -0
- package/dist/src/components/TkxStatistic.d.ts +1746 -0
- package/dist/src/components/TkxStatistic.d.ts.map +1 -0
- package/dist/src/components/TkxToolbar.d.ts +22 -0
- package/dist/src/components/TkxToolbar.d.ts.map +1 -0
- package/dist/src/components/TkxTooltip.d.ts.map +1 -1
- package/dist/src/components/TkxTransferList.d.ts +20 -0
- package/dist/src/components/TkxTransferList.d.ts.map +1 -0
- package/dist/src/components/TkxTreeView.d.ts +23 -0
- package/dist/src/components/TkxTreeView.d.ts.map +1 -0
- package/dist/src/components/TkxTypography.d.ts +2614 -0
- package/dist/src/components/TkxTypography.d.ts.map +1 -0
- package/dist/src/components/index.d.ts +20 -0
- package/dist/src/components/index.d.ts.map +1 -1
- package/dist/src/i18n/I18nProvider.d.ts +10 -0
- package/dist/src/i18n/I18nProvider.d.ts.map +1 -0
- package/dist/src/i18n/index.d.ts +50 -0
- package/dist/src/i18n/index.d.ts.map +1 -0
- package/dist/src/themes/index.d.ts +108 -0
- package/dist/src/themes/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/TkxAppBar.tsx +260 -0
- package/src/components/TkxAutocomplete.tsx +453 -0
- package/src/components/TkxBottomNav.tsx +239 -0
- package/src/components/TkxBreadcrumb.tsx +322 -0
- package/src/components/TkxCard.tsx +3 -1
- package/src/components/TkxConfigProvider.tsx +458 -0
- package/src/components/TkxDataGrid.tsx +540 -0
- package/src/components/TkxEmpty.tsx +219 -0
- package/src/components/TkxForm.tsx +607 -0
- package/src/components/TkxLayout.tsx +647 -0
- package/src/components/TkxMasonry.tsx +176 -0
- package/src/components/TkxModal.tsx +4 -3
- package/src/components/TkxPopover.tsx +295 -0
- package/src/components/TkxRichTextDisplay.tsx +394 -0
- package/src/components/TkxSelect.tsx +2 -0
- package/src/components/TkxSlider.tsx +1 -0
- package/src/components/TkxSnackbar.tsx +314 -0
- package/src/components/TkxSpeedDial.tsx +395 -0
- package/src/components/TkxSpin.tsx +261 -0
- package/src/components/TkxStatistic.tsx +261 -0
- package/src/components/TkxToolbar.tsx +260 -0
- package/src/components/TkxTooltip.tsx +3 -2
- package/src/components/TkxTransferList.tsx +487 -0
- package/src/components/TkxTreeView.tsx +421 -0
- package/src/components/TkxTypography.tsx +263 -0
- package/src/components/index.ts +20 -0
- package/src/hooks/index.ts +1 -1
- package/src/i18n/I18nProvider.tsx +36 -0
- package/src/i18n/index.ts +237 -0
- package/src/themes/index.ts +149 -0
- package/dist/tests/TkxButton.test.d.ts +0 -2
- package/dist/tests/TkxButton.test.d.ts.map +0 -1
- package/dist/tests/a11y/components.test.d.ts +0 -2
- package/dist/tests/a11y/components.test.d.ts.map +0 -1
- package/dist/tests/quantum.test.d.ts +0 -2
- package/dist/tests/quantum.test.d.ts.map +0 -1
- package/dist/tests/security.test.d.ts +0 -2
- package/dist/tests/security.test.d.ts.map +0 -1
- package/dist/tests/setup.d.ts +0 -2
- package/dist/tests/setup.d.ts.map +0 -1
- package/dist/tests/tkx.test.d.ts +0 -2
- package/dist/tests/tkx.test.d.ts.map +0 -1
- package/dist/tests/wcag.test.d.ts +0 -2
- 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
|
+
…
|
|
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
|
|