tekivex-ui 2.2.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 (47) hide show
  1. package/dist/index.cjs +75 -14
  2. package/dist/index.d.ts +32 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +10957 -4936
  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/TkxDataGrid.d.ts +5 -1
  12. package/dist/src/components/TkxDataGrid.d.ts.map +1 -1
  13. package/dist/src/components/TkxList.d.ts +24 -0
  14. package/dist/src/components/TkxList.d.ts.map +1 -0
  15. package/dist/src/components/TkxMentions.d.ts +15 -0
  16. package/dist/src/components/TkxMentions.d.ts.map +1 -0
  17. package/dist/src/components/TkxQRCode.d.ts +11 -0
  18. package/dist/src/components/TkxQRCode.d.ts.map +1 -0
  19. package/dist/src/components/TkxResult.d.ts +11 -0
  20. package/dist/src/components/TkxResult.d.ts.map +1 -0
  21. package/dist/src/components/TkxSegmented.d.ts +16 -0
  22. package/dist/src/components/TkxSegmented.d.ts.map +1 -0
  23. package/dist/src/components/TkxSelect.d.ts +5 -1
  24. package/dist/src/components/TkxSelect.d.ts.map +1 -1
  25. package/dist/src/components/TkxTour.d.ts +15 -0
  26. package/dist/src/components/TkxTour.d.ts.map +1 -0
  27. package/dist/src/components/TkxWatermark.d.ts +12 -0
  28. package/dist/src/components/TkxWatermark.d.ts.map +1 -0
  29. package/dist/src/components/index.d.ts +10 -0
  30. package/dist/src/components/index.d.ts.map +1 -1
  31. package/dist/src/engine/tkx.d.ts +38 -0
  32. package/dist/src/engine/tkx.d.ts.map +1 -1
  33. package/package.json +1 -1
  34. package/src/components/TkxAffix.tsx +138 -0
  35. package/src/components/TkxAnchor.tsx +193 -0
  36. package/src/components/TkxCascader.tsx +276 -0
  37. package/src/components/TkxDataGrid.tsx +65 -2
  38. package/src/components/TkxList.tsx +242 -0
  39. package/src/components/TkxMentions.tsx +210 -0
  40. package/src/components/TkxQRCode.tsx +174 -0
  41. package/src/components/TkxResult.tsx +128 -0
  42. package/src/components/TkxSegmented.tsx +169 -0
  43. package/src/components/TkxSelect.tsx +209 -6
  44. package/src/components/TkxTour.tsx +239 -0
  45. package/src/components/TkxWatermark.tsx +143 -0
  46. package/src/components/index.ts +10 -0
  47. package/src/engine/tkx.ts +62 -0
