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,101 @@
1
+ import { forwardRef, useState, type ImgHTMLAttributes } from 'react';
2
+ import { useTheme } from '../themes';
3
+ import { getAccessibleForeground } from '../engine/wcag';
4
+ import { sanitizeString } from '../engine/security';
5
+ import { tkx, cx } from '../engine/tkx';
6
+
7
+ export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
8
+ export type AvatarShape = 'circle' | 'square';
9
+ export type AvatarStatus = 'online' | 'offline' | 'away' | 'busy';
10
+
11
+ export interface TkxAvatarProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src' | 'alt'> {
12
+ src?: string;
13
+ alt: string;
14
+ initials?: string;
15
+ size?: AvatarSize;
16
+ shape?: AvatarShape;
17
+ status?: AvatarStatus;
18
+ }
19
+
20
+ const SIZE_PX: Record<AvatarSize, number> = { xs: 24, sm: 32, md: 40, lg: 56, xl: 72 };
21
+ const STATUS_COLORS: Record<AvatarStatus, string> = {
22
+ online: '#06d6a0', offline: '#8888aa', away: '#ffbe0b', busy: '#f72585',
23
+ };
24
+
25
+ export const TkxAvatar = forwardRef<HTMLDivElement, TkxAvatarProps>(
26
+ ({ src, alt, initials, size = 'md', shape = 'circle', status, className, style, ...rest }, ref) => {
27
+ const theme = useTheme();
28
+ const [imageError, setImageError] = useState(false);
29
+ const px = SIZE_PX[size];
30
+ const isCircle = shape === 'circle';
31
+ const bgColor = theme.surfaceAlt;
32
+ const textColor = getAccessibleForeground(bgColor, [theme.text, theme.textMuted]);
33
+ const showImage = src && !imageError;
34
+ const showInitials = !showImage && initials;
35
+ const statusDotSize = Math.max(8, px * 0.22);
36
+
37
+ const containerClass = tkx(
38
+ 'relative inline-flex shrink-0',
39
+ isCircle ? 'rounded-full' : 'rounded-lg',
40
+ );
41
+
42
+ const mediaClass = tkx(
43
+ 'block object-cover',
44
+ isCircle ? 'rounded-full' : 'rounded-lg',
45
+ );
46
+
47
+ const fallbackClass = tkx(
48
+ 'flex items-center justify-center font-semibold font-sans select-none',
49
+ isCircle ? 'rounded-full' : 'rounded-lg',
50
+ );
51
+
52
+ return (
53
+ <div
54
+ ref={ref}
55
+ className={cx(containerClass, className)}
56
+ style={{ width: px, height: px, ...style }}
57
+ {...(rest as React.HTMLAttributes<HTMLDivElement>)}
58
+ >
59
+ {showImage ? (
60
+ <img
61
+ src={sanitizeString(src!)}
62
+ alt={sanitizeString(alt)}
63
+ onError={() => setImageError(true)}
64
+ className={mediaClass}
65
+ style={{ width: px, height: px }}
66
+ />
67
+ ) : (
68
+ <div
69
+ role="img"
70
+ aria-label={sanitizeString(alt)}
71
+ className={fallbackClass}
72
+ style={{ width: px, height: px, backgroundColor: bgColor, color: textColor, fontSize: px * 0.38 }}
73
+ >
74
+ {showInitials ? (
75
+ sanitizeString(initials!).slice(0, 2).toUpperCase()
76
+ ) : (
77
+ <svg width={px * 0.5} height={px * 0.5} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} aria-hidden="true">
78
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
79
+ <circle cx="12" cy="7" r="4" />
80
+ </svg>
81
+ )}
82
+ </div>
83
+ )}
84
+
85
+ {status && (
86
+ <span
87
+ aria-label={`Status: ${status}`}
88
+ className={tkx('absolute bottom-0 right-0 rounded-full')}
89
+ style={{
90
+ width: statusDotSize, height: statusDotSize,
91
+ backgroundColor: STATUS_COLORS[status],
92
+ border: `2px solid ${theme.bg}`,
93
+ }}
94
+ />
95
+ )}
96
+ </div>
97
+ );
98
+ },
99
+ );
100
+
101
+ TkxAvatar.displayName = 'TkxAvatar';
@@ -0,0 +1,73 @@
1
+ import { forwardRef, type HTMLAttributes } from 'react';
2
+ import { useTheme } from '../themes';
3
+ import { getAccessibleForeground } from '../engine/wcag';
4
+ import { sanitizeString } from '../engine/security';
5
+ import { tkx, cx } from '../engine/tkx';
6
+
7
+ export type BadgeVariant = 'default' | 'primary' | 'secondary' | 'danger' | 'warning' | 'success' | 'info';
8
+ export type BadgeSize = 'sm' | 'md' | 'lg';
9
+
10
+ export interface TkxBadgeProps extends HTMLAttributes<HTMLSpanElement> {
11
+ variant?: BadgeVariant;
12
+ size?: BadgeSize;
13
+ dot?: boolean;
14
+ pulse?: boolean;
15
+ outlined?: boolean;
16
+ }
17
+
18
+ const SIZE_CLASS: Record<BadgeSize, string> = {
19
+ sm: 'text-xs px-1.5 py-[2px] min-h-[16px]',
20
+ md: 'text-xs px-2 py-[3px] min-h-[20px]',
21
+ lg: 'text-sm px-3 py-1 min-h-[24px]',
22
+ };
23
+
24
+ const DOT_SIZE: Record<BadgeSize, string> = { sm: 'size-[6px]', md: 'size-2', lg: 'size-2.5' };
25
+
26
+ export const TkxBadge = forwardRef<HTMLSpanElement, TkxBadgeProps>(
27
+ ({ variant = 'default', size = 'md', dot = false, pulse = false, outlined = false, children, className, style, ...rest }, ref) => {
28
+ const theme = useTheme();
29
+
30
+ const colorMap: Record<BadgeVariant, string> = {
31
+ default: theme.border, primary: theme.primary, secondary: theme.secondary,
32
+ danger: theme.danger, warning: theme.warning, success: theme.success, info: theme.info,
33
+ };
34
+ const bgColor = colorMap[variant];
35
+ const textColor = getAccessibleForeground(bgColor, ['#ffffff', '#000000', theme.bg]);
36
+
37
+ const safeChildren = typeof children === 'string' ? sanitizeString(children) : children;
38
+
39
+ if (dot) {
40
+ return (
41
+ <span
42
+ ref={ref}
43
+ aria-label={typeof children === 'string' ? sanitizeString(children) : undefined}
44
+ className={cx(tkx('inline-block rounded-full', DOT_SIZE[size], pulse && 'animate-pulse'), className)}
45
+ style={{ backgroundColor: bgColor, ...style }}
46
+ {...rest}
47
+ />
48
+ );
49
+ }
50
+
51
+ return (
52
+ <span
53
+ ref={ref}
54
+ className={cx(tkx(
55
+ 'inline-flex items-center justify-center rounded-full font-semibold font-sans tracking-wide',
56
+ SIZE_CLASS[size],
57
+ pulse && 'animate-pulse',
58
+ ), className)}
59
+ style={{
60
+ backgroundColor: outlined ? 'transparent' : bgColor,
61
+ color: outlined ? bgColor : textColor,
62
+ border: outlined ? `1px solid ${bgColor}` : 'none',
63
+ ...style,
64
+ }}
65
+ {...rest}
66
+ >
67
+ {safeChildren}
68
+ </span>
69
+ );
70
+ },
71
+ );
72
+
73
+ TkxBadge.displayName = 'TkxBadge';
@@ -0,0 +1,153 @@
1
+ import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react';
2
+ import { useTheme } from '../themes';
3
+ import { getAccessibleForeground } from '../engine/wcag';
4
+ import { sanitizeString } from '../engine/security';
5
+ import { useReducedMotion } from '../hooks';
6
+ import { tkx, cx } from '../engine/tkx';
7
+
8
+ export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'link';
9
+ export type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
10
+ export type ButtonColorScheme = 'primary' | 'secondary' | 'danger' | 'warning' | 'success';
11
+
12
+ export interface TkxButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
13
+ variant?: ButtonVariant;
14
+ size?: ButtonSize;
15
+ colorScheme?: ButtonColorScheme;
16
+ isLoading?: boolean;
17
+ loadingText?: string;
18
+ leftIcon?: ReactNode;
19
+ rightIcon?: ReactNode;
20
+ isFullWidth?: boolean;
21
+ glow?: boolean;
22
+ }
23
+
24
+ const SIZE_CLASS: Record<ButtonSize, string> = {
25
+ sm: 'text-xs font-semibold',
26
+ md: 'text-sm font-semibold',
27
+ lg: 'text-base font-semibold',
28
+ xl: 'text-lg font-semibold',
29
+ };
30
+
31
+ const SIZE_PADDING: Record<ButtonSize, string> = {
32
+ sm: 'py-1.5 px-3',
33
+ md: 'py-2 px-4',
34
+ lg: 'py-2.5 px-5',
35
+ xl: 'py-3 px-6',
36
+ };
37
+
38
+ const SIZE_MIN_H: Record<ButtonSize, string> = {
39
+ sm: 'min-h-[32px]',
40
+ md: 'min-h-[40px]',
41
+ lg: 'min-h-[48px]',
42
+ xl: 'min-h-[56px]',
43
+ };
44
+
45
+ const SPINNER_SIZE: Record<ButtonSize, number> = { sm: 14, md: 16, lg: 18, xl: 20 };
46
+
47
+ function Spinner({ size }: { size: ButtonSize }) {
48
+ const dim = SPINNER_SIZE[size];
49
+ return (
50
+ <svg
51
+ width={dim}
52
+ height={dim}
53
+ viewBox="0 0 24 24"
54
+ fill="none"
55
+ aria-hidden="true"
56
+ className={tkx('animate-spin shrink-0')}
57
+ >
58
+ <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="3" strokeOpacity="0.25" />
59
+ <path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
60
+ </svg>
61
+ );
62
+ }
63
+
64
+ export const TkxButton = forwardRef<HTMLButtonElement, TkxButtonProps>(
65
+ (
66
+ {
67
+ variant = 'solid',
68
+ size = 'md',
69
+ colorScheme = 'primary',
70
+ isLoading = false,
71
+ loadingText,
72
+ leftIcon,
73
+ rightIcon,
74
+ isFullWidth = false,
75
+ glow = false,
76
+ disabled,
77
+ children,
78
+ className,
79
+ style,
80
+ ...rest
81
+ },
82
+ ref,
83
+ ) => {
84
+ const theme = useTheme();
85
+ const reducedMotion = useReducedMotion();
86
+ const isDisabled = disabled || isLoading;
87
+
88
+ const colorMap: Record<ButtonColorScheme, string> = {
89
+ primary: theme.primary, secondary: theme.secondary,
90
+ danger: theme.danger, warning: theme.warning, success: theme.success,
91
+ };
92
+ const accentColor = colorMap[colorScheme];
93
+ const textOnAccent = getAccessibleForeground(accentColor, ['#ffffff', '#000000', theme.bg]);
94
+
95
+ const safeChildren = typeof children === 'string' ? sanitizeString(children) : children;
96
+ const safeLoadingText = loadingText ? sanitizeString(loadingText) : undefined;
97
+
98
+ // Variant-specific inline styles (dynamic hex colors from theme)
99
+ const variantStyle: React.CSSProperties = variant === 'solid'
100
+ ? {
101
+ backgroundColor: accentColor,
102
+ color: textOnAccent,
103
+ border: 'none',
104
+ boxShadow: glow && !reducedMotion ? `0 0 20px ${accentColor}55` : undefined,
105
+ }
106
+ : variant === 'outline'
107
+ ? { backgroundColor: 'transparent', color: accentColor, border: `2px solid ${accentColor}` }
108
+ : variant === 'ghost'
109
+ ? { backgroundColor: 'transparent', color: accentColor, border: 'none' }
110
+ : { backgroundColor: 'transparent', color: accentColor, border: 'none', textDecoration: 'underline' };
111
+
112
+ const base = tkx(
113
+ 'inline-flex items-center justify-center gap-2',
114
+ 'rounded-lg font-sans font-semibold',
115
+ 'select-none relative overflow-hidden',
116
+ SIZE_CLASS[size],
117
+ SIZE_PADDING[size],
118
+ SIZE_MIN_H[size],
119
+ variant !== 'link' && SIZE_MIN_H[size],
120
+ isFullWidth && 'w-full',
121
+ isDisabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer',
122
+ !reducedMotion && 'transition-all duration-200',
123
+ 'focus-visible:focus-ring',
124
+ );
125
+
126
+ return (
127
+ <button
128
+ ref={ref}
129
+ aria-busy={isLoading}
130
+ aria-disabled={isDisabled}
131
+ disabled={isDisabled}
132
+ className={cx(base, className)}
133
+ style={{ ...variantStyle, ...style }}
134
+ {...rest}
135
+ >
136
+ {isLoading ? (
137
+ <>
138
+ <Spinner size={size} />
139
+ {safeLoadingText ?? safeChildren}
140
+ </>
141
+ ) : (
142
+ <>
143
+ {leftIcon && <span aria-hidden="true">{leftIcon}</span>}
144
+ {safeChildren}
145
+ {rightIcon && <span aria-hidden="true">{rightIcon}</span>}
146
+ </>
147
+ )}
148
+ </button>
149
+ );
150
+ },
151
+ );
152
+
153
+ TkxButton.displayName = 'TkxButton';
@@ -0,0 +1,92 @@
1
+ import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
2
+ import { useTheme } from '../themes';
3
+ import { tkx, cx } from '../engine/tkx';
4
+
5
+ export type CardVariant = 'default' | 'glass' | 'quantum' | 'elevated' | 'outlined';
6
+ export type CardPadding = 'none' | 'sm' | 'md' | 'lg';
7
+
8
+ export interface TkxCardProps extends HTMLAttributes<HTMLElement> {
9
+ variant?: CardVariant;
10
+ isHoverable?: boolean;
11
+ isClickable?: boolean;
12
+ padding?: CardPadding;
13
+ as?: 'div' | 'article' | 'section' | 'button';
14
+ }
15
+
16
+ const PADDING_CLASS: Record<CardPadding, string> = {
17
+ none: '', sm: 'p-3', md: 'p-5', lg: 'p-7',
18
+ };
19
+
20
+ export const TkxCard = forwardRef<HTMLElement, TkxCardProps>(
21
+ ({ variant = 'default', isHoverable, isClickable, padding = 'md', as, children, className, style, ...rest }, ref) => {
22
+ const theme = useTheme();
23
+ const Tag = (as ?? (isClickable ? 'button' : 'div')) as 'div';
24
+
25
+ const variantStyle: React.CSSProperties = {
26
+ default: { backgroundColor: theme.surface, border: `1px solid ${theme.border}` },
27
+ glass: { backgroundColor: `${theme.surface}cc`, backdropFilter: 'blur(12px)', border: `1px solid ${theme.border}55`, boxShadow: `0 4px 24px ${theme.bg}40` },
28
+ quantum: { backgroundColor: theme.surfaceAlt, border: `1px solid ${theme.primary}33`, boxShadow: `0 0 24px ${theme.primary}18` },
29
+ elevated: { backgroundColor: theme.surface, boxShadow: `0 4px 20px ${theme.bg}60` },
30
+ outlined: { backgroundColor: 'transparent', border: `2px solid ${theme.border}` },
31
+ }[variant];
32
+
33
+ const base = tkx(
34
+ 'rounded-xl w-full text-left block font-sans focus-visible:focus-ring',
35
+ PADDING_CLASS[padding],
36
+ (isHoverable || isClickable) && 'transition-transform duration-150 hover:scale-[1.01]',
37
+ isClickable ? 'cursor-pointer' : 'cursor-default',
38
+ );
39
+
40
+ return (
41
+ <Tag
42
+ ref={ref as React.Ref<HTMLDivElement>}
43
+ className={cx('tkx-card', base, className)}
44
+ style={{ color: theme.text, ...variantStyle, ...style }}
45
+ {...(rest as HTMLAttributes<HTMLDivElement>)}
46
+ >
47
+ {children}
48
+ </Tag>
49
+ );
50
+ },
51
+ );
52
+
53
+ TkxCard.displayName = 'TkxCard';
54
+
55
+ // ── Sub-components ────────────────────────────────────────────────────────────
56
+
57
+ export interface TkxCardHeaderProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
58
+ title?: ReactNode;
59
+ subtitle?: ReactNode;
60
+ action?: ReactNode;
61
+ }
62
+
63
+ export function TkxCardHeader({ title, subtitle, action, children, className, ...rest }: TkxCardHeaderProps) {
64
+ const theme = useTheme();
65
+ return (
66
+ <div className={cx(tkx('flex justify-between items-start mb-4'), className)} {...rest}>
67
+ <div className={tkx('flex-1 min-w-0')}>
68
+ {title && <h3 className={tkx('text-base font-semibold m-0')} style={{ color: theme.text }}>{title}</h3>}
69
+ {subtitle && <p className={tkx('text-sm mt-1 mb-0')} style={{ color: theme.textMuted }}>{subtitle}</p>}
70
+ {children}
71
+ </div>
72
+ {action && <div className={tkx('ml-3 shrink-0')}>{action}</div>}
73
+ </div>
74
+ );
75
+ }
76
+
77
+ export function TkxCardBody({ children, className, ...rest }: HTMLAttributes<HTMLDivElement>) {
78
+ return <div className={cx(tkx(''), className)} {...rest}>{children}</div>;
79
+ }
80
+
81
+ export function TkxCardFooter({ children, className, style, ...rest }: HTMLAttributes<HTMLDivElement>) {
82
+ const theme = useTheme();
83
+ return (
84
+ <div
85
+ className={cx(tkx('flex items-center justify-end gap-2 mt-4 pt-4'), className)}
86
+ style={{ borderTop: `1px solid ${theme.border}`, ...style }}
87
+ {...rest}
88
+ >
89
+ {children}
90
+ </div>
91
+ );
92
+ }