@ertekinno/human-like
Version:
A sophisticated React typewriter effect library with realistic human typing behavior, mobile/desktop keyboards, and comprehensive theming support
1,180 lines (1,179 loc) • 52.8 kB
JavaScript
import { jsx, jsxs } from "react/jsx-runtime";
import { forwardRef, useState, useEffect, useImperativeHandle, useCallback, useRef } from "react";
import { u as useHumanLike } from "./useHumanLike-B7saIOJv.js";
import { d, D, c, K, e, M, b, Q, f, g } from "./useHumanLike-B7saIOJv.js";
var ShiftState = /* @__PURE__ */ ((ShiftState2) => {
ShiftState2["Off"] = "off";
ShiftState2["On"] = "on";
ShiftState2["Locked"] = "locked";
return ShiftState2;
})(ShiftState || {});
var KeyboardView = /* @__PURE__ */ ((KeyboardView2) => {
KeyboardView2["Letters"] = "letters";
KeyboardView2["Numbers"] = "numbers";
KeyboardView2["Symbols"] = "symbols";
return KeyboardView2;
})(KeyboardView || {});
const MobileKeyboard = forwardRef(({
currentView = KeyboardView.Letters,
onViewChange,
highlightedKey,
keyboardMode = "light",
onKeyPress,
unstyled = false,
className = "",
classes = {},
style = {},
labelOverrides = {},
iconOverrides = {},
uppercaseLettersWhenShifted = true,
shiftState = ShiftState.Off,
onShiftStateChange
}, ref) => {
const [activeKey, setActiveKey] = useState("");
const [internalView, setInternalView] = useState(currentView);
const [internalShiftState, setInternalShiftState] = useState(shiftState);
const [lastTapTime, setLastTapTime] = useState(0);
useEffect(() => {
setInternalView(currentView);
}, [currentView]);
useEffect(() => {
setInternalShiftState(shiftState);
}, [shiftState]);
useEffect(() => {
if (highlightedKey) {
setActiveKey(highlightedKey);
const timer = setTimeout(() => setActiveKey(""), 200);
return () => clearTimeout(timer);
}
}, [highlightedKey]);
useImperativeHandle(ref, () => ({
resetKeyboard: () => {
setInternalView(KeyboardView.Letters);
setInternalShiftState(ShiftState.Off);
setActiveKey("");
},
setView: (view) => {
const previousView = internalView;
setInternalView(view);
onViewChange == null ? void 0 : onViewChange({
previousView,
currentView: view,
timestamp: Date.now()
});
},
setShift: (state) => {
const previousState = internalShiftState;
setInternalShiftState(state);
onShiftStateChange == null ? void 0 : onShiftStateChange({
previousState,
currentState: state,
timestamp: Date.now()
});
},
highlightKey: (key) => {
setActiveKey(key);
setTimeout(() => setActiveKey(""), 200);
}
}), [internalView, internalShiftState, onViewChange, onShiftStateChange]);
const letters = [
["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
["a", "s", "d", "f", "g", "h", "j", "k", "l"],
["⇧", "z", "x", "c", "v", "b", "n", "m", "⌫"],
["123", "space", "return"]
];
const numbers = [
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
["-", "/", ":", ";", "(", ")", "$", "&", "@", '"'],
["#+=", ".", ",", "?", "!", "'", "⌫"],
["ABC", "space", "return"]
];
const symbols = [
["[", "]", "{", "}", "#", "%", "^", "*", "+", "="],
["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "•"],
["123", ".", ",", "?", "!", "'", "⌫"],
["ABC", "space", "return"]
];
const handleShiftKey = () => {
const currentTime = Date.now();
const timeSinceLastTap = currentTime - lastTapTime;
if (timeSinceLastTap < 300) {
const newState = internalShiftState === ShiftState.Locked ? ShiftState.Off : ShiftState.Locked;
const previousState = internalShiftState;
setInternalShiftState(newState);
onShiftStateChange == null ? void 0 : onShiftStateChange({
previousState,
currentState: newState,
timestamp: currentTime
});
} else {
let newState;
const previousState = internalShiftState;
switch (internalShiftState) {
case ShiftState.Off:
newState = ShiftState.On;
break;
case ShiftState.On:
newState = ShiftState.Off;
break;
case ShiftState.Locked:
newState = ShiftState.Off;
break;
default:
newState = ShiftState.On;
}
setInternalShiftState(newState);
onShiftStateChange == null ? void 0 : onShiftStateChange({
previousState,
currentState: newState,
timestamp: currentTime
});
}
setLastTapTime(currentTime);
};
const handleViewChange = (newView) => {
const previousView = internalView;
setInternalView(newView);
onViewChange == null ? void 0 : onViewChange({
previousView,
currentView: newView,
timestamp: Date.now()
});
};
const getCurrentLayout = () => {
let layout;
switch (internalView) {
case KeyboardView.Numbers:
layout = numbers;
break;
case KeyboardView.Symbols:
layout = symbols;
break;
default:
layout = letters;
break;
}
if (internalView === KeyboardView.Letters && uppercaseLettersWhenShifted && (internalShiftState === ShiftState.On || internalShiftState === ShiftState.Locked)) {
return layout.map(
(row) => row.map((key) => {
if (key.length === 1 && /[a-z]/.test(key)) {
return key.toUpperCase();
}
return key;
})
);
}
return layout;
};
const handleKeyPress = (key) => {
if (key === "⇧") {
handleShiftKey();
} else if (key === "123") {
handleViewChange(KeyboardView.Numbers);
} else if (key === "ABC") {
handleViewChange(KeyboardView.Letters);
} else if (key === "#+=") {
handleViewChange(KeyboardView.Symbols);
} else {
if (internalShiftState === ShiftState.On && key.length === 1 && /[a-z]/i.test(key)) {
const previousState = internalShiftState;
setInternalShiftState(ShiftState.Off);
onShiftStateChange == null ? void 0 : onShiftStateChange({
previousState,
currentState: ShiftState.Off,
timestamp: Date.now()
});
}
onKeyPress == null ? void 0 : onKeyPress(key);
}
};
const getDisplayLabel = (key) => {
if (iconOverrides[key]) {
return iconOverrides[key];
}
if (labelOverrides[key]) {
return labelOverrides[key];
}
if (key === "space") return "space";
if (key === "return") return "return";
if (key === "⇧") {
return "⇧";
}
return key;
};
const getKeyClasses = (key) => {
const normalizeKey = (k) => {
if (k === "⇧") return "⇧";
if (k === "backspace" || k === "⌫") return "⌫";
if (k === "space") return "space";
if (k === "ABC") return "ABC";
if (k === "123") return "123";
if (k === "#+=") return "#+=";
if (k === "return" || k === "enter") return "return";
return k.toLowerCase();
};
const isActive = normalizeKey(activeKey) === normalizeKey(key) || normalizeKey(highlightedKey || "") === normalizeKey(key);
const isModifier = ["⇧", "ABC", "123", "#+=", "space", "⌫", "return"].includes(key);
const isShiftKey = key === "⇧";
const isShiftLocked = isShiftKey && internalShiftState === ShiftState.Locked;
let keyClasses = "human-like-mobile-keyboard__key";
if (key === "space") keyClasses += " human-like-mobile-keyboard__key--space";
else if (key === "⇧") keyClasses += " human-like-mobile-keyboard__key--shift";
else if (key === "⌫") keyClasses += " human-like-mobile-keyboard__key--backspace";
else if (key === "return") keyClasses += " human-like-mobile-keyboard__key--return";
else if (["ABC", "123", "#+="].includes(key)) keyClasses += " human-like-mobile-keyboard__key--view-switch";
else keyClasses += " human-like-mobile-keyboard__key--regular";
if (isActive) keyClasses += " human-like-mobile-keyboard__key--active";
if (isShiftKey) {
if (isShiftLocked) keyClasses += " human-like-mobile-keyboard__key--shift-locked";
else if (internalShiftState === ShiftState.On) keyClasses += " human-like-mobile-keyboard__key--shift-on";
}
if (isModifier) keyClasses += " human-like-mobile-keyboard__key--modifier";
return keyClasses;
};
const getKeyStyle = (key) => {
if (unstyled) {
const keyWidth = key === "space" ? "180px" : key === "⇧" || key === "⌫" ? "55px" : key === "return" ? "80px" : ["ABC", "123", "#+="].includes(key) ? "50px" : "35px";
return {
minWidth: keyWidth,
height: "42px",
border: "none",
background: "none",
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
justifyContent: "center"
};
}
return {};
};
const getContainerClassName = () => {
let classNames = "human-like-mobile-keyboard";
if (classes.root) classNames += ` ${classes.root}`;
if (className) classNames += ` ${className}`;
return classNames.trim();
};
const containerStyle = unstyled ? style : style;
return /* @__PURE__ */ jsx(
"div",
{
className: getContainerClassName(),
style: containerStyle,
"data-theme": keyboardMode,
children: getCurrentLayout().map((row, rowIndex) => /* @__PURE__ */ jsx(
"div",
{
className: `human-like-mobile-keyboard__row ${classes.row || ""}`,
children: row.map((key) => {
return /* @__PURE__ */ jsx(
"button",
{
className: getKeyClasses(key),
style: getKeyStyle(key),
onClick: () => handleKeyPress(key),
children: getDisplayLabel(key)
},
key
);
})
},
rowIndex
))
}
);
});
MobileKeyboard.displayName = "MobileKeyboard";
const DesktopKeyboard = ({
highlightedKey,
keyboardMode = "light",
onKeyPress
}) => {
const [activeKey, setActiveKey] = useState("");
const [shiftPressed, setShiftPressed] = useState(false);
useEffect(() => {
if (highlightedKey) {
setActiveKey(highlightedKey);
if (highlightedKey === "shift") {
setShiftPressed(true);
setTimeout(() => setShiftPressed(false), 300);
}
const timer = setTimeout(() => setActiveKey(""), 200);
return () => clearTimeout(timer);
}
}, [highlightedKey]);
const numberRow = [
{ key: "`", shift: "~" },
{ key: "1", shift: "!" },
{ key: "2", shift: "@" },
{ key: "3", shift: "#" },
{ key: "4", shift: "$" },
{ key: "5", shift: "%" },
{ key: "6", shift: "^" },
{ key: "7", shift: "&" },
{ key: "8", shift: "*" },
{ key: "9", shift: "(" },
{ key: "0", shift: ")" },
{ key: "-", shift: "_" },
{ key: "=", shift: "+" }
];
const topRow = ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]", "\\"];
const homeRow = ["a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "'"];
const bottomRow = ["z", "x", "c", "v", "b", "n", "m", ",", ".", "/"];
const getKeyDisplay = (keyData) => {
if (typeof keyData === "string") {
return shiftPressed ? keyData.toUpperCase() : keyData;
}
return shiftPressed ? keyData.shift : keyData.key;
};
const getKeyClasses = (keyValue, isModifier = false) => {
const normalizeKey = (k) => {
if (k === "caps lock" || k === "CAPS") return "caps lock";
if (k === "caps") return "caps lock";
if (k === "backspace" || k === "⌫") return "backspace";
if (k === "space") return "space";
if (k === "shift") return "shift";
if (k === "enter" || k === "return") return "enter";
return k.toLowerCase();
};
const isActive = normalizeKey(activeKey) === normalizeKey(keyValue) || normalizeKey(highlightedKey || "") === normalizeKey(keyValue) || keyValue === "shift" && shiftPressed;
let keyClasses = "human-like-desktop-keyboard__key";
if (keyValue === "tab") keyClasses += " human-like-desktop-keyboard__key--tab";
else if (keyValue === "caps lock") keyClasses += " human-like-desktop-keyboard__key--caps-lock";
else if (keyValue === "shift") keyClasses += " human-like-desktop-keyboard__key--shift";
else if (keyValue === "enter") keyClasses += " human-like-desktop-keyboard__key--enter";
else if (keyValue === "backspace") keyClasses += " human-like-desktop-keyboard__key--backspace";
else if (keyValue === "space") keyClasses += " human-like-desktop-keyboard__key--space";
else if (["ctrl", "alt", "cmd"].includes(keyValue)) keyClasses += " human-like-desktop-keyboard__key--ctrl";
else if (isModifier) keyClasses += " human-like-desktop-keyboard__key--function";
else if (/^\d$/.test(keyValue)) keyClasses += " human-like-desktop-keyboard__key--number";
if (isActive) keyClasses += " human-like-desktop-keyboard__key--active";
return keyClasses;
};
return /* @__PURE__ */ jsxs("div", { className: "human-like-desktop-keyboard", "data-theme": keyboardMode, children: [
/* @__PURE__ */ jsxs("div", { className: "human-like-desktop-keyboard__row", children: [
numberRow.map((keyData) => /* @__PURE__ */ jsx(
"button",
{
className: getKeyClasses(keyData.key),
onClick: () => onKeyPress == null ? void 0 : onKeyPress(keyData.key),
children: /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", lineHeight: "1.2" }, children: [
/* @__PURE__ */ jsx("div", { style: { fontSize: "10px", opacity: 0.7 }, children: keyData.shift }),
/* @__PURE__ */ jsx("div", { children: keyData.key })
] })
},
keyData.key
)),
/* @__PURE__ */ jsx("button", { className: getKeyClasses("backspace", true), onClick: () => onKeyPress == null ? void 0 : onKeyPress("backspace"), children: "⌫" })
] }),
/* @__PURE__ */ jsxs("div", { className: "human-like-desktop-keyboard__row", children: [
/* @__PURE__ */ jsx("button", { className: getKeyClasses("tab", true), onClick: () => onKeyPress == null ? void 0 : onKeyPress("tab"), children: "tab" }),
topRow.map((key) => /* @__PURE__ */ jsx(
"button",
{
className: getKeyClasses(key),
onClick: () => onKeyPress == null ? void 0 : onKeyPress(key),
children: getKeyDisplay(key)
},
key
))
] }),
/* @__PURE__ */ jsxs("div", { className: "human-like-desktop-keyboard__row", children: [
/* @__PURE__ */ jsx("button", { className: getKeyClasses("caps lock", true), onClick: () => onKeyPress == null ? void 0 : onKeyPress("caps lock"), children: "caps lock" }),
homeRow.map((key) => /* @__PURE__ */ jsx(
"button",
{
className: getKeyClasses(key),
onClick: () => onKeyPress == null ? void 0 : onKeyPress(key),
children: getKeyDisplay(key)
},
key
)),
/* @__PURE__ */ jsx("button", { className: getKeyClasses("enter", true), onClick: () => onKeyPress == null ? void 0 : onKeyPress("enter"), children: "↵" })
] }),
/* @__PURE__ */ jsxs("div", { className: "human-like-desktop-keyboard__row", children: [
/* @__PURE__ */ jsx("button", { className: getKeyClasses("shift", true), onClick: () => onKeyPress == null ? void 0 : onKeyPress("shift"), children: "shift" }),
bottomRow.map((key) => /* @__PURE__ */ jsx(
"button",
{
className: getKeyClasses(key),
onClick: () => onKeyPress == null ? void 0 : onKeyPress(key),
children: getKeyDisplay(key)
},
key
)),
/* @__PURE__ */ jsx("button", { className: getKeyClasses("shift", true), onClick: () => onKeyPress == null ? void 0 : onKeyPress("shift"), children: "shift" })
] }),
/* @__PURE__ */ jsxs("div", { className: "human-like-desktop-keyboard__row", children: [
/* @__PURE__ */ jsx("button", { className: getKeyClasses("ctrl", true), onClick: () => onKeyPress == null ? void 0 : onKeyPress("ctrl"), children: "ctrl" }),
/* @__PURE__ */ jsx("button", { className: getKeyClasses("alt", true), onClick: () => onKeyPress == null ? void 0 : onKeyPress("alt"), children: "alt" }),
/* @__PURE__ */ jsx("button", { className: getKeyClasses("cmd", true), onClick: () => onKeyPress == null ? void 0 : onKeyPress("cmd"), children: "cmd" }),
/* @__PURE__ */ jsx("button", { className: getKeyClasses("space", false), onClick: () => onKeyPress == null ? void 0 : onKeyPress("space"), children: "space" }),
/* @__PURE__ */ jsx("button", { className: getKeyClasses("cmd", true), onClick: () => onKeyPress == null ? void 0 : onKeyPress("cmd"), children: "cmd" }),
/* @__PURE__ */ jsx("button", { className: getKeyClasses("alt", true), onClick: () => onKeyPress == null ? void 0 : onKeyPress("alt"), children: "alt" }),
/* @__PURE__ */ jsx("button", { className: getKeyClasses("ctrl", true), onClick: () => onKeyPress == null ? void 0 : onKeyPress("ctrl"), children: "ctrl" })
] })
] });
};
const useKeyPressIndicator = (maxHistory = 10) => {
const [keyHistory, setKeyHistory] = useState([]);
const addKeyPress = (keyInfo) => {
const newEvent = {
keyInfo,
timestamp: Date.now(),
id: Math.random().toString(36).substring(7)
};
setKeyHistory((prev) => [newEvent, ...prev].slice(0, maxHistory));
};
const clearHistory = () => setKeyHistory([]);
return { keyHistory, addKeyPress, clearHistory };
};
const KeyboardSimulationDemo = ({
text: propText,
speed: propSpeed,
mistakeFrequency: propMistakeFrequency,
keyboardMode: propKeyboardMode,
theme: propTheme,
autoStart = false
}) => {
var _a, _b, _c;
const defaultText = "HELLO World! Check out these amazing symbols: @#$%^&*() and numbers 12345. This demonstrates natural keyboard timing with view switching on mobile keyboards! 🚀";
const [customText, setCustomText] = useState(propText || defaultText);
const [currentText, setCurrentText] = useState(propText || customText);
const [keyboardMode, setKeyboardMode] = useState(propKeyboardMode || "mobile");
const [currentView, setCurrentView] = useState(KeyboardView.Letters);
const [theme, setTheme] = useState(propTheme || "light");
const [highlightedKey, setHighlightedKey] = useState("");
const { keyHistory, addKeyPress, clearHistory } = useKeyPressIndicator();
useEffect(() => {
if (propTheme && propTheme !== theme) {
setTheme(propTheme);
}
}, [propTheme, theme]);
useEffect(() => {
if (propKeyboardMode && propKeyboardMode !== keyboardMode) {
setKeyboardMode(propKeyboardMode);
}
}, [propKeyboardMode, keyboardMode]);
useEffect(() => {
if (propText && propText !== currentText) {
setCurrentText(propText);
setCustomText(propText);
}
}, [propText, currentText]);
const [config, setConfig] = useState({
speed: propSpeed || 60,
// Good average speed (~100 WPM)
speedVariation: 20,
mistakeFrequency: propMistakeFrequency || 0.02,
keyboardMode: "mobile",
mistakeTypes: {
adjacent: true,
random: false,
doubleChar: true,
commonTypos: true
},
fatigueEffect: false,
concentrationLapses: false,
overcorrection: true
});
const [cursorConfig, setCursorConfig] = useState({
showCursor: true,
cursorChar: "|",
cursorBlinkSpeed: 530
});
const handleKeyPress = useCallback((keyInfo) => {
console.log(`🔑 Key: ${keyInfo.key}, Type: ${keyInfo.type}, View: ${keyInfo.keyboardView}, Duration: ${keyInfo.duration}ms, Sequence: ${keyInfo.sequenceIndex + 1}/${keyInfo.sequenceLength}`);
setHighlightedKey(keyInfo.key);
setTimeout(() => {
setHighlightedKey("");
}, keyInfo.duration);
const view = keyInfo.keyboardView;
if (view === "letters") setCurrentView(KeyboardView.Letters);
else if (view === "numbers") setCurrentView(KeyboardView.Numbers);
else if (view === "symbols") setCurrentView(KeyboardView.Symbols);
addKeyPress(keyInfo);
}, [addKeyPress]);
const {
displayText,
showCursor,
cursorChar,
currentState,
progress,
currentWPM,
mistakeCount,
start,
stop,
pause,
resume,
skip,
reset
} = useHumanLike({
text: currentText,
config: {
...config,
keyboardMode
},
showCursor: cursorConfig.showCursor,
cursorChar: cursorConfig.cursorChar,
cursorBlinkSpeed: cursorConfig.cursorBlinkSpeed,
autoStart,
keyboardMode,
onKey: handleKeyPress,
onStart: () => {
clearHistory();
console.log("🚀 Keyboard simulation demo started");
},
onComplete: () => console.log("✅ Demo completed"),
onMistake: (mistake) => console.log(`❌ Mistake: "${mistake.originalChar}" → "${mistake.mistakeChar}"`),
onChar: (char, index) => console.log(`📝 Character: "${char}" at position ${index}`)
});
const prevConfigRef = useRef(config);
useEffect(() => {
const configChanged = JSON.stringify(prevConfigRef.current) !== JSON.stringify(config);
if (configChanged && (currentState === "typing" || currentState === "correcting" || currentState === "paused")) {
console.log("🔄 Configuration changed during typing, restarting...");
reset();
setTimeout(() => {
start();
}, 100);
}
prevConfigRef.current = config;
}, [config, currentState, reset, start]);
const sampleTexts = {
capsLock: "CAPS LOCK DEMO: THIS SHOWS CAPS LOCK BEHAVIOR vs Single Caps Like This!",
symbols: "Symbol test: @username #hashtag $100 %discount ^power &and *star (parentheses) +plus =equals",
numbers: "Numbers: 1234567890 and mixed content like user123@email.com",
mixed: "Mixed content: Hello WORLD! Check symbols @#$% numbers 123 and normal text.",
programming: "const greeting = 'Hello World!'; if (user.isActive) { console.log(`Welcome ${user.name}!`); }",
sentences: "Short sentence. Another one! What about questions? This demonstrates sentence pauses after punctuation.",
words: "This text has many individual words to demonstrate word pause timing between each word in the sentence.",
complex: "Sophisticated terminology demonstrates thinking pauses before complex multisyllabic words like 'implementation', 'configuration', and 'demonstration'."
};
const containerStyle = {
backgroundColor: theme === "dark" ? "#2c2c2c" : "#ffffff",
color: theme === "dark" ? "#ffffff" : "#000000",
padding: "20px",
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif',
borderRadius: "8px",
border: theme === "dark" ? "1px solid #555555" : "1px solid #cccccc",
width: "100%",
minWidth: "800px"
};
const controlsStyle = {
display: "flex",
flexWrap: "wrap",
gap: "10px",
justifyContent: "start",
marginBottom: "16px"
};
const buttonStyle = (variant = "secondary") => ({
padding: "8px 16px",
border: theme === "dark" ? "1px solid #555555" : "1px solid #cccccc",
borderRadius: "6px",
fontSize: "14px",
fontWeight: "500",
cursor: "pointer",
transition: "all 0.2s ease",
backgroundColor: variant === "primary" ? "#007aff" : theme === "dark" ? "#404040" : "#f8f8f8",
color: variant === "primary" ? "#ffffff" : theme === "dark" ? "#ffffff" : "#000000"
});
const typingAreaStyle = {
backgroundColor: theme === "dark" ? "#1c1c1c" : "#f2f2f2",
borderRadius: "8px",
padding: "25px",
width: "100%",
minHeight: "120px",
textAlign: "left",
fontSize: "18px",
lineHeight: "1.6",
fontFamily: 'SF Mono, Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace',
border: theme === "dark" ? "1px solid #555555" : "1px solid #cccccc",
position: "relative",
whiteSpace: "pre-wrap",
wordWrap: "break-word"
};
const statsStyle = {
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(120px, 1fr))",
gap: "15px",
marginBottom: "20px"
};
const statItemStyle = {
backgroundColor: theme === "dark" ? "#1c1c1c" : "#f2f2f2",
padding: "15px",
borderRadius: "8px",
textAlign: "center",
border: `1px solid ${theme === "dark" ? "#3c3c3e" : "#d1d1d6"}`
};
const statValueStyle = {
fontSize: "24px",
fontWeight: "700",
color: theme === "dark" ? "#007AFF" : "#007AFF"
};
const statLabelStyle = {
fontSize: "12px",
opacity: 0.7,
textTransform: "uppercase",
fontWeight: "600",
marginTop: "5px"
};
return /* @__PURE__ */ jsxs("div", { style: containerStyle, children: [
/* @__PURE__ */ jsxs("div", { style: { marginBottom: "16px" }, children: [
/* @__PURE__ */ jsx("label", { style: {
fontSize: "14px",
fontWeight: "600",
marginBottom: "8px",
display: "block",
color: theme === "dark" ? "#ffffff" : "#000000",
textAlign: "left"
}, children: "Keyboard Mode:" }),
/* @__PURE__ */ jsxs("div", { style: controlsStyle, children: [
/* @__PURE__ */ jsx(
"button",
{
style: buttonStyle(keyboardMode === "mobile" ? "primary" : "secondary"),
onClick: () => {
setKeyboardMode("mobile");
setConfig((prev) => ({ ...prev, keyboardMode: "mobile" }));
reset();
},
children: "Mobile"
}
),
/* @__PURE__ */ jsx(
"button",
{
style: buttonStyle(keyboardMode === "desktop" ? "primary" : "secondary"),
onClick: () => {
setKeyboardMode("desktop");
setConfig((prev) => ({ ...prev, keyboardMode: "desktop" }));
reset();
},
children: "Desktop"
}
)
] })
] }),
/* @__PURE__ */ jsxs("div", { style: { marginBottom: "20px" }, children: [
/* @__PURE__ */ jsx("label", { style: {
fontSize: "14px",
fontWeight: "600",
marginBottom: "8px",
display: "block",
color: theme === "dark" ? "#ffffff" : "#000000",
textAlign: "left"
}, children: "Playback Controls:" }),
/* @__PURE__ */ jsxs("div", { style: controlsStyle, children: [
/* @__PURE__ */ jsx("button", { style: buttonStyle("primary"), onClick: start, children: "Start" }),
/* @__PURE__ */ jsx("button", { style: buttonStyle(), onClick: pause, children: "Pause" }),
/* @__PURE__ */ jsx("button", { style: buttonStyle(), onClick: resume, children: "Resume" }),
/* @__PURE__ */ jsx("button", { style: buttonStyle(), onClick: stop, children: "Stop" }),
/* @__PURE__ */ jsx("button", { style: buttonStyle(), onClick: reset, children: "Reset" }),
/* @__PURE__ */ jsx("button", { style: buttonStyle(), onClick: skip, children: "Skip" })
] })
] }),
/* @__PURE__ */ jsxs("div", { style: statsStyle, children: [
/* @__PURE__ */ jsxs("div", { style: statItemStyle, children: [
/* @__PURE__ */ jsxs("div", { style: statValueStyle, children: [
Math.round(progress),
"%"
] }),
/* @__PURE__ */ jsx("div", { style: statLabelStyle, children: "Progress" })
] }),
/* @__PURE__ */ jsxs("div", { style: statItemStyle, children: [
/* @__PURE__ */ jsx("div", { style: statValueStyle, children: currentWPM }),
/* @__PURE__ */ jsx("div", { style: statLabelStyle, children: "WPM" })
] }),
/* @__PURE__ */ jsxs("div", { style: statItemStyle, children: [
/* @__PURE__ */ jsx("div", { style: statValueStyle, children: mistakeCount }),
/* @__PURE__ */ jsx("div", { style: statLabelStyle, children: "Mistakes" })
] }),
/* @__PURE__ */ jsxs("div", { style: statItemStyle, children: [
/* @__PURE__ */ jsx("div", { style: statValueStyle, children: currentState }),
/* @__PURE__ */ jsx("div", { style: statLabelStyle, children: "State" })
] })
] }),
/* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "row", gap: "16px", marginBottom: "24px" }, children: [
/* @__PURE__ */ jsxs("div", { style: typingAreaStyle, children: [
displayText,
showCursor && /* @__PURE__ */ jsx("span", { style: {
opacity: 0.7,
marginLeft: "2px"
}, children: cursorChar })
] }),
/* @__PURE__ */ jsx("div", { style: {
backgroundColor: theme === "dark" ? "#404040" : "#f8f8f8",
borderRadius: "8px",
padding: "16px",
border: theme === "dark" ? "1px solid #555555" : "1px solid #cccccc",
height: "320px",
minWidth: "240px",
overflowY: "auto"
}, children: keyHistory.length === 0 ? /* @__PURE__ */ jsx("div", { style: {
textAlign: "left",
color: theme === "dark" ? "#999999" : "#666666",
fontSize: "14px"
}, children: "Start typing to see key presses..." }) : keyHistory.map((event, index) => {
const keyInfo = event.keyInfo;
const getKeyTypeColor = (type) => {
const colors = {
letter: "#34C759",
number: "#FF9F0A",
symbol: "#FF2D92",
modifier: "#007AFF",
"view-switch": "#AF52DE",
space: "#8E8E93",
enter: "#8E8E93",
backspace: "#FF3B30"
};
return colors[type] || "#999999";
};
return /* @__PURE__ */ jsxs(
"div",
{
style: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "8px 12px",
marginBottom: "6px",
backgroundColor: theme === "dark" ? "#2c2c2c" : "#ffffff",
borderRadius: "6px",
border: `2px solid ${getKeyTypeColor(keyInfo.type)}`,
opacity: Math.max(0.3, 1 - index * 0.1),
transform: `scale(${Math.max(0.9, 1 - index * 0.02)})`,
transition: "all 0.3s ease"
},
children: [
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx("span", { style: {
display: "inline-block",
padding: "2px 6px",
borderRadius: "4px",
backgroundColor: getKeyTypeColor(keyInfo.type),
color: "#ffffff",
fontSize: "10px",
fontWeight: "500",
textTransform: "uppercase",
marginRight: "8px"
}, children: keyInfo.type }),
/* @__PURE__ */ jsx("span", { style: {
fontWeight: "600",
fontSize: "14px",
fontFamily: "SF Mono, Monaco, monospace"
}, children: (() => {
const key = keyInfo.key;
const type = keyInfo.type;
if (type === "view-switch") return key.toUpperCase();
if (key === "space") return "SPACE";
if (key === "enter" || key === "return") return "ENTER";
if (key === "shift") return "SHIFT";
if (key === "caps lock" || key === "CAPS") return "CAPS LOCK";
if (key === "backspace" || key === "⌫") return "BACKSPACE";
if (key === ".?123" || key === "numbers") return "123";
if (key === "#+=" || key === "symbols") return "#+=";
if (key === "ABC" || key === "letters") return "ABC";
return key.toUpperCase();
})() })
] }),
/* @__PURE__ */ jsxs("div", { style: {
fontSize: "12px",
opacity: 0.7,
textAlign: "right"
}, children: [
/* @__PURE__ */ jsxs("div", { children: [
keyInfo.duration,
"ms"
] }),
/* @__PURE__ */ jsxs("div", { style: { fontSize: "10px" }, children: [
keyInfo.sequenceIndex + 1,
"/",
keyInfo.sequenceLength
] })
] })
]
},
event.id
);
}) })
] }),
/* @__PURE__ */ jsx("div", { style: { width: "100%", marginBottom: "24px" }, children: keyboardMode === "mobile" ? /* @__PURE__ */ jsx(
MobileKeyboard,
{
currentView,
highlightedKey,
keyboardMode: theme,
onViewChange: (event) => {
setCurrentView(event.currentView);
},
onKeyPress: (key) => {
setHighlightedKey(key);
setTimeout(() => setHighlightedKey(""), 200);
},
style: { maxWidth: "none" }
}
) : /* @__PURE__ */ jsx(
DesktopKeyboard,
{
highlightedKey,
keyboardMode: theme,
onKeyPress: (key) => {
setHighlightedKey(key);
setTimeout(() => setHighlightedKey(""), 200);
}
}
) }),
/* @__PURE__ */ jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "24px" }, children: [
/* @__PURE__ */ jsxs("div", { style: {
backgroundColor: theme === "dark" ? "#404040" : "#f8f8f8",
borderRadius: "8px",
padding: "16px",
border: theme === "dark" ? "1px solid #555555" : "1px solid #cccccc",
textAlign: "left"
}, children: [
/* @__PURE__ */ jsx("h4", { style: { marginTop: "0px", marginBottom: "16px", fontSize: "16px", fontWeight: "600" }, children: "Sample Texts" }),
/* @__PURE__ */ jsx("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "8px", marginBottom: "24px" }, children: Object.entries(sampleTexts).map(([key, text]) => /* @__PURE__ */ jsx(
"button",
{
style: {
...buttonStyle(),
fontSize: "13px",
padding: "10px 12px",
textAlign: "left"
},
onClick: () => {
setCurrentText(text);
setCustomText(text);
reset();
},
title: text.substring(0, 100) + "...",
children: key.charAt(0).toUpperCase() + key.slice(1)
},
key
)) }),
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx("h4", { style: { marginBottom: "12px", fontSize: "16px", fontWeight: "600" }, children: "Custom Text" }),
/* @__PURE__ */ jsx(
"textarea",
{
value: customText,
onChange: (e2) => setCustomText(e2.target.value),
style: {
width: "100%",
height: "120px",
padding: "12px",
borderRadius: "6px",
border: theme === "dark" ? "1px solid #555555" : "1px solid #cccccc",
backgroundColor: theme === "dark" ? "#2c2c2c" : "#ffffff",
color: theme === "dark" ? "#ffffff" : "#000000",
fontSize: "14px",
fontFamily: "SF Mono, Monaco, monospace",
resize: "vertical",
outline: "none",
boxSizing: "border-box"
},
placeholder: "Enter your custom text here..."
}
),
/* @__PURE__ */ jsx(
"button",
{
style: {
...buttonStyle("primary"),
marginTop: "12px",
width: "100%",
fontSize: "14px",
fontWeight: "600"
},
onClick: () => {
setCurrentText(customText);
reset();
},
children: "Update Text"
}
)
] })
] }),
/* @__PURE__ */ jsx("div", { style: {
backgroundColor: theme === "dark" ? "#404040" : "#f8f8f8",
borderRadius: "8px",
padding: "16px",
border: theme === "dark" ? "1px solid #555555" : "1px solid #cccccc",
textAlign: "left"
}, children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "18px" }, children: [
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx("h4", { style: { marginTop: "0px", marginBottom: "12px", fontSize: "16px", fontWeight: "600", opacity: 0.9 }, children: "Basic Settings" }),
/* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsxs("label", { style: { fontSize: "14px", fontWeight: "500", marginBottom: "6px", display: "block" }, children: [
"Speed: ",
config.speed || 60,
"ms"
] }),
/* @__PURE__ */ jsx(
"input",
{
type: "range",
min: "20",
max: "300",
value: config.speed || 60,
onChange: (e2) => setConfig((prev) => ({ ...prev, speed: parseInt(e2.target.value) })),
style: { width: "100%", height: "4px" }
}
),
/* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", fontSize: "11px", opacity: 0.6, marginTop: "2px" }, children: [
/* @__PURE__ */ jsx("span", { children: "Fast" }),
/* @__PURE__ */ jsx("span", { children: "Slow" })
] })
] }),
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsxs("label", { style: { fontSize: "14px", fontWeight: "500", marginBottom: "6px", display: "block" }, children: [
"Speed Variation: ",
config.speedVariation,
"ms"
] }),
/* @__PURE__ */ jsx(
"input",
{
type: "range",
min: "0",
max: "100",
value: config.speedVariation,
onChange: (e2) => setConfig((prev) => ({ ...prev, speedVariation: parseInt(e2.target.value) })),
style: { width: "100%", height: "4px" }
}
)
] }),
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsxs("label", { style: { fontSize: "14px", fontWeight: "500", marginBottom: "6px", display: "block" }, children: [
"Mistake Rate: ",
((config.mistakeFrequency || 0) * 100).toFixed(1),
"%"
] }),
/* @__PURE__ */ jsx(
"input",
{
type: "range",
min: "0",
max: "0.5",
step: "0.01",
value: config.mistakeFrequency || 0,
onChange: (e2) => setConfig((prev) => ({ ...prev, mistakeFrequency: parseFloat(e2.target.value) })),
style: { width: "100%", height: "4px" }
}
)
] })
] })
] }),
/* @__PURE__ */ jsxs("div", { style: { paddingTop: "6px", borderTop: theme === "dark" ? "1px solid #555555" : "1px solid #cccccc" }, children: [
/* @__PURE__ */ jsx("h4", { style: { marginBottom: "12px", fontSize: "16px", fontWeight: "600", opacity: 0.9 }, children: "Pause Timing" }),
/* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsxs("label", { style: { fontSize: "14px", fontWeight: "500", marginBottom: "6px", display: "block" }, children: [
"Sentence Pause: ",
config.sentencePause || 500,
"ms",
/* @__PURE__ */ jsx("span", { style: { fontSize: "12px", opacity: 0.7, fontWeight: "normal", display: "block" }, children: "After . ! ?" })
] }),
/* @__PURE__ */ jsx(
"input",
{
type: "range",
min: "100",
max: "1500",
step: "50",
value: config.sentencePause || 500,
onChange: (e2) => setConfig((prev) => ({ ...prev, sentencePause: parseInt(e2.target.value) })),
style: { width: "100%", height: "4px" }
}
)
] }),
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsxs("label", { style: { fontSize: "14px", fontWeight: "500", marginBottom: "6px", display: "block" }, children: [
"Word Pause: ",
config.wordPause || 150,
"ms",
/* @__PURE__ */ jsx("span", { style: { fontSize: "12px", opacity: 0.7, fontWeight: "normal", display: "block" }, children: "Between words" })
] }),
/* @__PURE__ */ jsx(
"input",
{
type: "range",
min: "50",
max: "800",
step: "25",
value: config.wordPause || 150,
onChange: (e2) => setConfig((prev) => ({ ...prev, wordPause: parseInt(e2.target.value) })),
style: { width: "100%", height: "4px" }
}
)
] }),
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsxs("label", { style: { fontSize: "14px", fontWeight: "500", marginBottom: "6px", display: "block" }, children: [
"Thinking Pause: ",
config.thinkingPause || 400,
"ms",
/* @__PURE__ */ jsx("span", { style: { fontSize: "12px", opacity: 0.7, fontWeight: "normal", display: "block" }, children: "Before complex words" })
] }),
/* @__PURE__ */ jsx(
"input",
{
type: "range",
min: "100",
max: "1200",
step: "50",
value: config.thinkingPause || 400,
onChange: (e2) => setConfig((prev) => ({ ...prev, thinkingPause: parseInt(e2.target.value) })),
style: { width: "100%", height: "4px" }
}
)
] }),
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsxs("label", { style: { fontSize: "14px", fontWeight: "500", marginBottom: "6px", display: "block" }, children: [
"Correction Pause: ",
config.correctionPause || 200,
"ms",
/* @__PURE__ */ jsx("span", { style: { fontSize: "12px", opacity: 0.7, fontWeight: "normal", display: "block" }, children: "Before fixing mistakes" })
] }),
/* @__PURE__ */ jsx(
"input",
{
type: "range",
min: "50",
max: "1000",
step: "25",
value: config.correctionPause || 200,
onChange: (e2) => setConfig((prev) => ({ ...prev, correctionPause: parseInt(e2.target.value) })),
style: { width: "100%", height: "4px" }
}
)
] })
] })
] }),
/* @__PURE__ */ jsxs("div", { style: { paddingTop: "6px", borderTop: theme === "dark" ? "1px solid #555555" : "1px solid #cccccc" }, children: [
/* @__PURE__ */ jsx("h4", { style: { marginBottom: "12px", fontSize: "16px", fontWeight: "600", opacity: 0.9 }, children: "Mistake Types & Effects" }),
/* @__PURE__ */ jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "8px" }, children: [
/* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "13px" }, children: [
/* @__PURE__ */ jsx(
"input",
{
type: "checkbox",
checked: ((_a = config.mistakeTypes) == null ? void 0 : _a.adjacent) || false,
onChange: (e2) => setConfig((prev) => {
var _a2, _b2, _c2;
return {
...prev,
mistakeTypes: {
adjacent: e2.target.checked,
random: ((_a2 = prev.mistakeTypes) == null ? void 0 : _a2.random) || false,
doubleChar: ((_b2 = prev.mistakeTypes) == null ? void 0 : _b2.doubleChar) || false,
commonTypos: ((_c2 = prev.mistakeTypes) == null ? void 0 : _c2.commonTypos) || false
}
};
})
}
),
"Adjacent Keys"
] }),
/* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "13px" }, children: [
/* @__PURE__ */ jsx(
"input",
{
type: "checkbox",
checked: ((_b = config.mistakeTypes) == null ? void 0 : _b.doubleChar) || false,
onChange: (e2) => setConfig((prev) => {
var _a2, _b2, _c2;
return {
...prev,
mistakeTypes: {
adjacent: ((_a2 = prev.mistakeTypes) == null ? void 0 : _a2.adjacent) || false,
random: ((_b2 = prev.mistakeTypes) == null ? void 0 : _b2.random) || false,
doubleChar: e2.target.checked,
commonTypos: ((_c2 = prev.mistakeTypes) == null ? void 0 : _c2.commonTypos) || false
}
};
})
}
),
"Double Characters"
] }),
/* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "13px" }, children: [
/* @__PURE__ */ jsx(
"input",
{
type: "checkbox",
checked: ((_c = config.mistakeTypes) == null ? void 0 : _c.commonTypos) || false,
onChange: (e2) => setConfig((prev) => {
var _a2, _b2, _c2;
return {
...prev,
mistakeTypes: {
adjacent: ((_a2 = prev.mistakeTypes) == null ? void 0 : _a2.adjacent) || false,
random: ((_b2 = prev.mistakeTypes) == null ? void 0 : _b2.random) || false,
doubleChar: ((_c2 = prev.mistakeTypes) == null ? void 0 : _c2.doubleChar) || false,
commonTypos: e2.target.checked
}
};
})
}
),
"Common Typos"
] }),
/* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "13px" }, children: [
/* @__PURE__ */ jsx(
"input",
{
type: "checkbox",
checked: config.fatigueEffect || false,
onChange: (e2) => setConfig((prev) => ({ ...prev, fatigueEffect: e2.target.checked }))
}
),
"Fatigue Effect"
] }),
/* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "13px" }, children: [
/* @__PURE__ */ jsx(
"input",
{
type: "checkbox",
checked: config.concentrationLapses || false,
onChange: (e2) => setConfig((prev) => ({ ...prev, concentrationLapses: e2.target.checked }))
}
),
"Concentration Lapses"
] }),
/* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "13px" }, children: [
/* @__PURE__ */ jsx(
"input",
{
type: "checkbox",
checked: config.overcorrection || false,
onChange: (e2) => setConfig((prev) => ({ ...prev, overcorrection: e2.target.checked }))
}
),
"Overcorrection"
] })
] })
] }),
/* @__PURE__ */ jsxs("div", { style: { paddingTop: "6px", borderTop: theme === "dark" ? "1px solid #555555" : "1px solid #cccccc" }, children: [
/* @__PURE__ */ jsx("h4", { style: { marginBottom: "12px", fontSize: "16px", fontWeight: "600", opacity: 0.9 }, children: "Cursor Settings" }),
/* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
/* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "14px" }, children: [
/* @__PURE__ */ jsx(
"input",
{
type: "checkbox",
checked: cursorConfig.showCursor,
onChange: (e2) => setCursorConfig((prev) => ({ ...prev, showCursor: e2.target.checked }))
}
),
"Show Cursor"
] }),
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx("label", { style: { fontSize: "14px", fontWeight: "500", marginBottom: "6px", display: "block" }, children: "Cursor Character:" }),
/* @__PURE__ */ jsx(
"input",
{
type: "text",
value: cursorConfig.cursorChar,
onChange: (e2) => setCursorConfig((prev) => ({ ...prev, cursorChar: e2.target.value || "|" })),
style: {