tekivex-ui 2.0.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 (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -0
  3. package/dist/index.cjs +70 -0
  4. package/dist/index.d.ts +89 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +11002 -0
  7. package/dist/src/a11y/index.d.ts +24 -0
  8. package/dist/src/a11y/index.d.ts.map +1 -0
  9. package/dist/src/components/TkxAccordion.d.ts +32 -0
  10. package/dist/src/components/TkxAccordion.d.ts.map +1 -0
  11. package/dist/src/components/TkxAlert.d.ts +14 -0
  12. package/dist/src/components/TkxAlert.d.ts.map +1 -0
  13. package/dist/src/components/TkxAvatar.d.ts +14 -0
  14. package/dist/src/components/TkxAvatar.d.ts.map +1 -0
  15. package/dist/src/components/TkxBadge.d.ts +12 -0
  16. package/dist/src/components/TkxBadge.d.ts.map +1 -0
  17. package/dist/src/components/TkxButton.d.ts +17 -0
  18. package/dist/src/components/TkxButton.d.ts.map +1 -0
  19. package/dist/src/components/TkxCard.d.ts +20 -0
  20. package/dist/src/components/TkxCard.d.ts.map +1 -0
  21. package/dist/src/components/TkxCarousel.d.ts +32 -0
  22. package/dist/src/components/TkxCarousel.d.ts.map +1 -0
  23. package/dist/src/components/TkxChat.d.ts +32 -0
  24. package/dist/src/components/TkxChat.d.ts.map +1 -0
  25. package/dist/src/components/TkxCheckbox.d.ts +13 -0
  26. package/dist/src/components/TkxCheckbox.d.ts.map +1 -0
  27. package/dist/src/components/TkxClock.d.ts +23 -0
  28. package/dist/src/components/TkxClock.d.ts.map +1 -0
  29. package/dist/src/components/TkxColorPicker.d.ts +18 -0
  30. package/dist/src/components/TkxColorPicker.d.ts.map +1 -0
  31. package/dist/src/components/TkxCommand.d.ts +34 -0
  32. package/dist/src/components/TkxCommand.d.ts.map +1 -0
  33. package/dist/src/components/TkxDatePicker.d.ts +47 -0
  34. package/dist/src/components/TkxDatePicker.d.ts.map +1 -0
  35. package/dist/src/components/TkxDivider.d.ts +8 -0
  36. package/dist/src/components/TkxDivider.d.ts.map +1 -0
  37. package/dist/src/components/TkxDrawer.d.ts +16 -0
  38. package/dist/src/components/TkxDrawer.d.ts.map +1 -0
  39. package/dist/src/components/TkxFileUpload.d.ts +16 -0
  40. package/dist/src/components/TkxFileUpload.d.ts.map +1 -0
  41. package/dist/src/components/TkxIcon.d.ts +16 -0
  42. package/dist/src/components/TkxIcon.d.ts.map +1 -0
  43. package/dist/src/components/TkxImage.d.ts +16 -0
  44. package/dist/src/components/TkxImage.d.ts.map +1 -0
  45. package/dist/src/components/TkxInput.d.ts +13 -0
  46. package/dist/src/components/TkxInput.d.ts.map +1 -0
  47. package/dist/src/components/TkxLogo.d.ts +21 -0
  48. package/dist/src/components/TkxLogo.d.ts.map +1 -0
  49. package/dist/src/components/TkxMenu.d.ts +64 -0
  50. package/dist/src/components/TkxMenu.d.ts.map +1 -0
  51. package/dist/src/components/TkxModal.d.ts +14 -0
  52. package/dist/src/components/TkxModal.d.ts.map +1 -0
  53. package/dist/src/components/TkxNumberInput.d.ts +32 -0
  54. package/dist/src/components/TkxNumberInput.d.ts.map +1 -0
  55. package/dist/src/components/TkxOTP.d.ts +24 -0
  56. package/dist/src/components/TkxOTP.d.ts.map +1 -0
  57. package/dist/src/components/TkxPagination.d.ts +19 -0
  58. package/dist/src/components/TkxPagination.d.ts.map +1 -0
  59. package/dist/src/components/TkxProgress.d.ts +13 -0
  60. package/dist/src/components/TkxProgress.d.ts.map +1 -0
  61. package/dist/src/components/TkxRadio.d.ts +22 -0
  62. package/dist/src/components/TkxRadio.d.ts.map +1 -0
  63. package/dist/src/components/TkxRating.d.ts +18 -0
  64. package/dist/src/components/TkxRating.d.ts.map +1 -0
  65. package/dist/src/components/TkxSelect.d.ts +37 -0
  66. package/dist/src/components/TkxSelect.d.ts.map +1 -0
  67. package/dist/src/components/TkxSkeleton.d.ts +12 -0
  68. package/dist/src/components/TkxSkeleton.d.ts.map +1 -0
  69. package/dist/src/components/TkxSlider.d.ts +33 -0
  70. package/dist/src/components/TkxSlider.d.ts.map +1 -0
  71. package/dist/src/components/TkxStepper.d.ts +31 -0
  72. package/dist/src/components/TkxStepper.d.ts.map +1 -0
  73. package/dist/src/components/TkxTable.d.ts +23 -0
  74. package/dist/src/components/TkxTable.d.ts.map +1 -0
  75. package/dist/src/components/TkxTabs.d.ts +32 -0
  76. package/dist/src/components/TkxTabs.d.ts.map +1 -0
  77. package/dist/src/components/TkxTag.d.ts +30 -0
  78. package/dist/src/components/TkxTag.d.ts.map +1 -0
  79. package/dist/src/components/TkxTimeline.d.ts +20 -0
  80. package/dist/src/components/TkxTimeline.d.ts.map +1 -0
  81. package/dist/src/components/TkxToast.d.ts +23 -0
  82. package/dist/src/components/TkxToast.d.ts.map +1 -0
  83. package/dist/src/components/TkxToggle.d.ts +11 -0
  84. package/dist/src/components/TkxToggle.d.ts.map +1 -0
  85. package/dist/src/components/TkxTooltip.d.ts +10 -0
  86. package/dist/src/components/TkxTooltip.d.ts.map +1 -0
  87. package/dist/src/components/TkxVideoPlayer.d.ts +43 -0
  88. package/dist/src/components/TkxVideoPlayer.d.ts.map +1 -0
  89. package/dist/src/components/index.d.ts +41 -0
  90. package/dist/src/components/index.d.ts.map +1 -0
  91. package/dist/src/engine/css.d.ts +24 -0
  92. package/dist/src/engine/css.d.ts.map +1 -0
  93. package/dist/src/engine/index.d.ts +10 -0
  94. package/dist/src/engine/index.d.ts.map +1 -0
  95. package/dist/src/engine/quantum.d.ts +47 -0
  96. package/dist/src/engine/quantum.d.ts.map +1 -0
  97. package/dist/src/engine/security.d.ts +54 -0
  98. package/dist/src/engine/security.d.ts.map +1 -0
  99. package/dist/src/engine/tkx.d.ts +29 -0
  100. package/dist/src/engine/tkx.d.ts.map +1 -0
  101. package/dist/src/engine/wcag.d.ts +34 -0
  102. package/dist/src/engine/wcag.d.ts.map +1 -0
  103. package/dist/src/hooks/index.d.ts +8 -0
  104. package/dist/src/hooks/index.d.ts.map +1 -0
  105. package/dist/src/themes/index.d.ts +26 -0
  106. package/dist/src/themes/index.d.ts.map +1 -0
  107. package/dist/style.css +1 -0
  108. package/dist/tests/TkxButton.test.d.ts +2 -0
  109. package/dist/tests/TkxButton.test.d.ts.map +1 -0
  110. package/dist/tests/a11y/components.test.d.ts +2 -0
  111. package/dist/tests/a11y/components.test.d.ts.map +1 -0
  112. package/dist/tests/quantum.test.d.ts +2 -0
  113. package/dist/tests/quantum.test.d.ts.map +1 -0
  114. package/dist/tests/security.test.d.ts +2 -0
  115. package/dist/tests/security.test.d.ts.map +1 -0
  116. package/dist/tests/setup.d.ts +2 -0
  117. package/dist/tests/setup.d.ts.map +1 -0
  118. package/dist/tests/tkx.test.d.ts +2 -0
  119. package/dist/tests/tkx.test.d.ts.map +1 -0
  120. package/dist/tests/wcag.test.d.ts +2 -0
  121. package/dist/tests/wcag.test.d.ts.map +1 -0
  122. package/package.json +78 -0
  123. package/src/a11y/index.tsx +87 -0
  124. package/src/components/TkxAccordion.tsx +588 -0
  125. package/src/components/TkxAlert.tsx +91 -0
  126. package/src/components/TkxAvatar.tsx +101 -0
  127. package/src/components/TkxBadge.tsx +73 -0
  128. package/src/components/TkxButton.tsx +153 -0
  129. package/src/components/TkxCard.tsx +92 -0
  130. package/src/components/TkxCarousel.tsx +552 -0
  131. package/src/components/TkxChat.tsx +354 -0
  132. package/src/components/TkxCheckbox.tsx +218 -0
  133. package/src/components/TkxClock.tsx +601 -0
  134. package/src/components/TkxColorPicker.tsx +633 -0
  135. package/src/components/TkxCommand.tsx +644 -0
  136. package/src/components/TkxDatePicker.tsx +1567 -0
  137. package/src/components/TkxDivider.tsx +49 -0
  138. package/src/components/TkxDrawer.tsx +234 -0
  139. package/src/components/TkxFileUpload.tsx +371 -0
  140. package/src/components/TkxIcon.tsx +683 -0
  141. package/src/components/TkxImage.tsx +277 -0
  142. package/src/components/TkxInput.tsx +101 -0
  143. package/src/components/TkxLogo.tsx +297 -0
  144. package/src/components/TkxMenu.tsx +1042 -0
  145. package/src/components/TkxModal.tsx +119 -0
  146. package/src/components/TkxNumberInput.tsx +408 -0
  147. package/src/components/TkxOTP.tsx +295 -0
  148. package/src/components/TkxPagination.tsx +388 -0
  149. package/src/components/TkxProgress.tsx +114 -0
  150. package/src/components/TkxRadio.tsx +284 -0
  151. package/src/components/TkxRating.tsx +286 -0
  152. package/src/components/TkxSelect.tsx +923 -0
  153. package/src/components/TkxSkeleton.tsx +105 -0
  154. package/src/components/TkxSlider.tsx +492 -0
  155. package/src/components/TkxStepper.tsx +485 -0
  156. package/src/components/TkxTable.tsx +148 -0
  157. package/src/components/TkxTabs.tsx +124 -0
  158. package/src/components/TkxTag.tsx +319 -0
  159. package/src/components/TkxTimeline.tsx +303 -0
  160. package/src/components/TkxToast.tsx +284 -0
  161. package/src/components/TkxToggle.tsx +93 -0
  162. package/src/components/TkxTooltip.tsx +69 -0
  163. package/src/components/TkxVideoPlayer.tsx +1013 -0
  164. package/src/components/index.ts +40 -0
  165. package/src/engine/css.ts +126 -0
  166. package/src/engine/index.ts +10 -0
  167. package/src/engine/quantum.ts +207 -0
  168. package/src/engine/security.ts +217 -0
  169. package/src/engine/tkx.ts +874 -0
  170. package/src/engine/wcag.ts +275 -0
  171. package/src/hooks/index.ts +120 -0
  172. package/src/styles/global.css +167 -0
  173. package/src/themes/index.ts +111 -0
@@ -0,0 +1,485 @@
1
+ import {
2
+ type CSSProperties,
3
+ type ReactNode,
4
+ } from 'react';
5
+ import { useTheme } from '../themes';
6
+ import { sanitizeString } from '../engine/security';
7
+ import { useReducedMotion } from '../hooks';
8
+
9
+ // ── Types ────────────────────────────────────────────────────────────────────
10
+
11
+ export type StepStatus = 'completed' | 'active' | 'error' | 'pending';
12
+ export type StepperOrientation = 'horizontal' | 'vertical';
13
+
14
+ export interface Step {
15
+ id: string;
16
+ title: string;
17
+ description?: string;
18
+ icon?: ReactNode;
19
+ status?: StepStatus;
20
+ optional?: boolean;
21
+ error?: string;
22
+ }
23
+
24
+ export interface TkxStepperProps {
25
+ steps: Step[];
26
+ activeStep?: number;
27
+ orientation?: StepperOrientation;
28
+ variant?: 'default' | 'outlined' | 'filled';
29
+ size?: 'sm' | 'md' | 'lg';
30
+ clickable?: boolean;
31
+ onStepClick?: (index: number) => void;
32
+ showStepNumbers?: boolean;
33
+ alternateLabel?: boolean;
34
+ connector?: 'solid' | 'dashed' | 'dotted';
35
+ className?: string;
36
+ style?: CSSProperties;
37
+ }
38
+
39
+ // ── Size constants ────────────────────────────────────────────────────────────
40
+
41
+ const SIZE_MAP = {
42
+ sm: { circle: 24, fontSize: '0.75rem', titleSize: '0.8rem', descSize: '0.7rem', connectorWidth: 2 },
43
+ md: { circle: 32, fontSize: '0.875rem', titleSize: '0.9rem', descSize: '0.775rem', connectorWidth: 2 },
44
+ lg: { circle: 40, fontSize: '1rem', titleSize: '1rem', descSize: '0.85rem', connectorWidth: 3 },
45
+ };
46
+
47
+ // ── Icon SVGs ────────────────────────────────────────────────────────────────
48
+
49
+ function CheckIcon({ size }: { size: number }) {
50
+ const s = size * 0.55;
51
+ return (
52
+ <svg width={s} height={s} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
53
+ <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" />
54
+ </svg>
55
+ );
56
+ }
57
+
58
+ function XIcon({ size }: { size: number }) {
59
+ const s = size * 0.55;
60
+ return (
61
+ <svg width={s} height={s} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
62
+ <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
63
+ </svg>
64
+ );
65
+ }
66
+
67
+ // ── Step circle ───────────────────────────────────────────────────────────────
68
+
69
+ interface StepCircleProps {
70
+ status: StepStatus;
71
+ index: number;
72
+ showStepNumbers: boolean;
73
+ icon?: ReactNode;
74
+ circleSize: number;
75
+ fontSize: string;
76
+ primary: string;
77
+ danger: string;
78
+ success: string;
79
+ text: string;
80
+ textMuted: string;
81
+ border: string;
82
+ surface: string;
83
+ variant: 'default' | 'outlined' | 'filled';
84
+ reducedMotion: boolean;
85
+ clickable: boolean;
86
+ }
87
+
88
+ function StepCircle({
89
+ status,
90
+ index,
91
+ showStepNumbers,
92
+ icon,
93
+ circleSize,
94
+ fontSize,
95
+ primary,
96
+ danger,
97
+ success,
98
+ text,
99
+ textMuted,
100
+ border,
101
+ surface,
102
+ variant,
103
+ reducedMotion,
104
+ clickable,
105
+ }: StepCircleProps) {
106
+ const isCompleted = status === 'completed';
107
+ const isActive = status === 'active';
108
+ const isError = status === 'error';
109
+ const isPending = status === 'pending';
110
+
111
+ let bgColor = surface;
112
+ let borderColor = border;
113
+ let color = textMuted;
114
+ let borderWidth = 2;
115
+
116
+ if (isCompleted) {
117
+ bgColor = variant === 'filled' ? success : surface;
118
+ borderColor = success;
119
+ color = variant === 'filled' ? '#fff' : success;
120
+ } else if (isActive) {
121
+ bgColor = variant === 'outlined' ? surface : primary;
122
+ borderColor = primary;
123
+ color = variant === 'outlined' ? primary : '#fff';
124
+ borderWidth = 2;
125
+ } else if (isError) {
126
+ bgColor = variant === 'filled' ? danger : surface;
127
+ borderColor = danger;
128
+ color = variant === 'filled' ? '#fff' : danger;
129
+ } else if (isPending) {
130
+ bgColor = surface;
131
+ borderColor = border;
132
+ color = textMuted;
133
+ }
134
+
135
+ const pulseStyle: CSSProperties = isActive && !reducedMotion
136
+ ? {
137
+ boxShadow: `0 0 0 4px ${primary}30`,
138
+ animation: 'tkxstepper-pulse 2s ease-in-out infinite',
139
+ }
140
+ : {};
141
+
142
+ const hoverStyle: CSSProperties = clickable
143
+ ? { cursor: 'pointer', transition: 'transform 0.15s ease, box-shadow 0.15s ease' }
144
+ : {};
145
+
146
+ let content: ReactNode;
147
+ if (icon && !isCompleted && !isError) {
148
+ content = <span style={{ fontSize, lineHeight: 1 }}>{icon}</span>;
149
+ } else if (isCompleted) {
150
+ content = <CheckIcon size={circleSize} />;
151
+ } else if (isError) {
152
+ content = <XIcon size={circleSize} />;
153
+ } else if (showStepNumbers) {
154
+ content = <span style={{ fontSize, fontWeight: 600, lineHeight: 1 }}>{index + 1}</span>;
155
+ } else {
156
+ content = <span style={{ fontSize: circleSize * 0.22, lineHeight: 1, background: color, borderRadius: '50%', width: circleSize * 0.22, height: circleSize * 0.22, display: 'block' }} />;
157
+ }
158
+
159
+ return (
160
+ <>
161
+ <style>{`
162
+ @keyframes tkxstepper-pulse {
163
+ 0%, 100% { box-shadow: 0 0 0 0 ${primary}50; }
164
+ 50% { box-shadow: 0 0 0 6px ${primary}20; }
165
+ }
166
+ `}</style>
167
+ <div
168
+ style={{
169
+ width: circleSize,
170
+ height: circleSize,
171
+ minWidth: circleSize,
172
+ borderRadius: '50%',
173
+ border: `${borderWidth}px solid ${borderColor}`,
174
+ backgroundColor: bgColor,
175
+ color,
176
+ display: 'flex',
177
+ alignItems: 'center',
178
+ justifyContent: 'center',
179
+ flexShrink: 0,
180
+ transition: reducedMotion ? 'none' : 'background-color 0.2s, border-color 0.2s',
181
+ ...pulseStyle,
182
+ ...hoverStyle,
183
+ }}
184
+ >
185
+ {content}
186
+ </div>
187
+ </>
188
+ );
189
+ }
190
+
191
+ // ── Connector line ─────────────────────────────────────────────────────────────
192
+
193
+ interface ConnectorProps {
194
+ completed: boolean;
195
+ orientation: StepperOrientation;
196
+ connectorStyle: 'solid' | 'dashed' | 'dotted';
197
+ connectorWidth: number;
198
+ primary: string;
199
+ border: string;
200
+ reducedMotion: boolean;
201
+ }
202
+
203
+ function Connector({
204
+ completed,
205
+ orientation,
206
+ connectorStyle,
207
+ connectorWidth,
208
+ primary,
209
+ border,
210
+ reducedMotion,
211
+ }: ConnectorProps) {
212
+ const color = completed ? primary : border;
213
+
214
+ if (orientation === 'horizontal') {
215
+ return (
216
+ <div
217
+ aria-hidden="true"
218
+ style={{
219
+ flex: 1,
220
+ height: connectorWidth,
221
+ background: connectorStyle === 'solid'
222
+ ? color
223
+ : 'none',
224
+ backgroundImage: connectorStyle !== 'solid'
225
+ ? `repeating-linear-gradient(90deg, ${color} 0, ${color} 6px, transparent 6px, transparent ${connectorStyle === 'dashed' ? 12 : 9}px)`
226
+ : 'none',
227
+ borderRadius: 1,
228
+ transition: reducedMotion ? 'none' : 'background 0.3s ease',
229
+ alignSelf: 'center',
230
+ marginLeft: 4,
231
+ marginRight: 4,
232
+ }}
233
+ />
234
+ );
235
+ }
236
+
237
+ return (
238
+ <div
239
+ aria-hidden="true"
240
+ style={{
241
+ width: connectorWidth,
242
+ flex: 1,
243
+ minHeight: 20,
244
+ background: connectorStyle === 'solid'
245
+ ? color
246
+ : 'none',
247
+ backgroundImage: connectorStyle !== 'solid'
248
+ ? `repeating-linear-gradient(180deg, ${color} 0, ${color} 6px, transparent 6px, transparent ${connectorStyle === 'dashed' ? 12 : 9}px)`
249
+ : 'none',
250
+ borderRadius: 1,
251
+ transition: reducedMotion ? 'none' : 'background 0.3s ease',
252
+ alignSelf: 'stretch',
253
+ marginTop: 4,
254
+ marginBottom: 4,
255
+ marginLeft: 'auto',
256
+ marginRight: 'auto',
257
+ }}
258
+ />
259
+ );
260
+ }
261
+
262
+ // ── Main component ────────────────────────────────────────────────────────────
263
+
264
+ export function TkxStepper({
265
+ steps,
266
+ activeStep = 0,
267
+ orientation = 'horizontal',
268
+ variant = 'default',
269
+ size = 'md',
270
+ clickable = false,
271
+ onStepClick,
272
+ showStepNumbers = true,
273
+ alternateLabel = false,
274
+ connector = 'solid',
275
+ className,
276
+ style,
277
+ }: TkxStepperProps) {
278
+ const theme = useTheme();
279
+ const reducedMotion = useReducedMotion();
280
+
281
+ const sizes = SIZE_MAP[size];
282
+
283
+ const resolveStatus = (step: Step, index: number): StepStatus => {
284
+ if (step.status) return step.status;
285
+ if (index < activeStep) return 'completed';
286
+ if (index === activeStep) return 'active';
287
+ return 'pending';
288
+ };
289
+
290
+ const isHorizontal = orientation === 'horizontal';
291
+
292
+ return (
293
+ <div
294
+ className={className}
295
+ role="list"
296
+ aria-label="Steps"
297
+ style={{
298
+ display: 'flex',
299
+ flexDirection: isHorizontal ? 'row' : 'column',
300
+ alignItems: isHorizontal ? (alternateLabel ? 'flex-start' : 'center') : 'stretch',
301
+ width: '100%',
302
+ gap: 0,
303
+ ...style,
304
+ }}
305
+ >
306
+ {steps.map((step, i) => {
307
+ const status = resolveStatus(step, i);
308
+ const isLast = i === steps.length - 1;
309
+ const isCompleted = status === 'completed' || i < activeStep;
310
+ const isEven = i % 2 === 0;
311
+
312
+ const handleClick = () => {
313
+ if (clickable) onStepClick?.(i);
314
+ };
315
+
316
+ const circle = (
317
+ <StepCircle
318
+ status={status}
319
+ index={i}
320
+ showStepNumbers={showStepNumbers}
321
+ icon={step.icon}
322
+ circleSize={sizes.circle}
323
+ fontSize={sizes.fontSize}
324
+ primary={theme.primary}
325
+ danger={theme.danger}
326
+ success={theme.success}
327
+ text={theme.text}
328
+ textMuted={theme.textMuted}
329
+ border={theme.border}
330
+ surface={theme.surface}
331
+ variant={variant}
332
+ reducedMotion={reducedMotion}
333
+ clickable={clickable}
334
+ />
335
+ );
336
+
337
+ const labelBlock = (
338
+ <div
339
+ style={{
340
+ textAlign: isHorizontal && !alternateLabel ? 'center' : 'left',
341
+ maxWidth: isHorizontal ? 120 : undefined,
342
+ }}
343
+ >
344
+ <div
345
+ style={{
346
+ fontSize: sizes.titleSize,
347
+ fontWeight: status === 'active' ? 600 : 500,
348
+ color: status === 'pending' ? theme.textMuted : status === 'error' ? theme.danger : theme.text,
349
+ lineHeight: 1.3,
350
+ transition: reducedMotion ? 'none' : 'color 0.2s',
351
+ }}
352
+ >
353
+ {sanitizeString(step.title)}
354
+ {step.optional && (
355
+ <span style={{ fontSize: sizes.descSize, color: theme.textMuted, fontWeight: 400, marginLeft: 4 }}>
356
+ (optional)
357
+ </span>
358
+ )}
359
+ </div>
360
+ {step.description && (
361
+ <div
362
+ style={{
363
+ fontSize: sizes.descSize,
364
+ color: theme.textMuted,
365
+ marginTop: 2,
366
+ lineHeight: 1.4,
367
+ }}
368
+ >
369
+ {sanitizeString(step.description)}
370
+ </div>
371
+ )}
372
+ {step.error && status === 'error' && (
373
+ <div
374
+ style={{
375
+ fontSize: sizes.descSize,
376
+ color: theme.danger,
377
+ marginTop: 2,
378
+ }}
379
+ >
380
+ {sanitizeString(step.error)}
381
+ </div>
382
+ )}
383
+ </div>
384
+ );
385
+
386
+ if (isHorizontal) {
387
+ return (
388
+ <div
389
+ key={step.id}
390
+ role="listitem"
391
+ style={{ display: 'flex', flex: isLast ? 0 : 1, alignItems: 'center', minWidth: 0 }}
392
+ >
393
+ {/* Step node */}
394
+ <div
395
+ onClick={handleClick}
396
+ style={{
397
+ display: 'flex',
398
+ flexDirection: 'column',
399
+ alignItems: 'center',
400
+ gap: 6,
401
+ cursor: clickable ? 'pointer' : 'default',
402
+ flexShrink: 0,
403
+ }}
404
+ aria-current={status === 'active' ? 'step' : undefined}
405
+ >
406
+ {alternateLabel && !isEven && (
407
+ <div style={{ minHeight: sizes.circle * 1.5 + 6, display: 'flex', alignItems: 'flex-end', paddingBottom: 6 }}>
408
+ {labelBlock}
409
+ </div>
410
+ )}
411
+ {circle}
412
+ {!alternateLabel && labelBlock}
413
+ {alternateLabel && isEven && (
414
+ <div style={{ marginTop: 6 }}>{labelBlock}</div>
415
+ )}
416
+ </div>
417
+
418
+ {/* Connector */}
419
+ {!isLast && (
420
+ <Connector
421
+ completed={isCompleted}
422
+ orientation="horizontal"
423
+ connectorStyle={connector}
424
+ connectorWidth={sizes.connectorWidth}
425
+ primary={theme.primary}
426
+ border={theme.border}
427
+ reducedMotion={reducedMotion}
428
+ />
429
+ )}
430
+ </div>
431
+ );
432
+ }
433
+
434
+ // Vertical
435
+ return (
436
+ <div key={step.id} role="listitem" style={{ display: 'flex', flexDirection: 'row', minHeight: 0 }}>
437
+ {/* Left column: circle + connector */}
438
+ <div
439
+ style={{
440
+ display: 'flex',
441
+ flexDirection: 'column',
442
+ alignItems: 'center',
443
+ marginRight: 14,
444
+ flexShrink: 0,
445
+ }}
446
+ >
447
+ <div
448
+ onClick={handleClick}
449
+ style={{ cursor: clickable ? 'pointer' : 'default' }}
450
+ aria-current={status === 'active' ? 'step' : undefined}
451
+ >
452
+ {circle}
453
+ </div>
454
+ {!isLast && (
455
+ <Connector
456
+ completed={isCompleted}
457
+ orientation="vertical"
458
+ connectorStyle={connector}
459
+ connectorWidth={sizes.connectorWidth}
460
+ primary={theme.primary}
461
+ border={theme.border}
462
+ reducedMotion={reducedMotion}
463
+ />
464
+ )}
465
+ </div>
466
+
467
+ {/* Right column: label */}
468
+ <div
469
+ style={{
470
+ paddingTop: (sizes.circle - parseFloat(sizes.titleSize) * 16) / 2,
471
+ paddingBottom: isLast ? 0 : 16,
472
+ flex: 1,
473
+ minWidth: 0,
474
+ }}
475
+ >
476
+ {labelBlock}
477
+ </div>
478
+ </div>
479
+ );
480
+ })}
481
+ </div>
482
+ );
483
+ }
484
+
485
+ TkxStepper.displayName = 'TkxStepper';
@@ -0,0 +1,148 @@
1
+ import { useState, useMemo, type ReactNode } from 'react';
2
+ import { useTheme } from '../themes';
3
+ import { sanitizeString } from '../engine/security';
4
+ import { TkxSkeleton } from './TkxSkeleton';
5
+ import { tkx } from '../engine/tkx';
6
+
7
+ export type SortDirection = 'asc' | 'desc' | 'none';
8
+
9
+ export interface ColumnDef<T> {
10
+ key: keyof T;
11
+ header: string;
12
+ render?: (value: T[keyof T], row: T) => ReactNode;
13
+ width?: string;
14
+ sortable?: boolean;
15
+ }
16
+
17
+ export interface TkxTableProps<T extends Record<string, unknown>> {
18
+ columns: ColumnDef<T>[];
19
+ data: T[];
20
+ caption?: string;
21
+ sortable?: boolean;
22
+ stickyHeader?: boolean;
23
+ isLoading?: boolean;
24
+ emptyState?: ReactNode;
25
+ style?: React.CSSProperties;
26
+ className?: string;
27
+ striped?: boolean;
28
+ }
29
+
30
+ function SortIcon({ dir }: { dir: SortDirection }) {
31
+ return (
32
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={tkx('shrink-0')}>
33
+ {dir === 'asc' ? <path d="M7 14l5-5 5 5H7z" /> : dir === 'desc' ? <path d="M7 10l5 5 5-5H7z" /> : <path d="M7 10l5 5 5-5H7zM7 14l5-5 5 5H7z" opacity="0.4" />}
34
+ </svg>
35
+ );
36
+ }
37
+
38
+ export function TkxTable<T extends Record<string, unknown>>({
39
+ columns, data, caption, sortable, stickyHeader, isLoading, emptyState, style, className, striped,
40
+ }: TkxTableProps<T>) {
41
+ const theme = useTheme();
42
+ const [sortKey, setSortKey] = useState<keyof T | null>(null);
43
+ const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
44
+
45
+ const sorted = useMemo(() => {
46
+ if (!sortKey || !sortable) return data;
47
+ return [...data].sort((a, b) => {
48
+ const av = a[sortKey], bv = b[sortKey];
49
+ let cmp = 0;
50
+ if (typeof av === 'string' && typeof bv === 'string') cmp = av.localeCompare(bv);
51
+ else if (typeof av === 'number' && typeof bv === 'number') cmp = av - bv;
52
+ return sortDir === 'asc' ? cmp : -cmp;
53
+ });
54
+ }, [data, sortKey, sortDir, sortable]);
55
+
56
+ function handleSort(key: keyof T) {
57
+ if (sortKey === key) setSortDir((d) => d === 'asc' ? 'desc' : 'asc');
58
+ else { setSortKey(key); setSortDir('asc'); }
59
+ }
60
+
61
+ function getDir(key: keyof T): SortDirection {
62
+ return sortKey !== key ? 'none' : sortDir;
63
+ }
64
+
65
+ return (
66
+ <div
67
+ className={tkx('w-full overflow-x-auto rounded-lg', className ?? '')}
68
+ style={{ border: `1px solid ${theme.border}`, ...style }}
69
+ >
70
+ <table
71
+ className={tkx('w-full font-sans text-sm')}
72
+ style={{ borderCollapse: 'collapse', color: theme.text }}
73
+ >
74
+ {caption && (
75
+ <caption className={tkx('text-sm text-left p-2')} style={{ color: theme.textMuted }}>
76
+ {sanitizeString(caption)}
77
+ </caption>
78
+ )}
79
+
80
+ <thead style={stickyHeader ? { position: 'sticky', top: 0, backgroundColor: theme.surfaceAlt, zIndex: 1 } : { backgroundColor: theme.surfaceAlt }}>
81
+ <tr>
82
+ {columns.map((col) => {
83
+ const canSort = sortable && col.sortable !== false;
84
+ const dir = getDir(col.key);
85
+ return (
86
+ <th
87
+ key={String(col.key)}
88
+ scope="col"
89
+ aria-sort={canSort ? (dir === 'none' ? 'none' : dir === 'asc' ? 'ascending' : 'descending') : undefined}
90
+ className={tkx('px-4 py-3 text-left font-semibold whitespace-nowrap', canSort ? 'cursor-pointer select-none focus-visible:focus-ring' : '')}
91
+ style={{ color: theme.textMuted, borderBottom: `2px solid ${theme.border}`, width: col.width }}
92
+ onClick={() => canSort && handleSort(col.key)}
93
+ onKeyDown={(e) => { if (canSort && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); handleSort(col.key); } }}
94
+ tabIndex={canSort ? 0 : undefined}
95
+ >
96
+ <span className={tkx('inline-flex items-center gap-1.5')}>
97
+ {sanitizeString(col.header)}
98
+ {canSort && <SortIcon dir={dir} />}
99
+ </span>
100
+ </th>
101
+ );
102
+ })}
103
+ </tr>
104
+ </thead>
105
+
106
+ <tbody>
107
+ {isLoading
108
+ ? Array.from({ length: 5 }, (_, i) => (
109
+ <tr key={i}>
110
+ {columns.map((col) => (
111
+ <td key={String(col.key)} className={tkx('px-4 py-3')}>
112
+ <TkxSkeleton variant="text" height="16px" />
113
+ </td>
114
+ ))}
115
+ </tr>
116
+ ))
117
+ : sorted.length === 0
118
+ ? (
119
+ <tr>
120
+ <td colSpan={columns.length} className={tkx('px-4 py-8 text-center')} style={{ color: theme.textMuted }}>
121
+ {emptyState ?? 'No data available'}
122
+ </td>
123
+ </tr>
124
+ )
125
+ : sorted.map((row, ri) => (
126
+ <tr
127
+ key={ri}
128
+ style={{
129
+ backgroundColor: striped && ri % 2 === 1 ? theme.surfaceAlt : 'transparent',
130
+ borderBottom: `1px solid ${theme.border}`,
131
+ }}
132
+ >
133
+ {columns.map((col) => {
134
+ const raw = row[col.key];
135
+ const cell = col.render ? col.render(raw, row) : (typeof raw === 'string' ? sanitizeString(raw) : String(raw ?? ''));
136
+ return (
137
+ <td key={String(col.key)} className={tkx('px-4 py-3')} style={{ color: theme.text }}>
138
+ {cell}
139
+ </td>
140
+ );
141
+ })}
142
+ </tr>
143
+ ))}
144
+ </tbody>
145
+ </table>
146
+ </div>
147
+ );
148
+ }