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,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;
@@ -0,0 +1,263 @@
1
+ import {
2
+ useState,
3
+ useCallback,
4
+ type ReactNode,
5
+ type CSSProperties,
6
+ createElement,
7
+ } from 'react';
8
+ import { useTheme } from '../themes';
9
+ import { sanitizeString } from '../engine/security';
10
+
11
+ // ── Interfaces ──────────────────────────────────────────────────────────────
12
+
13
+ export type TypographyType = 'default' | 'secondary' | 'success' | 'warning' | 'danger';
14
+
15
+ export interface TkxTitleProps {
16
+ level?: 1 | 2 | 3 | 4 | 5;
17
+ children: ReactNode;
18
+ copyable?: boolean;
19
+ type?: TypographyType;
20
+ style?: CSSProperties;
21
+ }
22
+
23
+ export interface TkxTextProps {
24
+ children: ReactNode;
25
+ type?: TypographyType;
26
+ strong?: boolean;
27
+ italic?: boolean;
28
+ underline?: boolean;
29
+ delete?: boolean;
30
+ code?: boolean;
31
+ mark?: boolean;
32
+ copyable?: boolean;
33
+ style?: CSSProperties;
34
+ }
35
+
36
+ export interface TkxParagraphProps {
37
+ children: ReactNode;
38
+ type?: 'default' | 'secondary';
39
+ copyable?: boolean;
40
+ ellipsis?: boolean | { rows?: number };
41
+ style?: CSSProperties;
42
+ }
43
+
44
+ // ── Helpers ─────────────────────────────────────────────────────────────────
45
+
46
+ function getTextContent(node: ReactNode): string {
47
+ if (typeof node === 'string') return node;
48
+ if (typeof node === 'number') return String(node);
49
+ if (Array.isArray(node)) return node.map(getTextContent).join('');
50
+ if (node && typeof node === 'object' && 'props' in node) {
51
+ return getTextContent((node as { props: { children?: ReactNode } }).props.children);
52
+ }
53
+ return '';
54
+ }
55
+
56
+ // ── CopyButton ──────────────────────────────────────────────────────────────
57
+
58
+ function CopyButton({ text }: { text: string }) {
59
+ const theme = useTheme();
60
+ const [copied, setCopied] = useState(false);
61
+
62
+ const handleCopy = useCallback(() => {
63
+ navigator.clipboard.writeText(text).then(() => {
64
+ setCopied(true);
65
+ setTimeout(() => setCopied(false), 2000);
66
+ });
67
+ }, [text]);
68
+
69
+ return createElement(
70
+ 'button',
71
+ {
72
+ type: 'button' as const,
73
+ onClick: handleCopy,
74
+ 'aria-label': copied ? 'Copied' : 'Copy to clipboard',
75
+ style: {
76
+ display: 'inline-flex',
77
+ alignItems: 'center',
78
+ marginLeft: '4px',
79
+ padding: '2px 6px',
80
+ border: 'none',
81
+ background: 'transparent',
82
+ color: copied ? theme.success : theme.textMuted,
83
+ cursor: 'pointer',
84
+ fontSize: '0.8em',
85
+ borderRadius: '4px',
86
+ transition: 'color 0.2s',
87
+ },
88
+ },
89
+ copied ? 'Copied!' : '\u2398',
90
+ );
91
+ }
92
+
93
+ // ── Color Map ───────────────────────────────────────────────────────────────
94
+
95
+ function useTypeColor(type: TypographyType = 'default') {
96
+ const theme = useTheme();
97
+ const map: Record<TypographyType, string> = {
98
+ default: theme.text,
99
+ secondary: theme.textMuted,
100
+ success: theme.success,
101
+ warning: theme.warning,
102
+ danger: theme.danger,
103
+ };
104
+ return map[type];
105
+ }
106
+
107
+ // ── Title Sizes ─────────────────────────────────────────────────────────────
108
+
109
+ const TITLE_SIZES: Record<number, { fontSize: string; lineHeight: string; fontWeight: number }> = {
110
+ 1: { fontSize: '2.25rem', lineHeight: '1.2', fontWeight: 700 },
111
+ 2: { fontSize: '1.875rem', lineHeight: '1.25', fontWeight: 700 },
112
+ 3: { fontSize: '1.5rem', lineHeight: '1.3', fontWeight: 600 },
113
+ 4: { fontSize: '1.25rem', lineHeight: '1.35', fontWeight: 600 },
114
+ 5: { fontSize: '1rem', lineHeight: '1.4', fontWeight: 600 },
115
+ };
116
+
117
+ // ── TkxTitle ────────────────────────────────────────────────────────────────
118
+
119
+ export function TkxTitle({
120
+ level = 1,
121
+ children,
122
+ copyable = false,
123
+ type = 'default',
124
+ style,
125
+ }: TkxTitleProps) {
126
+ const color = useTypeColor(type);
127
+ const theme = useTheme();
128
+ const sizeConfig = TITLE_SIZES[level];
129
+ const tag = `h${level}` as keyof JSX.IntrinsicElements;
130
+
131
+ const safeChildren = typeof children === 'string' ? sanitizeString(children) : children;
132
+ const textContent = getTextContent(children);
133
+
134
+ return createElement(
135
+ tag,
136
+ {
137
+ style: {
138
+ color,
139
+ fontSize: sizeConfig.fontSize,
140
+ lineHeight: sizeConfig.lineHeight,
141
+ fontWeight: sizeConfig.fontWeight,
142
+ margin: '0 0 0.5em 0',
143
+ fontFamily: 'inherit',
144
+ ...style,
145
+ },
146
+ },
147
+ safeChildren,
148
+ copyable && createElement(CopyButton, { text: textContent }),
149
+ );
150
+ }
151
+
152
+ // ── TkxText ─────────────────────────────────────────────────────────────────
153
+
154
+ export function TkxText({
155
+ children,
156
+ type = 'default',
157
+ strong = false,
158
+ italic = false,
159
+ underline = false,
160
+ delete: strikethrough = false,
161
+ code = false,
162
+ mark = false,
163
+ copyable = false,
164
+ style,
165
+ }: TkxTextProps) {
166
+ const color = useTypeColor(type);
167
+ const theme = useTheme();
168
+ const textContent = getTextContent(children);
169
+ const safeChildren = typeof children === 'string' ? sanitizeString(children) : children;
170
+
171
+ const decorations: string[] = [];
172
+ if (underline) decorations.push('underline');
173
+ if (strikethrough) decorations.push('line-through');
174
+
175
+ let content: ReactNode = safeChildren;
176
+
177
+ if (code) {
178
+ content = createElement(
179
+ 'code',
180
+ {
181
+ style: {
182
+ padding: '0.15em 0.4em',
183
+ fontSize: '0.875em',
184
+ backgroundColor: theme.surfaceAlt,
185
+ border: `1px solid ${theme.border}`,
186
+ borderRadius: '4px',
187
+ fontFamily: 'monospace',
188
+ },
189
+ },
190
+ content,
191
+ );
192
+ }
193
+
194
+ if (mark) {
195
+ content = createElement(
196
+ 'mark',
197
+ {
198
+ style: {
199
+ backgroundColor: `${theme.warning}33`,
200
+ color,
201
+ padding: '0 2px',
202
+ borderRadius: '2px',
203
+ },
204
+ },
205
+ content,
206
+ );
207
+ }
208
+
209
+ return createElement(
210
+ 'span',
211
+ {
212
+ style: {
213
+ color,
214
+ fontWeight: strong ? 600 : 'inherit',
215
+ fontStyle: italic ? 'italic' : 'normal',
216
+ textDecoration: decorations.length > 0 ? decorations.join(' ') : 'none',
217
+ ...style,
218
+ },
219
+ },
220
+ content,
221
+ copyable && createElement(CopyButton, { text: textContent }),
222
+ );
223
+ }
224
+
225
+ // ── TkxParagraph ────────────────────────────────────────────────────────────
226
+
227
+ export function TkxParagraph({
228
+ children,
229
+ type = 'default',
230
+ copyable = false,
231
+ ellipsis = false,
232
+ style,
233
+ }: TkxParagraphProps) {
234
+ const theme = useTheme();
235
+ const color = type === 'secondary' ? theme.textMuted : theme.text;
236
+ const textContent = getTextContent(children);
237
+ const safeChildren = typeof children === 'string' ? sanitizeString(children) : children;
238
+
239
+ const ellipsisStyle: CSSProperties = {};
240
+ if (ellipsis) {
241
+ const rows = typeof ellipsis === 'object' ? (ellipsis.rows ?? 3) : 3;
242
+ ellipsisStyle.display = '-webkit-box';
243
+ ellipsisStyle.WebkitLineClamp = rows;
244
+ ellipsisStyle.WebkitBoxOrient = 'vertical';
245
+ ellipsisStyle.overflow = 'hidden';
246
+ }
247
+
248
+ return createElement(
249
+ 'div',
250
+ {
251
+ style: {
252
+ color,
253
+ fontSize: '1rem',
254
+ lineHeight: '1.6',
255
+ margin: '0 0 1em 0',
256
+ ...ellipsisStyle,
257
+ ...style,
258
+ },
259
+ },
260
+ safeChildren,
261
+ copyable && createElement(CopyButton, { text: textContent }),
262
+ );
263
+ }
@@ -38,3 +38,23 @@ 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';
54
+ export * from './TkxForm';
55
+ export * from './TkxLayout';
56
+ export * from './TkxConfigProvider';
57
+ export * from './TkxTypography';
58
+ export * from './TkxSpin';
59
+ export * from './TkxEmpty';
60
+ export * from './TkxStatistic';