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,647 @@
1
+ import {
2
+ forwardRef,
3
+ createContext,
4
+ useContext,
5
+ useState,
6
+ useEffect,
7
+ useCallback,
8
+ type ReactNode,
9
+ type CSSProperties,
10
+ type HTMLAttributes,
11
+ } from 'react';
12
+ import { useTheme } from '../themes';
13
+ import { useReducedMotion } from '../hooks';
14
+ import { tkx, cx } from '../engine/tkx';
15
+
16
+ // ══════════════════════════════════════════════════════════════════════════════
17
+ // TkxLayout — Enterprise layout system with Header, Sider, Content, Footer
18
+ // plus a 24-column responsive Grid (TkxRow / TkxCol).
19
+ // ══════════════════════════════════════════════════════════════════════════════
20
+
21
+ // ── Layout Props ─────────────────────────────────────────────────────────────
22
+
23
+ export interface TkxLayoutProps extends HTMLAttributes<HTMLElement> {
24
+ children: ReactNode;
25
+ /** When true the layout flows horizontally (used when Sider is present). */
26
+ hasSider?: boolean;
27
+ style?: CSSProperties;
28
+ className?: string;
29
+ }
30
+
31
+ export interface TkxHeaderProps extends HTMLAttributes<HTMLElement> {
32
+ children: ReactNode;
33
+ /** Header height in px or CSS string. Default 64. */
34
+ height?: number | string;
35
+ /** Stick to top of viewport. */
36
+ fixed?: boolean;
37
+ style?: CSSProperties;
38
+ }
39
+
40
+ export interface TkxSiderProps extends HTMLAttributes<HTMLElement> {
41
+ children: ReactNode;
42
+ /** Expanded width. Default 240. */
43
+ width?: number | string;
44
+ /** Width when collapsed. Default 64. */
45
+ collapsedWidth?: number;
46
+ /** Controlled collapsed state. */
47
+ collapsed?: boolean;
48
+ /** Callback fired when collapsed state changes. */
49
+ onCollapse?: (collapsed: boolean) => void;
50
+ /** Show built-in collapse trigger at the bottom. */
51
+ collapsible?: boolean;
52
+ /** Auto-collapse when viewport is narrower than this breakpoint. */
53
+ breakpoint?: 'sm' | 'md' | 'lg' | 'xl';
54
+ /** Custom trigger node. Pass null to hide the default trigger entirely. */
55
+ trigger?: ReactNode | null;
56
+ style?: CSSProperties;
57
+ }
58
+
59
+ export interface TkxContentProps extends HTMLAttributes<HTMLDivElement> {
60
+ children: ReactNode;
61
+ style?: CSSProperties;
62
+ }
63
+
64
+ export interface TkxFooterProps extends HTMLAttributes<HTMLElement> {
65
+ children: ReactNode;
66
+ style?: CSSProperties;
67
+ }
68
+
69
+ // ── Grid Props ───────────────────────────────────────────────────────────────
70
+
71
+ export interface TkxRowProps extends HTMLAttributes<HTMLDivElement> {
72
+ children: ReactNode;
73
+ /** Gutter in px — single number or [horizontal, vertical]. */
74
+ gutter?: number | [number, number];
75
+ justify?: 'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly';
76
+ align?: 'top' | 'middle' | 'bottom' | 'stretch';
77
+ /** Allow wrapping. Default true. */
78
+ wrap?: boolean;
79
+ style?: CSSProperties;
80
+ }
81
+
82
+ interface ColSpanConfig {
83
+ span: number;
84
+ offset?: number;
85
+ }
86
+
87
+ export interface TkxColProps extends HTMLAttributes<HTMLDivElement> {
88
+ children?: ReactNode;
89
+ /** Column span out of 24. */
90
+ span?: number;
91
+ /** Left offset in columns. */
92
+ offset?: number;
93
+ /** Push columns to the right via relative positioning. */
94
+ push?: number;
95
+ /** Pull columns to the left via relative positioning. */
96
+ pull?: number;
97
+ /** Flex order override. */
98
+ order?: number;
99
+ /** Responsive config at >= 576px. */
100
+ sm?: number | ColSpanConfig;
101
+ /** Responsive config at >= 768px. */
102
+ md?: number | ColSpanConfig;
103
+ /** Responsive config at >= 992px. */
104
+ lg?: number | ColSpanConfig;
105
+ /** Responsive config at >= 1200px. */
106
+ xl?: number | ColSpanConfig;
107
+ style?: CSSProperties;
108
+ }
109
+
110
+ // ── Constants ────────────────────────────────────────────────────────────────
111
+
112
+ const BREAKPOINTS: Record<string, number> = {
113
+ sm: 576,
114
+ md: 768,
115
+ lg: 992,
116
+ xl: 1200,
117
+ };
118
+
119
+ // ── Utility ──────────────────────────────────────────────────────────────────
120
+
121
+ function normDim(value: number | string): string {
122
+ return typeof value === 'number' ? `${value}px` : value;
123
+ }
124
+
125
+ // ── Gutter Context (Row -> Col communication) ────────────────────────────────
126
+
127
+ interface GutterValue {
128
+ h: number;
129
+ v: number;
130
+ }
131
+
132
+ const GutterContext = createContext<GutterValue>({ h: 0, v: 0 });
133
+
134
+ // ══════════════════════════════════════════════════════════════════════════════
135
+ // TkxLayout
136
+ // ══════════════════════════════════════════════════════════════════════════════
137
+
138
+ export const TkxLayout = forwardRef<HTMLElement, TkxLayoutProps>(
139
+ ({ children, hasSider = false, className, style, ...rest }, ref) => {
140
+ const theme = useTheme();
141
+
142
+ const base = tkx('flex min-h-0 w-full font-sans');
143
+ const direction: CSSProperties = hasSider
144
+ ? { flexDirection: 'row' }
145
+ : { flexDirection: 'column' };
146
+
147
+ return (
148
+ <section
149
+ ref={ref}
150
+ className={cx('tkx-layout', base, className)}
151
+ style={{
152
+ minHeight: '100%',
153
+ color: theme.text,
154
+ backgroundColor: theme.bg,
155
+ ...direction,
156
+ ...style,
157
+ }}
158
+ {...rest}
159
+ >
160
+ {children}
161
+ </section>
162
+ );
163
+ },
164
+ );
165
+
166
+ TkxLayout.displayName = 'TkxLayout';
167
+
168
+ // ══════════════════════════════════════════════════════════════════════════════
169
+ // TkxHeader
170
+ // ══════════════════════════════════════════════════════════════════════════════
171
+
172
+ export const TkxHeader = forwardRef<HTMLElement, TkxHeaderProps>(
173
+ ({ children, height = 64, fixed = false, className, style, ...rest }, ref) => {
174
+ const theme = useTheme();
175
+ const h = normDim(height);
176
+
177
+ const base = tkx('flex items-center px-6 shrink-0 w-full');
178
+
179
+ const fixedStyles: CSSProperties = fixed
180
+ ? { position: 'fixed', top: 0, left: 0, right: 0, zIndex: 1000 }
181
+ : {};
182
+
183
+ return (
184
+ <header
185
+ ref={ref}
186
+ className={cx('tkx-layout-header', base, className)}
187
+ style={{
188
+ height: h,
189
+ minHeight: h,
190
+ backgroundColor: theme.surface,
191
+ borderBottom: `1px solid ${theme.border}`,
192
+ ...fixedStyles,
193
+ ...style,
194
+ }}
195
+ {...rest}
196
+ >
197
+ {children}
198
+ </header>
199
+ );
200
+ },
201
+ );
202
+
203
+ TkxHeader.displayName = 'TkxHeader';
204
+
205
+ // ══════════════════════════════════════════════════════════════════════════════
206
+ // TkxSider
207
+ // ══════════════════════════════════════════════════════════════════════════════
208
+
209
+ /** Default chevron trigger for collapsible sidebars. */
210
+ function DefaultTrigger({
211
+ collapsed,
212
+ onClick,
213
+ color,
214
+ }: {
215
+ collapsed: boolean;
216
+ onClick: () => void;
217
+ color: string;
218
+ }) {
219
+ const reducedMotion = useReducedMotion();
220
+
221
+ return (
222
+ <button
223
+ type="button"
224
+ aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
225
+ onClick={onClick}
226
+ className={tkx(
227
+ 'flex items-center justify-center w-full py-3 cursor-pointer',
228
+ 'border-0 bg-transparent',
229
+ )}
230
+ style={{ color }}
231
+ >
232
+ <svg
233
+ width="16"
234
+ height="16"
235
+ viewBox="0 0 16 16"
236
+ fill="none"
237
+ style={{
238
+ transform: collapsed ? 'rotate(0deg)' : 'rotate(180deg)',
239
+ transition: reducedMotion ? 'none' : 'transform 200ms ease',
240
+ }}
241
+ aria-hidden="true"
242
+ >
243
+ <path
244
+ d="M6 3l5 5-5 5"
245
+ stroke="currentColor"
246
+ strokeWidth="2"
247
+ strokeLinecap="round"
248
+ strokeLinejoin="round"
249
+ />
250
+ </svg>
251
+ </button>
252
+ );
253
+ }
254
+
255
+ export const TkxSider = forwardRef<HTMLElement, TkxSiderProps>(
256
+ (
257
+ {
258
+ children,
259
+ width = 240,
260
+ collapsedWidth = 64,
261
+ collapsed: controlledCollapsed,
262
+ onCollapse,
263
+ collapsible = false,
264
+ breakpoint,
265
+ trigger,
266
+ className,
267
+ style,
268
+ ...rest
269
+ },
270
+ ref,
271
+ ) => {
272
+ const theme = useTheme();
273
+ const reducedMotion = useReducedMotion();
274
+
275
+ // Internal collapsed state (uncontrolled fallback).
276
+ const [internalCollapsed, setInternalCollapsed] = useState(false);
277
+ const isCollapsed = controlledCollapsed ?? internalCollapsed;
278
+
279
+ const handleCollapse = useCallback(
280
+ (next: boolean) => {
281
+ if (controlledCollapsed === undefined) {
282
+ setInternalCollapsed(next);
283
+ }
284
+ onCollapse?.(next);
285
+ },
286
+ [controlledCollapsed, onCollapse],
287
+ );
288
+
289
+ // Breakpoint-based auto-collapse.
290
+ useEffect(() => {
291
+ if (!breakpoint) return;
292
+ const bp = BREAKPOINTS[breakpoint];
293
+ if (!bp) return;
294
+
295
+ const mq = window.matchMedia(`(max-width: ${bp - 1}px)`);
296
+
297
+ const handler = (e: MediaQueryListEvent) => {
298
+ handleCollapse(e.matches);
299
+ };
300
+
301
+ // Set initial state based on current viewport.
302
+ handleCollapse(mq.matches);
303
+
304
+ mq.addEventListener('change', handler);
305
+ return () => mq.removeEventListener('change', handler);
306
+ }, [breakpoint, handleCollapse]);
307
+
308
+ const expandedWidth = normDim(width);
309
+ const currentWidth = isCollapsed ? `${collapsedWidth}px` : expandedWidth;
310
+
311
+ const showTrigger = collapsible && trigger !== null;
312
+
313
+ const transitionCSS = reducedMotion
314
+ ? 'none'
315
+ : 'width 200ms ease, min-width 200ms ease, max-width 200ms ease';
316
+
317
+ return (
318
+ <aside
319
+ ref={ref}
320
+ className={cx(
321
+ 'tkx-layout-sider',
322
+ tkx('shrink-0 overflow-hidden flex flex-col'),
323
+ className,
324
+ )}
325
+ style={{
326
+ width: currentWidth,
327
+ minWidth: currentWidth,
328
+ maxWidth: currentWidth,
329
+ backgroundColor: theme.surface,
330
+ borderRight: `1px solid ${theme.border}`,
331
+ transition: transitionCSS,
332
+ ...style,
333
+ }}
334
+ {...rest}
335
+ >
336
+ <div
337
+ className={tkx('flex-1 overflow-y-auto overflow-x-hidden')}
338
+ style={{ width: currentWidth, transition: transitionCSS }}
339
+ >
340
+ {children}
341
+ </div>
342
+
343
+ {showTrigger && (
344
+ <div
345
+ className={tkx('shrink-0')}
346
+ style={{ borderTop: `1px solid ${theme.border}` }}
347
+ >
348
+ {trigger !== undefined ? (
349
+ trigger
350
+ ) : (
351
+ <DefaultTrigger
352
+ collapsed={isCollapsed}
353
+ onClick={() => handleCollapse(!isCollapsed)}
354
+ color={theme.textMuted}
355
+ />
356
+ )}
357
+ </div>
358
+ )}
359
+ </aside>
360
+ );
361
+ },
362
+ );
363
+
364
+ TkxSider.displayName = 'TkxSider';
365
+
366
+ // ══════════════════════════════════════════════════════════════════════════════
367
+ // TkxContent
368
+ // ══════════════════════════════════════════════════════════════════════════════
369
+
370
+ export const TkxContent = forwardRef<HTMLDivElement, TkxContentProps>(
371
+ ({ children, className, style, ...rest }, ref) => {
372
+ const theme = useTheme();
373
+
374
+ return (
375
+ <main
376
+ ref={ref}
377
+ className={cx('tkx-layout-content', tkx('flex-1 min-w-0 min-h-0'), className)}
378
+ style={{
379
+ backgroundColor: theme.bg,
380
+ padding: '24px',
381
+ ...style,
382
+ }}
383
+ {...rest}
384
+ >
385
+ {children}
386
+ </main>
387
+ );
388
+ },
389
+ );
390
+
391
+ TkxContent.displayName = 'TkxContent';
392
+
393
+ // ══════════════════════════════════════════════════════════════════════════════
394
+ // TkxFooter
395
+ // ══════════════════════════════════════════════════════════════════════════════
396
+
397
+ export const TkxFooter = forwardRef<HTMLElement, TkxFooterProps>(
398
+ ({ children, className, style, ...rest }, ref) => {
399
+ const theme = useTheme();
400
+
401
+ return (
402
+ <footer
403
+ ref={ref}
404
+ className={cx('tkx-layout-footer', tkx('flex items-center px-6 py-4 shrink-0'), className)}
405
+ style={{
406
+ backgroundColor: theme.surface,
407
+ borderTop: `1px solid ${theme.border}`,
408
+ color: theme.textMuted,
409
+ ...style,
410
+ }}
411
+ {...rest}
412
+ >
413
+ {children}
414
+ </footer>
415
+ );
416
+ },
417
+ );
418
+
419
+ TkxFooter.displayName = 'TkxFooter';
420
+
421
+ // ══════════════════════════════════════════════════════════════════════════════
422
+ // TkxRow — 24-column flexbox grid row
423
+ // ══════════════════════════════════════════════════════════════════════════════
424
+
425
+ const JUSTIFY_MAP: Record<string, string> = {
426
+ start: 'flex-start',
427
+ center: 'center',
428
+ end: 'flex-end',
429
+ 'space-between': 'space-between',
430
+ 'space-around': 'space-around',
431
+ 'space-evenly': 'space-evenly',
432
+ };
433
+
434
+ const ALIGN_MAP: Record<string, string> = {
435
+ top: 'flex-start',
436
+ middle: 'center',
437
+ bottom: 'flex-end',
438
+ stretch: 'stretch',
439
+ };
440
+
441
+ export const TkxRow = forwardRef<HTMLDivElement, TkxRowProps>(
442
+ (
443
+ {
444
+ children,
445
+ gutter,
446
+ justify = 'start',
447
+ align = 'top',
448
+ wrap = true,
449
+ className,
450
+ style,
451
+ ...rest
452
+ },
453
+ ref,
454
+ ) => {
455
+ const [hGutter, vGutter] = Array.isArray(gutter)
456
+ ? gutter
457
+ : [gutter ?? 0, 0];
458
+
459
+ const halfH = hGutter / 2;
460
+ const halfV = vGutter / 2;
461
+
462
+ // Negative margins on the row compensate for column padding,
463
+ // keeping the outer edges flush with the layout.
464
+ const marginStyle: CSSProperties =
465
+ hGutter || vGutter
466
+ ? { marginLeft: -halfH, marginRight: -halfH, marginTop: -halfV, marginBottom: -halfV }
467
+ : {};
468
+
469
+ const gutterValue: GutterValue = { h: halfH, v: halfV };
470
+
471
+ return (
472
+ <GutterContext.Provider value={gutterValue}>
473
+ <div
474
+ ref={ref}
475
+ className={cx('tkx-row', tkx('flex w-full'), className)}
476
+ style={{
477
+ flexWrap: wrap ? 'wrap' : 'nowrap',
478
+ justifyContent: JUSTIFY_MAP[justify] ?? 'flex-start',
479
+ alignItems: ALIGN_MAP[align] ?? 'flex-start',
480
+ ...marginStyle,
481
+ ...style,
482
+ }}
483
+ {...rest}
484
+ >
485
+ {children}
486
+ </div>
487
+ </GutterContext.Provider>
488
+ );
489
+ },
490
+ );
491
+
492
+ TkxRow.displayName = 'TkxRow';
493
+
494
+ // ══════════════════════════════════════════════════════════════════════════════
495
+ // TkxCol — 24-column flexbox grid column
496
+ // ══════════════════════════════════════════════════════════════════════════════
497
+
498
+ /** Inject responsive media-query classes once into the document head. */
499
+ let responsiveStyleInjected = false;
500
+
501
+ function injectResponsiveStyles(): void {
502
+ if (responsiveStyleInjected || typeof document === 'undefined') return;
503
+ responsiveStyleInjected = true;
504
+
505
+ const rules: string[] = [];
506
+
507
+ for (const [bp, minWidth] of Object.entries(BREAKPOINTS)) {
508
+ // Span classes: .tkx-col-{bp}-{1..24}
509
+ for (let i = 1; i <= 24; i++) {
510
+ const pct = `${(i / 24) * 100}%`;
511
+ rules.push(
512
+ `@media (min-width: ${minWidth}px) { .tkx-col-${bp}-${i} { flex: 0 0 ${pct} !important; max-width: ${pct} !important; } }`,
513
+ );
514
+ }
515
+ // Offset classes: .tkx-col-{bp}-offset-{0..24}
516
+ for (let i = 0; i <= 24; i++) {
517
+ const pct = `${(i / 24) * 100}%`;
518
+ rules.push(
519
+ `@media (min-width: ${minWidth}px) { .tkx-col-${bp}-offset-${i} { margin-left: ${pct} !important; } }`,
520
+ );
521
+ }
522
+ }
523
+
524
+ const el = document.createElement('style');
525
+ el.id = 'tkx-col-responsive';
526
+ el.textContent = rules.join('\n');
527
+ document.head.appendChild(el);
528
+ }
529
+
530
+ function normaliseBreakpointProp(
531
+ value: number | ColSpanConfig | undefined,
532
+ ): ColSpanConfig | undefined {
533
+ if (value === undefined) return undefined;
534
+ return typeof value === 'number' ? { span: value } : value;
535
+ }
536
+
537
+ export const TkxCol = forwardRef<HTMLDivElement, TkxColProps>(
538
+ (
539
+ {
540
+ children,
541
+ span,
542
+ offset = 0,
543
+ push,
544
+ pull,
545
+ order,
546
+ sm,
547
+ md,
548
+ lg,
549
+ xl,
550
+ className,
551
+ style,
552
+ ...rest
553
+ },
554
+ ref,
555
+ ) => {
556
+ // Inject responsive stylesheet on first render.
557
+ useEffect(() => {
558
+ injectResponsiveStyles();
559
+ }, []);
560
+
561
+ // Read gutter from parent TkxRow via context.
562
+ const gutter = useContext(GutterContext);
563
+
564
+ // Base span/offset (no breakpoint).
565
+ const baseWidth = span !== undefined ? `${(span / 24) * 100}%` : undefined;
566
+ const baseOffset = offset ? `${(offset / 24) * 100}%` : undefined;
567
+
568
+ // Build responsive class list.
569
+ const responsiveClasses: string[] = [];
570
+
571
+ const bpEntries: [string, number | ColSpanConfig | undefined][] = [
572
+ ['sm', sm],
573
+ ['md', md],
574
+ ['lg', lg],
575
+ ['xl', xl],
576
+ ];
577
+
578
+ for (const [bp, raw] of bpEntries) {
579
+ const cfg = normaliseBreakpointProp(raw);
580
+ if (!cfg) continue;
581
+ responsiveClasses.push(`tkx-col-${bp}-${cfg.span}`);
582
+ if (cfg.offset !== undefined && cfg.offset > 0) {
583
+ responsiveClasses.push(`tkx-col-${bp}-offset-${cfg.offset}`);
584
+ }
585
+ }
586
+
587
+ // Positional overrides (push/pull/order).
588
+ const positional: CSSProperties = {};
589
+ if (push !== undefined) {
590
+ positional.left = `${(push / 24) * 100}%`;
591
+ positional.position = 'relative';
592
+ }
593
+ if (pull !== undefined) {
594
+ positional.right = `${(pull / 24) * 100}%`;
595
+ positional.position = 'relative';
596
+ }
597
+ if (order !== undefined) {
598
+ positional.order = order;
599
+ }
600
+
601
+ return (
602
+ <div
603
+ ref={ref}
604
+ className={cx(
605
+ 'tkx-col',
606
+ tkx('box-border min-h-[1px]'),
607
+ ...responsiveClasses,
608
+ className,
609
+ )}
610
+ style={{
611
+ flex: baseWidth ? `0 0 ${baseWidth}` : '1',
612
+ maxWidth: baseWidth ?? '100%',
613
+ marginLeft: baseOffset,
614
+ paddingLeft: gutter.h || undefined,
615
+ paddingRight: gutter.h || undefined,
616
+ paddingTop: gutter.v || undefined,
617
+ paddingBottom: gutter.v || undefined,
618
+ ...positional,
619
+ ...style,
620
+ }}
621
+ {...rest}
622
+ >
623
+ {children}
624
+ </div>
625
+ );
626
+ },
627
+ );
628
+
629
+ TkxCol.displayName = 'TkxCol';
630
+
631
+ // ══════════════════════════════════════════════════════════════════════════════
632
+ // Usage example:
633
+ //
634
+ // <TkxLayout>
635
+ // <TkxHeader fixed>Logo / Nav</TkxHeader>
636
+ // <TkxLayout hasSider>
637
+ // <TkxSider collapsible breakpoint="md">Menu</TkxSider>
638
+ // <TkxContent>
639
+ // <TkxRow gutter={[16, 16]}>
640
+ // <TkxCol span={12} md={8}>Column A</TkxCol>
641
+ // <TkxCol span={12} md={16}>Column B</TkxCol>
642
+ // </TkxRow>
643
+ // </TkxContent>
644
+ // </TkxLayout>
645
+ // <TkxFooter>Footer</TkxFooter>
646
+ // </TkxLayout>
647
+ // ══════════════════════════════════════════════════════════════════════════════