tekivex-ui 2.1.0 → 2.3.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.
- package/dist/index.cjs +75 -14
- package/dist/index.d.ts +34 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11013 -4880
- package/dist/src/components/TkxAffix.d.ts +10 -0
- package/dist/src/components/TkxAffix.d.ts.map +1 -0
- package/dist/src/components/TkxAnchor.d.ts +14 -0
- package/dist/src/components/TkxAnchor.d.ts.map +1 -0
- package/dist/src/components/TkxCascader.d.ts +16 -0
- package/dist/src/components/TkxCascader.d.ts.map +1 -0
- package/dist/src/components/TkxConfigProvider.d.ts +100 -0
- package/dist/src/components/TkxConfigProvider.d.ts.map +1 -0
- package/dist/src/components/TkxDataGrid.d.ts +5 -1
- package/dist/src/components/TkxDataGrid.d.ts.map +1 -1
- package/dist/src/components/TkxEmpty.d.ts +870 -0
- package/dist/src/components/TkxEmpty.d.ts.map +1 -0
- package/dist/src/components/TkxForm.d.ts +54 -0
- package/dist/src/components/TkxForm.d.ts.map +1 -0
- package/dist/src/components/TkxLayout.d.ts +87 -0
- package/dist/src/components/TkxLayout.d.ts.map +1 -0
- package/dist/src/components/TkxList.d.ts +24 -0
- package/dist/src/components/TkxList.d.ts.map +1 -0
- package/dist/src/components/TkxMentions.d.ts +15 -0
- package/dist/src/components/TkxMentions.d.ts.map +1 -0
- package/dist/src/components/TkxQRCode.d.ts +11 -0
- package/dist/src/components/TkxQRCode.d.ts.map +1 -0
- package/dist/src/components/TkxResult.d.ts +11 -0
- package/dist/src/components/TkxResult.d.ts.map +1 -0
- package/dist/src/components/TkxSegmented.d.ts +16 -0
- package/dist/src/components/TkxSegmented.d.ts.map +1 -0
- package/dist/src/components/TkxSelect.d.ts +5 -1
- package/dist/src/components/TkxSelect.d.ts.map +1 -1
- package/dist/src/components/TkxSpin.d.ts +15 -0
- package/dist/src/components/TkxSpin.d.ts.map +1 -0
- package/dist/src/components/TkxStatistic.d.ts +1746 -0
- package/dist/src/components/TkxStatistic.d.ts.map +1 -0
- package/dist/src/components/TkxTour.d.ts +15 -0
- package/dist/src/components/TkxTour.d.ts.map +1 -0
- package/dist/src/components/TkxTypography.d.ts +2614 -0
- package/dist/src/components/TkxTypography.d.ts.map +1 -0
- package/dist/src/components/TkxWatermark.d.ts +12 -0
- package/dist/src/components/TkxWatermark.d.ts.map +1 -0
- package/dist/src/components/index.d.ts +17 -0
- package/dist/src/components/index.d.ts.map +1 -1
- package/dist/src/engine/tkx.d.ts +38 -0
- package/dist/src/engine/tkx.d.ts.map +1 -1
- package/dist/src/themes/index.d.ts +108 -0
- package/dist/src/themes/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/TkxAffix.tsx +138 -0
- package/src/components/TkxAnchor.tsx +193 -0
- package/src/components/TkxCascader.tsx +276 -0
- package/src/components/TkxConfigProvider.tsx +458 -0
- package/src/components/TkxDataGrid.tsx +65 -2
- package/src/components/TkxEmpty.tsx +219 -0
- package/src/components/TkxForm.tsx +607 -0
- package/src/components/TkxLayout.tsx +647 -0
- package/src/components/TkxList.tsx +242 -0
- package/src/components/TkxMentions.tsx +210 -0
- package/src/components/TkxQRCode.tsx +174 -0
- package/src/components/TkxResult.tsx +128 -0
- package/src/components/TkxSegmented.tsx +169 -0
- package/src/components/TkxSelect.tsx +209 -6
- package/src/components/TkxSpin.tsx +261 -0
- package/src/components/TkxStatistic.tsx +261 -0
- package/src/components/TkxTour.tsx +239 -0
- package/src/components/TkxTypography.tsx +263 -0
- package/src/components/TkxWatermark.tsx +143 -0
- package/src/components/index.ts +17 -0
- package/src/engine/tkx.ts +62 -0
- package/src/themes/index.ts +149 -0
|
@@ -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
|
+
// ══════════════════════════════════════════════════════════════════════════════
|