@k3-universe/react-kit 0.0.37 → 0.0.39

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.
@@ -0,0 +1,916 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Delete, ArrowLeft, CornerDownLeft } from "lucide-react";
5
+ import { cn } from "../../../shadcn/lib/utils";
6
+ import { Button } from "../../../shadcn/ui/button";
7
+
8
+ // Constants
9
+ const KEY_BUTTON_BASE_CLASSES =
10
+ "rounded-xl border-2 border-teal-200 bg-white text-teal-600 transition-all duration-150 hover:bg-teal-50 h-16 min-w-[80px] max-w-[88px] text-lg font-semibold";
11
+ const KEY_BUTTON_COMPACT_CLASSES =
12
+ "rounded-xl border-2 border-teal-200 bg-white text-teal-600 transition-all duration-150 hover:bg-teal-50 h-16 min-w-[80px] max-w-[80px] text-lg font-semibold";
13
+ const BACKSPACE_BUTTON_CLASSES =
14
+ "rounded-xl bg-red-600 text-white transition-all duration-150 hover:bg-red-700 h-16 min-w-[80px] max-w-[80px]";
15
+ const SPACE_BUTTON_CLASSES =
16
+ "rounded-xl border-2 border-teal-200 bg-white text-teal-600 transition-all duration-150 hover:bg-teal-50 h-16 min-w-[630px] max-w-[630px] flex-1 text-lg font-semibold";
17
+ const ENTER_BUTTON_CLASSES =
18
+ "rounded-xl border-2 border-teal-200 bg-green-500 text-white transition-all duration-150 hover:bg-green-600 h-16 min-w-[80px] max-w-[80px] text-lg font-semibold";
19
+ const PRESSED_KEY_CLASSES = "bg-teal-100 border-teal-400 scale-95";
20
+ const ACTIVE_TOGGLE_CLASSES =
21
+ "bg-teal-600 text-white border-teal-600 hover:bg-teal-700";
22
+
23
+ export type KeyboardLayout = "qwerty" | "qwertz" | "azerty";
24
+
25
+ export interface KeyboardProps
26
+ extends Omit<
27
+ React.HTMLAttributes<HTMLDivElement>,
28
+ "onChange" | "onKeyPress"
29
+ > {
30
+ value?: string;
31
+ onChange?: (value: string) => void;
32
+ onKeyPress?: (key: string) => void;
33
+ disabled?: boolean;
34
+ layout?: KeyboardLayout;
35
+ showNumbers?: boolean;
36
+ showSymbols?: boolean;
37
+ showShift?: boolean;
38
+ showSpace?: boolean;
39
+ showBackspace?: boolean;
40
+ showEnter?: boolean;
41
+ className?: string;
42
+ buttonClassName?: string;
43
+ customKeyStyle?: React.CSSProperties;
44
+ customKeyClassName?: string;
45
+ capsLock?: boolean;
46
+ onCapsLockChange?: (capsLock: boolean) => void;
47
+ inputRef?: React.RefObject<HTMLInputElement | HTMLTextAreaElement | null>;
48
+ }
49
+
50
+ const QWERTY_LAYOUT = {
51
+ row1: ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
52
+ row2: ["a", "s", "d", "f", "g", "h", "j", "k", "l"],
53
+ row3: ["z", "x", "c", "v", "b", "n", "m"],
54
+ };
55
+
56
+ const QWERTZ_LAYOUT = {
57
+ row1: ["q", "w", "e", "r", "t", "z", "u", "i", "o", "p"],
58
+ row2: ["a", "s", "d", "f", "g", "h", "j", "k", "l"],
59
+ row3: ["y", "x", "c", "v", "b", "n", "m"],
60
+ };
61
+
62
+ const AZERTY_LAYOUT = {
63
+ row1: ["a", "z", "e", "r", "t", "y", "u", "i", "o", "p"],
64
+ row2: ["q", "s", "d", "f", "g", "h", "j", "k", "l", "m"],
65
+ row3: ["w", "x", "c", "v", "b", "n"],
66
+ };
67
+
68
+ const NUMBER_ROW = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"];
69
+
70
+ const getLayout = (layout: KeyboardLayout) => {
71
+ switch (layout) {
72
+ case "qwertz":
73
+ return QWERTZ_LAYOUT;
74
+ case "azerty":
75
+ return AZERTY_LAYOUT;
76
+ default:
77
+ return QWERTY_LAYOUT;
78
+ }
79
+ };
80
+
81
+ export function Keyboard({
82
+ value: controlledValue,
83
+ onChange,
84
+ onKeyPress,
85
+ disabled = false,
86
+ layout = "qwerty",
87
+ showNumbers = true,
88
+ showSymbols = true,
89
+ showShift = true,
90
+ showSpace = true,
91
+ showBackspace = true,
92
+ showEnter = false,
93
+ className,
94
+ buttonClassName,
95
+ customKeyStyle,
96
+ customKeyClassName,
97
+ capsLock: controlledCapsLock,
98
+ onCapsLockChange,
99
+ inputRef,
100
+ ...props
101
+ }: KeyboardProps) {
102
+ const [internalValue, setInternalValue] = React.useState("");
103
+ const [internalCapsLock, setInternalCapsLock] = React.useState(false);
104
+ const [shiftPressed, setShiftPressed] = React.useState(false);
105
+ const [symbolMode, setSymbolMode] = React.useState(false);
106
+ const [pressedKey, setPressedKey] = React.useState<string | null>(null);
107
+ const [inputValue, setInputValue] = React.useState("");
108
+ const pendingCursorPos = React.useRef<number | null>(null);
109
+
110
+ // Use a ref to track if virtual Caps Lock was just clicked (to prevent sync override)
111
+ const virtualCapsLockJustToggled = React.useRef(false);
112
+ // Track the last known Caps Lock state to detect changes
113
+ const lastKnownCapsLockState = React.useRef(false);
114
+
115
+ const isControlled = controlledValue !== undefined;
116
+ const value = isControlled ? controlledValue : internalValue;
117
+
118
+ // Sync inputValue with actual input value when inputRef is provided
119
+ React.useEffect(() => {
120
+ if (inputRef?.current) {
121
+ const updateInputValue = () => {
122
+ setInputValue(inputRef.current?.value ?? "");
123
+ };
124
+
125
+ // Initial sync
126
+ updateInputValue();
127
+
128
+ // Listen to input events to keep in sync
129
+ const input = inputRef.current;
130
+ input.addEventListener("input", updateInputValue);
131
+ input.addEventListener("change", updateInputValue);
132
+
133
+ return () => {
134
+ input.removeEventListener("input", updateInputValue);
135
+ input.removeEventListener("change", updateInputValue);
136
+ };
137
+ }
138
+ setInputValue(value);
139
+ return undefined;
140
+ }, [inputRef, value]);
141
+
142
+ // Restore cursor position after React re-renders
143
+ React.useEffect(() => {
144
+ if (inputRef?.current && pendingCursorPos.current !== null) {
145
+ const pos = pendingCursorPos.current;
146
+ pendingCursorPos.current = null;
147
+ // Use setTimeout to ensure React has finished rendering
148
+ setTimeout(() => {
149
+ if (inputRef?.current) {
150
+ inputRef.current.setSelectionRange(pos, pos);
151
+ }
152
+ }, 0);
153
+ }
154
+ }, [value, inputRef]);
155
+
156
+ // Get the actual input value for disabled checks
157
+ const actualInputValue = inputRef?.current ? inputValue : value;
158
+
159
+ const isCapsLockControlled = controlledCapsLock !== undefined;
160
+ const capsLock = isCapsLockControlled ? controlledCapsLock : internalCapsLock;
161
+
162
+ // Update ref when capsLock changes
163
+ React.useEffect(() => {
164
+ lastKnownCapsLockState.current = capsLock;
165
+ }, [capsLock]);
166
+
167
+ const keyboardLayout = React.useMemo(() => getLayout(layout), [layout]);
168
+
169
+ const isShiftActive = capsLock || shiftPressed;
170
+
171
+ // Map real keyboard keys to virtual keyboard keys
172
+ const mapRealKeyToVirtual = React.useCallback(
173
+ (key: string, event: KeyboardEvent): string | null => {
174
+ const keyLower = key.toLowerCase();
175
+
176
+ // Handle special keys
177
+ if (key === "Backspace" || key === "Delete") return "Backspace";
178
+ if (key === "Enter") return "Enter";
179
+ if (key === " ") return "Space";
180
+ if (key === "Tab") return "Tab";
181
+ if (key === "Shift" || key === "ShiftLeft" || key === "ShiftRight")
182
+ return "Shift";
183
+ if (key === "CapsLock") return "CapsLock";
184
+ if (key === "#" || key === "+" || key === "=") return "#+=";
185
+
186
+ // Handle numbers
187
+ if (/^[0-9]$/.test(key)) {
188
+ return symbolMode ? null : key;
189
+ }
190
+
191
+ // Handle letters
192
+ if (/^[a-z]$/i.test(key)) {
193
+ if (symbolMode) {
194
+ // In symbol mode, letters map to symbols
195
+ return null; // Symbols are shown in dedicated rows
196
+ }
197
+ return keyLower;
198
+ }
199
+
200
+ // Handle symbols when shift is pressed
201
+ if (event.shiftKey) {
202
+ const shiftSymbolMap: Record<string, string> = {
203
+ "1": ".",
204
+ "2": "@",
205
+ "3": "#",
206
+ "4": "_",
207
+ "5": "%",
208
+ "6": "^",
209
+ "7": "&",
210
+ "8": "*",
211
+ "9": "(",
212
+ "0": ")",
213
+ "-": ",",
214
+ "=": "+",
215
+ "[": "{",
216
+ "]": "}",
217
+ "\\": "|",
218
+ ";": ":",
219
+ "'": '"',
220
+ ",": "<",
221
+ ".": ">",
222
+ "/": "?",
223
+ "`": "~",
224
+ };
225
+ return shiftSymbolMap[key] || null;
226
+ }
227
+
228
+ // Handle regular symbols
229
+ const symbolMap: Record<string, string> = {
230
+ "-": "-",
231
+ "=": "=",
232
+ "[": "[",
233
+ "]": "]",
234
+ "\\": "\\",
235
+ ";": ";",
236
+ "'": "'",
237
+ ",": ",",
238
+ ".": ".",
239
+ "/": "/",
240
+ "`": "`",
241
+ };
242
+ return symbolMap[key] || null;
243
+ },
244
+ [symbolMode],
245
+ );
246
+
247
+ const handleKeyPress = React.useCallback(
248
+ (key: string) => {
249
+ if (disabled) return;
250
+
251
+ // If inputRef is provided, manipulate the input directly to handle selection
252
+ if (inputRef?.current) {
253
+ const input = inputRef.current;
254
+ const start = input.selectionStart ?? 0;
255
+ const end = input.selectionEnd ?? 0;
256
+ const currentValue = input.value;
257
+
258
+ // Replace selected text with the new key
259
+ const newValue =
260
+ currentValue.slice(0, start) + key + currentValue.slice(end);
261
+ const newCursorPos = start + key.length;
262
+
263
+ // Update our internal state first
264
+ setInputValue(newValue);
265
+
266
+ // Store cursor position to restore after React re-renders
267
+ pendingCursorPos.current = newCursorPos;
268
+
269
+ // Call onChange to update parent state - React will handle the re-render
270
+ onChange?.(newValue);
271
+ if (!isControlled) {
272
+ setInternalValue(newValue);
273
+ }
274
+
275
+ onKeyPress?.(key);
276
+ return;
277
+ }
278
+
279
+ // Fallback to old behavior if no inputRef
280
+ const newValue = value + key;
281
+ if (isControlled) {
282
+ onChange?.(newValue);
283
+ } else {
284
+ setInternalValue(newValue);
285
+ onChange?.(newValue);
286
+ }
287
+ onKeyPress?.(key);
288
+ },
289
+ [disabled, value, isControlled, onChange, onKeyPress, inputRef],
290
+ );
291
+
292
+ const handleBackspace = React.useCallback(() => {
293
+ if (disabled) return;
294
+
295
+ // If inputRef is provided, check the actual input value
296
+ if (inputRef?.current) {
297
+ const input = inputRef.current;
298
+ const start = input.selectionStart ?? 0;
299
+ const end = input.selectionEnd ?? 0;
300
+ const currentValue = input.value;
301
+
302
+ if (start === end && start === 0) return; // Nothing to delete
303
+
304
+ // Set pressed key for animation
305
+ setPressedKey("Backspace");
306
+ setTimeout(() => {
307
+ setPressedKey(null);
308
+ }, 150);
309
+
310
+ // If text is selected, delete the selection
311
+ // Otherwise, delete the character before the cursor
312
+ let newValue: string;
313
+ let newCursorPos: number;
314
+
315
+ if (start !== end) {
316
+ // Delete selected text
317
+ newValue = currentValue.slice(0, start) + currentValue.slice(end);
318
+ newCursorPos = start;
319
+ } else {
320
+ // Delete character before cursor
321
+ newValue = currentValue.slice(0, start - 1) + currentValue.slice(start);
322
+ newCursorPos = Math.max(0, start - 1);
323
+ }
324
+
325
+ // Update our internal state first
326
+ setInputValue(newValue);
327
+
328
+ // Store cursor position to restore after React re-renders
329
+ pendingCursorPos.current = newCursorPos;
330
+
331
+ // Call onChange to update parent state - React will handle the re-render
332
+ onChange?.(newValue);
333
+ if (!isControlled) {
334
+ setInternalValue(newValue);
335
+ }
336
+
337
+ onKeyPress?.("Backspace");
338
+ return;
339
+ }
340
+
341
+ // Fallback to old behavior if no inputRef
342
+ if (value.length === 0) return;
343
+
344
+ // Set pressed key for animation
345
+ setPressedKey("Backspace");
346
+ setTimeout(() => {
347
+ setPressedKey(null);
348
+ }, 150);
349
+
350
+ const newValue = value.slice(0, -1);
351
+ if (isControlled) {
352
+ onChange?.(newValue);
353
+ } else {
354
+ setInternalValue(newValue);
355
+ onChange?.(newValue);
356
+ }
357
+ onKeyPress?.("Backspace");
358
+ }, [disabled, value, isControlled, onChange, onKeyPress, inputRef]);
359
+
360
+ const handleSpace = React.useCallback(() => {
361
+ // Set pressed key for animation
362
+ setPressedKey("Space");
363
+ setTimeout(() => {
364
+ setPressedKey(null);
365
+ }, 150);
366
+ handleKeyPress(" ");
367
+ }, [handleKeyPress]);
368
+
369
+ const handleEnter = React.useCallback(() => {
370
+ if (disabled) return;
371
+
372
+ // Set pressed key for animation
373
+ setPressedKey("Enter");
374
+ setTimeout(() => {
375
+ setPressedKey(null);
376
+ }, 150);
377
+
378
+ // If inputRef is provided, handle Enter like a real keyboard
379
+ if (inputRef?.current) {
380
+ const input = inputRef?.current;
381
+ const isTextarea = input.tagName === "TEXTAREA";
382
+ const isInput = input.tagName === "INPUT";
383
+
384
+ if (isTextarea) {
385
+ // For textarea: insert newline like real keyboard
386
+ const start = input.selectionStart ?? 0;
387
+ const end = input.selectionEnd ?? 0;
388
+ const currentValue = input.value;
389
+
390
+ // Replace selected text with newline, or insert newline at cursor
391
+ const newValue =
392
+ currentValue.slice(0, start) + "\n" + currentValue.slice(end);
393
+ const newCursorPos = start + 1;
394
+
395
+ // Update input value
396
+ input.value = newValue;
397
+
398
+ // Set cursor position
399
+ input.setSelectionRange(newCursorPos, newCursorPos);
400
+
401
+ // Trigger input event for React
402
+ const inputEvent = new Event("input", { bubbles: true });
403
+ input.dispatchEvent(inputEvent);
404
+
405
+ // Update controlled/uncontrolled state
406
+ if (isControlled) {
407
+ onChange?.(newValue);
408
+ } else {
409
+ setInternalValue(newValue);
410
+ onChange?.(newValue);
411
+ }
412
+ } else if (isInput) {
413
+ // For input: trigger Enter key events like real keyboard
414
+ // This will trigger form submission if inside a form
415
+
416
+ // Create and dispatch keydown event
417
+ const keyDownEvent = new KeyboardEvent("keydown", {
418
+ key: "Enter",
419
+ code: "Enter",
420
+ keyCode: 13,
421
+ which: 13,
422
+ bubbles: true,
423
+ cancelable: true,
424
+ });
425
+ input.dispatchEvent(keyDownEvent);
426
+
427
+ // Only trigger keypress and keyup if keydown wasn't prevented
428
+ if (!keyDownEvent.defaultPrevented) {
429
+ // Create and dispatch keypress event
430
+ const keyPressEvent = new KeyboardEvent("keypress", {
431
+ key: "Enter",
432
+ code: "Enter",
433
+ keyCode: 13,
434
+ which: 13,
435
+ bubbles: true,
436
+ cancelable: true,
437
+ });
438
+ input.dispatchEvent(keyPressEvent);
439
+
440
+ // Create and dispatch keyup event
441
+ const keyUpEvent = new KeyboardEvent("keyup", {
442
+ key: "Enter",
443
+ code: "Enter",
444
+ keyCode: 13,
445
+ which: 13,
446
+ bubbles: true,
447
+ cancelable: true,
448
+ });
449
+ input.dispatchEvent(keyUpEvent);
450
+
451
+ // If input is inside a form, trigger form submit
452
+ const form = input.closest("form");
453
+ if (form && !keyPressEvent.defaultPrevented) {
454
+ // Trigger form submit event
455
+ const submitEvent = new Event("submit", {
456
+ bubbles: true,
457
+ cancelable: true,
458
+ });
459
+ form.dispatchEvent(submitEvent);
460
+
461
+ // If submit wasn't prevented, actually submit the form
462
+ if (!submitEvent.defaultPrevented) {
463
+ form.requestSubmit();
464
+ }
465
+ }
466
+ }
467
+ }
468
+
469
+ // Call onKeyPress callback
470
+ onKeyPress?.("Enter");
471
+ return;
472
+ }
473
+
474
+ // Fallback: if no inputRef, just add newline to value
475
+ const newValue = value + "\n";
476
+ if (isControlled) {
477
+ onChange?.(newValue);
478
+ } else {
479
+ setInternalValue(newValue);
480
+ onChange?.(newValue);
481
+ }
482
+ onKeyPress?.("Enter");
483
+ }, [disabled, value, isControlled, onChange, onKeyPress, inputRef]);
484
+
485
+ const toggleCapsLock = React.useCallback(() => {
486
+ // Set pressed key for animation
487
+ setPressedKey("CapsLock");
488
+ setTimeout(() => {
489
+ setPressedKey(null);
490
+ }, 150);
491
+
492
+ // Set flag to prevent sync from overriding our toggle for a short period
493
+ virtualCapsLockJustToggled.current = true;
494
+
495
+ // Always use functional update to get the latest state
496
+ if (isCapsLockControlled) {
497
+ // For controlled components, use the current prop value
498
+ const currentCapsLock = controlledCapsLock ?? false;
499
+ const newCapsLock = !currentCapsLock;
500
+ lastKnownCapsLockState.current = newCapsLock;
501
+ onCapsLockChange?.(newCapsLock);
502
+ } else {
503
+ // For uncontrolled components, use functional update to avoid stale closures
504
+ setInternalCapsLock((prev) => {
505
+ const newCapsLock = !prev;
506
+ lastKnownCapsLockState.current = newCapsLock;
507
+ onCapsLockChange?.(newCapsLock);
508
+ return newCapsLock;
509
+ });
510
+ }
511
+
512
+ // Reset flag after a delay to allow sync again
513
+ setTimeout(() => {
514
+ virtualCapsLockJustToggled.current = false;
515
+ }, 200);
516
+ }, [isCapsLockControlled, onCapsLockChange, controlledCapsLock]);
517
+
518
+ const toggleSymbolMode = React.useCallback(() => {
519
+ // Set pressed key for animation
520
+ setPressedKey("#+=");
521
+ setTimeout(() => {
522
+ setPressedKey(null);
523
+ }, 150);
524
+ setSymbolMode((prev) => !prev);
525
+ }, []);
526
+
527
+ // Listen to real keyboard events and sync Caps Lock state
528
+ React.useEffect(() => {
529
+ if (disabled) return;
530
+
531
+ const handleKeyDown = (event: KeyboardEvent) => {
532
+ // Handle Caps Lock key press on physical keyboard
533
+ if (event.key === "CapsLock" || event.key === "Caps") {
534
+ setPressedKey("CapsLock");
535
+ virtualCapsLockJustToggled.current = false; // Reset flag - physical keyboard takes priority
536
+
537
+ // When Caps Lock is pressed, the state changes AFTER this event
538
+ // So we toggle based on the last known state
539
+ const newCapsLock = !lastKnownCapsLockState.current;
540
+ lastKnownCapsLockState.current = newCapsLock;
541
+
542
+ if (!isCapsLockControlled) {
543
+ setInternalCapsLock(newCapsLock);
544
+ onCapsLockChange?.(newCapsLock);
545
+ } else {
546
+ onCapsLockChange?.(newCapsLock);
547
+ }
548
+ return;
549
+ }
550
+
551
+ // Sync with browser's actual caps lock state on any key press
552
+ // This ensures we stay in sync with physical keyboard
553
+ // But skip if virtual Caps Lock was just toggled (to prevent override)
554
+ if (!virtualCapsLockJustToggled.current) {
555
+ const browserCapsLock = event.getModifierState("CapsLock");
556
+
557
+ // Only update if state has actually changed
558
+ if (browserCapsLock !== lastKnownCapsLockState.current) {
559
+ lastKnownCapsLockState.current = browserCapsLock;
560
+
561
+ if (!isCapsLockControlled) {
562
+ setInternalCapsLock(browserCapsLock);
563
+ onCapsLockChange?.(browserCapsLock);
564
+ } else {
565
+ onCapsLockChange?.(browserCapsLock);
566
+ }
567
+ }
568
+ }
569
+
570
+ const virtualKey = mapRealKeyToVirtual(event.key, event);
571
+ if (virtualKey) {
572
+ setPressedKey(virtualKey);
573
+ }
574
+ };
575
+
576
+ const handleKeyUp = (event: KeyboardEvent) => {
577
+ setPressedKey(null);
578
+
579
+ // Also check Caps Lock state on keyup to catch any changes
580
+ if (!virtualCapsLockJustToggled.current) {
581
+ const browserCapsLock = event.getModifierState("CapsLock");
582
+ if (browserCapsLock !== lastKnownCapsLockState.current) {
583
+ lastKnownCapsLockState.current = browserCapsLock;
584
+
585
+ if (!isCapsLockControlled) {
586
+ setInternalCapsLock(browserCapsLock);
587
+ onCapsLockChange?.(browserCapsLock);
588
+ } else {
589
+ onCapsLockChange?.(browserCapsLock);
590
+ }
591
+ }
592
+ }
593
+ };
594
+
595
+ window.addEventListener("keydown", handleKeyDown);
596
+ window.addEventListener("keyup", handleKeyUp);
597
+
598
+ return () => {
599
+ window.removeEventListener("keydown", handleKeyDown);
600
+ window.removeEventListener("keyup", handleKeyUp);
601
+ };
602
+ }, [disabled, mapRealKeyToVirtual, isCapsLockControlled, onCapsLockChange]);
603
+
604
+ // Helper to animate key press
605
+ const animateKeyPress = React.useCallback((key: string) => {
606
+ setPressedKey(key);
607
+ setTimeout(() => setPressedKey(null), 150);
608
+ }, []);
609
+
610
+ const formatKey = (key: string): string => {
611
+ if (symbolMode) {
612
+ const symbolMap: Record<string, string> = {
613
+ q: "[",
614
+ w: "]",
615
+ e: "\\",
616
+ r: ";",
617
+ t: "'",
618
+ y: ",",
619
+ u: ".",
620
+ i: "/",
621
+ o: "=",
622
+ p: "-",
623
+ a: "{",
624
+ s: "}",
625
+ d: ":",
626
+ f: '"',
627
+ g: "<",
628
+ h: ">",
629
+ j: "?",
630
+ k: "+",
631
+ l: "_",
632
+ z: "|",
633
+ x: "~",
634
+ c: "`",
635
+ v: "|",
636
+ b: "~",
637
+ n: "`",
638
+ m: "|",
639
+ };
640
+ return symbolMap[key.toLowerCase()] || key;
641
+ }
642
+ return isShiftActive ? key.toUpperCase() : key.toLowerCase();
643
+ };
644
+
645
+ const getNumberSymbol = (num: string, shift: boolean): string => {
646
+ if (!shift) return num;
647
+ const symbols: Record<string, string> = {
648
+ "1": ".",
649
+ "2": "@",
650
+ "3": "#",
651
+ "4": "_",
652
+ "5": "%",
653
+ "6": "^",
654
+ "7": "&",
655
+ "8": "*",
656
+ "9": "(",
657
+ "0": ")",
658
+ };
659
+ return symbols[num] || num;
660
+ };
661
+
662
+ // Helper component for key buttons
663
+ const KeyButton = React.useCallback(
664
+ ({
665
+ children,
666
+ onClick,
667
+ isPressed = false,
668
+ className = "",
669
+ compact = false,
670
+ }: {
671
+ children: React.ReactNode;
672
+ onClick: () => void;
673
+ isPressed?: boolean;
674
+ className?: string;
675
+ compact?: boolean;
676
+ }) => (
677
+ <Button
678
+ type="button"
679
+ disabled={disabled}
680
+ onClick={onClick}
681
+ style={customKeyStyle}
682
+ className={cn(
683
+ compact ? KEY_BUTTON_COMPACT_CLASSES : KEY_BUTTON_BASE_CLASSES,
684
+ isPressed && PRESSED_KEY_CLASSES,
685
+ buttonClassName,
686
+ customKeyClassName,
687
+ className
688
+ )}
689
+ >
690
+ {children}
691
+ </Button>
692
+ ),
693
+ [disabled, customKeyStyle, buttonClassName, customKeyClassName]
694
+ );
695
+
696
+ // Helper component for symbol buttons
697
+ const SymbolButton = React.useCallback(
698
+ ({ sym }: { sym: string }) => {
699
+ const isPressed = pressedKey === sym;
700
+ return (
701
+ <KeyButton
702
+ isPressed={isPressed}
703
+ onClick={() => {
704
+ animateKeyPress(sym);
705
+ handleKeyPress(sym);
706
+ }}
707
+ >
708
+ {sym}
709
+ </KeyButton>
710
+ );
711
+ },
712
+ [pressedKey, animateKeyPress, handleKeyPress, KeyButton]
713
+ );
714
+
715
+ // Helper component for letter buttons
716
+ const LetterButton = React.useCallback(
717
+ ({ keyChar }: { keyChar: string }) => {
718
+ const displayChar = formatKey(keyChar);
719
+ const isPressed = pressedKey === keyChar.toLowerCase();
720
+ return (
721
+ <KeyButton
722
+ isPressed={isPressed}
723
+ onClick={() => {
724
+ animateKeyPress(keyChar.toLowerCase());
725
+ handleKeyPress(displayChar);
726
+ if (shiftPressed) setShiftPressed(false);
727
+ }}
728
+ >
729
+ {displayChar}
730
+ </KeyButton>
731
+ );
732
+ },
733
+ [
734
+ pressedKey,
735
+ shiftPressed,
736
+ animateKeyPress,
737
+ handleKeyPress,
738
+ formatKey,
739
+ KeyButton,
740
+ ]
741
+ );
742
+
743
+ return (
744
+ <div
745
+ className={cn(
746
+ "flex flex-col gap-3 p-6 bg-background border border-border rounded-lg shadow-sm",
747
+ className,
748
+ )}
749
+ {...props}
750
+ >
751
+ {/* Number row - hidden when symbol mode is active */}
752
+ {showNumbers && (
753
+ <div className="flex gap-3 justify-center">
754
+ {NUMBER_ROW.map((num) => {
755
+ const displayChar = shiftPressed ? getNumberSymbol(num, true) : num;
756
+ const isPressed = pressedKey === num;
757
+ return (
758
+ <KeyButton
759
+ key={num}
760
+ isPressed={isPressed}
761
+ onClick={() => {
762
+ animateKeyPress(num);
763
+ handleKeyPress(displayChar);
764
+ if (shiftPressed) setShiftPressed(false);
765
+ }}
766
+ >
767
+ {displayChar}
768
+ </KeyButton>
769
+ );
770
+ })}
771
+ </div>
772
+ )}
773
+
774
+ {/* Symbol rows - shows when symbol mode is active */}
775
+ {symbolMode && (
776
+ <>
777
+ {/* Row 1: Special symbols - 10 keys balanced */}
778
+ <div className="flex gap-3 justify-center">
779
+ {["!", "@", "#", "$", "%", "^", "&", "*", "(", ")"].map((sym) => (
780
+ <SymbolButton key={sym} sym={sym} />
781
+ ))}
782
+ </div>
783
+ {/* Row 2: Brackets and quotes - 10 keys balanced */}
784
+ <div className="flex gap-3 justify-center">
785
+ {["[", "]", "{", "}", "|", "\\", ";", ":", "'", '"'].map((sym) => (
786
+ <SymbolButton key={sym} sym={sym} />
787
+ ))}
788
+ </div>
789
+ {/* Row 3: Punctuation and operators - 9 keys */}
790
+ <div className="flex gap-3 justify-center">
791
+ {[",", ".", "/", "?", "+", "-", "=", "_", "~"].map((sym) => (
792
+ <SymbolButton key={sym} sym={sym} />
793
+ ))}
794
+ {showBackspace && (
795
+ <button
796
+ type="button"
797
+ disabled={disabled || actualInputValue.length === 0}
798
+ onClick={handleBackspace}
799
+ style={customKeyStyle}
800
+ className={cn(
801
+ BACKSPACE_BUTTON_CLASSES,
802
+ buttonClassName,
803
+ customKeyClassName
804
+ )}
805
+ >
806
+ <Delete className="mx-auto h-6 w-6" />
807
+ </button>
808
+ )}
809
+ </div>
810
+ </>
811
+ )}
812
+
813
+ {/* Letter rows - hidden when symbol mode is active */}
814
+ {!symbolMode && (
815
+ <>
816
+ {/* Row 1 - Letter row Q-P */}
817
+ <div className="flex gap-3 justify-center">
818
+ {keyboardLayout.row1.map((keyChar) => (
819
+ <LetterButton key={keyChar} keyChar={keyChar} />
820
+ ))}
821
+ </div>
822
+
823
+ {/* Row 2 */}
824
+ <div className="flex gap-3 justify-center">
825
+ {keyboardLayout.row2.map((keyChar) => (
826
+ <LetterButton key={keyChar} keyChar={keyChar} />
827
+ ))}
828
+ </div>
829
+ </>
830
+ )}
831
+
832
+ {/* Row 3 - Bottom letter row with controls */}
833
+ <div className="flex gap-3 justify-center">
834
+ {!symbolMode && (
835
+ <>
836
+ {showShift && (
837
+ <KeyButton
838
+ compact
839
+ isPressed={pressedKey === "CapsLock" && !capsLock}
840
+ onClick={toggleCapsLock}
841
+ className={capsLock ? ACTIVE_TOGGLE_CLASSES : ""}
842
+ >
843
+ <ArrowLeft className="h-6 w-6 rotate-90" />
844
+ </KeyButton>
845
+ )}
846
+ {keyboardLayout.row3.map((keyChar) => (
847
+ <LetterButton key={keyChar} keyChar={keyChar} />
848
+ ))}
849
+ {showBackspace && (
850
+ <button
851
+ type="button"
852
+ disabled={disabled || actualInputValue.length === 0}
853
+ onClick={handleBackspace}
854
+ style={customKeyStyle}
855
+ className={cn(
856
+ BACKSPACE_BUTTON_CLASSES,
857
+ buttonClassName,
858
+ customKeyClassName
859
+ )}
860
+ >
861
+ <Delete className="mx-auto h-6 w-6" />
862
+ </button>
863
+ )}
864
+ </>
865
+ )}
866
+ </div>
867
+
868
+ {/* Bottom row */}
869
+ <div className="flex gap-3 justify-center">
870
+ <KeyButton
871
+ compact
872
+ isPressed={pressedKey === "#+=" && !symbolMode}
873
+ onClick={toggleSymbolMode}
874
+ className={symbolMode ? ACTIVE_TOGGLE_CLASSES : ""}
875
+ >
876
+ #+=
877
+ </KeyButton>
878
+ {showSpace && (
879
+ <Button
880
+ type="button"
881
+ disabled={disabled}
882
+ onClick={handleSpace}
883
+ style={customKeyStyle}
884
+ className={cn(
885
+ SPACE_BUTTON_CLASSES,
886
+ pressedKey === "Space" && PRESSED_KEY_CLASSES,
887
+ buttonClassName,
888
+ customKeyClassName
889
+ )}
890
+ >
891
+ Space
892
+ </Button>
893
+ )}
894
+ {showEnter && (
895
+ <Button
896
+ type="button"
897
+ disabled={disabled}
898
+ onClick={handleEnter}
899
+ style={customKeyStyle}
900
+ className={cn(
901
+ ENTER_BUTTON_CLASSES,
902
+ buttonClassName,
903
+ customKeyClassName
904
+ )}
905
+ >
906
+ <CornerDownLeft className="mx-auto h-6 w-6" />
907
+ </Button>
908
+ )}
909
+ </div>
910
+ </div>
911
+ );
912
+ }
913
+
914
+ Keyboard.displayName = "Keyboard";
915
+
916
+ export default Keyboard;