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.
Files changed (71) hide show
  1. package/dist/index.cjs +75 -14
  2. package/dist/index.d.ts +34 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +11013 -4880
  5. package/dist/src/components/TkxAffix.d.ts +10 -0
  6. package/dist/src/components/TkxAffix.d.ts.map +1 -0
  7. package/dist/src/components/TkxAnchor.d.ts +14 -0
  8. package/dist/src/components/TkxAnchor.d.ts.map +1 -0
  9. package/dist/src/components/TkxCascader.d.ts +16 -0
  10. package/dist/src/components/TkxCascader.d.ts.map +1 -0
  11. package/dist/src/components/TkxConfigProvider.d.ts +100 -0
  12. package/dist/src/components/TkxConfigProvider.d.ts.map +1 -0
  13. package/dist/src/components/TkxDataGrid.d.ts +5 -1
  14. package/dist/src/components/TkxDataGrid.d.ts.map +1 -1
  15. package/dist/src/components/TkxEmpty.d.ts +870 -0
  16. package/dist/src/components/TkxEmpty.d.ts.map +1 -0
  17. package/dist/src/components/TkxForm.d.ts +54 -0
  18. package/dist/src/components/TkxForm.d.ts.map +1 -0
  19. package/dist/src/components/TkxLayout.d.ts +87 -0
  20. package/dist/src/components/TkxLayout.d.ts.map +1 -0
  21. package/dist/src/components/TkxList.d.ts +24 -0
  22. package/dist/src/components/TkxList.d.ts.map +1 -0
  23. package/dist/src/components/TkxMentions.d.ts +15 -0
  24. package/dist/src/components/TkxMentions.d.ts.map +1 -0
  25. package/dist/src/components/TkxQRCode.d.ts +11 -0
  26. package/dist/src/components/TkxQRCode.d.ts.map +1 -0
  27. package/dist/src/components/TkxResult.d.ts +11 -0
  28. package/dist/src/components/TkxResult.d.ts.map +1 -0
  29. package/dist/src/components/TkxSegmented.d.ts +16 -0
  30. package/dist/src/components/TkxSegmented.d.ts.map +1 -0
  31. package/dist/src/components/TkxSelect.d.ts +5 -1
  32. package/dist/src/components/TkxSelect.d.ts.map +1 -1
  33. package/dist/src/components/TkxSpin.d.ts +15 -0
  34. package/dist/src/components/TkxSpin.d.ts.map +1 -0
  35. package/dist/src/components/TkxStatistic.d.ts +1746 -0
  36. package/dist/src/components/TkxStatistic.d.ts.map +1 -0
  37. package/dist/src/components/TkxTour.d.ts +15 -0
  38. package/dist/src/components/TkxTour.d.ts.map +1 -0
  39. package/dist/src/components/TkxTypography.d.ts +2614 -0
  40. package/dist/src/components/TkxTypography.d.ts.map +1 -0
  41. package/dist/src/components/TkxWatermark.d.ts +12 -0
  42. package/dist/src/components/TkxWatermark.d.ts.map +1 -0
  43. package/dist/src/components/index.d.ts +17 -0
  44. package/dist/src/components/index.d.ts.map +1 -1
  45. package/dist/src/engine/tkx.d.ts +38 -0
  46. package/dist/src/engine/tkx.d.ts.map +1 -1
  47. package/dist/src/themes/index.d.ts +108 -0
  48. package/dist/src/themes/index.d.ts.map +1 -1
  49. package/package.json +1 -1
  50. package/src/components/TkxAffix.tsx +138 -0
  51. package/src/components/TkxAnchor.tsx +193 -0
  52. package/src/components/TkxCascader.tsx +276 -0
  53. package/src/components/TkxConfigProvider.tsx +458 -0
  54. package/src/components/TkxDataGrid.tsx +65 -2
  55. package/src/components/TkxEmpty.tsx +219 -0
  56. package/src/components/TkxForm.tsx +607 -0
  57. package/src/components/TkxLayout.tsx +647 -0
  58. package/src/components/TkxList.tsx +242 -0
  59. package/src/components/TkxMentions.tsx +210 -0
  60. package/src/components/TkxQRCode.tsx +174 -0
  61. package/src/components/TkxResult.tsx +128 -0
  62. package/src/components/TkxSegmented.tsx +169 -0
  63. package/src/components/TkxSelect.tsx +209 -6
  64. package/src/components/TkxSpin.tsx +261 -0
  65. package/src/components/TkxStatistic.tsx +261 -0
  66. package/src/components/TkxTour.tsx +239 -0
  67. package/src/components/TkxTypography.tsx +263 -0
  68. package/src/components/TkxWatermark.tsx +143 -0
  69. package/src/components/index.ts +17 -0
  70. package/src/engine/tkx.ts +62 -0
  71. package/src/themes/index.ts +149 -0
