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,275 @@
1
+ // ── WCAG Accessibility Engine ────────────────────────────────────────────────
2
+ // Contrast calculation, focus management, keyboard patterns, screen reader
3
+
4
+ // ── Color Contrast ───────────────────────────────────────────────────────────
5
+
6
+ function hexToRgb(hex: string): [number, number, number] {
7
+ const clean = hex.replace('#', '');
8
+ const full = clean.length === 3
9
+ ? clean.split('').map((c) => c + c).join('')
10
+ : clean;
11
+ const r = parseInt(full.slice(0, 2), 16);
12
+ const g = parseInt(full.slice(2, 4), 16);
13
+ const b = parseInt(full.slice(4, 6), 16);
14
+ return [r, g, b];
15
+ }
16
+
17
+ export function relativeLuminance(hex: string): number {
18
+ const [r, g, b] = hexToRgb(hex).map((c) => {
19
+ const s = c / 255;
20
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
21
+ });
22
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
23
+ }
24
+
25
+ export function contrastRatio(fg: string, bg: string): number {
26
+ const l1 = relativeLuminance(fg);
27
+ const l2 = relativeLuminance(bg);
28
+ const lighter = Math.max(l1, l2);
29
+ const darker = Math.min(l1, l2);
30
+ return (lighter + 0.05) / (darker + 0.05);
31
+ }
32
+
33
+ export function meetsAA(fg: string, bg: string, largeText = false): boolean {
34
+ return contrastRatio(fg, bg) >= (largeText ? 3 : 4.5);
35
+ }
36
+
37
+ export function meetsAAA(fg: string, bg: string, largeText = false): boolean {
38
+ return contrastRatio(fg, bg) >= (largeText ? 4.5 : 7);
39
+ }
40
+
41
+ export function getAccessibleForeground(bg: string, candidates?: string[]): string {
42
+ const defaults = ['#ffffff', '#000000'];
43
+ const pool = candidates ?? defaults;
44
+ return pool.reduce((best, candidate) => {
45
+ return contrastRatio(candidate, bg) > contrastRatio(best, bg) ? candidate : best;
46
+ }, pool[0]);
47
+ }
48
+
49
+ // ── LiveRegion Announcer ─────────────────────────────────────────────────────
50
+
51
+ export interface Announcer {
52
+ announce(message: string, politeness?: 'polite' | 'assertive'): void;
53
+ destroy(): void;
54
+ }
55
+
56
+ export function createAnnouncer(): Announcer {
57
+ const politeEl = document.createElement('div');
58
+ const assertiveEl = document.createElement('div');
59
+
60
+ for (const el of [politeEl, assertiveEl]) {
61
+ el.setAttribute('aria-atomic', 'true');
62
+ el.style.cssText =
63
+ 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
64
+ document.body.appendChild(el);
65
+ }
66
+
67
+ politeEl.setAttribute('aria-live', 'polite');
68
+ assertiveEl.setAttribute('aria-live', 'assertive');
69
+
70
+ return {
71
+ announce(message, politeness = 'polite') {
72
+ const el = politeness === 'assertive' ? assertiveEl : politeEl;
73
+ el.textContent = '';
74
+ setTimeout(() => {
75
+ el.textContent = message;
76
+ }, 0);
77
+ },
78
+ destroy() {
79
+ politeEl.remove();
80
+ assertiveEl.remove();
81
+ },
82
+ };
83
+ }
84
+
85
+ // ── Focus Trap ───────────────────────────────────────────────────────────────
86
+
87
+ const FOCUSABLE_SELECTORS = [
88
+ 'a[href]',
89
+ 'button:not([disabled])',
90
+ 'input:not([disabled])',
91
+ 'select:not([disabled])',
92
+ 'textarea:not([disabled])',
93
+ '[tabindex]:not([tabindex="-1"])',
94
+ 'details > summary',
95
+ ].join(', ');
96
+
97
+ export interface FocusTrap {
98
+ activate(): void;
99
+ deactivate(): void;
100
+ }
101
+
102
+ export function createFocusTrap(container: HTMLElement): FocusTrap {
103
+ let previousFocus: Element | null = null;
104
+ let keyHandler: ((e: KeyboardEvent) => void) | null = null;
105
+
106
+ function getFocusable(): HTMLElement[] {
107
+ return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTORS)).filter(
108
+ (el) => !el.closest('[hidden]') && !el.closest('[aria-hidden="true"]'),
109
+ );
110
+ }
111
+
112
+ return {
113
+ activate() {
114
+ previousFocus = document.activeElement;
115
+ const focusable = getFocusable();
116
+ if (focusable.length > 0) focusable[0].focus();
117
+
118
+ keyHandler = (e: KeyboardEvent) => {
119
+ if (e.key !== 'Tab') return;
120
+ const focusable = getFocusable();
121
+ if (focusable.length === 0) return;
122
+
123
+ const first = focusable[0];
124
+ const last = focusable[focusable.length - 1];
125
+
126
+ if (e.shiftKey) {
127
+ if (document.activeElement === first) {
128
+ e.preventDefault();
129
+ last.focus();
130
+ }
131
+ } else {
132
+ if (document.activeElement === last) {
133
+ e.preventDefault();
134
+ first.focus();
135
+ }
136
+ }
137
+ };
138
+
139
+ container.addEventListener('keydown', keyHandler);
140
+ },
141
+ deactivate() {
142
+ if (keyHandler) container.removeEventListener('keydown', keyHandler);
143
+ if (previousFocus instanceof HTMLElement) previousFocus.focus();
144
+ previousFocus = null;
145
+ keyHandler = null;
146
+ },
147
+ };
148
+ }
149
+
150
+ // ── WAI-ARIA Keyboard Handlers ───────────────────────────────────────────────
151
+
152
+ export function handleTabsKeyboard(
153
+ e: KeyboardEvent,
154
+ currentIndex: number,
155
+ tabCount: number,
156
+ onSelect: (index: number) => void,
157
+ ): void {
158
+ let next = currentIndex;
159
+
160
+ switch (e.key) {
161
+ case 'ArrowLeft':
162
+ case 'ArrowUp':
163
+ e.preventDefault();
164
+ next = currentIndex === 0 ? tabCount - 1 : currentIndex - 1;
165
+ break;
166
+ case 'ArrowRight':
167
+ case 'ArrowDown':
168
+ e.preventDefault();
169
+ next = currentIndex === tabCount - 1 ? 0 : currentIndex + 1;
170
+ break;
171
+ case 'Home':
172
+ e.preventDefault();
173
+ next = 0;
174
+ break;
175
+ case 'End':
176
+ e.preventDefault();
177
+ next = tabCount - 1;
178
+ break;
179
+ case 'Enter':
180
+ case ' ':
181
+ e.preventDefault();
182
+ onSelect(currentIndex);
183
+ return;
184
+ default:
185
+ return;
186
+ }
187
+
188
+ onSelect(next);
189
+ }
190
+
191
+ export function handleMenuKeyboard(
192
+ e: KeyboardEvent,
193
+ currentIndex: number,
194
+ itemCount: number,
195
+ onSelect: (index: number) => void,
196
+ onClose?: () => void,
197
+ ): void {
198
+ let next = currentIndex;
199
+
200
+ switch (e.key) {
201
+ case 'ArrowUp':
202
+ e.preventDefault();
203
+ next = currentIndex === 0 ? itemCount - 1 : currentIndex - 1;
204
+ break;
205
+ case 'ArrowDown':
206
+ e.preventDefault();
207
+ next = currentIndex === itemCount - 1 ? 0 : currentIndex + 1;
208
+ break;
209
+ case 'Home':
210
+ e.preventDefault();
211
+ next = 0;
212
+ break;
213
+ case 'End':
214
+ e.preventDefault();
215
+ next = itemCount - 1;
216
+ break;
217
+ case 'Enter':
218
+ case ' ':
219
+ e.preventDefault();
220
+ onSelect(currentIndex);
221
+ return;
222
+ case 'Escape':
223
+ e.preventDefault();
224
+ onClose?.();
225
+ return;
226
+ default:
227
+ return;
228
+ }
229
+
230
+ onSelect(next);
231
+ }
232
+
233
+ // ── Media Query Detection ────────────────────────────────────────────────────
234
+
235
+ export function prefersReducedMotion(): boolean {
236
+ if (typeof window === 'undefined') return false;
237
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
238
+ }
239
+
240
+ export function prefersHighContrast(): boolean {
241
+ if (typeof window === 'undefined') return false;
242
+ return window.matchMedia('(forced-colors: active)').matches;
243
+ }
244
+
245
+ export function announceToScreenReader(message: string, politeness: 'polite' | 'assertive' = 'polite'): void {
246
+ if (typeof document === 'undefined') return;
247
+ let el = document.getElementById('tkx-live-region');
248
+ if (!el) {
249
+ el = document.createElement('div');
250
+ el.id = 'tkx-live-region';
251
+ el.setAttribute('aria-live', politeness);
252
+ el.setAttribute('aria-atomic', 'true');
253
+ el.style.cssText =
254
+ 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
255
+ document.body.appendChild(el);
256
+ }
257
+ el.textContent = '';
258
+ setTimeout(() => {
259
+ if (el) el.textContent = message;
260
+ }, 0);
261
+ }
262
+
263
+ export const WCAGEngine = {
264
+ contrastRatio,
265
+ meetsAA,
266
+ meetsAAA,
267
+ getAccessibleForeground,
268
+ createAnnouncer,
269
+ createFocusTrap,
270
+ handleTabsKeyboard,
271
+ handleMenuKeyboard,
272
+ prefersReducedMotion,
273
+ prefersHighContrast,
274
+ announce: announceToScreenReader,
275
+ };
@@ -0,0 +1,120 @@
1
+ import {
2
+ useState,
3
+ useEffect,
4
+ useCallback,
5
+ useRef,
6
+ type RefObject,
7
+ } from 'react';
8
+ import {
9
+ prefersReducedMotion,
10
+ prefersHighContrast,
11
+ createFocusTrap,
12
+ createAnnouncer,
13
+ type Announcer,
14
+ } from '../engine/wcag';
15
+
16
+ // ── useReducedMotion ─────────────────────────────────────────────────────────
17
+
18
+ export function useReducedMotion(): boolean {
19
+ const [reduced, setReduced] = useState(() => {
20
+ if (typeof window === 'undefined') return false;
21
+ return prefersReducedMotion();
22
+ });
23
+
24
+ useEffect(() => {
25
+ if (typeof window === 'undefined') return;
26
+ const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
27
+ const handler = (e: MediaQueryListEvent) => setReduced(e.matches);
28
+ mq.addEventListener('change', handler);
29
+ return () => mq.removeEventListener('change', handler);
30
+ }, []);
31
+
32
+ return reduced;
33
+ }
34
+
35
+ // ── useHighContrast ──────────────────────────────────────────────────────────
36
+
37
+ export function useHighContrast(): boolean {
38
+ const [highContrast, setHighContrast] = useState(() => {
39
+ if (typeof window === 'undefined') return false;
40
+ return prefersHighContrast();
41
+ });
42
+
43
+ useEffect(() => {
44
+ if (typeof window === 'undefined') return;
45
+ const mq = window.matchMedia('(forced-colors: active)');
46
+ const handler = (e: MediaQueryListEvent) => setHighContrast(e.matches);
47
+ mq.addEventListener('change', handler);
48
+ return () => mq.removeEventListener('change', handler);
49
+ }, []);
50
+
51
+ return highContrast;
52
+ }
53
+
54
+ // ── useFocusTrap ─────────────────────────────────────────────────────────────
55
+
56
+ export function useFocusTrap(active: boolean): RefObject<HTMLElement | null> {
57
+ const ref = useRef<HTMLElement>(null);
58
+
59
+ useEffect(() => {
60
+ if (!active || !ref.current) return;
61
+ const trap = createFocusTrap(ref.current);
62
+ trap.activate();
63
+ return () => trap.deactivate();
64
+ }, [active]);
65
+
66
+ return ref;
67
+ }
68
+
69
+ // ── useAnnounce ──────────────────────────────────────────────────────────────
70
+
71
+ export function useAnnounce(): (message: string, politeness?: 'polite' | 'assertive') => void {
72
+ const announcerRef = useRef<Announcer | null>(null);
73
+
74
+ useEffect(() => {
75
+ if (typeof document === 'undefined') return;
76
+ announcerRef.current = createAnnouncer();
77
+ return () => {
78
+ announcerRef.current?.destroy();
79
+ };
80
+ }, []);
81
+
82
+ return useCallback((message: string, politeness: 'polite' | 'assertive' = 'polite') => {
83
+ announcerRef.current?.announce(message, politeness);
84
+ }, []);
85
+ }
86
+
87
+ // ── useEscapeKey ─────────────────────────────────────────────────────────────
88
+
89
+ export function useEscapeKey(handler: () => void, active = true): void {
90
+ const handlerRef = useRef(handler);
91
+ handlerRef.current = handler;
92
+
93
+ useEffect(() => {
94
+ if (!active) return;
95
+ const listener = (e: KeyboardEvent) => {
96
+ if (e.key === 'Escape') handlerRef.current();
97
+ };
98
+ document.addEventListener('keydown', listener);
99
+ return () => document.removeEventListener('keydown', listener);
100
+ }, [active]);
101
+ }
102
+
103
+ // ── useClickOutside ──────────────────────────────────────────────────────────
104
+
105
+ export function useClickOutside(
106
+ ref: RefObject<HTMLElement | null>,
107
+ handler: () => void,
108
+ ): void {
109
+ const handlerRef = useRef(handler);
110
+ handlerRef.current = handler;
111
+
112
+ useEffect(() => {
113
+ const listener = (e: PointerEvent) => {
114
+ if (!ref.current || ref.current.contains(e.target as Node)) return;
115
+ handlerRef.current();
116
+ };
117
+ document.addEventListener('pointerdown', listener);
118
+ return () => document.removeEventListener('pointerdown', listener);
119
+ }, [ref]);
120
+ }
@@ -0,0 +1,167 @@
1
+ /* TekiVex UI — Global Styles */
2
+
3
+ /* ── Reset ────────────────────────────────────────────────────────────────── */
4
+ *,
5
+ *::before,
6
+ *::after {
7
+ box-sizing: border-box;
8
+ }
9
+
10
+ /* ── CSS Custom Property Defaults (Quantum Dark) ──────────────────────────── */
11
+ :root {
12
+ --tkx-bg: #0a0a0f;
13
+ --tkx-surface: #12121a;
14
+ --tkx-surfaceAlt: #1a1a2e;
15
+ --tkx-border: #2a2a3e;
16
+ --tkx-text: #e8e8f4;
17
+ --tkx-textMuted: #8888aa;
18
+ --tkx-primary: #00f5d4;
19
+ --tkx-secondary: #7b2ff7;
20
+ --tkx-danger: #f72585;
21
+ --tkx-warning: #ffbe0b;
22
+ --tkx-success: #06d6a0;
23
+ --tkx-info: #3a86ff;
24
+
25
+ /* Spacing scale */
26
+ --tkx-space-xs: 4px;
27
+ --tkx-space-sm: 8px;
28
+ --tkx-space-md: 16px;
29
+ --tkx-space-lg: 24px;
30
+ --tkx-space-xl: 32px;
31
+
32
+ /* Border radius */
33
+ --tkx-radius-sm: 4px;
34
+ --tkx-radius-md: 8px;
35
+ --tkx-radius-lg: 12px;
36
+ --tkx-radius-full: 9999px;
37
+
38
+ /* Typography */
39
+ --tkx-font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
40
+ --tkx-font-mono: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
41
+
42
+ /* Transitions */
43
+ --tkx-transition: 200ms ease;
44
+ --tkx-transition-fast: 100ms ease;
45
+ }
46
+
47
+ /* ── Focus-Visible Styles ─────────────────────────────────────────────────── */
48
+ .tkx-focus-ring:focus-visible {
49
+ outline: 2px solid var(--tkx-primary);
50
+ outline-offset: 2px;
51
+ border-radius: 2px;
52
+ }
53
+
54
+ :focus:not(:focus-visible) {
55
+ outline: none;
56
+ }
57
+
58
+ /* ── Skip Navigation ──────────────────────────────────────────────────────── */
59
+ .tkx-skip-nav {
60
+ position: absolute;
61
+ top: -9999px;
62
+ left: 0;
63
+ z-index: 9999;
64
+ padding: 8px 16px;
65
+ background: var(--tkx-primary);
66
+ color: var(--tkx-bg);
67
+ font-weight: 600;
68
+ text-decoration: none;
69
+ border-radius: 0 0 4px 0;
70
+ transition: top var(--tkx-transition-fast);
71
+ }
72
+
73
+ .tkx-skip-nav:focus {
74
+ top: 0;
75
+ outline: 2px solid var(--tkx-bg);
76
+ outline-offset: 2px;
77
+ }
78
+
79
+ /* ── Visually Hidden (Screen-reader only) ─────────────────────────────────── */
80
+ .tkx-sr-only {
81
+ position: absolute;
82
+ width: 1px;
83
+ height: 1px;
84
+ padding: 0;
85
+ margin: -1px;
86
+ overflow: hidden;
87
+ clip: rect(0, 0, 0, 0);
88
+ white-space: nowrap;
89
+ border: 0;
90
+ }
91
+
92
+ /* ── Animation Keyframes ──────────────────────────────────────────────────── */
93
+ @keyframes tkx-fade-in {
94
+ from { opacity: 0; }
95
+ to { opacity: 1; }
96
+ }
97
+
98
+ @keyframes tkx-slide-up {
99
+ from {
100
+ transform: translateY(8px);
101
+ opacity: 0;
102
+ }
103
+ to {
104
+ transform: translateY(0);
105
+ opacity: 1;
106
+ }
107
+ }
108
+
109
+ @keyframes tkx-pulse {
110
+ 0%, 100% { opacity: 1; }
111
+ 50% { opacity: 0.4; }
112
+ }
113
+
114
+ @keyframes tkx-spin {
115
+ from { transform: rotate(0deg); }
116
+ to { transform: rotate(360deg); }
117
+ }
118
+
119
+ @keyframes tkx-shimmer {
120
+ 0% { background-position: -200% 0; }
121
+ 100% { background-position: 200% 0; }
122
+ }
123
+
124
+ @keyframes tkx-ripple {
125
+ 0% {
126
+ transform: scale(0);
127
+ opacity: 0.6;
128
+ }
129
+ 100% {
130
+ transform: scale(4);
131
+ opacity: 0;
132
+ }
133
+ }
134
+
135
+ /* ── Reduced Motion ───────────────────────────────────────────────────────── */
136
+ @media (prefers-reduced-motion: reduce) {
137
+ *,
138
+ *::before,
139
+ *::after {
140
+ animation-duration: 0.01ms !important;
141
+ animation-iteration-count: 1 !important;
142
+ transition-duration: 0.01ms !important;
143
+ scroll-behavior: auto !important;
144
+ }
145
+ }
146
+
147
+ /* ── High Contrast (prefers-contrast: more) ───────────────────────────────── */
148
+ @media (prefers-contrast: more) {
149
+ :root {
150
+ --tkx-border: currentColor;
151
+ }
152
+
153
+ .tkx-focus-ring:focus-visible {
154
+ outline-width: 3px;
155
+ }
156
+ }
157
+
158
+ /* ── Forced Colors (Windows High Contrast Mode) ───────────────────────────── */
159
+ @media (forced-colors: active) {
160
+ .tkx-button,
161
+ .tkx-badge,
162
+ .tkx-toggle,
163
+ .tkx-input,
164
+ .tkx-card {
165
+ forced-color-adjust: none;
166
+ }
167
+ }
@@ -0,0 +1,111 @@
1
+ import { createContext, useContext, useEffect, type ReactNode, createElement } from 'react';
2
+ import { cssVar } from '../engine/css';
3
+ import { meetsAA } from '../engine/wcag';
4
+
5
+ // ── Theme Token Type ─────────────────────────────────────────────────────────
6
+
7
+ export interface ThemeTokens {
8
+ bg: string;
9
+ surface: string;
10
+ surfaceAlt: string;
11
+ border: string;
12
+ text: string;
13
+ textMuted: string;
14
+ primary: string;
15
+ secondary: string;
16
+ danger: string;
17
+ warning: string;
18
+ success: string;
19
+ info: string;
20
+ }
21
+
22
+ // ── Built-in Themes ──────────────────────────────────────────────────────────
23
+
24
+ export const quantumDark: ThemeTokens = {
25
+ bg: '#0a0a0f',
26
+ surface: '#12121a',
27
+ surfaceAlt: '#1a1a2e',
28
+ border: '#2a2a3e',
29
+ text: '#e8e8f4',
30
+ textMuted: '#8888aa',
31
+ primary: '#00f5d4',
32
+ secondary: '#7b2ff7',
33
+ danger: '#f72585',
34
+ warning: '#ffbe0b',
35
+ success: '#06d6a0',
36
+ info: '#3a86ff',
37
+ };
38
+
39
+ export const auroraLight: ThemeTokens = {
40
+ bg: '#f8f6f1',
41
+ surface: '#ffffff',
42
+ surfaceAlt: '#f0ede6',
43
+ border: '#ddd8cc',
44
+ text: '#1a1815',
45
+ textMuted: '#6b6560',
46
+ primary: '#0d7c5f',
47
+ secondary: '#6930c3',
48
+ danger: '#c1121f',
49
+ warning: '#d4a017',
50
+ success: '#0d7c5f',
51
+ info: '#1d4ed8',
52
+ };
53
+
54
+ // ── Theme Factory ────────────────────────────────────────────────────────────
55
+
56
+ const HEX_PATTERN = /^#[0-9a-fA-F]{3,8}$/;
57
+
58
+ export function createTheme(base: ThemeTokens, overrides?: Partial<ThemeTokens>): ThemeTokens {
59
+ const theme = { ...base, ...overrides };
60
+
61
+ for (const [key, value] of Object.entries(theme)) {
62
+ if (!HEX_PATTERN.test(value)) {
63
+ throw new Error(`Invalid hex color for theme token '${key}': ${value}`);
64
+ }
65
+ }
66
+
67
+ if (!meetsAA(theme.text, theme.bg)) {
68
+ // eslint-disable-next-line no-console
69
+ console.warn(
70
+ `[TekiVex] Theme contrast warning: text (${theme.text}) vs bg (${theme.bg}) does not meet WCAG AA minimum (4.5:1)`,
71
+ );
72
+ }
73
+ if (!meetsAA(theme.primary, theme.bg)) {
74
+ // eslint-disable-next-line no-console
75
+ console.warn(
76
+ `[TekiVex] Theme contrast warning: primary (${theme.primary}) vs bg (${theme.bg}) does not meet WCAG AA minimum`,
77
+ );
78
+ }
79
+
80
+ return theme;
81
+ }
82
+
83
+ // ── Theme Context ────────────────────────────────────────────────────────────
84
+
85
+ export const ThemeContext = createContext<ThemeTokens>(quantumDark);
86
+
87
+ export interface ThemeProviderProps {
88
+ theme?: ThemeTokens;
89
+ children: ReactNode;
90
+ }
91
+
92
+ export function ThemeProvider({ theme = quantumDark, children }: ThemeProviderProps) {
93
+ useEffect(() => {
94
+ const vars = (Object.entries(theme) as [keyof ThemeTokens, string][])
95
+ .map(([key, value]) => cssVar(key, value))
96
+ .join(' ');
97
+ let styleEl = document.getElementById('tkx-theme') as HTMLStyleElement | null;
98
+ if (!styleEl) {
99
+ styleEl = document.createElement('style');
100
+ styleEl.id = 'tkx-theme';
101
+ document.head.appendChild(styleEl);
102
+ }
103
+ styleEl.textContent = `:root { ${vars} }`;
104
+ }, [theme]);
105
+
106
+ return createElement(ThemeContext.Provider, { value: theme }, children);
107
+ }
108
+
109
+ export function useTheme(): ThemeTokens {
110
+ return useContext(ThemeContext);
111
+ }