@@ -0,0 +1,242 @@
1
+ import { type ReactNode } from 'react';
2
+ import { useTheme } from '../themes';
3
+ import { sanitizeString } from '../engine/security';
4
+ import { useReducedMotion } from '../hooks';
5
+ import { tkx } from '../engine/tkx';
6
+
7
+ // ── Types ────────────────────────────────────────────────────────────────────
8
+
9
+ export interface ListItem {
10
+ key: string;
11
+ title: string;
12
+ description?: string;
13
+ avatar?: ReactNode;
14
+ actions?: ReactNode;
15
+ extra?: ReactNode;
16
+ }
17
+
18
+ export interface TkxListProps {
19
+ items: ListItem[];
20
+ header?: ReactNode;
21
+ footer?: ReactNode;
22
+ bordered?: boolean;
23
+ size?: 'sm' | 'md' | 'lg';
24
+ loading?: boolean;
25
+ emptyText?: string;
26
+ grid?: { column?: number; gutter?: number };
27
+ }
28
+
29
+ // ── Size Tokens ──────────────────────────────────────────────────────────────
30
+
31
+ const SIZE_PADDING = {
32
+ sm: '8px 12px',
33
+ md: '12px 16px',
34
+ lg: '16px 20px',
35
+ } as const;
36
+
37
+ // ── Loading Skeleton ─────────────────────────────────────────────────────────
38
+
39
+ function Skeleton({ theme, reducedMotion }: { theme: ReturnType<typeof useTheme>; reducedMotion: boolean }) {
40
+ return (
41
+ <div className={tkx('flex items-center gap-3 px-4 py-3')} aria-hidden="true">
42
+ <div
43
+ style={{
44
+ width: 40,
45
+ height: 40,
46
+ borderRadius: '50%',
47
+ backgroundColor: theme.surfaceAlt,
48
+ animation: reducedMotion ? 'none' : 'tkxPulse 1.5s ease infinite',
49
+ }}
50
+ />
51
+ <div className={tkx('flex-1')}>
52
+ <div
53
+ style={{
54
+ width: '60%',
55
+ height: 14,
56
+ borderRadius: 4,
57
+ backgroundColor: theme.surfaceAlt,
58
+ marginBottom: 8,
59
+ animation: reducedMotion ? 'none' : 'tkxPulse 1.5s ease infinite',
60
+ }}
61
+ />
62
+ <div
63
+ style={{
64
+ width: '40%',
65
+ height: 12,
66
+ borderRadius: 4,
67
+ backgroundColor: theme.surfaceAlt,
68
+ animation: reducedMotion ? 'none' : 'tkxPulse 1.5s ease infinite',
69
+ }}
70
+ />
71
+ </div>
72
+ </div>
73
+ );
74
+ }
75
+
76
+ // ── Component ────────────────────────────────────────────────────────────────
77
+
78
+ export function TkxList({
79
+ items,
80
+ header,
81
+ footer,
82
+ bordered = true,
83
+ size = 'md',
84
+ loading = false,
85
+ emptyText = 'No data',
86
+ grid,
87
+ }: TkxListProps) {
88
+ const theme = useTheme();
89
+ const reducedMotion = useReducedMotion();
90
+ const safeEmptyText = sanitizeString(emptyText);
91
+ const padding = SIZE_PADDING[size];
92
+
93
+ const isGrid = !!grid;
94
+ const columns = grid?.column ?? 1;
95
+ const gutter = grid?.gutter ?? 16;
96
+
97
+ return (
98
+ <div
99
+ role="list"
100
+ aria-label="List"
101
+ aria-busy={loading}
102
+ className={tkx('font-sans rounded-lg overflow-hidden')}
103
+ style={{
104
+ border: bordered ? `1px solid ${theme.border}` : 'none',
105
+ backgroundColor: theme.surface,
106
+ }}
107
+ >
108
+ {/* Header */}
109
+ {header && (
110
+ <div
111
+ className={tkx('font-semibold text-sm')}
112
+ style={{
113
+ padding,
114
+ color: theme.text,
115
+ borderBottom: `1px solid ${theme.border}`,
116
+ backgroundColor: theme.surfaceAlt,
117
+ }}
118
+ >
119
+ {header}
120
+ </div>
121
+ )}
122
+
123
+ {/* Loading */}
124
+ {loading && (
125
+ <div>
126
+ {Array.from({ length: 3 }).map((_, i) => (
127
+ <Skeleton key={i} theme={theme} reducedMotion={reducedMotion} />
128
+ ))}
129
+ </div>
130
+ )}
131
+
132
+ {/* Empty State */}
133
+ {!loading && items.length === 0 && (
134
+ <div
135
+ role="status"
136
+ className={tkx('text-center py-12 text-sm')}
137
+ style={{ color: theme.textMuted }}
138
+ >
139
+ {safeEmptyText}
140
+ </div>
141
+ )}
142
+
143
+ {/* Items */}
144
+ {!loading && items.length > 0 && (
145
+ <div
146
+ style={
147
+ isGrid
148
+ ? {
149
+ display: 'grid',
150
+ gridTemplateColumns: `repeat(${columns}, 1fr)`,
151
+ gap: gutter,
152
+ padding: gutter,
153
+ }
154
+ : undefined
155
+ }
156
+ >
157
+ {items.map((item, idx) => {
158
+ const safeTitle = sanitizeString(item.title);
159
+ const safeDesc = item.description ? sanitizeString(item.description) : undefined;
160
+
161
+ return (
162
+ <div
163
+ key={item.key}
164
+ role="listitem"
165
+ className={tkx('flex items-start gap-3')}
166
+ style={{
167
+ padding: isGrid ? padding : padding,
168
+ borderBottom:
169
+ !isGrid && idx < items.length - 1
170
+ ? `1px solid ${theme.border}`
171
+ : 'none',
172
+ ...(isGrid
173
+ ? {
174
+ border: `1px solid ${theme.border}`,
175
+ borderRadius: 8,
176
+ backgroundColor: theme.surface,
177
+ }
178
+ : {}),
179
+ animation: reducedMotion ? 'none' : `tkxFadeIn 0.2s ease ${idx * 0.03}s both`,
180
+ }}
181
+ >
182
+ {/* Avatar */}
183
+ {item.avatar && (
184
+ <div aria-hidden="true" className={tkx('shrink-0')}>
185
+ {item.avatar}
186
+ </div>
187
+ )}
188
+
189
+ {/* Content */}
190
+ <div className={tkx('flex-1 min-w-0')}>
191
+ <div
192
+ className={tkx('text-sm font-medium')}
193
+ style={{ color: theme.text }}
194
+ >
195
+ {safeTitle}
196
+ </div>
197
+ {safeDesc && (
198
+ <div
199
+ className={tkx('text-xs mt-1 leading-relaxed')}
200
+ style={{ color: theme.textMuted }}
201
+ >
202
+ {safeDesc}
203
+ </div>
204
+ )}
205
+ </div>
206
+
207
+ {/* Extra */}
208
+ {item.extra && (
209
+ <div className={tkx('shrink-0 ml-auto')} style={{ color: theme.textMuted }}>
210
+ {item.extra}
211
+ </div>
212
+ )}
213
+
214
+ {/* Actions */}
215
+ {item.actions && (
216
+ <div className={tkx('shrink-0 flex items-center gap-2')}>
217
+ {item.actions}
218
+ </div>
219
+ )}
220
+ </div>
221
+ );
222
+ })}
223
+ </div>
224
+ )}
225
+
226
+ {/* Footer */}
227
+ {footer && (
228
+ <div
229
+ className={tkx('text-sm')}
230
+ style={{
231
+ padding,
232
+ color: theme.textMuted,
233
+ borderTop: `1px solid ${theme.border}`,
234
+ backgroundColor: theme.surfaceAlt,
235
+ }}
236
+ >
237
+ {footer}
238
+ </div>
239
+ )}
240
+ </div>
241
+ );
242
+ }
@@ -0,0 +1,210 @@
1
+ import { type ReactNode, useState, useRef, useCallback, useEffect } from 'react';
2
+ import { useTheme } from '../themes';
3
+ import { sanitizeString } from '../engine/security';
4
+ import { useReducedMotion } from '../hooks';
5
+ import { tkx } from '../engine/tkx';
6
+
7
+ // ── Types ────────────────────────────────────────────────────────────────────
8
+
9
+ export interface MentionOption {
10
+ value: string;
11
+ label: string;
12
+ avatar?: string;
13
+ }
14
+
15
+ export interface TkxMentionsProps {
16
+ options: MentionOption[];
17
+ value?: string;
18
+ onChange?: (value: string) => void;
19
+ trigger?: string;
20
+ placeholder?: string;
21
+ label?: string;
22
+ }
23
+
24
+ // ── Component ────────────────────────────────────────────────────────────────
25
+
26
+ export function TkxMentions({
27
+ options,
28
+ value = '',
29
+ onChange,
30
+ trigger = '@',
31
+ placeholder,
32
+ label,
33
+ }: TkxMentionsProps) {
34
+ const theme = useTheme();
35
+ const reducedMotion = useReducedMotion();
36
+ const inputRef = useRef<HTMLTextAreaElement>(null);
37
+ const dropdownRef = useRef<HTMLUListElement>(null);
38
+ const [text, setText] = useState(value);
39
+ const [showDropdown, setShowDropdown] = useState(false);
40
+ const [query, setQuery] = useState('');
41
+ const [activeIdx, setActiveIdx] = useState(0);
42
+ const [triggerPos, setTriggerPos] = useState(-1);
43
+
44
+ useEffect(() => {
45
+ if (value !== undefined) setText(value);
46
+ }, [value]);
47
+
48
+ const filtered = options.filter((o) =>
49
+ o.label.toLowerCase().includes(query.toLowerCase()),
50
+ );
51
+
52
+ const handleInput = useCallback(
53
+ (e: React.ChangeEvent<HTMLTextAreaElement>) => {
54
+ const val = e.target.value;
55
+ setText(val);
56
+ onChange?.(val);
57
+
58
+ const cursorPos = e.target.selectionStart ?? val.length;
59
+ const beforeCursor = val.slice(0, cursorPos);
60
+ const lastTrigger = beforeCursor.lastIndexOf(trigger);
61
+
62
+ if (lastTrigger >= 0) {
63
+ const afterTrigger = beforeCursor.slice(lastTrigger + trigger.length);
64
+ if (!/\s/.test(afterTrigger)) {
65
+ setQuery(afterTrigger);
66
+ setTriggerPos(lastTrigger);
67
+ setShowDropdown(true);
68
+ setActiveIdx(0);
69
+ return;
70
+ }
71
+ }
72
+ setShowDropdown(false);
73
+ },
74
+ [trigger, onChange],
75
+ );
76
+
77
+ const insertMention = useCallback(
78
+ (opt: MentionOption) => {
79
+ const before = text.slice(0, triggerPos);
80
+ const after = text.slice(
81
+ triggerPos + trigger.length + query.length,
82
+ );
83
+ const newText = `${before}${trigger}${opt.value} ${after}`;
84
+ setText(newText);
85
+ onChange?.(newText);
86
+ setShowDropdown(false);
87
+ inputRef.current?.focus();
88
+ },
89
+ [text, triggerPos, trigger, query, onChange],
90
+ );
91
+
92
+ const handleKeyDown = useCallback(
93
+ (e: React.KeyboardEvent) => {
94
+ if (!showDropdown) return;
95
+ if (e.key === 'ArrowDown') {
96
+ e.preventDefault();
97
+ setActiveIdx((i) => (i + 1) % filtered.length);
98
+ } else if (e.key === 'ArrowUp') {
99
+ e.preventDefault();
100
+ setActiveIdx((i) => (i - 1 + filtered.length) % filtered.length);
101
+ } else if (e.key === 'Enter' && filtered[activeIdx]) {
102
+ e.preventDefault();
103
+ insertMention(filtered[activeIdx]);
104
+ } else if (e.key === 'Escape') {
105
+ setShowDropdown(false);
106
+ }
107
+ },
108
+ [showDropdown, filtered, activeIdx, insertMention],
109
+ );
110
+
111
+ // Scroll active option into view
112
+ useEffect(() => {
113
+ if (!showDropdown || !dropdownRef.current) return;
114
+ const active = dropdownRef.current.children[activeIdx] as HTMLElement;
115
+ active?.scrollIntoView({ block: 'nearest' });
116
+ }, [activeIdx, showDropdown]);
117
+
118
+ const safeLabel = label ? sanitizeString(label) : undefined;
119
+ const safePlaceholder = placeholder ? sanitizeString(placeholder) : undefined;
120
+
121
+ return (
122
+ <div className={tkx('relative font-sans')} style={{ width: '100%' }}>
123
+ {safeLabel && (
124
+ <label
125
+ className={tkx('block text-sm font-medium mb-1')}
126
+ style={{ color: theme.text }}
127
+ >
128
+ {safeLabel}
129
+ </label>
130
+ )}
131
+
132
+ <textarea
133
+ ref={inputRef}
134
+ role="combobox"
135
+ aria-expanded={showDropdown}
136
+ aria-haspopup="listbox"
137
+ aria-autocomplete="list"
138
+ aria-label={safeLabel ?? 'Mentions input'}
139
+ value={text}
140
+ placeholder={safePlaceholder}
141
+ onChange={handleInput}
142
+ onKeyDown={handleKeyDown}
143
+ onBlur={() => setTimeout(() => setShowDropdown(false), 200)}
144
+ className={tkx('w-full rounded-lg border px-3 py-2 text-sm resize-y')}
145
+ style={{
146
+ backgroundColor: theme.surface,
147
+ borderColor: theme.border,
148
+ color: theme.text,
149
+ minHeight: 80,
150
+ outline: 'none',
151
+ }}
152
+ rows={3}
153
+ />
154
+
155
+ {showDropdown && filtered.length > 0 && (
156
+ <ul
157
+ ref={dropdownRef}
158
+ role="listbox"
159
+ aria-label="Mention suggestions"
160
+ className={tkx('absolute left-0 right-0 z-50 rounded-lg border overflow-auto')}
161
+ style={{
162
+ backgroundColor: theme.surface,
163
+ borderColor: theme.border,
164
+ maxHeight: 200,
165
+ top: '100%',
166
+ marginTop: 4,
167
+ listStyle: 'none',
168
+ padding: 0,
169
+ boxShadow: `0 4px 12px ${theme.bg}80`,
170
+ animation: reducedMotion ? 'none' : 'tkxFadeIn 0.15s ease',
171
+ }}
172
+ >
173
+ {filtered.map((opt, idx) => (
174
+ <li
175
+ key={opt.value}
176
+ role="option"
177
+ aria-selected={idx === activeIdx}
178
+ className={tkx('flex items-center gap-3 px-3 py-2 cursor-pointer text-sm')}
179
+ style={{
180
+ backgroundColor: idx === activeIdx ? theme.surfaceAlt : 'transparent',
181
+ color: theme.text,
182
+ }}
183
+ onMouseDown={(e) => {
184
+ e.preventDefault();
185
+ insertMention(opt);
186
+ }}
187
+ onMouseEnter={() => setActiveIdx(idx)}
188
+ >
189
+ {opt.avatar && (
190
+ <img
191
+ src={sanitizeString(opt.avatar)}
192
+ alt=""
193
+ aria-hidden="true"
194
+ width={24}
195
+ height={24}
196
+ className={tkx('rounded-full')}
197
+ style={{ objectFit: 'cover' }}
198
+ />
199
+ )}
200
+ <span>{sanitizeString(opt.label)}</span>
201
+ <span style={{ color: theme.textMuted, marginLeft: 'auto' }}>
202
+ {trigger}{sanitizeString(opt.value)}
203
+ </span>
204
+ </li>
205
+ ))}
206
+ </ul>
207
+ )}
208
+ </div>
209
+ );
210
+ }
@@ -0,0 +1,174 @@
1
+ import { useRef, useEffect, useMemo } from 'react';
2
+ import { useTheme } from '../themes';
3
+ import { sanitizeString } from '../engine/security';
4
+ import { useReducedMotion } from '../hooks';
5
+ import { tkx } from '../engine/tkx';
6
+
7
+ // ── Types ────────────────────────────────────────────────────────────────────
8
+
9
+ export interface TkxQRCodeProps {
10
+ value: string;
11
+ size?: number;
12
+ color?: string;
13
+ bgColor?: string;
14
+ errorLevel?: 'L' | 'M' | 'Q' | 'H';
15
+ icon?: string;
16
+ bordered?: boolean;
17
+ }
18
+
19
+ // ── Hash Utility ─────────────────────────────────────────────────────────────
20
+
21
+ function simpleHash(str: string): number {
22
+ let hash = 5381;
23
+ for (let i = 0; i < str.length; i++) {
24
+ hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0;
25
+ }
26
+ return Math.abs(hash);
27
+ }
28
+
29
+ // Deterministic PRNG from seed
30
+ function seededRandom(seed: number): () => number {
31
+ let s = seed;
32
+ return () => {
33
+ s = (s * 1664525 + 1013904223) | 0;
34
+ return ((s >>> 0) / 0xffffffff);
35
+ };
36
+ }
37
+
38
+ // ── Grid Generator ───────────────────────────────────────────────────────────
39
+
40
+ const ERROR_DENSITY: Record<string, number> = { L: 0.38, M: 0.42, Q: 0.48, H: 0.55 };
41
+
42
+ function generateGrid(value: string, gridSize: number, errorLevel: string): boolean[][] {
43
+ const hash = simpleHash(value);
44
+ const rand = seededRandom(hash);
45
+ const density = ERROR_DENSITY[errorLevel] ?? 0.42;
46
+ const grid: boolean[][] = Array.from({ length: gridSize }, () =>
47
+ Array.from({ length: gridSize }, () => false),
48
+ );
49
+
50
+ // Finder patterns (top-left, top-right, bottom-left)
51
+ const drawFinder = (startR: number, startC: number) => {
52
+ for (let r = 0; r < 7; r++) {
53
+ for (let c = 0; c < 7; c++) {
54
+ const isEdge = r === 0 || r === 6 || c === 0 || c === 6;
55
+ const isInner = r >= 2 && r <= 4 && c >= 2 && c <= 4;
56
+ grid[startR + r][startC + c] = isEdge || isInner;
57
+ }
58
+ }
59
+ };
60
+
61
+ drawFinder(0, 0);
62
+ drawFinder(0, gridSize - 7);
63
+ drawFinder(gridSize - 7, 0);
64
+
65
+ // Timing patterns
66
+ for (let i = 7; i < gridSize - 7; i++) {
67
+ grid[6][i] = i % 2 === 0;
68
+ grid[i][6] = i % 2 === 0;
69
+ }
70
+
71
+ // Data area: fill with deterministic pseudo-random pattern
72
+ for (let r = 0; r < gridSize; r++) {
73
+ for (let c = 0; c < gridSize; c++) {
74
+ const inFinder =
75
+ (r < 8 && c < 8) ||
76
+ (r < 8 && c >= gridSize - 8) ||
77
+ (r >= gridSize - 8 && c < 8);
78
+ const isTiming = r === 6 || c === 6;
79
+ if (inFinder || isTiming) continue;
80
+ grid[r][c] = rand() < density;
81
+ }
82
+ }
83
+
84
+ return grid;
85
+ }
86
+
87
+ // ── Component ────────────────────────────────────────────────────────────────
88
+
89
+ export function TkxQRCode({
90
+ value,
91
+ size = 160,
92
+ color,
93
+ bgColor,
94
+ errorLevel = 'M',
95
+ icon,
96
+ bordered = true,
97
+ }: TkxQRCodeProps) {
98
+ const theme = useTheme();
99
+ const reducedMotion = useReducedMotion();
100
+ const canvasRef = useRef<HTMLCanvasElement>(null);
101
+ const safeValue = sanitizeString(value);
102
+ const fg = color ?? theme.text;
103
+ const bg = bgColor ?? theme.surface;
104
+
105
+ const gridSize = 25;
106
+ const grid = useMemo(
107
+ () => generateGrid(safeValue, gridSize, errorLevel),
108
+ [safeValue, errorLevel],
109
+ );
110
+
111
+ useEffect(() => {
112
+ const canvas = canvasRef.current;
113
+ if (!canvas) return;
114
+ const ctx = canvas.getContext('2d');
115
+ if (!ctx) return;
116
+
117
+ const dpr = window.devicePixelRatio || 1;
118
+ canvas.width = size * dpr;
119
+ canvas.height = size * dpr;
120
+ ctx.scale(dpr, dpr);
121
+
122
+ const cellSize = size / gridSize;
123
+
124
+ // Background
125
+ ctx.fillStyle = bg;
126
+ ctx.fillRect(0, 0, size, size);
127
+
128
+ // Cells
129
+ ctx.fillStyle = fg;
130
+ for (let r = 0; r < gridSize; r++) {
131
+ for (let c = 0; c < gridSize; c++) {
132
+ if (grid[r][c]) {
133
+ ctx.fillRect(c * cellSize, r * cellSize, cellSize, cellSize);
134
+ }
135
+ }
136
+ }
137
+
138
+ // Center icon area (clear a zone for icon)
139
+ if (icon) {
140
+ const iconSize = size * 0.22;
141
+ const offset = (size - iconSize) / 2;
142
+ ctx.fillStyle = bg;
143
+ ctx.fillRect(offset - 2, offset - 2, iconSize + 4, iconSize + 4);
144
+
145
+ const img = new Image();
146
+ img.crossOrigin = 'anonymous';
147
+ img.onload = () => {
148
+ ctx.drawImage(img, offset, offset, iconSize, iconSize);
149
+ };
150
+ img.src = sanitizeString(icon);
151
+ }
152
+ }, [grid, size, fg, bg, icon]);
153
+
154
+ return (
155
+ <div
156
+ role="img"
157
+ aria-label={`QR code for: ${safeValue}`}
158
+ className={tkx('inline-block rounded-lg')}
159
+ style={{
160
+ padding: bordered ? 12 : 0,
161
+ backgroundColor: bordered ? bg : 'transparent',
162
+ border: bordered ? `1px solid ${theme.border}` : 'none',
163
+ lineHeight: 0,
164
+ animation: reducedMotion ? 'none' : 'tkxFadeIn 0.2s ease',
165
+ }}
166
+ >
167
+ <canvas
168
+ ref={canvasRef}
169
+ aria-hidden="true"
170
+ style={{ width: size, height: size, display: 'block' }}
171
+ />
172
+ </div>
173
+ );
174
+ }