@@ -0,0 +1,261 @@
1
+ import {
2
+ useState,
3
+ useEffect,
4
+ useRef,
5
+ type ReactNode,
6
+ type CSSProperties,
7
+ createElement,
8
+ } from 'react';
9
+ import { useTheme } from '../themes';
10
+ import { sanitizeString } from '../engine/security';
11
+ import { useReducedMotion } from '../hooks';
12
+
13
+ // ── Interface ───────────────────────────────────────────────────────────────
14
+
15
+ export interface TkxSpinProps {
16
+ spinning?: boolean;
17
+ size?: 'sm' | 'md' | 'lg';
18
+ tip?: string;
19
+ indicator?: ReactNode;
20
+ children?: ReactNode;
21
+ fullscreen?: boolean;
22
+ delay?: number;
23
+ style?: CSSProperties;
24
+ }
25
+
26
+ // ── Size Config ─────────────────────────────────────────────────────────────
27
+
28
+ const SIZE_MAP: Record<string, { dimension: number; borderWidth: number; fontSize: string }> = {
29
+ sm: { dimension: 20, borderWidth: 2, fontSize: '0.75rem' },
30
+ md: { dimension: 32, borderWidth: 3, fontSize: '0.875rem' },
31
+ lg: { dimension: 48, borderWidth: 4, fontSize: '1rem' },
32
+ };
33
+
34
+ // ── Keyframes Injection ─────────────────────────────────────────────────────
35
+
36
+ let stylesInjected = false;
37
+
38
+ function injectSpinStyles() {
39
+ if (stylesInjected || typeof document === 'undefined') return;
40
+ stylesInjected = true;
41
+ const styleEl = document.createElement('style');
42
+ styleEl.id = 'tkx-spin-styles';
43
+ styleEl.textContent = `
44
+ @keyframes tkx-spin-rotate {
45
+ 0% { transform: rotate(0deg); }
46
+ 100% { transform: rotate(360deg); }
47
+ }
48
+ `;
49
+ document.head.appendChild(styleEl);
50
+ }
51
+
52
+ // ── Static Indicator (reduced motion) ───────────────────────────────────────
53
+
54
+ function StaticIndicator({ size, color }: { size: number; color: string }) {
55
+ return createElement(
56
+ 'svg',
57
+ {
58
+ width: size,
59
+ height: size,
60
+ viewBox: '0 0 24 24',
61
+ fill: 'none',
62
+ role: 'img',
63
+ 'aria-label': 'Loading',
64
+ },
65
+ createElement('circle', {
66
+ cx: 12,
67
+ cy: 12,
68
+ r: 10,
69
+ stroke: `${color}33`,
70
+ strokeWidth: 2.5,
71
+ fill: 'none',
72
+ }),
73
+ createElement('path', {
74
+ d: 'M12 2a10 10 0 0 1 10 10',
75
+ stroke: color,
76
+ strokeWidth: 2.5,
77
+ strokeLinecap: 'round',
78
+ fill: 'none',
79
+ }),
80
+ );
81
+ }
82
+
83
+ // ── Default Spinner ─────────────────────────────────────────────────────────
84
+
85
+ function DefaultSpinner({
86
+ size,
87
+ borderWidth,
88
+ color,
89
+ reducedMotion,
90
+ }: {
91
+ size: number;
92
+ borderWidth: number;
93
+ color: string;
94
+ reducedMotion: boolean;
95
+ }) {
96
+ if (reducedMotion) {
97
+ return createElement(StaticIndicator, { size, color });
98
+ }
99
+
100
+ injectSpinStyles();
101
+
102
+ return createElement('div', {
103
+ role: 'img',
104
+ 'aria-label': 'Loading',
105
+ style: {
106
+ width: `${size}px`,
107
+ height: `${size}px`,
108
+ border: `${borderWidth}px solid ${color}33`,
109
+ borderTopColor: color,
110
+ borderRadius: '50%',
111
+ animation: 'tkx-spin-rotate 0.8s linear infinite',
112
+ boxSizing: 'border-box',
113
+ },
114
+ });
115
+ }
116
+
117
+ // ── TkxSpin ─────────────────────────────────────────────────────────────────
118
+
119
+ export function TkxSpin({
120
+ spinning = true,
121
+ size = 'md',
122
+ tip,
123
+ indicator,
124
+ children,
125
+ fullscreen = false,
126
+ delay = 0,
127
+ style,
128
+ }: TkxSpinProps) {
129
+ const theme = useTheme();
130
+ const reducedMotion = useReducedMotion();
131
+ const [visible, setVisible] = useState(delay === 0 && spinning);
132
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
133
+
134
+ useEffect(() => {
135
+ if (spinning && delay > 0) {
136
+ timerRef.current = setTimeout(() => setVisible(true), delay);
137
+ } else {
138
+ setVisible(spinning);
139
+ }
140
+ return () => {
141
+ if (timerRef.current !== null) clearTimeout(timerRef.current);
142
+ };
143
+ }, [spinning, delay]);
144
+
145
+ if (!visible) {
146
+ return children ? createElement('div', { style }, children) : null;
147
+ }
148
+
149
+ const sizeConfig = SIZE_MAP[size];
150
+ const safeTip = tip ? sanitizeString(tip) : undefined;
151
+
152
+ const spinnerContent = createElement(
153
+ 'div',
154
+ {
155
+ role: 'status',
156
+ 'aria-live': 'polite',
157
+ 'aria-label': safeTip ?? 'Loading',
158
+ style: {
159
+ display: 'flex',
160
+ flexDirection: 'column' as const,
161
+ alignItems: 'center',
162
+ justifyContent: 'center',
163
+ gap: '8px',
164
+ },
165
+ },
166
+ indicator ??
167
+ createElement(DefaultSpinner, {
168
+ size: sizeConfig.dimension,
169
+ borderWidth: sizeConfig.borderWidth,
170
+ color: theme.primary,
171
+ reducedMotion,
172
+ }),
173
+ safeTip &&
174
+ createElement(
175
+ 'span',
176
+ {
177
+ style: {
178
+ color: theme.primary,
179
+ fontSize: sizeConfig.fontSize,
180
+ marginTop: '4px',
181
+ },
182
+ },
183
+ safeTip,
184
+ ),
185
+ );
186
+
187
+ // Fullscreen overlay
188
+ if (fullscreen) {
189
+ return createElement(
190
+ 'div',
191
+ {
192
+ style: {
193
+ position: 'fixed',
194
+ inset: 0,
195
+ display: 'flex',
196
+ alignItems: 'center',
197
+ justifyContent: 'center',
198
+ backgroundColor: `${theme.bg}cc`,
199
+ zIndex: 9999,
200
+ ...style,
201
+ },
202
+ },
203
+ spinnerContent,
204
+ );
205
+ }
206
+
207
+ // Wrapper mode: overlay on children
208
+ if (children) {
209
+ return createElement(
210
+ 'div',
211
+ {
212
+ style: {
213
+ position: 'relative',
214
+ ...style,
215
+ },
216
+ },
217
+ createElement(
218
+ 'div',
219
+ {
220
+ style: {
221
+ filter: 'blur(1px)',
222
+ opacity: 0.5,
223
+ pointerEvents: 'none',
224
+ userSelect: 'none',
225
+ transition: reducedMotion ? 'none' : 'opacity 0.2s, filter 0.2s',
226
+ },
227
+ },
228
+ children,
229
+ ),
230
+ createElement(
231
+ 'div',
232
+ {
233
+ style: {
234
+ position: 'absolute',
235
+ inset: 0,
236
+ display: 'flex',
237
+ alignItems: 'center',
238
+ justifyContent: 'center',
239
+ backgroundColor: `${theme.bg}66`,
240
+ borderRadius: '4px',
241
+ },
242
+ },
243
+ spinnerContent,
244
+ ),
245
+ );
246
+ }
247
+
248
+ // Standalone spinner
249
+ return createElement(
250
+ 'div',
251
+ {
252
+ style: {
253
+ display: 'inline-flex',
254
+ alignItems: 'center',
255
+ justifyContent: 'center',
256
+ ...style,
257
+ },
258
+ },
259
+ spinnerContent,
260
+ );
261
+ }
@@ -0,0 +1,261 @@
1
+ import { useState, useEffect, useRef, type ReactNode, type CSSProperties, createElement } from 'react';
2
+ import { useTheme } from '../themes';
3
+ import { sanitizeString } from '../engine/security';
4
+ import { useReducedMotion } from '../hooks';
5
+
6
+ // ── Interfaces ──────────────────────────────────────────────────────────────
7
+
8
+ export interface TkxStatisticProps {
9
+ title: string; value: number | string; prefix?: ReactNode; suffix?: ReactNode;
10
+ precision?: number; groupSeparator?: string; valueStyle?: CSSProperties;
11
+ loading?: boolean; trend?: 'up' | 'down'; trendValue?: string; style?: CSSProperties;
12
+ }
13
+
14
+ export interface TkxCountdownProps {
15
+ title: string; value: number; format?: string; onFinish?: () => void;
16
+ prefix?: ReactNode; suffix?: ReactNode; style?: CSSProperties;
17
+ }
18
+
19
+ // ── Number Formatting ───────────────────────────────────────────────────────
20
+
21
+ function formatNumber(value: number | string, precision?: number, separator = ','): string {
22
+ if (typeof value === 'string') return value;
23
+
24
+ const fixed = precision !== undefined ? value.toFixed(precision) : String(value);
25
+ const [intPart, decPart] = fixed.split('.');
26
+ const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
27
+ return decPart !== undefined ? `${grouped}.${decPart}` : grouped;
28
+ }
29
+
30
+ // ── Countdown Formatting ────────────────────────────────────────────────────
31
+
32
+ function formatCountdown(ms: number, fmt: string): string {
33
+ if (ms <= 0) return fmt.replace(/DD?/g, '0').replace(/HH?/g, '00').replace(/mm?/g, '00').replace(/ss?/g, '00');
34
+ const t = Math.floor(ms / 1000);
35
+ const d = Math.floor(t / 86400), h = Math.floor((t % 86400) / 3600);
36
+ const min = Math.floor((t % 3600) / 60), s = t % 60;
37
+ const pad = (n: number) => String(n).padStart(2, '0');
38
+ return fmt.replace(/DD/g, pad(d)).replace(/D/g, String(d))
39
+ .replace(/HH/g, pad(h)).replace(/H/g, String(h))
40
+ .replace(/mm/g, pad(min)).replace(/m/g, String(min))
41
+ .replace(/ss/g, pad(s)).replace(/s/g, String(s));
42
+ }
43
+
44
+ // ── Trend Arrow ─────────────────────────────────────────────────────────────
45
+
46
+ function TrendArrow({ direction, color }: { direction: 'up' | 'down'; color: string }) {
47
+ const d = direction === 'up' ? 'M6 10 L10 4 L14 10' : 'M6 4 L10 10 L14 4';
48
+ return createElement('svg', {
49
+ width: 16, height: 14, viewBox: '0 0 20 14', fill: 'none',
50
+ 'aria-hidden': 'true', style: { display: 'inline-block', verticalAlign: 'middle' },
51
+ }, createElement('path', {
52
+ d, stroke: color, strokeWidth: 2.5, strokeLinecap: 'round', strokeLinejoin: 'round', fill: 'none',
53
+ }));
54
+ }
55
+
56
+ // ── Loading Skeleton ────────────────────────────────────────────────────────
57
+
58
+ let skeletonInjected = false;
59
+ function injectSkeletonStyles() {
60
+ if (skeletonInjected || typeof document === 'undefined') return;
61
+ skeletonInjected = true;
62
+ const el = document.createElement('style');
63
+ el.id = 'tkx-statistic-skeleton';
64
+ el.textContent = '@keyframes tkx-stat-shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}';
65
+ document.head.appendChild(el);
66
+ }
67
+
68
+ function Skeleton({ w, h, alt, bdr, rm }: { w: string; h: string; alt: string; bdr: string; rm: boolean }) {
69
+ if (!rm) injectSkeletonStyles();
70
+ return createElement('div', { 'aria-hidden': 'true', style: {
71
+ width: w, height: h, borderRadius: '4px',
72
+ background: rm ? alt : `linear-gradient(90deg, ${alt} 25%, ${bdr} 50%, ${alt} 75%)`,
73
+ backgroundSize: '200% 100%', animation: rm ? 'none' : 'tkx-stat-shimmer 1.5s ease-in-out infinite',
74
+ }});
75
+ }
76
+
77
+ // ── TkxStatistic ────────────────────────────────────────────────────────────
78
+
79
+ export function TkxStatistic({
80
+ title,
81
+ value,
82
+ prefix,
83
+ suffix,
84
+ precision,
85
+ groupSeparator = ',',
86
+ valueStyle,
87
+ loading = false,
88
+ trend,
89
+ trendValue,
90
+ style,
91
+ }: TkxStatisticProps) {
92
+ const theme = useTheme();
93
+ const reducedMotion = useReducedMotion();
94
+ const safeTitle = sanitizeString(title);
95
+ const safeTrendValue = trendValue ? sanitizeString(trendValue) : undefined;
96
+
97
+ const trendColor = trend === 'up' ? theme.success : trend === 'down' ? theme.danger : undefined;
98
+
99
+ return createElement(
100
+ 'div',
101
+ {
102
+ style: {
103
+ display: 'flex',
104
+ flexDirection: 'column' as const,
105
+ gap: '4px',
106
+ ...style,
107
+ },
108
+ },
109
+ // Title
110
+ createElement(
111
+ 'div',
112
+ {
113
+ style: {
114
+ color: theme.textMuted,
115
+ fontSize: '0.875rem',
116
+ lineHeight: '1.4',
117
+ },
118
+ },
119
+ safeTitle,
120
+ ),
121
+ // Value row
122
+ loading
123
+ ? createElement(Skeleton, { w: '120px', h: '32px', alt: theme.surfaceAlt, bdr: theme.border, rm: reducedMotion })
124
+ : createElement(
125
+ 'div',
126
+ {
127
+ style: {
128
+ display: 'flex',
129
+ alignItems: 'baseline',
130
+ gap: '4px',
131
+ fontSize: '1.75rem',
132
+ fontWeight: 700,
133
+ lineHeight: '1.2',
134
+ color: theme.text,
135
+ fontVariantNumeric: 'tabular-nums',
136
+ ...valueStyle,
137
+ },
138
+ },
139
+ prefix &&
140
+ createElement(
141
+ 'span',
142
+ { style: { fontSize: '0.75em', fontWeight: 400 } },
143
+ prefix,
144
+ ),
145
+ createElement('span', null, formatNumber(value, precision, groupSeparator)),
146
+ suffix &&
147
+ createElement(
148
+ 'span',
149
+ { style: { fontSize: '0.6em', fontWeight: 400, color: theme.textMuted } },
150
+ suffix,
151
+ ),
152
+ ),
153
+ // Trend line
154
+ trend &&
155
+ createElement(
156
+ 'div',
157
+ {
158
+ style: {
159
+ display: 'flex',
160
+ alignItems: 'center',
161
+ gap: '4px',
162
+ fontSize: '0.8125rem',
163
+ color: trendColor,
164
+ marginTop: '2px',
165
+ },
166
+ },
167
+ createElement(TrendArrow, { direction: trend, color: trendColor! }),
168
+ safeTrendValue &&
169
+ createElement('span', null, safeTrendValue),
170
+ ),
171
+ );
172
+ }
173
+
174
+ // ── TkxCountdown ────────────────────────────────────────────────────────────
175
+
176
+ export function TkxCountdown({
177
+ title,
178
+ value,
179
+ format = 'HH:mm:ss',
180
+ onFinish,
181
+ prefix,
182
+ suffix,
183
+ style,
184
+ }: TkxCountdownProps) {
185
+ const theme = useTheme();
186
+ const safeTitle = sanitizeString(title);
187
+ const onFinishRef = useRef(onFinish);
188
+ onFinishRef.current = onFinish;
189
+ const finishedRef = useRef(false);
190
+
191
+ const [remaining, setRemaining] = useState(() => Math.max(0, value - Date.now()));
192
+
193
+ useEffect(() => {
194
+ finishedRef.current = false;
195
+ const tick = () => {
196
+ const diff = Math.max(0, value - Date.now());
197
+ setRemaining(diff);
198
+ if (diff <= 0 && !finishedRef.current) {
199
+ finishedRef.current = true;
200
+ onFinishRef.current?.();
201
+ }
202
+ };
203
+
204
+ tick();
205
+ const id = setInterval(tick, 1000);
206
+ return () => clearInterval(id);
207
+ }, [value]);
208
+
209
+ const display = formatCountdown(remaining, format);
210
+
211
+ return createElement(
212
+ 'div',
213
+ {
214
+ style: {
215
+ display: 'flex',
216
+ flexDirection: 'column' as const,
217
+ gap: '4px',
218
+ ...style,
219
+ },
220
+ },
221
+ createElement(
222
+ 'div',
223
+ {
224
+ style: {
225
+ color: theme.textMuted,
226
+ fontSize: '0.875rem',
227
+ lineHeight: '1.4',
228
+ },
229
+ },
230
+ safeTitle,
231
+ ),
232
+ createElement(
233
+ 'div',
234
+ {
235
+ style: {
236
+ display: 'flex',
237
+ alignItems: 'baseline',
238
+ gap: '4px',
239
+ fontSize: '1.75rem',
240
+ fontWeight: 700,
241
+ lineHeight: '1.2',
242
+ color: remaining <= 0 ? theme.danger : theme.text,
243
+ fontVariantNumeric: 'tabular-nums',
244
+ },
245
+ },
246
+ prefix &&
247
+ createElement(
248
+ 'span',
249
+ { style: { fontSize: '0.75em', fontWeight: 400 } },
250
+ prefix,
251
+ ),
252
+ createElement('span', null, display),
253
+ suffix &&
254
+ createElement(
255
+ 'span',
256
+ { style: { fontSize: '0.6em', fontWeight: 400, color: theme.textMuted } },
257
+ suffix,
258
+ ),
259
+ ),
260
+ );
261
+ }