@launchui/launch-ui 1.0.1 → 1.0.7
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/package.json +1 -1
- package/registry/components/3d-card/3d-card-2.tsx +258 -0
- package/registry/components/3d-pin-card/3d-pin-card.tsx +172 -0
- package/registry/components/Keyboard/Keyboard.tsx +885 -0
- package/registry/components/comet-card/comet-card.tsx +122 -0
- package/registry/components/macbook-scroll/macbook-scroll.tsx +611 -0
- package/registry/components/text-flipping-board/text-flipping-board.tsx +449 -0
- /package/registry/components/3d-card/{index.tsx → 3d-card.tsx} +0 -0
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, {
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
useCallback,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { motion, AnimatePresence } from "motion/react";
|
|
12
|
+
import {
|
|
13
|
+
IconBrightnessDown,
|
|
14
|
+
IconBrightnessUp,
|
|
15
|
+
IconCaretRightFilled,
|
|
16
|
+
IconCaretUpFilled,
|
|
17
|
+
IconChevronUp,
|
|
18
|
+
IconMicrophone,
|
|
19
|
+
IconMoon,
|
|
20
|
+
IconPlayerSkipForward,
|
|
21
|
+
IconPlayerTrackNext,
|
|
22
|
+
IconPlayerTrackPrev,
|
|
23
|
+
IconTable,
|
|
24
|
+
IconVolume,
|
|
25
|
+
IconVolume2,
|
|
26
|
+
IconVolume3,
|
|
27
|
+
IconSearch,
|
|
28
|
+
IconWorld,
|
|
29
|
+
IconCommand,
|
|
30
|
+
IconCaretLeftFilled,
|
|
31
|
+
IconCaretDownFilled,
|
|
32
|
+
} from "@tabler/icons-react";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Internal utility replacing external lib requirements ensuring zero-friction installations.
|
|
36
|
+
*/
|
|
37
|
+
const cn = (...classes: any[]) => {
|
|
38
|
+
return classes.filter(Boolean).join(" ");
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Sound sprite definitions from config.json [startMs, durationMs]
|
|
42
|
+
// Key down sounds - half duration for a snappy press sound
|
|
43
|
+
const SOUND_DEFINES_DOWN: Record<string, [number, number]> = {
|
|
44
|
+
Escape: [2894, 113],
|
|
45
|
+
F1: [3610, 98],
|
|
46
|
+
F2: [4210, 90],
|
|
47
|
+
F3: [4758, 90],
|
|
48
|
+
F4: [5250, 100],
|
|
49
|
+
F5: [5831, 105],
|
|
50
|
+
F6: [6396, 105],
|
|
51
|
+
F7: [6900, 105],
|
|
52
|
+
F8: [7443, 111],
|
|
53
|
+
F9: [7955, 91],
|
|
54
|
+
F10: [8504, 105],
|
|
55
|
+
F11: [9046, 94],
|
|
56
|
+
F12: [9582, 96],
|
|
57
|
+
Backquote: [12476, 100],
|
|
58
|
+
Digit1: [12946, 96],
|
|
59
|
+
Digit2: [13470, 95],
|
|
60
|
+
Digit3: [13963, 100],
|
|
61
|
+
Digit4: [14481, 102],
|
|
62
|
+
Digit5: [14994, 94],
|
|
63
|
+
Digit6: [15505, 109],
|
|
64
|
+
Digit7: [15990, 97],
|
|
65
|
+
Digit8: [16529, 92],
|
|
66
|
+
Digit9: [17012, 103],
|
|
67
|
+
Digit0: [17550, 87],
|
|
68
|
+
Minus: [18052, 93],
|
|
69
|
+
Equal: [18553, 89],
|
|
70
|
+
Backspace: [19065, 110],
|
|
71
|
+
Tab: [21734, 119],
|
|
72
|
+
KeyQ: [22245, 95],
|
|
73
|
+
KeyW: [22790, 89],
|
|
74
|
+
KeyE: [23317, 83],
|
|
75
|
+
KeyR: [23817, 92],
|
|
76
|
+
KeyT: [24297, 92],
|
|
77
|
+
KeyY: [24811, 93],
|
|
78
|
+
KeyU: [25313, 95],
|
|
79
|
+
KeyI: [25795, 91],
|
|
80
|
+
KeyO: [26309, 84],
|
|
81
|
+
KeyP: [26804, 83],
|
|
82
|
+
BracketLeft: [27330, 85],
|
|
83
|
+
BracketRight: [27883, 99],
|
|
84
|
+
Backslash: [28393, 100],
|
|
85
|
+
CapsLock: [31011, 126],
|
|
86
|
+
KeyA: [31542, 85],
|
|
87
|
+
KeyS: [32031, 88],
|
|
88
|
+
KeyD: [32492, 85],
|
|
89
|
+
KeyF: [32973, 87],
|
|
90
|
+
KeyG: [33453, 94],
|
|
91
|
+
KeyH: [33986, 93],
|
|
92
|
+
KeyJ: [34425, 88],
|
|
93
|
+
KeyK: [34932, 90],
|
|
94
|
+
KeyL: [35410, 95],
|
|
95
|
+
Semicolon: [35914, 95],
|
|
96
|
+
Quote: [36428, 87],
|
|
97
|
+
Enter: [36902, 117],
|
|
98
|
+
ShiftLeft: [38136, 133],
|
|
99
|
+
KeyZ: [38694, 80],
|
|
100
|
+
KeyX: [39148, 76],
|
|
101
|
+
KeyC: [39632, 95],
|
|
102
|
+
KeyV: [40136, 94],
|
|
103
|
+
KeyB: [40621, 107],
|
|
104
|
+
KeyN: [41103, 90],
|
|
105
|
+
KeyM: [41610, 93],
|
|
106
|
+
Comma: [42110, 92],
|
|
107
|
+
Period: [42594, 90],
|
|
108
|
+
Slash: [43105, 95],
|
|
109
|
+
ShiftRight: [43565, 137],
|
|
110
|
+
Fn: [44251, 110],
|
|
111
|
+
ControlLeft: [45327, 83],
|
|
112
|
+
AltLeft: [45750, 82],
|
|
113
|
+
MetaLeft: [46199, 100],
|
|
114
|
+
Space: [51541, 144],
|
|
115
|
+
MetaRight: [47929, 75],
|
|
116
|
+
AltRight: [49329, 82],
|
|
117
|
+
ArrowUp: [44251, 110],
|
|
118
|
+
ArrowLeft: [49837, 88],
|
|
119
|
+
ArrowDown: [50333, 90],
|
|
120
|
+
ArrowRight: [50783, 111],
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const SOUND_DEFINES_UP: Record<string, [number, number]> = {
|
|
124
|
+
Escape: [2894 + 120, 100],
|
|
125
|
+
F1: [3610 + 100, 90],
|
|
126
|
+
F2: [4210 + 95, 80],
|
|
127
|
+
F3: [4758 + 95, 80],
|
|
128
|
+
F4: [5250 + 105, 90],
|
|
129
|
+
F5: [5831 + 110, 95],
|
|
130
|
+
F6: [6396 + 110, 95],
|
|
131
|
+
F7: [6900 + 110, 95],
|
|
132
|
+
F8: [7443 + 115, 100],
|
|
133
|
+
F9: [7955 + 95, 80],
|
|
134
|
+
F10: [8504 + 110, 95],
|
|
135
|
+
F11: [9046 + 100, 85],
|
|
136
|
+
F12: [9582 + 100, 85],
|
|
137
|
+
Backquote: [12476 + 105, 90],
|
|
138
|
+
Digit1: [12946 + 100, 85],
|
|
139
|
+
Digit2: [13470 + 100, 85],
|
|
140
|
+
Digit3: [13963 + 105, 90],
|
|
141
|
+
Digit4: [14481 + 110, 90],
|
|
142
|
+
Digit5: [14994 + 100, 85],
|
|
143
|
+
Digit6: [15505 + 115, 100],
|
|
144
|
+
Digit7: [15990 + 100, 90],
|
|
145
|
+
Digit8: [16529 + 95, 85],
|
|
146
|
+
Digit9: [17012 + 110, 90],
|
|
147
|
+
Digit0: [17550 + 90, 80],
|
|
148
|
+
Minus: [18052 + 100, 85],
|
|
149
|
+
Equal: [18553 + 90, 85],
|
|
150
|
+
Backspace: [19065 + 115, 100],
|
|
151
|
+
Tab: [21734 + 125, 110],
|
|
152
|
+
KeyQ: [22245 + 100, 85],
|
|
153
|
+
KeyW: [22790 + 90, 85],
|
|
154
|
+
KeyE: [23317 + 85, 80],
|
|
155
|
+
KeyR: [23817 + 95, 85],
|
|
156
|
+
KeyT: [24297 + 95, 85],
|
|
157
|
+
KeyY: [24811 + 100, 85],
|
|
158
|
+
KeyU: [25313 + 100, 85],
|
|
159
|
+
KeyI: [25795 + 95, 85],
|
|
160
|
+
KeyO: [26309 + 85, 80],
|
|
161
|
+
KeyP: [26804 + 85, 80],
|
|
162
|
+
BracketLeft: [27330 + 85, 80],
|
|
163
|
+
BracketRight: [27883 + 105, 90],
|
|
164
|
+
Backslash: [28393 + 105, 90],
|
|
165
|
+
CapsLock: [31011 + 135, 110],
|
|
166
|
+
KeyA: [31542 + 90, 80],
|
|
167
|
+
KeyS: [32031 + 90, 80],
|
|
168
|
+
KeyD: [32492 + 85, 80],
|
|
169
|
+
KeyF: [32973 + 90, 80],
|
|
170
|
+
KeyG: [33453 + 100, 85],
|
|
171
|
+
KeyH: [33986 + 95, 85],
|
|
172
|
+
KeyJ: [34425 + 90, 85],
|
|
173
|
+
KeyK: [34932 + 95, 85],
|
|
174
|
+
KeyL: [35410 + 100, 85],
|
|
175
|
+
Semicolon: [35914 + 100, 85],
|
|
176
|
+
Quote: [36428 + 90, 80],
|
|
177
|
+
Enter: [36902 + 125, 105],
|
|
178
|
+
ShiftLeft: [38136 + 140, 120],
|
|
179
|
+
KeyZ: [38694 + 85, 75],
|
|
180
|
+
KeyX: [39148 + 80, 70],
|
|
181
|
+
KeyC: [39632 + 100, 85],
|
|
182
|
+
KeyV: [40136 + 100, 85],
|
|
183
|
+
KeyB: [40621 + 115, 95],
|
|
184
|
+
KeyN: [41103 + 95, 85],
|
|
185
|
+
KeyM: [41610 + 100, 85],
|
|
186
|
+
Comma: [42110 + 95, 85],
|
|
187
|
+
Period: [42594 + 95, 85],
|
|
188
|
+
Slash: [43105 + 100, 85],
|
|
189
|
+
ShiftRight: [43565 + 145, 125],
|
|
190
|
+
Fn: [44251 + 115, 100],
|
|
191
|
+
ControlLeft: [45327 + 85, 80],
|
|
192
|
+
AltLeft: [45750 + 85, 80],
|
|
193
|
+
MetaLeft: [46199 + 105, 90],
|
|
194
|
+
Space: [51541 + 150, 130],
|
|
195
|
+
MetaRight: [47929 + 75, 70],
|
|
196
|
+
AltRight: [49329 + 85, 80],
|
|
197
|
+
ArrowUp: [44251 + 115, 100],
|
|
198
|
+
ArrowLeft: [49837 + 90, 85],
|
|
199
|
+
ArrowDown: [50333 + 95, 80],
|
|
200
|
+
ArrowRight: [50783 + 115, 100],
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const KEY_DISPLAY_LABELS: Record<string, string> = {
|
|
204
|
+
Escape: "esc",
|
|
205
|
+
Backspace: "delete",
|
|
206
|
+
Tab: "tab",
|
|
207
|
+
Enter: "return",
|
|
208
|
+
ShiftLeft: "shift",
|
|
209
|
+
ShiftRight: "shift",
|
|
210
|
+
ControlLeft: "control",
|
|
211
|
+
ControlRight: "control",
|
|
212
|
+
AltLeft: "option",
|
|
213
|
+
AltRight: "option",
|
|
214
|
+
MetaLeft: "command",
|
|
215
|
+
MetaRight: "command",
|
|
216
|
+
Space: "space",
|
|
217
|
+
CapsLock: "caps",
|
|
218
|
+
ArrowUp: "↑",
|
|
219
|
+
ArrowDown: "↓",
|
|
220
|
+
ArrowLeft: "←",
|
|
221
|
+
ArrowRight: "→",
|
|
222
|
+
Backquote: "`",
|
|
223
|
+
Minus: "-",
|
|
224
|
+
Equal: "=",
|
|
225
|
+
BracketLeft: "[",
|
|
226
|
+
BracketRight: "]",
|
|
227
|
+
Backslash: "\\",
|
|
228
|
+
Semicolon: ";",
|
|
229
|
+
Quote: "'",
|
|
230
|
+
Comma: ",",
|
|
231
|
+
Period: ".",
|
|
232
|
+
Slash: "/",
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const getKeyDisplayLabel = (keyCode: string): string => {
|
|
236
|
+
if (KEY_DISPLAY_LABELS[keyCode]) return KEY_DISPLAY_LABELS[keyCode];
|
|
237
|
+
if (keyCode.startsWith("Key")) return keyCode.slice(3);
|
|
238
|
+
if (keyCode.startsWith("Digit")) return keyCode.slice(5);
|
|
239
|
+
if (keyCode.startsWith("F") && keyCode.length <= 3) return keyCode;
|
|
240
|
+
return keyCode;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
interface KeyboardContextType {
|
|
244
|
+
playSoundDown: (keyCode: string) => void;
|
|
245
|
+
playSoundUp: (keyCode: string) => void;
|
|
246
|
+
pressedKeys: Set<string>;
|
|
247
|
+
setPressed: (keyCode: string) => void;
|
|
248
|
+
setReleased: (keyCode: string) => void;
|
|
249
|
+
lastPressedKey: string | null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const KeyboardContext = createContext<KeyboardContextType | null>(null);
|
|
253
|
+
|
|
254
|
+
const useKeyboardSound = () => {
|
|
255
|
+
const context = useContext(KeyboardContext);
|
|
256
|
+
if (!context) {
|
|
257
|
+
throw new Error("useKeyboardSound must be used within KeyboardProvider");
|
|
258
|
+
}
|
|
259
|
+
return context;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const KeyboardProvider = ({
|
|
263
|
+
children,
|
|
264
|
+
enableSound = false,
|
|
265
|
+
containerRef,
|
|
266
|
+
}: {
|
|
267
|
+
children: React.ReactNode;
|
|
268
|
+
enableSound?: boolean;
|
|
269
|
+
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
270
|
+
}) => {
|
|
271
|
+
const audioContextRef = useRef<AudioContext | null>(null);
|
|
272
|
+
const audioBufferRef = useRef<AudioBuffer | null>(null);
|
|
273
|
+
const [pressedKeys, setPressedKeys] = useState<Set<string>>(new Set());
|
|
274
|
+
const [lastPressedKey, setLastPressedKey] = useState<string | null>(null);
|
|
275
|
+
const [soundLoaded, setSoundLoaded] = useState(false);
|
|
276
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
277
|
+
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
if (!enableSound) return;
|
|
280
|
+
|
|
281
|
+
const initAudio = async () => {
|
|
282
|
+
try {
|
|
283
|
+
audioContextRef.current = new AudioContext();
|
|
284
|
+
const response = await fetch("/sounds/sound.ogg");
|
|
285
|
+
if (!response.ok) {
|
|
286
|
+
console.warn("Sound file not available at /sounds/sound.ogg");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
290
|
+
audioBufferRef.current =
|
|
291
|
+
await audioContextRef.current.decodeAudioData(arrayBuffer);
|
|
292
|
+
setSoundLoaded(true);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.warn("Failed to load sound:", error);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
initAudio();
|
|
299
|
+
|
|
300
|
+
return () => {
|
|
301
|
+
audioContextRef.current?.close();
|
|
302
|
+
};
|
|
303
|
+
}, [enableSound]);
|
|
304
|
+
|
|
305
|
+
const playSoundDown = useCallback(
|
|
306
|
+
(keyCode: string) => {
|
|
307
|
+
if (!enableSound || !soundLoaded) return;
|
|
308
|
+
if (!audioContextRef.current || !audioBufferRef.current) return;
|
|
309
|
+
|
|
310
|
+
const soundDef = SOUND_DEFINES_DOWN[keyCode];
|
|
311
|
+
if (!soundDef) return;
|
|
312
|
+
|
|
313
|
+
const [startMs, durationMs] = soundDef;
|
|
314
|
+
const startTime = startMs / 1000;
|
|
315
|
+
const duration = durationMs / 1000;
|
|
316
|
+
|
|
317
|
+
if (audioContextRef.current.state === "suspended") {
|
|
318
|
+
audioContextRef.current.resume();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const source = audioContextRef.current.createBufferSource();
|
|
322
|
+
source.buffer = audioBufferRef.current;
|
|
323
|
+
source.connect(audioContextRef.current.destination);
|
|
324
|
+
source.start(0, startTime, duration);
|
|
325
|
+
},
|
|
326
|
+
[enableSound, soundLoaded],
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const playSoundUp = useCallback(
|
|
330
|
+
(keyCode: string) => {
|
|
331
|
+
if (!enableSound || !soundLoaded) return;
|
|
332
|
+
if (!audioContextRef.current || !audioBufferRef.current) return;
|
|
333
|
+
|
|
334
|
+
const soundDef = SOUND_DEFINES_UP[keyCode];
|
|
335
|
+
if (!soundDef) return;
|
|
336
|
+
|
|
337
|
+
const [startMs, durationMs] = soundDef;
|
|
338
|
+
const startTime = startMs / 1000;
|
|
339
|
+
const duration = durationMs / 1000;
|
|
340
|
+
|
|
341
|
+
if (audioContextRef.current.state === "suspended") {
|
|
342
|
+
audioContextRef.current.resume();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const source = audioContextRef.current.createBufferSource();
|
|
346
|
+
source.buffer = audioBufferRef.current;
|
|
347
|
+
source.connect(audioContextRef.current.destination);
|
|
348
|
+
source.start(0, startTime, duration);
|
|
349
|
+
},
|
|
350
|
+
[enableSound, soundLoaded],
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
const setPressed = useCallback((keyCode: string) => {
|
|
354
|
+
setPressedKeys((prev) => new Set(prev).add(keyCode));
|
|
355
|
+
setLastPressedKey(keyCode);
|
|
356
|
+
}, []);
|
|
357
|
+
|
|
358
|
+
const setReleased = useCallback((keyCode: string) => {
|
|
359
|
+
setPressedKeys((prev) => {
|
|
360
|
+
const next = new Set(prev);
|
|
361
|
+
next.delete(keyCode);
|
|
362
|
+
return next;
|
|
363
|
+
});
|
|
364
|
+
}, []);
|
|
365
|
+
|
|
366
|
+
useEffect(() => {
|
|
367
|
+
const element = containerRef.current;
|
|
368
|
+
if (!element) return;
|
|
369
|
+
|
|
370
|
+
const observer = new IntersectionObserver(
|
|
371
|
+
([entry]) => {
|
|
372
|
+
setIsVisible(entry.isIntersecting);
|
|
373
|
+
},
|
|
374
|
+
{ threshold: 0.1 },
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
observer.observe(element);
|
|
378
|
+
|
|
379
|
+
return () => {
|
|
380
|
+
observer.disconnect();
|
|
381
|
+
};
|
|
382
|
+
}, [containerRef]);
|
|
383
|
+
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
if (!isVisible) return;
|
|
386
|
+
|
|
387
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
388
|
+
if (e.repeat) return;
|
|
389
|
+
const keyCode = e.code;
|
|
390
|
+
playSoundDown(keyCode);
|
|
391
|
+
setPressed(keyCode);
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const handleKeyUp = (e: KeyboardEvent) => {
|
|
395
|
+
const keyCode = e.code;
|
|
396
|
+
playSoundUp(keyCode);
|
|
397
|
+
setReleased(keyCode);
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
401
|
+
document.addEventListener("keyup", handleKeyUp);
|
|
402
|
+
|
|
403
|
+
return () => {
|
|
404
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
405
|
+
document.removeEventListener("keyup", handleKeyUp);
|
|
406
|
+
};
|
|
407
|
+
}, [isVisible, playSoundDown, playSoundUp, setPressed, setReleased]);
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<KeyboardContext.Provider
|
|
411
|
+
value={{
|
|
412
|
+
playSoundDown,
|
|
413
|
+
playSoundUp,
|
|
414
|
+
pressedKeys,
|
|
415
|
+
setPressed,
|
|
416
|
+
setReleased,
|
|
417
|
+
lastPressedKey,
|
|
418
|
+
}}
|
|
419
|
+
>
|
|
420
|
+
{children}
|
|
421
|
+
</KeyboardContext.Provider>
|
|
422
|
+
);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const KeystrokePreview = () => {
|
|
426
|
+
const { lastPressedKey, pressedKeys } = useKeyboardSound();
|
|
427
|
+
const [displayKey, setDisplayKey] = useState<string | null>(null);
|
|
428
|
+
const [animationKey, setAnimationKey] = useState(0);
|
|
429
|
+
|
|
430
|
+
useEffect(() => {
|
|
431
|
+
if (lastPressedKey) {
|
|
432
|
+
if (
|
|
433
|
+
lastPressedKey === "Space" ||
|
|
434
|
+
lastPressedKey === "ShiftLeft" ||
|
|
435
|
+
lastPressedKey === "ShiftRight"
|
|
436
|
+
) {
|
|
437
|
+
setDisplayKey(null);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
setDisplayKey(getKeyDisplayLabel(lastPressedKey));
|
|
442
|
+
setAnimationKey((prev) => prev + 1);
|
|
443
|
+
}
|
|
444
|
+
}, [lastPressedKey]);
|
|
445
|
+
|
|
446
|
+
const isPressed = pressedKeys.size > 0;
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<div className="relative flex h-12 w-full items-center justify-center">
|
|
450
|
+
<AnimatePresence mode="popLayout">
|
|
451
|
+
{displayKey && (
|
|
452
|
+
<motion.div
|
|
453
|
+
key={animationKey}
|
|
454
|
+
layout
|
|
455
|
+
initial={{ opacity: 0, scale: 0.5, y: 5 }}
|
|
456
|
+
animate={{
|
|
457
|
+
opacity: 1,
|
|
458
|
+
scale: isPressed ? 0.95 : 1,
|
|
459
|
+
y: 0,
|
|
460
|
+
}}
|
|
461
|
+
exit={{ opacity: 0, scale: 0.8, y: -5 }}
|
|
462
|
+
transition={{
|
|
463
|
+
type: "spring",
|
|
464
|
+
stiffness: 500,
|
|
465
|
+
damping: 30,
|
|
466
|
+
mass: 0.5,
|
|
467
|
+
}}
|
|
468
|
+
className="absolute flex items-center justify-center rounded-lg px-4 py-2 font-mono text-2xl font-black text-neutral-200"
|
|
469
|
+
>
|
|
470
|
+
<motion.span
|
|
471
|
+
initial={{ opacity: 0, scale: 1.2, filter: "blur(10px)" }}
|
|
472
|
+
animate={{ opacity: 0.6, scale: 1, filter: "blur(0px)" }}
|
|
473
|
+
transition={{ duration: 0.05 }}
|
|
474
|
+
className="text-2xl text-indigo-400 drop-shadow-[0_0_8px_rgba(99,102,241,0.5)]"
|
|
475
|
+
>
|
|
476
|
+
{displayKey}
|
|
477
|
+
</motion.span>
|
|
478
|
+
</motion.div>
|
|
479
|
+
)}
|
|
480
|
+
</AnimatePresence>
|
|
481
|
+
</div>
|
|
482
|
+
);
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
export const Keyboard = ({
|
|
486
|
+
className,
|
|
487
|
+
enableSound = false,
|
|
488
|
+
showPreview = false,
|
|
489
|
+
}: {
|
|
490
|
+
className?: string;
|
|
491
|
+
enableSound?: boolean;
|
|
492
|
+
showPreview?: boolean;
|
|
493
|
+
}) => {
|
|
494
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
495
|
+
|
|
496
|
+
return (
|
|
497
|
+
<KeyboardProvider enableSound={enableSound} containerRef={containerRef}>
|
|
498
|
+
<div
|
|
499
|
+
ref={containerRef}
|
|
500
|
+
className={cn(
|
|
501
|
+
"mx-auto w-fit [zoom:0.8] sm:[zoom:1.25] md:[zoom:1.5] lg:[zoom:1.75] xl:[zoom:2]",
|
|
502
|
+
className,
|
|
503
|
+
)}
|
|
504
|
+
>
|
|
505
|
+
{showPreview && <KeystrokePreview />}
|
|
506
|
+
<Keypad />
|
|
507
|
+
</div>
|
|
508
|
+
</KeyboardProvider>
|
|
509
|
+
);
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
export const Keypad = () => {
|
|
513
|
+
return (
|
|
514
|
+
// CUSTOMIZED: Transformed baseline palette from generic neutral into advanced DARK INDUSTRIAL graphite aesthetic.
|
|
515
|
+
<div className="h-full w-fit rounded-xl bg-[#18181b] p-1 shadow-xl ring-1 ring-black shadow-black/60">
|
|
516
|
+
|
|
517
|
+
{/* Function Row */}
|
|
518
|
+
<Row>
|
|
519
|
+
<Key
|
|
520
|
+
keyCode="Escape"
|
|
521
|
+
containerClassName="rounded-tl-xl"
|
|
522
|
+
className="w-10 rounded-tl-lg"
|
|
523
|
+
childrenClassName="items-start justify-end pb-[2px] pl-[4px]"
|
|
524
|
+
>
|
|
525
|
+
<span>esc</span>
|
|
526
|
+
</Key>
|
|
527
|
+
<Key keyCode="F1">
|
|
528
|
+
<IconBrightnessDown className="h-[6px] w-[6px]" />
|
|
529
|
+
<span className="mt-1">F1</span>
|
|
530
|
+
</Key>
|
|
531
|
+
<Key keyCode="F2">
|
|
532
|
+
<IconBrightnessUp className="h-[6px] w-[6px]" />
|
|
533
|
+
<span className="mt-1">F2</span>
|
|
534
|
+
</Key>
|
|
535
|
+
<Key keyCode="F3">
|
|
536
|
+
<IconTable className="h-[6px] w-[6px]" />
|
|
537
|
+
<span className="mt-1">F3</span>
|
|
538
|
+
</Key>
|
|
539
|
+
<Key keyCode="F4">
|
|
540
|
+
<IconSearch className="h-[6px] w-[6px]" />
|
|
541
|
+
<span className="mt-1">F4</span>
|
|
542
|
+
</Key>
|
|
543
|
+
<Key keyCode="F5">
|
|
544
|
+
<IconMicrophone className="h-[6px] w-[6px]" />
|
|
545
|
+
<span className="mt-1">F5</span>
|
|
546
|
+
</Key>
|
|
547
|
+
<Key keyCode="F6">
|
|
548
|
+
<IconMoon className="h-[6px] w-[6px]" />
|
|
549
|
+
<span className="mt-1">F6</span>
|
|
550
|
+
</Key>
|
|
551
|
+
<Key keyCode="F7">
|
|
552
|
+
<IconPlayerTrackPrev className="h-[6px] w-[6px]" />
|
|
553
|
+
<span className="mt-1">F7</span>
|
|
554
|
+
</Key>
|
|
555
|
+
<Key keyCode="F8">
|
|
556
|
+
<IconPlayerSkipForward className="h-[6px] w-[6px]" />
|
|
557
|
+
<span className="mt-1">F8</span>
|
|
558
|
+
</Key>
|
|
559
|
+
<Key keyCode="F9">
|
|
560
|
+
<IconPlayerTrackNext className="h-[6px] w-[6px]" />
|
|
561
|
+
<span className="mt-1">F9</span>
|
|
562
|
+
</Key>
|
|
563
|
+
<Key keyCode="F10">
|
|
564
|
+
<IconVolume3 className="h-[6px] w-[6px]" />
|
|
565
|
+
<span className="mt-1">F10</span>
|
|
566
|
+
</Key>
|
|
567
|
+
<Key keyCode="F11">
|
|
568
|
+
<IconVolume2 className="h-[6px] w-[6px]" />
|
|
569
|
+
<span className="mt-1">F11</span>
|
|
570
|
+
</Key>
|
|
571
|
+
<Key keyCode="F12">
|
|
572
|
+
<IconVolume className="h-[6px] w-[6px]" />
|
|
573
|
+
<span className="mt-1">F12</span>
|
|
574
|
+
</Key>
|
|
575
|
+
<Key containerClassName="rounded-tr-xl" className="rounded-tr-lg">
|
|
576
|
+
<div className="h-4 w-4 rounded-full bg-gradient-to-b from-zinc-600 via-zinc-800 to-zinc-950 p-px shadow-inner">
|
|
577
|
+
<div className="h-full w-full rounded-full bg-[#121212]" />
|
|
578
|
+
</div>
|
|
579
|
+
</Key>
|
|
580
|
+
</Row>
|
|
581
|
+
|
|
582
|
+
{/* Number Row */}
|
|
583
|
+
<Row>
|
|
584
|
+
<Key keyCode="Backquote"><span>~</span><span>`</span></Key>
|
|
585
|
+
<Key keyCode="Digit1"><span>!</span><span>1</span></Key>
|
|
586
|
+
<Key keyCode="Digit2"><span>@</span><span>2</span></Key>
|
|
587
|
+
<Key keyCode="Digit3"><span>#</span><span>3</span></Key>
|
|
588
|
+
<Key keyCode="Digit4"><span>$</span><span>4</span></Key>
|
|
589
|
+
<Key keyCode="Digit5"><span>%</span><span>5</span></Key>
|
|
590
|
+
<Key keyCode="Digit6"><span>^</span><span>6</span></Key>
|
|
591
|
+
<Key keyCode="Digit7"><span>&</span><span>7</span></Key>
|
|
592
|
+
<Key keyCode="Digit8"><span>*</span><span>8</span></Key>
|
|
593
|
+
<Key keyCode="Digit9"><span>(</span><span>9</span></Key>
|
|
594
|
+
<Key keyCode="Digit0"><span>)</span><span>0</span></Key>
|
|
595
|
+
<Key keyCode="Minus"><span>—</span><span>_</span></Key>
|
|
596
|
+
<Key keyCode="Equal"><span>+</span><span>=</span></Key>
|
|
597
|
+
<Key
|
|
598
|
+
keyCode="Backspace"
|
|
599
|
+
className="w-10"
|
|
600
|
+
childrenClassName="items-end justify-end pr-[4px] pb-[2px]"
|
|
601
|
+
>
|
|
602
|
+
<span>delete</span>
|
|
603
|
+
</Key>
|
|
604
|
+
</Row>
|
|
605
|
+
|
|
606
|
+
{/* QWERTY Row */}
|
|
607
|
+
<Row>
|
|
608
|
+
<Key
|
|
609
|
+
keyCode="Tab"
|
|
610
|
+
className="w-10"
|
|
611
|
+
childrenClassName="items-start justify-end pb-[2px] pl-[4px]"
|
|
612
|
+
>
|
|
613
|
+
<span>tab</span>
|
|
614
|
+
</Key>
|
|
615
|
+
{["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"].map((letter) => (
|
|
616
|
+
<Key key={letter} keyCode={`Key${letter}`}>
|
|
617
|
+
{letter}
|
|
618
|
+
</Key>
|
|
619
|
+
))}
|
|
620
|
+
<Key keyCode="BracketLeft"><span>{`{`}</span><span>{`[`}</span></Key>
|
|
621
|
+
<Key keyCode="BracketRight"><span>{`}`}</span><span>{`]`}</span></Key>
|
|
622
|
+
<Key keyCode="Backslash"><span>{`|`}</span><span>{`\\`}</span></Key>
|
|
623
|
+
</Row>
|
|
624
|
+
|
|
625
|
+
{/* Home Row */}
|
|
626
|
+
<Row>
|
|
627
|
+
<Key
|
|
628
|
+
keyCode="CapsLock"
|
|
629
|
+
className="w-[2.8rem]"
|
|
630
|
+
childrenClassName="items-start justify-end pb-[2px] pl-[4px]"
|
|
631
|
+
>
|
|
632
|
+
<span>caps lock</span>
|
|
633
|
+
</Key>
|
|
634
|
+
{["A", "S", "D", "F", "G", "H", "J", "K", "L"].map((letter) => (
|
|
635
|
+
<Key key={letter} keyCode={`Key${letter}`}>
|
|
636
|
+
{letter}
|
|
637
|
+
</Key>
|
|
638
|
+
))}
|
|
639
|
+
<Key keyCode="Semicolon"><span>:</span><span>;</span></Key>
|
|
640
|
+
<Key keyCode="Quote"><span>{`"`}</span><span>{`'`}</span></Key>
|
|
641
|
+
<Key
|
|
642
|
+
keyCode="Enter"
|
|
643
|
+
className="w-[2.85rem]"
|
|
644
|
+
childrenClassName="items-end justify-end pr-[4px] pb-[2px]"
|
|
645
|
+
>
|
|
646
|
+
<span>return</span>
|
|
647
|
+
</Key>
|
|
648
|
+
</Row>
|
|
649
|
+
|
|
650
|
+
{/* Bottom Letter Row */}
|
|
651
|
+
<Row>
|
|
652
|
+
<Key
|
|
653
|
+
keyCode="ShiftLeft"
|
|
654
|
+
className="w-[3.65rem]"
|
|
655
|
+
childrenClassName="items-start justify-end pb-[2px] pl-[4px]"
|
|
656
|
+
>
|
|
657
|
+
<span>shift</span>
|
|
658
|
+
</Key>
|
|
659
|
+
{["Z", "X", "C", "V", "B", "N", "M"].map((letter) => (
|
|
660
|
+
<Key key={letter} keyCode={`Key${letter}`}>
|
|
661
|
+
{letter}
|
|
662
|
+
</Key>
|
|
663
|
+
))}
|
|
664
|
+
<Key keyCode="Comma"><span>{`<`}</span><span>,</span></Key>
|
|
665
|
+
<Key keyCode="Period"><span>{`>`}</span><span>.</span></Key>
|
|
666
|
+
<Key keyCode="Slash"><span>?</span><span>/</span></Key>
|
|
667
|
+
<Key
|
|
668
|
+
keyCode="ShiftRight"
|
|
669
|
+
className="w-[3.65rem]"
|
|
670
|
+
childrenClassName="items-end justify-end pr-[4px] pb-[2px]"
|
|
671
|
+
>
|
|
672
|
+
<span>shift</span>
|
|
673
|
+
</Key>
|
|
674
|
+
</Row>
|
|
675
|
+
|
|
676
|
+
{/* Modifier Row */}
|
|
677
|
+
<Row>
|
|
678
|
+
<ModifierKey
|
|
679
|
+
keyCode="Fn"
|
|
680
|
+
containerClassName="rounded-bl-xl"
|
|
681
|
+
className="rounded-bl-lg"
|
|
682
|
+
>
|
|
683
|
+
<span>fn</span>
|
|
684
|
+
<IconWorld className="h-[6px] w-[6px]" />
|
|
685
|
+
</ModifierKey>
|
|
686
|
+
<ModifierKey keyCode="ControlLeft">
|
|
687
|
+
<IconChevronUp className="h-[6px] w-[6px]" />
|
|
688
|
+
<span>control</span>
|
|
689
|
+
</ModifierKey>
|
|
690
|
+
<ModifierKey keyCode="AltLeft">
|
|
691
|
+
<OptionKey className="h-[6px] w-[6px]" />
|
|
692
|
+
<span>option</span>
|
|
693
|
+
</ModifierKey>
|
|
694
|
+
<ModifierKey keyCode="MetaLeft" className="w-8">
|
|
695
|
+
<IconCommand className="h-[6px] w-[6px]" />
|
|
696
|
+
<span>command</span>
|
|
697
|
+
</ModifierKey>
|
|
698
|
+
<Key keyCode="Space" className="w-[8.2rem]" />
|
|
699
|
+
<ModifierKey keyCode="MetaRight" className="w-8">
|
|
700
|
+
<IconCommand className="h-[6px] w-[6px]" />
|
|
701
|
+
<span>command</span>
|
|
702
|
+
</ModifierKey>
|
|
703
|
+
<ModifierKey keyCode="AltRight">
|
|
704
|
+
<OptionKey className="h-[6px] w-[6px]" />
|
|
705
|
+
<span>option</span>
|
|
706
|
+
</ModifierKey>
|
|
707
|
+
|
|
708
|
+
{/* Arrow Keys */}
|
|
709
|
+
<div className="flex h-6 w-[4.9rem] items-center justify-end rounded-[4px] p-[0.5px]">
|
|
710
|
+
<Key keyCode="ArrowLeft" className="h-6 w-6">
|
|
711
|
+
<IconCaretLeftFilled className="h-[6px] w-[6px]" />
|
|
712
|
+
</Key>
|
|
713
|
+
<div className="flex flex-col">
|
|
714
|
+
<Key keyCode="ArrowUp" className="h-3 w-6">
|
|
715
|
+
<IconCaretUpFilled className="h-[6px] w-[6px]" />
|
|
716
|
+
</Key>
|
|
717
|
+
<Key keyCode="ArrowDown" className="h-3 w-6">
|
|
718
|
+
<IconCaretDownFilled className="h-[6px] w-[6px]" />
|
|
719
|
+
</Key>
|
|
720
|
+
</div>
|
|
721
|
+
<Key
|
|
722
|
+
keyCode="ArrowRight"
|
|
723
|
+
containerClassName="rounded-br-xl"
|
|
724
|
+
className="h-6 w-6 rounded-br-lg"
|
|
725
|
+
>
|
|
726
|
+
<IconCaretRightFilled className="h-[6px] w-[6px]" />
|
|
727
|
+
</Key>
|
|
728
|
+
</div>
|
|
729
|
+
</Row>
|
|
730
|
+
</div>
|
|
731
|
+
);
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
const Row = ({ children }: { children: React.ReactNode }) => (
|
|
735
|
+
<div className="mb-[2px] flex w-full shrink-0 gap-[2px]">{children}</div>
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
const Key = ({
|
|
739
|
+
className,
|
|
740
|
+
childrenClassName,
|
|
741
|
+
containerClassName,
|
|
742
|
+
children,
|
|
743
|
+
keyCode,
|
|
744
|
+
}: {
|
|
745
|
+
className?: string;
|
|
746
|
+
childrenClassName?: string;
|
|
747
|
+
containerClassName?: string;
|
|
748
|
+
children?: React.ReactNode;
|
|
749
|
+
keyCode?: string;
|
|
750
|
+
}) => {
|
|
751
|
+
const { playSoundDown, playSoundUp, pressedKeys, setPressed, setReleased } =
|
|
752
|
+
useKeyboardSound();
|
|
753
|
+
const isPressed = keyCode ? pressedKeys.has(keyCode) : false;
|
|
754
|
+
|
|
755
|
+
const handleMouseDown = () => {
|
|
756
|
+
if (keyCode) {
|
|
757
|
+
playSoundDown(keyCode);
|
|
758
|
+
setPressed(keyCode);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
const handleMouseUp = () => {
|
|
763
|
+
if (keyCode && isPressed) {
|
|
764
|
+
playSoundUp(keyCode);
|
|
765
|
+
setReleased(keyCode);
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
const handleMouseLeave = () => {
|
|
770
|
+
if (keyCode && isPressed) {
|
|
771
|
+
setReleased(keyCode);
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
return (
|
|
776
|
+
<div className={cn("rounded-[4px] p-[0.5px]", containerClassName)}>
|
|
777
|
+
<button
|
|
778
|
+
type="button"
|
|
779
|
+
onMouseDown={handleMouseDown}
|
|
780
|
+
onMouseUp={handleMouseUp}
|
|
781
|
+
onMouseLeave={handleMouseLeave}
|
|
782
|
+
className={cn(
|
|
783
|
+
"flex w-6 cursor-pointer items-center justify-center rounded-[3.5px] bg-[#27272a] shadow-[0px_0px_1px_1px_rgba(0,0,0,0.8),0px_1px_1px_0px_rgba(255,255,255,0.05)_inset] transition-transform duration-75 active:scale-[0.97]",
|
|
784
|
+
!className?.includes("h-") && "h-6", // Dynamically applies height default ONLY if not supplied!
|
|
785
|
+
isPressed &&
|
|
786
|
+
"scale-[0.97] bg-[#3f3f46] shadow-[0px_0px_1px_0px_rgba(0,0,0,1),0px_0px_8px_rgba(99,102,241,0.2)]", // Subtle custom indigo pressed glow
|
|
787
|
+
className,
|
|
788
|
+
)}
|
|
789
|
+
>
|
|
790
|
+
<div
|
|
791
|
+
className={cn(
|
|
792
|
+
"flex h-full w-full flex-col items-center justify-center text-[5px] font-medium text-zinc-300", // Upgraded text contrast for readability
|
|
793
|
+
childrenClassName,
|
|
794
|
+
)}
|
|
795
|
+
>
|
|
796
|
+
{children}
|
|
797
|
+
</div>
|
|
798
|
+
</button>
|
|
799
|
+
</div>
|
|
800
|
+
);
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
const ModifierKey = ({
|
|
804
|
+
className,
|
|
805
|
+
containerClassName,
|
|
806
|
+
children,
|
|
807
|
+
keyCode,
|
|
808
|
+
}: {
|
|
809
|
+
className?: string;
|
|
810
|
+
containerClassName?: string;
|
|
811
|
+
children?: React.ReactNode;
|
|
812
|
+
keyCode?: string;
|
|
813
|
+
}) => {
|
|
814
|
+
const { playSoundDown, playSoundUp, pressedKeys, setPressed, setReleased } =
|
|
815
|
+
useKeyboardSound();
|
|
816
|
+
const isPressed = keyCode ? pressedKeys.has(keyCode) : false;
|
|
817
|
+
|
|
818
|
+
const handleMouseDown = () => {
|
|
819
|
+
if (keyCode) {
|
|
820
|
+
playSoundDown(keyCode);
|
|
821
|
+
setPressed(keyCode);
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
const handleMouseUp = () => {
|
|
826
|
+
if (keyCode && isPressed) {
|
|
827
|
+
playSoundUp(keyCode);
|
|
828
|
+
setReleased(keyCode);
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
const handleMouseLeave = () => {
|
|
833
|
+
if (keyCode && isPressed) {
|
|
834
|
+
setReleased(keyCode);
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
return (
|
|
839
|
+
<div className={cn("rounded-[4px] p-[0.5px]", containerClassName)}>
|
|
840
|
+
<button
|
|
841
|
+
type="button"
|
|
842
|
+
onMouseDown={handleMouseDown}
|
|
843
|
+
onMouseUp={handleMouseUp}
|
|
844
|
+
onMouseLeave={handleMouseLeave}
|
|
845
|
+
className={cn(
|
|
846
|
+
"flex w-6 cursor-pointer items-center justify-center rounded-[3.5px] bg-[#27272a] shadow-[0px_0px_1px_1px_rgba(0,0,0,0.8),0px_1px_1px_0px_rgba(255,255,255,0.05)_inset] transition-transform duration-75 active:scale-[0.97]",
|
|
847
|
+
!className?.includes("h-") && "h-6",
|
|
848
|
+
isPressed &&
|
|
849
|
+
"scale-[0.97] bg-[#3f3f46] shadow-[0px_0px_1px_0px_rgba(0,0,0,1),0px_0px_8px_rgba(99,102,241,0.2)]",
|
|
850
|
+
className,
|
|
851
|
+
)}
|
|
852
|
+
>
|
|
853
|
+
<div className="flex h-full w-full flex-col items-start justify-between p-1 text-[5px] font-medium text-zinc-300">
|
|
854
|
+
{children}
|
|
855
|
+
</div>
|
|
856
|
+
</button>
|
|
857
|
+
</div>
|
|
858
|
+
);
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
const OptionKey = ({ className }: { className?: string }) => {
|
|
862
|
+
return (
|
|
863
|
+
<svg
|
|
864
|
+
fill="none"
|
|
865
|
+
version="1.1"
|
|
866
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
867
|
+
viewBox="0 0 32 32"
|
|
868
|
+
className={className}
|
|
869
|
+
>
|
|
870
|
+
<rect
|
|
871
|
+
stroke="currentColor"
|
|
872
|
+
strokeWidth={2}
|
|
873
|
+
x="18"
|
|
874
|
+
y="5"
|
|
875
|
+
width="10"
|
|
876
|
+
height="2"
|
|
877
|
+
/>
|
|
878
|
+
<polygon
|
|
879
|
+
stroke="currentColor"
|
|
880
|
+
strokeWidth={2}
|
|
881
|
+
points="10.6,5 4,5 4,7 9.4,7 18.4,27 28,27 28,25 19.6,25"
|
|
882
|
+
/>
|
|
883
|
+
</svg>
|
|
884
|
+
);
|
|
885
|
+
};
|