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,458 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useMemo,
|
|
5
|
+
useCallback,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
type CSSProperties,
|
|
8
|
+
} from 'react';
|
|
9
|
+
|
|
10
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
11
|
+
// TkxConfigProvider — Global configuration provider for TekiVex UI.
|
|
12
|
+
//
|
|
13
|
+
// Provides locale, direction, component defaults, and a CSS class prefix to
|
|
14
|
+
// all descendant components via React context. Supports nesting: a child
|
|
15
|
+
// ConfigProvider merges its values on top of its parent.
|
|
16
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
17
|
+
|
|
18
|
+
// ── Component Defaults ───────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export interface ComponentDefaults {
|
|
21
|
+
/** Default size for all components that support sizing. */
|
|
22
|
+
size?: 'sm' | 'md' | 'lg';
|
|
23
|
+
/** Default variant name. Interpretation depends on the component. */
|
|
24
|
+
variant?: string;
|
|
25
|
+
/** Enable or disable animations globally. */
|
|
26
|
+
animation?: boolean;
|
|
27
|
+
/** Show borders by default on components that support it. */
|
|
28
|
+
bordered?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Per-component overrides ──────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export interface ButtonConfig {
|
|
34
|
+
size?: 'sm' | 'md' | 'lg';
|
|
35
|
+
variant?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface InputConfig {
|
|
39
|
+
size?: 'sm' | 'md' | 'lg';
|
|
40
|
+
bordered?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TableConfig {
|
|
44
|
+
size?: 'sm' | 'md' | 'lg';
|
|
45
|
+
bordered?: boolean;
|
|
46
|
+
striped?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ModalConfig {
|
|
50
|
+
centered?: boolean;
|
|
51
|
+
maskClosable?: boolean;
|
|
52
|
+
animation?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ComponentOverrides {
|
|
56
|
+
button?: ButtonConfig;
|
|
57
|
+
input?: InputConfig;
|
|
58
|
+
table?: TableConfig;
|
|
59
|
+
modal?: ModalConfig;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Config Context Value ─────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export interface ConfigContextValue {
|
|
65
|
+
/** BCP 47 locale code (e.g. "en-US", "ja-JP"). */
|
|
66
|
+
locale: string;
|
|
67
|
+
/** Text direction. */
|
|
68
|
+
direction: 'ltr' | 'rtl';
|
|
69
|
+
/** Global defaults applied to every component. */
|
|
70
|
+
componentDefaults: Required<ComponentDefaults>;
|
|
71
|
+
/** Per-component configuration overrides. */
|
|
72
|
+
componentOverrides: ComponentOverrides;
|
|
73
|
+
/** CSS class prefix used by all TekiVex components. */
|
|
74
|
+
prefixCls: string;
|
|
75
|
+
/** Whether we are inside at least one ConfigProvider. */
|
|
76
|
+
configured: boolean;
|
|
77
|
+
/** Helper: build a prefixed class name. */
|
|
78
|
+
cls: (name: string) => string;
|
|
79
|
+
/** Helper: resolve size for a component (component override > global default). */
|
|
80
|
+
getSize: (component?: keyof ComponentOverrides, local?: 'sm' | 'md' | 'lg') => 'sm' | 'md' | 'lg';
|
|
81
|
+
/** Helper: resolve whether animations should run. */
|
|
82
|
+
getAnimation: (local?: boolean) => boolean;
|
|
83
|
+
/** Helper: resolve bordered default. */
|
|
84
|
+
getBordered: (component?: keyof ComponentOverrides, local?: boolean) => boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Sensible Defaults ────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
const DEFAULT_COMPONENT_DEFAULTS: Required<ComponentDefaults> = {
|
|
90
|
+
size: 'md',
|
|
91
|
+
variant: 'default',
|
|
92
|
+
animation: true,
|
|
93
|
+
bordered: true,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const DEFAULT_CONFIG: ConfigContextValue = {
|
|
97
|
+
locale: 'en-US',
|
|
98
|
+
direction: 'ltr',
|
|
99
|
+
componentDefaults: DEFAULT_COMPONENT_DEFAULTS,
|
|
100
|
+
componentOverrides: {},
|
|
101
|
+
prefixCls: 'tkx',
|
|
102
|
+
configured: false,
|
|
103
|
+
cls: (name: string) => `tkx-${name}`,
|
|
104
|
+
getSize: () => 'md',
|
|
105
|
+
getAnimation: () => true,
|
|
106
|
+
getBordered: () => true,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// ── Context ──────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
const ConfigContext = createContext<ConfigContextValue>(DEFAULT_CONFIG);
|
|
112
|
+
|
|
113
|
+
// ── Props ────────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
export interface TkxConfigProviderProps {
|
|
116
|
+
children: ReactNode;
|
|
117
|
+
/** BCP 47 locale code. Default "en-US". */
|
|
118
|
+
locale?: string;
|
|
119
|
+
/** Text direction. Default "ltr". */
|
|
120
|
+
direction?: 'ltr' | 'rtl';
|
|
121
|
+
/** Global component defaults. Merged with parent provider values. */
|
|
122
|
+
componentDefaults?: ComponentDefaults;
|
|
123
|
+
/** Per-component overrides. Merged shallowly per component key. */
|
|
124
|
+
componentOverrides?: ComponentOverrides;
|
|
125
|
+
/** CSS class prefix. Default "tkx". */
|
|
126
|
+
prefixCls?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
130
|
+
// TkxConfigProvider Component
|
|
131
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
132
|
+
|
|
133
|
+
export function TkxConfigProvider({
|
|
134
|
+
children,
|
|
135
|
+
locale,
|
|
136
|
+
direction,
|
|
137
|
+
componentDefaults,
|
|
138
|
+
componentOverrides,
|
|
139
|
+
prefixCls,
|
|
140
|
+
}: TkxConfigProviderProps) {
|
|
141
|
+
// Read parent config (supports nesting).
|
|
142
|
+
const parent = useContext(ConfigContext);
|
|
143
|
+
|
|
144
|
+
// Merge defaults: child overrides parent.
|
|
145
|
+
const mergedDefaults: Required<ComponentDefaults> = useMemo(
|
|
146
|
+
() => ({
|
|
147
|
+
size: componentDefaults?.size ?? parent.componentDefaults.size,
|
|
148
|
+
variant: componentDefaults?.variant ?? parent.componentDefaults.variant,
|
|
149
|
+
animation: componentDefaults?.animation ?? parent.componentDefaults.animation,
|
|
150
|
+
bordered: componentDefaults?.bordered ?? parent.componentDefaults.bordered,
|
|
151
|
+
}),
|
|
152
|
+
[componentDefaults, parent.componentDefaults],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Merge per-component overrides: shallow merge per key.
|
|
156
|
+
const mergedOverrides: ComponentOverrides = useMemo(() => {
|
|
157
|
+
const parentOv = parent.componentOverrides;
|
|
158
|
+
const childOv = componentOverrides ?? {};
|
|
159
|
+
return {
|
|
160
|
+
button: { ...parentOv.button, ...childOv.button },
|
|
161
|
+
input: { ...parentOv.input, ...childOv.input },
|
|
162
|
+
table: { ...parentOv.table, ...childOv.table },
|
|
163
|
+
modal: { ...parentOv.modal, ...childOv.modal },
|
|
164
|
+
};
|
|
165
|
+
}, [componentOverrides, parent.componentOverrides]);
|
|
166
|
+
|
|
167
|
+
const resolvedLocale = locale ?? parent.locale;
|
|
168
|
+
const resolvedDirection = direction ?? parent.direction;
|
|
169
|
+
const resolvedPrefix = prefixCls ?? parent.prefixCls;
|
|
170
|
+
|
|
171
|
+
// Helper: build prefixed class name.
|
|
172
|
+
const cls = useCallback(
|
|
173
|
+
(name: string) => `${resolvedPrefix}-${name}`,
|
|
174
|
+
[resolvedPrefix],
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Helper: resolve component size.
|
|
178
|
+
const getSize = useCallback(
|
|
179
|
+
(component?: keyof ComponentOverrides, local?: 'sm' | 'md' | 'lg'): 'sm' | 'md' | 'lg' => {
|
|
180
|
+
// Local prop wins.
|
|
181
|
+
if (local !== undefined) return local;
|
|
182
|
+
// Then per-component override.
|
|
183
|
+
if (component) {
|
|
184
|
+
const ov = mergedOverrides[component] as { size?: 'sm' | 'md' | 'lg' } | undefined;
|
|
185
|
+
if (ov?.size) return ov.size;
|
|
186
|
+
}
|
|
187
|
+
// Then global default.
|
|
188
|
+
return mergedDefaults.size;
|
|
189
|
+
},
|
|
190
|
+
[mergedDefaults.size, mergedOverrides],
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Helper: resolve animation preference.
|
|
194
|
+
const getAnimation = useCallback(
|
|
195
|
+
(local?: boolean): boolean => {
|
|
196
|
+
if (local !== undefined) return local;
|
|
197
|
+
return mergedDefaults.animation;
|
|
198
|
+
},
|
|
199
|
+
[mergedDefaults.animation],
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Helper: resolve bordered preference.
|
|
203
|
+
const getBordered = useCallback(
|
|
204
|
+
(component?: keyof ComponentOverrides, local?: boolean): boolean => {
|
|
205
|
+
if (local !== undefined) return local;
|
|
206
|
+
if (component) {
|
|
207
|
+
const ov = mergedOverrides[component] as { bordered?: boolean } | undefined;
|
|
208
|
+
if (ov?.bordered !== undefined) return ov.bordered;
|
|
209
|
+
}
|
|
210
|
+
return mergedDefaults.bordered;
|
|
211
|
+
},
|
|
212
|
+
[mergedDefaults.bordered, mergedOverrides],
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// Assemble context value.
|
|
216
|
+
const value: ConfigContextValue = useMemo(
|
|
217
|
+
() => ({
|
|
218
|
+
locale: resolvedLocale,
|
|
219
|
+
direction: resolvedDirection,
|
|
220
|
+
componentDefaults: mergedDefaults,
|
|
221
|
+
componentOverrides: mergedOverrides,
|
|
222
|
+
prefixCls: resolvedPrefix,
|
|
223
|
+
configured: true,
|
|
224
|
+
cls,
|
|
225
|
+
getSize,
|
|
226
|
+
getAnimation,
|
|
227
|
+
getBordered,
|
|
228
|
+
}),
|
|
229
|
+
[
|
|
230
|
+
resolvedLocale,
|
|
231
|
+
resolvedDirection,
|
|
232
|
+
mergedDefaults,
|
|
233
|
+
mergedOverrides,
|
|
234
|
+
resolvedPrefix,
|
|
235
|
+
cls,
|
|
236
|
+
getSize,
|
|
237
|
+
getAnimation,
|
|
238
|
+
getBordered,
|
|
239
|
+
],
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Apply dir attribute to the wrapper so CSS logical properties work.
|
|
243
|
+
// Also set lang attribute for accessibility.
|
|
244
|
+
return (
|
|
245
|
+
<ConfigContext.Provider value={value}>
|
|
246
|
+
<div
|
|
247
|
+
dir={resolvedDirection}
|
|
248
|
+
lang={resolvedLocale}
|
|
249
|
+
className={`${resolvedPrefix}-config-root`}
|
|
250
|
+
style={{ direction: resolvedDirection }}
|
|
251
|
+
>
|
|
252
|
+
{children}
|
|
253
|
+
</div>
|
|
254
|
+
</ConfigContext.Provider>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
TkxConfigProvider.displayName = 'TkxConfigProvider';
|
|
259
|
+
|
|
260
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
261
|
+
// useConfig — Hook for components to read the global configuration.
|
|
262
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
263
|
+
|
|
264
|
+
export function useConfig(): ConfigContextValue {
|
|
265
|
+
return useContext(ConfigContext);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
269
|
+
// useDirection — Convenience hook for reading text direction.
|
|
270
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
271
|
+
|
|
272
|
+
export function useDirection(): 'ltr' | 'rtl' {
|
|
273
|
+
const { direction } = useConfig();
|
|
274
|
+
return direction;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
278
|
+
// useLocale — Convenience hook for reading the active locale.
|
|
279
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
280
|
+
|
|
281
|
+
export function useLocale(): string {
|
|
282
|
+
const { locale } = useConfig();
|
|
283
|
+
return locale;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
287
|
+
// usePrefixCls — Convenience hook for building prefixed class names.
|
|
288
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
289
|
+
|
|
290
|
+
export function usePrefixCls(componentName: string, customPrefix?: string): string {
|
|
291
|
+
const { prefixCls } = useConfig();
|
|
292
|
+
const prefix = customPrefix ?? prefixCls;
|
|
293
|
+
return `${prefix}-${componentName}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
297
|
+
// useComponentSize — Resolves the effective size for a component.
|
|
298
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
299
|
+
|
|
300
|
+
export function useComponentSize(
|
|
301
|
+
component: keyof ComponentOverrides,
|
|
302
|
+
localSize?: 'sm' | 'md' | 'lg',
|
|
303
|
+
): 'sm' | 'md' | 'lg' {
|
|
304
|
+
const { getSize } = useConfig();
|
|
305
|
+
return getSize(component, localSize);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
309
|
+
// SIZE & SPACING TOKENS — Maps config sizes to concrete pixel/rem values.
|
|
310
|
+
// Components can use these to avoid duplicating size-to-dimension logic.
|
|
311
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
312
|
+
|
|
313
|
+
export const SIZE_HEIGHT: Record<'sm' | 'md' | 'lg', number> = {
|
|
314
|
+
sm: 32,
|
|
315
|
+
md: 40,
|
|
316
|
+
lg: 48,
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
export const SIZE_FONT: Record<'sm' | 'md' | 'lg', string> = {
|
|
320
|
+
sm: '0.75rem',
|
|
321
|
+
md: '0.875rem',
|
|
322
|
+
lg: '1rem',
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
export const SIZE_PADDING_X: Record<'sm' | 'md' | 'lg', number> = {
|
|
326
|
+
sm: 8,
|
|
327
|
+
md: 12,
|
|
328
|
+
lg: 16,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
export const SIZE_PADDING_Y: Record<'sm' | 'md' | 'lg', number> = {
|
|
332
|
+
sm: 4,
|
|
333
|
+
md: 8,
|
|
334
|
+
lg: 12,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
export const SIZE_BORDER_RADIUS: Record<'sm' | 'md' | 'lg', number> = {
|
|
338
|
+
sm: 4,
|
|
339
|
+
md: 6,
|
|
340
|
+
lg: 8,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
export const SIZE_ICON: Record<'sm' | 'md' | 'lg', number> = {
|
|
344
|
+
sm: 14,
|
|
345
|
+
md: 16,
|
|
346
|
+
lg: 20,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
350
|
+
// RTL Utilities
|
|
351
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
352
|
+
|
|
353
|
+
/** Returns a transform that flips an element horizontally for RTL layouts. */
|
|
354
|
+
export function useRtlFlip(): CSSProperties | undefined {
|
|
355
|
+
const { direction } = useConfig();
|
|
356
|
+
if (direction === 'rtl') {
|
|
357
|
+
return { transform: 'scaleX(-1)' };
|
|
358
|
+
}
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Returns logical margin/padding properties that respect direction. */
|
|
363
|
+
export function useLogicalSpacing(
|
|
364
|
+
inlineStart: number | string,
|
|
365
|
+
inlineEnd: number | string = 0,
|
|
366
|
+
): CSSProperties {
|
|
367
|
+
const { direction } = useConfig();
|
|
368
|
+
const start = typeof inlineStart === 'number' ? `${inlineStart}px` : inlineStart;
|
|
369
|
+
const end = typeof inlineEnd === 'number' ? `${inlineEnd}px` : inlineEnd;
|
|
370
|
+
|
|
371
|
+
if (direction === 'rtl') {
|
|
372
|
+
return { marginRight: start, marginLeft: end };
|
|
373
|
+
}
|
|
374
|
+
return { marginLeft: start, marginRight: end };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
378
|
+
// Locale Utilities
|
|
379
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
380
|
+
|
|
381
|
+
/** Built-in locale display names for the most common locales. */
|
|
382
|
+
const LOCALE_DISPLAY_NAMES: Record<string, string> = {
|
|
383
|
+
'en-US': 'English (US)',
|
|
384
|
+
'en-GB': 'English (UK)',
|
|
385
|
+
'es-ES': 'Spanish',
|
|
386
|
+
'fr-FR': 'French',
|
|
387
|
+
'de-DE': 'German',
|
|
388
|
+
'ja-JP': 'Japanese',
|
|
389
|
+
'ko-KR': 'Korean',
|
|
390
|
+
'zh-CN': 'Chinese (Simplified)',
|
|
391
|
+
'zh-TW': 'Chinese (Traditional)',
|
|
392
|
+
'pt-BR': 'Portuguese (Brazil)',
|
|
393
|
+
'ar-SA': 'Arabic',
|
|
394
|
+
'hi-IN': 'Hindi',
|
|
395
|
+
'ru-RU': 'Russian',
|
|
396
|
+
'it-IT': 'Italian',
|
|
397
|
+
'nl-NL': 'Dutch',
|
|
398
|
+
'sv-SE': 'Swedish',
|
|
399
|
+
'pl-PL': 'Polish',
|
|
400
|
+
'tr-TR': 'Turkish',
|
|
401
|
+
'th-TH': 'Thai',
|
|
402
|
+
'vi-VN': 'Vietnamese',
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
/** RTL locales — used to auto-detect direction when not explicitly set. */
|
|
406
|
+
const RTL_LOCALES = new Set([
|
|
407
|
+
'ar', 'ar-SA', 'ar-EG', 'ar-AE',
|
|
408
|
+
'he', 'he-IL',
|
|
409
|
+
'fa', 'fa-IR',
|
|
410
|
+
'ur', 'ur-PK',
|
|
411
|
+
'ps', 'ps-AF',
|
|
412
|
+
]);
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Returns true if the given locale code is typically written right-to-left.
|
|
416
|
+
* Useful for auto-setting direction based on locale.
|
|
417
|
+
*/
|
|
418
|
+
export function isRtlLocale(locale: string): boolean {
|
|
419
|
+
return RTL_LOCALES.has(locale) || RTL_LOCALES.has(locale.split('-')[0]);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Returns the human-readable name for a locale code, falling back to
|
|
424
|
+
* the Intl.DisplayNames API or the raw code itself.
|
|
425
|
+
*/
|
|
426
|
+
export function getLocaleDisplayName(locale: string): string {
|
|
427
|
+
if (LOCALE_DISPLAY_NAMES[locale]) return LOCALE_DISPLAY_NAMES[locale];
|
|
428
|
+
|
|
429
|
+
if (typeof Intl !== 'undefined' && Intl.DisplayNames) {
|
|
430
|
+
try {
|
|
431
|
+
const dn = new Intl.DisplayNames(['en'], { type: 'language' });
|
|
432
|
+
return dn.of(locale) ?? locale;
|
|
433
|
+
} catch {
|
|
434
|
+
return locale;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return locale;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
442
|
+
// Usage example:
|
|
443
|
+
//
|
|
444
|
+
// <TkxConfigProvider
|
|
445
|
+
// locale="ja-JP"
|
|
446
|
+
// direction="ltr"
|
|
447
|
+
// componentDefaults={{ size: 'sm', animation: true, bordered: false }}
|
|
448
|
+
// componentOverrides={{ table: { striped: true } }}
|
|
449
|
+
// prefixCls="myapp"
|
|
450
|
+
// >
|
|
451
|
+
// <App />
|
|
452
|
+
// </TkxConfigProvider>
|
|
453
|
+
//
|
|
454
|
+
// Inside any component:
|
|
455
|
+
// const { locale, direction, getSize, cls } = useConfig();
|
|
456
|
+
// const size = getSize('button', props.size);
|
|
457
|
+
// const className = cls('button');
|
|
458
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
useRef,
|
|
5
5
|
useMemo,
|
|
6
6
|
useId,
|
|
7
|
+
useEffect,
|
|
7
8
|
type ReactNode,
|
|
8
9
|
type CSSProperties,
|
|
9
10
|
type MouseEvent as ReactMouseEvent,
|
|
@@ -46,6 +47,10 @@ export interface TkxDataGridProps<T = any> {
|
|
|
46
47
|
compact?: boolean;
|
|
47
48
|
maxHeight?: number | string;
|
|
48
49
|
onRowClick?: (row: T) => void;
|
|
50
|
+
/** Enable virtual scrolling. Defaults to auto (enabled when data has 50+ rows and maxHeight is set). */
|
|
51
|
+
virtualScroll?: boolean;
|
|
52
|
+
/** Row height in pixels for virtual scrolling calculations. Default: 40 */
|
|
53
|
+
rowHeight?: number;
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
// ── Sort icon ─────────────────────────────────────────────────────────────────
|
|
@@ -156,11 +161,51 @@ export function TkxDataGrid<T = any>({
|
|
|
156
161
|
compact = false,
|
|
157
162
|
maxHeight,
|
|
158
163
|
onRowClick,
|
|
164
|
+
virtualScroll,
|
|
165
|
+
rowHeight = 40,
|
|
159
166
|
}: TkxDataGridProps<T>) {
|
|
160
167
|
const theme = useTheme();
|
|
161
168
|
const reduced = useReducedMotion();
|
|
162
169
|
const gridId = useId();
|
|
163
170
|
|
|
171
|
+
// ── Virtual scroll ────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
const OVERSCAN = 10;
|
|
174
|
+
const isVirtual =
|
|
175
|
+
virtualScroll !== undefined
|
|
176
|
+
? virtualScroll
|
|
177
|
+
: maxHeight !== undefined && data.length >= 50;
|
|
178
|
+
|
|
179
|
+
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
180
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
181
|
+
const [containerHeight, setContainerHeight] = useState(0);
|
|
182
|
+
|
|
183
|
+
const handleScroll = useCallback(() => {
|
|
184
|
+
if (!scrollContainerRef.current) return;
|
|
185
|
+
setScrollTop(scrollContainerRef.current.scrollTop);
|
|
186
|
+
}, []);
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (!isVirtual || !scrollContainerRef.current) return;
|
|
190
|
+
const el = scrollContainerRef.current;
|
|
191
|
+
setContainerHeight(el.clientHeight);
|
|
192
|
+
const ro = new ResizeObserver(() => {
|
|
193
|
+
setContainerHeight(el.clientHeight);
|
|
194
|
+
});
|
|
195
|
+
ro.observe(el);
|
|
196
|
+
return () => ro.disconnect();
|
|
197
|
+
}, [isVirtual]);
|
|
198
|
+
|
|
199
|
+
const totalHeight = data.length * rowHeight;
|
|
200
|
+
const startIndex = isVirtual
|
|
201
|
+
? Math.max(0, Math.floor(scrollTop / rowHeight) - OVERSCAN)
|
|
202
|
+
: 0;
|
|
203
|
+
const endIndex = isVirtual
|
|
204
|
+
? Math.min(data.length, Math.ceil((scrollTop + containerHeight) / rowHeight) + OVERSCAN)
|
|
205
|
+
: data.length;
|
|
206
|
+
const visibleData = isVirtual ? data.slice(startIndex, endIndex) : data;
|
|
207
|
+
const offsetY = startIndex * rowHeight;
|
|
208
|
+
|
|
164
209
|
// ── Sort state ──────────────────────────────────────────────────────────
|
|
165
210
|
|
|
166
211
|
const [sortKey, setSortKey] = useState<string | null>(null);
|
|
@@ -281,6 +326,8 @@ export function TkxDataGrid<T = any>({
|
|
|
281
326
|
}}
|
|
282
327
|
>
|
|
283
328
|
<div
|
|
329
|
+
ref={scrollContainerRef}
|
|
330
|
+
onScroll={isVirtual ? handleScroll : undefined}
|
|
284
331
|
style={{
|
|
285
332
|
maxHeight: maxHeight ?? 'none',
|
|
286
333
|
overflowX: 'auto',
|
|
@@ -427,7 +474,15 @@ export function TkxDataGrid<T = any>({
|
|
|
427
474
|
</td>
|
|
428
475
|
</tr>
|
|
429
476
|
) : (
|
|
430
|
-
|
|
477
|
+
<>
|
|
478
|
+
{/* Top spacer for virtual scrolling */}
|
|
479
|
+
{isVirtual && offsetY > 0 && (
|
|
480
|
+
<tr aria-hidden="true">
|
|
481
|
+
<td colSpan={totalCols} style={{ height: offsetY, padding: 0, border: 'none' }} />
|
|
482
|
+
</tr>
|
|
483
|
+
)}
|
|
484
|
+
{visibleData.map((row, i) => {
|
|
485
|
+
const rowIndex = startIndex + i;
|
|
431
486
|
const id = getRowId(row, rowKey);
|
|
432
487
|
const isSelected = selectedSet.has(id);
|
|
433
488
|
const isStriped = striped && rowIndex % 2 === 1;
|
|
@@ -448,6 +503,7 @@ export function TkxDataGrid<T = any>({
|
|
|
448
503
|
transition: reduced
|
|
449
504
|
? 'none'
|
|
450
505
|
: 'background-color 120ms ease',
|
|
506
|
+
...(isVirtual ? { height: rowHeight, boxSizing: 'border-box' } : {}),
|
|
451
507
|
}}
|
|
452
508
|
onMouseEnter={
|
|
453
509
|
onRowClick
|
|
@@ -529,7 +585,14 @@ export function TkxDataGrid<T = any>({
|
|
|
529
585
|
})}
|
|
530
586
|
</tr>
|
|
531
587
|
);
|
|
532
|
-
})
|
|
588
|
+
})}
|
|
589
|
+
{/* Bottom spacer for virtual scrolling */}
|
|
590
|
+
{isVirtual && endIndex < data.length && (
|
|
591
|
+
<tr aria-hidden="true">
|
|
592
|
+
<td colSpan={totalCols} style={{ height: (data.length - endIndex) * rowHeight, padding: 0, border: 'none' }} />
|
|
593
|
+
</tr>
|
|
594
|
+
)}
|
|
595
|
+
</>
|
|
533
596
|
)}
|
|
534
597
|
</tbody>
|
|
535
598
|
)}
|