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,276 @@
1
+ import { useState, useRef, useEffect, useCallback } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { useTheme } from '../themes';
4
+ import { sanitizeString } from '../engine/security';
5
+ import { useReducedMotion } from '../hooks';
6
+ import { tkx } from '../engine/tkx';
7
+
8
+ // ── Types ────────────────────────────────────────────────────────────────────
9
+
10
+ export interface CascaderOption {
11
+ value: string;
12
+ label: string;
13
+ children?: CascaderOption[];
14
+ disabled?: boolean;
15
+ }
16
+
17
+ export interface TkxCascaderProps {
18
+ options: CascaderOption[];
19
+ value?: string[];
20
+ onChange?: (value: string[], selectedOptions: CascaderOption[]) => void;
21
+ placeholder?: string;
22
+ label?: string;
23
+ multiple?: boolean;
24
+ }
25
+
26
+ // ── Helpers ──────────────────────────────────────────────────────────────────
27
+
28
+ function getColumns(
29
+ options: CascaderOption[],
30
+ selected: string[],
31
+ ): CascaderOption[][] {
32
+ const columns: CascaderOption[][] = [options];
33
+ let current = options;
34
+ for (const val of selected) {
35
+ const found = current.find((o) => o.value === val);
36
+ if (found?.children?.length) {
37
+ columns.push(found.children);
38
+ current = found.children;
39
+ } else {
40
+ break;
41
+ }
42
+ }
43
+ return columns;
44
+ }
45
+
46
+ function getSelectedOptions(
47
+ options: CascaderOption[],
48
+ values: string[],
49
+ ): CascaderOption[] {
50
+ const result: CascaderOption[] = [];
51
+ let current = options;
52
+ for (const val of values) {
53
+ const found = current.find((o) => o.value === val);
54
+ if (found) {
55
+ result.push(found);
56
+ current = found.children ?? [];
57
+ } else break;
58
+ }
59
+ return result;
60
+ }
61
+
62
+ function getLabelPath(options: CascaderOption[], values: string[]): string {
63
+ return getSelectedOptions(options, values)
64
+ .map((o) => sanitizeString(o.label))
65
+ .join(' / ');
66
+ }
67
+
68
+ // ── Component ────────────────────────────────────────────────────────────────
69
+
70
+ export function TkxCascader({
71
+ options,
72
+ value = [],
73
+ onChange,
74
+ placeholder = 'Select...',
75
+ label,
76
+ multiple = false,
77
+ }: TkxCascaderProps) {
78
+ const theme = useTheme();
79
+ const reducedMotion = useReducedMotion();
80
+ const triggerRef = useRef<HTMLButtonElement>(null);
81
+ const dropdownRef = useRef<HTMLDivElement>(null);
82
+ const [open, setOpen] = useState(false);
83
+ const [hoverPath, setHoverPath] = useState<string[]>(value);
84
+ const [dropdownPos, setDropdownPos] = useState({ top: 0, left: 0, width: 0 });
85
+
86
+ const safeLabel = label ? sanitizeString(label) : undefined;
87
+ const safePlaceholder = sanitizeString(placeholder);
88
+ const displayText = value.length > 0 ? getLabelPath(options, value) : '';
89
+
90
+ // Position dropdown
91
+ useEffect(() => {
92
+ if (!open || !triggerRef.current) return;
93
+ const rect = triggerRef.current.getBoundingClientRect();
94
+ setDropdownPos({
95
+ top: rect.bottom + 4 + window.scrollY,
96
+ left: rect.left + window.scrollX,
97
+ width: Math.max(rect.width, 200),
98
+ });
99
+ }, [open]);
100
+
101
+ // Close on outside click
102
+ useEffect(() => {
103
+ if (!open) return;
104
+ const handler = (e: MouseEvent) => {
105
+ if (
106
+ !triggerRef.current?.contains(e.target as Node) &&
107
+ !dropdownRef.current?.contains(e.target as Node)
108
+ ) {
109
+ setOpen(false);
110
+ }
111
+ };
112
+ document.addEventListener('mousedown', handler);
113
+ return () => document.removeEventListener('mousedown', handler);
114
+ }, [open]);
115
+
116
+ const handleSelect = useCallback(
117
+ (colIdx: number, opt: CascaderOption) => {
118
+ if (opt.disabled) return;
119
+ const newPath = [...hoverPath.slice(0, colIdx), opt.value];
120
+ setHoverPath(newPath);
121
+
122
+ // If leaf node (no children), commit selection
123
+ if (!opt.children?.length) {
124
+ const selected = getSelectedOptions(options, newPath);
125
+ onChange?.(newPath, selected);
126
+ if (!multiple) setOpen(false);
127
+ }
128
+ },
129
+ [hoverPath, options, onChange, multiple],
130
+ );
131
+
132
+ const handleKeyDown = useCallback(
133
+ (e: React.KeyboardEvent) => {
134
+ if (e.key === 'Escape') {
135
+ setOpen(false);
136
+ triggerRef.current?.focus();
137
+ } else if (e.key === 'Enter' || e.key === ' ') {
138
+ if (!open) {
139
+ e.preventDefault();
140
+ setOpen(true);
141
+ }
142
+ }
143
+ },
144
+ [open],
145
+ );
146
+
147
+ const columns = getColumns(options, hoverPath);
148
+
149
+ const dropdown = open
150
+ ? createPortal(
151
+ <div
152
+ ref={dropdownRef}
153
+ role="tree"
154
+ aria-label={safeLabel ?? 'Cascader options'}
155
+ className={tkx('flex rounded-lg border overflow-hidden')}
156
+ style={{
157
+ position: 'absolute',
158
+ zIndex: 9999,
159
+ top: dropdownPos.top,
160
+ left: dropdownPos.left,
161
+ backgroundColor: theme.surface,
162
+ borderColor: theme.border,
163
+ boxShadow: `0 4px 16px ${theme.bg}80`,
164
+ animation: reducedMotion ? 'none' : 'tkxFadeIn 0.15s ease',
165
+ }}
166
+ >
167
+ {columns.map((col, colIdx) => (
168
+ <ul
169
+ key={colIdx}
170
+ role="group"
171
+ className={tkx('m-0 p-0 overflow-auto')}
172
+ style={{
173
+ listStyle: 'none',
174
+ minWidth: 160,
175
+ maxHeight: 260,
176
+ borderRight: colIdx < columns.length - 1 ? `1px solid ${theme.border}` : 'none',
177
+ }}
178
+ >
179
+ {col.map((opt) => {
180
+ const isSelected = hoverPath[colIdx] === opt.value;
181
+ const safeOptLabel = sanitizeString(opt.label);
182
+ return (
183
+ <li
184
+ key={opt.value}
185
+ role="treeitem"
186
+ aria-selected={isSelected}
187
+ aria-disabled={opt.disabled || undefined}
188
+ aria-expanded={opt.children?.length ? isSelected : undefined}
189
+ className={tkx('flex items-center justify-between px-3 py-2 cursor-pointer text-sm')}
190
+ style={{
191
+ backgroundColor: isSelected ? theme.surfaceAlt : 'transparent',
192
+ color: opt.disabled ? theme.textMuted : theme.text,
193
+ opacity: opt.disabled ? 0.5 : 1,
194
+ cursor: opt.disabled ? 'not-allowed' : 'pointer',
195
+ }}
196
+ onClick={() => handleSelect(colIdx, opt)}
197
+ onMouseEnter={() => {
198
+ if (!opt.disabled) {
199
+ setHoverPath((prev) => [...prev.slice(0, colIdx), opt.value]);
200
+ }
201
+ }}
202
+ >
203
+ <span>{safeOptLabel}</span>
204
+ {opt.children && opt.children.length > 0 && (
205
+ <svg
206
+ width="12"
207
+ height="12"
208
+ viewBox="0 0 24 24"
209
+ fill="currentColor"
210
+ aria-hidden="true"
211
+ style={{ color: theme.textMuted, flexShrink: 0 }}
212
+ >
213
+ <path d="M10 6l6 6-6 6V6z" />
214
+ </svg>
215
+ )}
216
+ </li>
217
+ );
218
+ })}
219
+ </ul>
220
+ ))}
221
+ </div>,
222
+ document.body,
223
+ )
224
+ : null;
225
+
226
+ return (
227
+ <div className={tkx('relative font-sans')} onKeyDown={handleKeyDown}>
228
+ {safeLabel && (
229
+ <label
230
+ className={tkx('block text-sm font-medium mb-1')}
231
+ style={{ color: theme.text }}
232
+ >
233
+ {safeLabel}
234
+ </label>
235
+ )}
236
+
237
+ <button
238
+ ref={triggerRef}
239
+ type="button"
240
+ role="combobox"
241
+ aria-expanded={open}
242
+ aria-haspopup="tree"
243
+ aria-label={safeLabel ?? 'Cascader'}
244
+ className={tkx('w-full flex items-center justify-between rounded-lg border px-3 py-2 text-sm cursor-pointer')}
245
+ style={{
246
+ backgroundColor: theme.surface,
247
+ borderColor: open ? theme.primary : theme.border,
248
+ color: displayText ? theme.text : theme.textMuted,
249
+ outline: 'none',
250
+ minHeight: 38,
251
+ textAlign: 'left',
252
+ }}
253
+ onClick={() => setOpen((v) => !v)}
254
+ >
255
+ <span className={tkx('truncate')}>{displayText || safePlaceholder}</span>
256
+ <svg
257
+ width="16"
258
+ height="16"
259
+ viewBox="0 0 24 24"
260
+ fill="currentColor"
261
+ aria-hidden="true"
262
+ style={{
263
+ color: theme.textMuted,
264
+ transform: open ? 'rotate(180deg)' : 'rotate(0deg)',
265
+ transition: reducedMotion ? 'none' : 'transform 0.2s ease',
266
+ flexShrink: 0,
267
+ }}
268
+ >
269
+ <path d="M7 10l5 5 5-5H7z" />
270
+ </svg>
271
+ </button>
272
+
273
+ {dropdown}
274
+ </div>
275
+ );
276
+ }