UNPKG

@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
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: {