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,394 @@
1
+ import {
2
+ useMemo,
3
+ type ReactNode,
4
+ type CSSProperties,
5
+ } from 'react';
6
+ import { useTheme } from '../themes';
7
+ import { sanitizeString } from '../engine/security';
8
+ import { useReducedMotion } from '../hooks';
9
+ import { tkx } from '../engine/tkx';
10
+
11
+ // ── Types ─────────────────────────────────────────────────────────────────────
12
+
13
+ export interface RichTextBlock {
14
+ type: 'heading' | 'paragraph' | 'blockquote' | 'code' | 'list' | 'divider' | 'image' | 'callout';
15
+ content?: string;
16
+ level?: 1 | 2 | 3;
17
+ language?: string;
18
+ items?: string[];
19
+ ordered?: boolean;
20
+ src?: string;
21
+ alt?: string;
22
+ variant?: 'info' | 'warning' | 'success' | 'danger';
23
+ }
24
+
25
+ export interface TkxRichTextDisplayProps {
26
+ blocks: RichTextBlock[];
27
+ className?: string;
28
+ style?: CSSProperties;
29
+ }
30
+
31
+ // ── Callout icons ───────────────────────────────────────────────────────────
32
+
33
+ const CALLOUT_ICONS: Record<string, ReactNode> = {
34
+ info: (
35
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
36
+ <path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z" />
37
+ </svg>
38
+ ),
39
+ warning: (
40
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
41
+ <path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z" />
42
+ </svg>
43
+ ),
44
+ success: (
45
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
46
+ <path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm-2 14.5l-4-4 1.41-1.41L10 13.67l6.59-6.59L18 8.5l-8 8z" />
47
+ </svg>
48
+ ),
49
+ danger: (
50
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
51
+ <path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" />
52
+ </svg>
53
+ ),
54
+ };
55
+
56
+ // ── Heading sizes ───────────────────────────────────────────────────────────
57
+
58
+ const HEADING_SIZES: Record<number, CSSProperties> = {
59
+ 1: { fontSize: '1.75rem', fontWeight: 700, lineHeight: 1.25, marginTop: 24, marginBottom: 12 },
60
+ 2: { fontSize: '1.375rem', fontWeight: 600, lineHeight: 1.3, marginTop: 20, marginBottom: 10 },
61
+ 3: { fontSize: '1.1rem', fontWeight: 600, lineHeight: 1.35, marginTop: 16, marginBottom: 8 },
62
+ };
63
+
64
+ // ── Block renderers ─────────────────────────────────────────────────────────
65
+
66
+ interface BlockRendererProps {
67
+ block: RichTextBlock;
68
+ index: number;
69
+ }
70
+
71
+ function HeadingBlock({ block }: BlockRendererProps) {
72
+ const theme = useTheme();
73
+ const level = block.level ?? 1;
74
+ const Tag = `h${level}` as 'h1' | 'h2' | 'h3';
75
+ const safeContent = sanitizeString(block.content ?? '');
76
+
77
+ return (
78
+ <Tag
79
+ className={tkx('m-0')}
80
+ style={{
81
+ ...HEADING_SIZES[level],
82
+ color: theme.text,
83
+ }}
84
+ >
85
+ {safeContent}
86
+ </Tag>
87
+ );
88
+ }
89
+
90
+ function ParagraphBlock({ block }: BlockRendererProps) {
91
+ const theme = useTheme();
92
+ const safeContent = sanitizeString(block.content ?? '');
93
+
94
+ return (
95
+ <p
96
+ className={tkx('m-0')}
97
+ style={{
98
+ fontSize: '0.938rem',
99
+ lineHeight: 1.7,
100
+ color: theme.text,
101
+ marginBottom: 12,
102
+ }}
103
+ >
104
+ {safeContent}
105
+ </p>
106
+ );
107
+ }
108
+
109
+ function BlockquoteBlock({ block }: BlockRendererProps) {
110
+ const theme = useTheme();
111
+ const safeContent = sanitizeString(block.content ?? '');
112
+
113
+ return (
114
+ <blockquote
115
+ className={tkx('m-0')}
116
+ style={{
117
+ borderLeft: `3px solid ${theme.primary}`,
118
+ paddingLeft: 16,
119
+ paddingTop: 8,
120
+ paddingBottom: 8,
121
+ marginTop: 12,
122
+ marginBottom: 12,
123
+ backgroundColor: `${theme.surfaceAlt}`,
124
+ borderRadius: '0 6px 6px 0',
125
+ color: theme.textMuted,
126
+ fontStyle: 'italic',
127
+ fontSize: '0.938rem',
128
+ lineHeight: 1.6,
129
+ }}
130
+ >
131
+ {safeContent}
132
+ </blockquote>
133
+ );
134
+ }
135
+
136
+ function CodeBlock({ block }: BlockRendererProps) {
137
+ const theme = useTheme();
138
+ const safeContent = sanitizeString(block.content ?? '');
139
+ const safeLanguage = block.language ? sanitizeString(block.language) : null;
140
+
141
+ return (
142
+ <div
143
+ className={tkx('rounded-lg overflow-hidden')}
144
+ style={{
145
+ marginTop: 12,
146
+ marginBottom: 12,
147
+ border: `1px solid ${theme.border}`,
148
+ backgroundColor: theme.surfaceAlt,
149
+ }}
150
+ >
151
+ {/* Language label */}
152
+ {safeLanguage && (
153
+ <div
154
+ className={tkx('px-3 py-1 text-[11px] font-semibold uppercase tracking-wider')}
155
+ style={{
156
+ color: theme.textMuted,
157
+ borderBottom: `1px solid ${theme.border}`,
158
+ backgroundColor: theme.surface,
159
+ }}
160
+ >
161
+ {safeLanguage}
162
+ </div>
163
+ )}
164
+
165
+ {/* Code content */}
166
+ <pre
167
+ className={tkx('m-0 p-4 overflow-x-auto')}
168
+ style={{
169
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
170
+ fontSize: '0.85rem',
171
+ lineHeight: 1.6,
172
+ color: theme.text,
173
+ tabSize: 2,
174
+ }}
175
+ >
176
+ <code aria-label={safeLanguage ? `Code block in ${safeLanguage}` : 'Code block'}>
177
+ {safeContent}
178
+ </code>
179
+ </pre>
180
+ </div>
181
+ );
182
+ }
183
+
184
+ function ListBlock({ block }: BlockRendererProps) {
185
+ const theme = useTheme();
186
+ const Tag = block.ordered ? 'ol' : 'ul';
187
+ const safeItems = (block.items ?? []).map((item) => sanitizeString(item));
188
+
189
+ return (
190
+ <Tag
191
+ className={tkx('m-0')}
192
+ style={{
193
+ paddingLeft: 24,
194
+ marginTop: 8,
195
+ marginBottom: 12,
196
+ color: theme.text,
197
+ fontSize: '0.938rem',
198
+ lineHeight: 1.7,
199
+ listStyleType: block.ordered ? 'decimal' : 'disc',
200
+ }}
201
+ role="list"
202
+ >
203
+ {safeItems.map((item, i) => (
204
+ <li
205
+ key={i}
206
+ role="listitem"
207
+ style={{ marginBottom: 4 }}
208
+ >
209
+ {item}
210
+ </li>
211
+ ))}
212
+ </Tag>
213
+ );
214
+ }
215
+
216
+ function DividerBlock() {
217
+ const theme = useTheme();
218
+ return (
219
+ <hr
220
+ aria-hidden="true"
221
+ style={{
222
+ border: 'none',
223
+ borderTop: `1px solid ${theme.border}`,
224
+ marginTop: 20,
225
+ marginBottom: 20,
226
+ }}
227
+ />
228
+ );
229
+ }
230
+
231
+ function ImageBlock({ block }: BlockRendererProps) {
232
+ const theme = useTheme();
233
+ const safeSrc = sanitizeString(block.src ?? '');
234
+ const safeAlt = sanitizeString(block.alt ?? 'Image');
235
+
236
+ return (
237
+ <figure
238
+ className={tkx('m-0')}
239
+ style={{ marginTop: 12, marginBottom: 12 }}
240
+ >
241
+ <img
242
+ src={safeSrc}
243
+ alt={safeAlt}
244
+ loading="lazy"
245
+ className={tkx('rounded-lg')}
246
+ style={{
247
+ maxWidth: '100%',
248
+ height: 'auto',
249
+ display: 'block',
250
+ border: `1px solid ${theme.border}`,
251
+ }}
252
+ />
253
+ {block.alt && (
254
+ <figcaption
255
+ className={tkx('text-xs mt-2 text-center')}
256
+ style={{ color: theme.textMuted }}
257
+ >
258
+ {safeAlt}
259
+ </figcaption>
260
+ )}
261
+ </figure>
262
+ );
263
+ }
264
+
265
+ function CalloutBlock({ block }: BlockRendererProps) {
266
+ const theme = useTheme();
267
+ const variant = block.variant ?? 'info';
268
+ const safeContent = sanitizeString(block.content ?? '');
269
+
270
+ const variantColorMap: Record<string, string> = {
271
+ info: theme.info,
272
+ warning: theme.warning,
273
+ success: theme.success,
274
+ danger: theme.danger,
275
+ };
276
+
277
+ const accentColor = variantColorMap[variant];
278
+ const icon = CALLOUT_ICONS[variant];
279
+
280
+ // Role mapping for semantics
281
+ const roleMap: Record<string, string> = {
282
+ info: 'note',
283
+ warning: 'alert',
284
+ success: 'status',
285
+ danger: 'alert',
286
+ };
287
+
288
+ return (
289
+ <div
290
+ role={roleMap[variant]}
291
+ className={tkx('flex gap-3 px-4 py-3 rounded-lg')}
292
+ style={{
293
+ backgroundColor: `${accentColor}12`,
294
+ border: `1px solid ${accentColor}33`,
295
+ marginTop: 12,
296
+ marginBottom: 12,
297
+ }}
298
+ >
299
+ <span
300
+ className={tkx('shrink-0 mt-[2px]')}
301
+ style={{ color: accentColor }}
302
+ >
303
+ {icon}
304
+ </span>
305
+ <div
306
+ className={tkx('flex-1')}
307
+ style={{
308
+ fontSize: '0.9rem',
309
+ lineHeight: 1.6,
310
+ color: theme.text,
311
+ }}
312
+ >
313
+ {safeContent}
314
+ </div>
315
+ </div>
316
+ );
317
+ }
318
+
319
+ // ── Block dispatcher ────────────────────────────────────────────────────────
320
+
321
+ const BLOCK_RENDERERS: Record<string, (props: BlockRendererProps) => ReactNode> = {
322
+ heading: HeadingBlock,
323
+ paragraph: ParagraphBlock,
324
+ blockquote: BlockquoteBlock,
325
+ code: CodeBlock,
326
+ list: ListBlock,
327
+ divider: DividerBlock,
328
+ image: ImageBlock,
329
+ callout: CalloutBlock,
330
+ };
331
+
332
+ // ── Main component ──────────────────────────────────────────────────────────
333
+
334
+ export function TkxRichTextDisplay({
335
+ blocks,
336
+ className,
337
+ style,
338
+ }: TkxRichTextDisplayProps) {
339
+ const theme = useTheme();
340
+ const reduced = useReducedMotion();
341
+
342
+ // Memoize the rendered blocks
343
+ const renderedBlocks = useMemo(() => {
344
+ return blocks.map((block, index) => {
345
+ const Renderer = BLOCK_RENDERERS[block.type];
346
+ if (!Renderer) return null;
347
+
348
+ return (
349
+ <div
350
+ key={index}
351
+ style={
352
+ reduced
353
+ ? undefined
354
+ : {
355
+ animation: `tkx-rich-text-fade-in 300ms ease both`,
356
+ animationDelay: `${Math.min(index * 30, 300)}ms`,
357
+ }
358
+ }
359
+ >
360
+ <Renderer block={block} index={index} />
361
+ </div>
362
+ );
363
+ });
364
+ }, [blocks, reduced]);
365
+
366
+ // Inject keyframes
367
+ useMemo(() => {
368
+ if (typeof document === 'undefined') return;
369
+ if (document.getElementById('tkx-rich-text-kf')) return;
370
+ const styleEl = document.createElement('style');
371
+ styleEl.id = 'tkx-rich-text-kf';
372
+ styleEl.textContent = `
373
+ @keyframes tkx-rich-text-fade-in {
374
+ from { opacity: 0; }
375
+ to { opacity: 1; }
376
+ }
377
+ `;
378
+ document.head.appendChild(styleEl);
379
+ }, []);
380
+
381
+ return (
382
+ <article
383
+ aria-label="Rich text content"
384
+ className={tkx('font-sans', className ?? '')}
385
+ style={{
386
+ color: theme.text,
387
+ maxWidth: 720,
388
+ ...style,
389
+ }}
390
+ >
391
+ {renderedBlocks}
392
+ </article>
393
+ );
394
+ }
@@ -609,6 +609,7 @@ export function TkxSelect({
609
609
  transition: 'background-color 80ms',
610
610
  opacity: opt.disabled ? 0.5 : 1,
611
611
  userSelect: 'none',
612
+ touchAction: 'manipulation',
612
613
  }}
613
614
  >
614
615
  {optionContent}
@@ -714,6 +715,7 @@ export function TkxSelect({
714
715
  outline: 'none',
715
716
  minHeight: size === 'sm' ? 34 : size === 'lg' ? 50 : 42,
716
717
  flexWrap: 'wrap',
718
+ touchAction: 'manipulation',
717
719
  }}
718
720
  >
719
721
  {/* Multi-select tags or single label */}
@@ -350,6 +350,7 @@ export function TkxSlider({
350
350
  backgroundColor: theme.border,
351
351
  cursor: isDisabled ? 'not-allowed' : 'pointer',
352
352
  userSelect: 'none',
353
+ touchAction: 'none',
353
354
  }}
354
355
  >
355
356
  {/* Fill */}