koin.js 1.0.14 → 1.0.16

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/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import React2, { memo, useState, useEffect, useRef, createContext, useMemo, useCallback, useContext } from 'react';
2
- import { Gauge, Play, Pause, RotateCcw, Rewind, Save, Download, Camera, Video, Circle, Monitor, ChevronDown, RefreshCw, HelpCircle, Maximize, Gamepad2, Joystick, Code, Settings, Power, Square, Keyboard, X, ChevronUp, VolumeX, Volume1, Volume2, Loader2, Trophy, AlertTriangle, Minimize2, List, PauseCircle, Check, Clock, Move, User, Copy, Lock, Zap, HardDrive, Trash2, Cpu, AlertCircle, FileCode, Globe, ExternalLink, EyeOff, Eye, Shield, CheckCircle, LogOut, Info, XCircle } from 'lucide-react';
2
+ import { Gauge, Play, Pause, RotateCcw, Rewind, Save, Download, Camera, Video, Circle, Monitor, ChevronDown, RefreshCw, HelpCircle, Maximize, Gamepad2, Joystick, Code, Settings, Power, Square, Keyboard, ChevronUp, VolumeX, Volume1, Volume2, Loader2, Trophy, X, AlertTriangle, Minimize2, List, PauseCircle, Check, Clock, Lock, Unlock, Move, User, Copy, Zap, HardDrive, Trash2, Cpu, AlertCircle, FileCode, Globe, ExternalLink, EyeOff, Eye, Shield, CheckCircle, LogOut, Info, XCircle } from 'lucide-react';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
  import { createPortal } from 'react-dom';
5
5
  import { Nostalgist } from 'nostalgist';
@@ -821,7 +821,7 @@ var SaveLoadControls = memo(function SaveLoadControls2({
821
821
  {
822
822
  progress: autoSaveProgress,
823
823
  state: autoSavePaused ? "idle" : autoSaveState,
824
- intervalSeconds: 20,
824
+ intervalSeconds: 60,
825
825
  isPaused: autoSavePaused,
826
826
  onClick: onAutoSaveToggle
827
827
  }
@@ -1441,6 +1441,40 @@ var PlayerControls = memo(function PlayerControls2({
1441
1441
  ] });
1442
1442
  });
1443
1443
  var PlayerControls_default = PlayerControls;
1444
+ function useAnimatedVisibility({
1445
+ exitDuration = 200,
1446
+ onExit,
1447
+ autoDismissMs
1448
+ } = {}) {
1449
+ const [isVisible, setIsVisible] = useState(false);
1450
+ const [isExiting, setIsExiting] = useState(false);
1451
+ useEffect(() => {
1452
+ requestAnimationFrame(() => {
1453
+ setIsVisible(true);
1454
+ });
1455
+ }, []);
1456
+ useEffect(() => {
1457
+ if (!autoDismissMs) return;
1458
+ const timer = setTimeout(() => {
1459
+ triggerExit();
1460
+ }, autoDismissMs);
1461
+ return () => clearTimeout(timer);
1462
+ }, [autoDismissMs]);
1463
+ const triggerExit = useCallback(() => {
1464
+ if (isExiting) return;
1465
+ setIsExiting(true);
1466
+ setTimeout(() => {
1467
+ onExit?.();
1468
+ }, exitDuration);
1469
+ }, [isExiting, exitDuration, onExit]);
1470
+ const slideInRightClasses = isVisible && !isExiting ? "translate-x-0 opacity-100" : "translate-x-full opacity-0";
1471
+ return {
1472
+ isVisible,
1473
+ isExiting,
1474
+ triggerExit,
1475
+ slideInRightClasses
1476
+ };
1477
+ }
1444
1478
  var TOAST_CONFIGS = {
1445
1479
  success: {
1446
1480
  icon: CheckCircle,
@@ -1485,32 +1519,20 @@ var TOAST_CONFIGS = {
1485
1519
  }
1486
1520
  };
1487
1521
  function ToastItem({ toast, onDismiss }) {
1488
- const [isVisible, setIsVisible] = useState(false);
1489
- const [isExiting, setIsExiting] = useState(false);
1522
+ const { slideInRightClasses, triggerExit } = useAnimatedVisibility({
1523
+ exitDuration: 200,
1524
+ onExit: () => onDismiss?.(toast.id)
1525
+ });
1490
1526
  const config = TOAST_CONFIGS[toast.type];
1491
1527
  const IconComponent = config.icon;
1492
- useEffect(() => {
1493
- requestAnimationFrame(() => {
1494
- setIsVisible(true);
1495
- });
1496
- }, []);
1497
- const handleDismiss = () => {
1498
- setIsExiting(true);
1499
- setTimeout(() => {
1500
- onDismiss?.(toast.id);
1501
- }, 200);
1502
- };
1503
1528
  return /* @__PURE__ */ jsx(
1504
1529
  "div",
1505
1530
  {
1506
- className: `
1507
- relative transition-all duration-300 ease-out
1508
- ${isVisible && !isExiting ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"}
1509
- `,
1531
+ className: `relative transition-all duration-300 ease-out ${slideInRightClasses}`,
1510
1532
  children: /* @__PURE__ */ jsxs(
1511
1533
  "div",
1512
1534
  {
1513
- className: "relative w-[320px]",
1535
+ className: "relative w-[320px] pointer-events-auto",
1514
1536
  style: {
1515
1537
  backgroundColor: config.bgColor,
1516
1538
  border: `2px solid ${config.borderColor}`,
@@ -1545,7 +1567,7 @@ function ToastItem({ toast, onDismiss }) {
1545
1567
  {
1546
1568
  onClick: () => {
1547
1569
  toast.action?.onClick();
1548
- handleDismiss();
1570
+ triggerExit();
1549
1571
  },
1550
1572
  className: "flex-shrink-0 text-[9px] font-black uppercase tracking-wider px-2 py-1 transition-all hover:-translate-y-0.5 active:translate-y-0",
1551
1573
  style: {
@@ -1559,7 +1581,7 @@ function ToastItem({ toast, onDismiss }) {
1559
1581
  /* @__PURE__ */ jsx(
1560
1582
  "button",
1561
1583
  {
1562
- onClick: handleDismiss,
1584
+ onClick: triggerExit,
1563
1585
  className: "flex-shrink-0 p-0.5 text-gray-500 hover:text-white transition-colors",
1564
1586
  children: /* @__PURE__ */ jsx(X, { size: 14 })
1565
1587
  }
@@ -1932,6 +1954,67 @@ var RecordingIndicator = memo(function RecordingIndicator2({
1932
1954
  );
1933
1955
  });
1934
1956
  var RecordingIndicator_default = RecordingIndicator;
1957
+ var MAX_WIDTH_CLASSES = {
1958
+ sm: "max-w-sm",
1959
+ md: "max-w-md",
1960
+ lg: "max-w-lg"
1961
+ };
1962
+ function ModalShell({
1963
+ isOpen,
1964
+ onClose,
1965
+ title,
1966
+ subtitle,
1967
+ icon,
1968
+ children,
1969
+ footer,
1970
+ maxWidth = "lg",
1971
+ systemColor,
1972
+ closeOnBackdrop = true
1973
+ }) {
1974
+ if (!isOpen) return null;
1975
+ const handleBackdropClick = () => {
1976
+ if (closeOnBackdrop) {
1977
+ onClose();
1978
+ }
1979
+ };
1980
+ return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
1981
+ /* @__PURE__ */ jsx(
1982
+ "div",
1983
+ {
1984
+ className: "absolute inset-0 bg-black/80 backdrop-blur-sm",
1985
+ onClick: handleBackdropClick
1986
+ }
1987
+ ),
1988
+ /* @__PURE__ */ jsxs(
1989
+ "div",
1990
+ {
1991
+ className: `relative bg-gray-900 border border-retro-primary/30 rounded-xl shadow-2xl w-full ${MAX_WIDTH_CLASSES[maxWidth]} mx-4 overflow-hidden`,
1992
+ style: systemColor ? { borderColor: `${systemColor}30` } : void 0,
1993
+ children: [
1994
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/10 bg-black/50", children: [
1995
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1996
+ /* @__PURE__ */ jsx("div", { style: systemColor ? { color: systemColor } : void 0, children: icon }),
1997
+ /* @__PURE__ */ jsxs("div", { children: [
1998
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-bold text-white", children: title }),
1999
+ subtitle && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: subtitle })
2000
+ ] })
2001
+ ] }),
2002
+ /* @__PURE__ */ jsx(
2003
+ "button",
2004
+ {
2005
+ onClick: onClose,
2006
+ className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
2007
+ children: /* @__PURE__ */ jsx(X, { size: 20 })
2008
+ }
2009
+ )
2010
+ ] }),
2011
+ children,
2012
+ footer && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-6 py-4 bg-black/30 border-t border-white/10", children: footer })
2013
+ ]
2014
+ }
2015
+ )
2016
+ ] });
2017
+ }
1935
2018
  var ShortcutsModal = memo(function ShortcutsModal2({
1936
2019
  isOpen,
1937
2020
  onClose,
@@ -1960,90 +2043,55 @@ var ShortcutsModal = memo(function ShortcutsModal2({
1960
2043
  ]
1961
2044
  }
1962
2045
  ], [t]);
1963
- if (!isOpen) return null;
1964
2046
  return /* @__PURE__ */ jsx(
1965
- "div",
2047
+ ModalShell,
1966
2048
  {
1967
- className: "absolute inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm",
1968
- onClick: onClose,
1969
- children: /* @__PURE__ */ jsxs(
1970
- "div",
1971
- {
1972
- className: "max-w-sm w-full mx-4 bg-black/95 border rounded-lg overflow-hidden",
1973
- style: { borderColor: `${systemColor}40` },
1974
- onClick: (e) => e.stopPropagation(),
1975
- children: [
1976
- /* @__PURE__ */ jsxs(
1977
- "div",
1978
- {
1979
- className: "flex items-center justify-between px-4 py-3 border-b",
1980
- style: { borderColor: `${systemColor}20`, backgroundColor: `${systemColor}10` },
1981
- children: [
1982
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1983
- /* @__PURE__ */ jsx(Keyboard, { size: 18, style: { color: systemColor } }),
1984
- /* @__PURE__ */ jsx("span", { className: "font-bold text-white", children: t.modals.shortcuts.playerShortcuts })
1985
- ] }),
1986
- /* @__PURE__ */ jsx(
1987
- "button",
1988
- {
1989
- onClick: onClose,
1990
- className: "p-1 rounded hover:bg-white/10 transition-colors",
1991
- children: /* @__PURE__ */ jsx(X, { size: 18, className: "text-white/60 hover:text-white" })
1992
- }
1993
- )
1994
- ]
1995
- }
1996
- ),
1997
- /* @__PURE__ */ jsxs("div", { className: "p-4 space-y-3", children: [
1998
- shortcuts.map(({ section, items }) => /* @__PURE__ */ jsxs("div", { children: [
2049
+ isOpen,
2050
+ onClose,
2051
+ title: t.modals.shortcuts.playerShortcuts,
2052
+ icon: /* @__PURE__ */ jsx(Keyboard, { size: 20, style: { color: systemColor } }),
2053
+ maxWidth: "sm",
2054
+ systemColor,
2055
+ footer: /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500 w-full text-center", children: t.modals.shortcuts.pressEsc }),
2056
+ children: /* @__PURE__ */ jsxs("div", { className: "p-4 space-y-3", children: [
2057
+ shortcuts.map(({ section, items }) => /* @__PURE__ */ jsxs("div", { children: [
2058
+ /* @__PURE__ */ jsx(
2059
+ "h3",
2060
+ {
2061
+ className: "text-[10px] font-bold uppercase tracking-wider mb-1.5 opacity-60",
2062
+ style: { color: systemColor },
2063
+ children: section
2064
+ }
2065
+ ),
2066
+ /* @__PURE__ */ jsx("div", { className: "space-y-1", children: items.map(({ key, description }) => /* @__PURE__ */ jsxs(
2067
+ "div",
2068
+ {
2069
+ className: "flex items-center justify-between text-sm",
2070
+ children: [
2071
+ /* @__PURE__ */ jsx("span", { className: "text-white/70", children: description }),
1999
2072
  /* @__PURE__ */ jsx(
2000
- "h3",
2073
+ "kbd",
2001
2074
  {
2002
- className: "text-[10px] font-bold uppercase tracking-wider mb-1.5 opacity-60",
2003
- style: { color: systemColor },
2004
- children: section
2075
+ className: "px-2 py-0.5 rounded text-xs font-mono font-bold",
2076
+ style: {
2077
+ backgroundColor: `${systemColor}20`,
2078
+ color: systemColor,
2079
+ border: `1px solid ${systemColor}40`
2080
+ },
2081
+ children: key
2005
2082
  }
2006
- ),
2007
- /* @__PURE__ */ jsx("div", { className: "space-y-1", children: items.map(({ key, description }) => /* @__PURE__ */ jsxs(
2008
- "div",
2009
- {
2010
- className: "flex items-center justify-between text-sm",
2011
- children: [
2012
- /* @__PURE__ */ jsx("span", { className: "text-white/70", children: description }),
2013
- /* @__PURE__ */ jsx(
2014
- "kbd",
2015
- {
2016
- className: "px-2 py-0.5 rounded text-xs font-mono font-bold",
2017
- style: {
2018
- backgroundColor: `${systemColor}20`,
2019
- color: systemColor,
2020
- border: `1px solid ${systemColor}40`
2021
- },
2022
- children: key
2023
- }
2024
- )
2025
- ]
2026
- },
2027
- key
2028
- )) })
2029
- ] }, section)),
2030
- /* @__PURE__ */ jsxs("div", { className: "pt-2 border-t border-white/10 text-xs text-white/40", children: [
2031
- "Game controls can be configured in ",
2032
- /* @__PURE__ */ jsx("strong", { className: "text-white/60", children: t.controls.keys }),
2033
- " settings."
2034
- ] })
2035
- ] }),
2036
- /* @__PURE__ */ jsx(
2037
- "div",
2038
- {
2039
- className: "px-4 py-2 text-center text-xs text-white/40 border-t",
2040
- style: { borderColor: `${systemColor}20` },
2041
- children: t.modals.shortcuts.pressEsc
2042
- }
2043
- )
2044
- ]
2045
- }
2046
- )
2083
+ )
2084
+ ]
2085
+ },
2086
+ key
2087
+ )) })
2088
+ ] }, section)),
2089
+ /* @__PURE__ */ jsxs("div", { className: "pt-2 border-t border-white/10 text-xs text-white/40", children: [
2090
+ "Game controls can be configured in ",
2091
+ /* @__PURE__ */ jsx("strong", { className: "text-white/60", children: t.controls.keys }),
2092
+ " settings."
2093
+ ] })
2094
+ ] })
2047
2095
  }
2048
2096
  );
2049
2097
  });
@@ -2640,6 +2688,39 @@ function useTouchHandlers({
2640
2688
  cleanup
2641
2689
  };
2642
2690
  }
2691
+ function useTouchEvents(ref, handlers, options = {}) {
2692
+ const { cleanup, passive = false } = options;
2693
+ const handlersRef = useRef(handlers);
2694
+ handlersRef.current = handlers;
2695
+ const handleTouchStart = useCallback((e) => {
2696
+ handlersRef.current.onTouchStart?.(e);
2697
+ }, []);
2698
+ const handleTouchMove = useCallback((e) => {
2699
+ handlersRef.current.onTouchMove?.(e);
2700
+ }, []);
2701
+ const handleTouchEnd = useCallback((e) => {
2702
+ handlersRef.current.onTouchEnd?.(e);
2703
+ }, []);
2704
+ const handleTouchCancel = useCallback((e) => {
2705
+ handlersRef.current.onTouchCancel?.(e);
2706
+ }, []);
2707
+ useEffect(() => {
2708
+ const element = ref.current;
2709
+ if (!element) return;
2710
+ const listenerOptions = { passive };
2711
+ element.addEventListener("touchstart", handleTouchStart, listenerOptions);
2712
+ element.addEventListener("touchmove", handleTouchMove, listenerOptions);
2713
+ element.addEventListener("touchend", handleTouchEnd, listenerOptions);
2714
+ element.addEventListener("touchcancel", handleTouchCancel, listenerOptions);
2715
+ return () => {
2716
+ element.removeEventListener("touchstart", handleTouchStart);
2717
+ element.removeEventListener("touchmove", handleTouchMove);
2718
+ element.removeEventListener("touchend", handleTouchEnd);
2719
+ element.removeEventListener("touchcancel", handleTouchCancel);
2720
+ cleanup?.();
2721
+ };
2722
+ }, [ref, passive, cleanup, handleTouchStart, handleTouchMove, handleTouchEnd, handleTouchCancel]);
2723
+ }
2643
2724
 
2644
2725
  // src/components/VirtualController/utils/buttonStyles.ts
2645
2726
  var DEFAULT_FACE = {
@@ -2797,21 +2878,12 @@ var VirtualButton = React2.memo(function VirtualButton2({
2797
2878
  onRelease,
2798
2879
  onPositionChange
2799
2880
  });
2800
- useEffect(() => {
2801
- const button = buttonRef.current;
2802
- if (!button) return;
2803
- button.addEventListener("touchstart", handleTouchStart, { passive: false });
2804
- button.addEventListener("touchmove", handleTouchMove, { passive: false });
2805
- button.addEventListener("touchend", handleTouchEnd, { passive: false });
2806
- button.addEventListener("touchcancel", handleTouchCancel, { passive: false });
2807
- return () => {
2808
- button.removeEventListener("touchstart", handleTouchStart);
2809
- button.removeEventListener("touchmove", handleTouchMove);
2810
- button.removeEventListener("touchend", handleTouchEnd);
2811
- button.removeEventListener("touchcancel", handleTouchCancel);
2812
- cleanup();
2813
- };
2814
- }, [handleTouchStart, handleTouchMove, handleTouchEnd, handleTouchCancel, cleanup]);
2881
+ useTouchEvents(buttonRef, {
2882
+ onTouchStart: handleTouchStart,
2883
+ onTouchMove: handleTouchMove,
2884
+ onTouchEnd: handleTouchEnd,
2885
+ onTouchCancel: handleTouchCancel
2886
+ }, { cleanup });
2815
2887
  const leftPercent = displayX / 100 * containerWidth - config.size / 2;
2816
2888
  const topPercent = displayY / 100 * containerHeight - config.size / 2;
2817
2889
  const transform = `translate3d(${leftPercent.toFixed(1)}px, ${topPercent.toFixed(1)}px, 0)`;
@@ -3487,7 +3559,7 @@ function dispatchKeyboardEvent(type, code) {
3487
3559
  canvas.dispatchEvent(event);
3488
3560
  return true;
3489
3561
  }
3490
- var CENTER_TOUCH_RADIUS = 0.25;
3562
+ var CENTER_TOUCH_RADIUS = 0.35;
3491
3563
  var Dpad = React2.memo(function Dpad2({
3492
3564
  size = 180,
3493
3565
  x,
@@ -3629,14 +3701,22 @@ var Dpad = React2.memo(function Dpad2({
3629
3701
  if (!touch) return;
3630
3702
  if (drag.isDragging) {
3631
3703
  drag.handleDragMove(touch.clientX, touch.clientY);
3704
+ } else if (onPositionChange) {
3705
+ const startedDrag = drag.checkMoveThreshold(touch.clientX, touch.clientY);
3706
+ if (!startedDrag) {
3707
+ drag.clearDragTimer();
3708
+ const rect = dpadRef.current?.getBoundingClientRect();
3709
+ if (rect) {
3710
+ updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3711
+ }
3712
+ }
3632
3713
  } else {
3633
3714
  const rect = dpadRef.current?.getBoundingClientRect();
3634
3715
  if (rect) {
3635
- drag.clearDragTimer();
3636
3716
  updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3637
3717
  }
3638
3718
  }
3639
- }, [drag, getDirectionsFromTouch, updateDirections]);
3719
+ }, [drag, getDirectionsFromTouch, updateDirections, onPositionChange]);
3640
3720
  const handleTouchEnd = useCallback((e) => {
3641
3721
  e.preventDefault();
3642
3722
  drag.clearDragTimer();
@@ -3661,21 +3741,12 @@ var Dpad = React2.memo(function Dpad2({
3661
3741
  }
3662
3742
  }
3663
3743
  }, [getKeyCode, updateVisuals, drag]);
3664
- useEffect(() => {
3665
- const dpad = dpadRef.current;
3666
- if (!dpad) return;
3667
- dpad.addEventListener("touchstart", handleTouchStart, { passive: false });
3668
- dpad.addEventListener("touchmove", handleTouchMove, { passive: false });
3669
- dpad.addEventListener("touchend", handleTouchEnd, { passive: false });
3670
- dpad.addEventListener("touchcancel", handleTouchEnd, { passive: false });
3671
- return () => {
3672
- dpad.removeEventListener("touchstart", handleTouchStart);
3673
- dpad.removeEventListener("touchmove", handleTouchMove);
3674
- dpad.removeEventListener("touchend", handleTouchEnd);
3675
- dpad.removeEventListener("touchcancel", handleTouchEnd);
3676
- drag.clearDragTimer();
3677
- };
3678
- }, [handleTouchStart, handleTouchMove, handleTouchEnd, drag]);
3744
+ useTouchEvents(dpadRef, {
3745
+ onTouchStart: handleTouchStart,
3746
+ onTouchMove: handleTouchMove,
3747
+ onTouchEnd: handleTouchEnd,
3748
+ onTouchCancel: handleTouchEnd
3749
+ }, { cleanup: drag.clearDragTimer });
3679
3750
  const leftPx = displayX / 100 * containerWidth - size / 2;
3680
3751
  const topPx = displayY / 100 * containerHeight - size / 2;
3681
3752
  const dUp = "M 35,5 L 65,5 L 65,35 L 50,50 L 35,35 Z";
@@ -3846,8 +3917,16 @@ function ControlsHint({ isVisible }) {
3846
3917
  children: [
3847
3918
  /* @__PURE__ */ jsx("div", { className: "flex justify-center mb-4", children: /* @__PURE__ */ jsx("div", { className: "w-16 h-16 rounded-full bg-green-500/20 border-2 border-green-400 flex items-center justify-center", children: /* @__PURE__ */ jsx(Move, { size: 32, className: "text-green-400" }) }) }),
3848
3919
  /* @__PURE__ */ jsx("h3", { className: "text-white text-lg font-bold mb-2", children: "Customize Your Controls" }),
3849
- /* @__PURE__ */ jsxs("p", { className: "text-white/70 text-sm mb-4", children: [
3850
- /* @__PURE__ */ jsx("strong", { className: "text-white", children: "Long-press" }),
3920
+ /* @__PURE__ */ jsxs("p", { className: "text-white/70 text-sm mb-3", children: [
3921
+ "Use the ",
3922
+ /* @__PURE__ */ jsx(Lock, { size: 12, className: "inline mx-1 text-white" }),
3923
+ " ",
3924
+ /* @__PURE__ */ jsx("strong", { className: "text-white", children: "lock icon" }),
3925
+ " at the top to unlock controls for repositioning."
3926
+ ] }),
3927
+ /* @__PURE__ */ jsxs("p", { className: "text-white/70 text-sm mb-3", children: [
3928
+ "When unlocked, ",
3929
+ /* @__PURE__ */ jsx("strong", { className: "text-white", children: "long-press" }),
3851
3930
  " any button or the ",
3852
3931
  /* @__PURE__ */ jsx("strong", { className: "text-white", children: "D-pad center" }),
3853
3932
  " to drag and reposition it."
@@ -3875,6 +3954,33 @@ function ControlsHint({ isVisible }) {
3875
3954
  }
3876
3955
  );
3877
3956
  }
3957
+ function LockButton({
3958
+ isLocked,
3959
+ onToggle,
3960
+ systemColor = "#00FF41"
3961
+ }) {
3962
+ const Icon = isLocked ? Lock : Unlock;
3963
+ return /* @__PURE__ */ jsx(
3964
+ "button",
3965
+ {
3966
+ onClick: onToggle,
3967
+ className: "fixed top-4 left-1/2 -translate-x-1/2 z-40 pointer-events-auto p-2 rounded-full backdrop-blur-sm transition-all active:scale-95",
3968
+ style: {
3969
+ backgroundColor: isLocked ? "rgba(0,0,0,0.6)" : `${systemColor}20`,
3970
+ border: `1px solid ${isLocked ? "rgba(255,255,255,0.2)" : systemColor}`
3971
+ },
3972
+ "aria-label": isLocked ? "Unlock controls for repositioning" : "Lock controls",
3973
+ children: /* @__PURE__ */ jsx(
3974
+ Icon,
3975
+ {
3976
+ size: 18,
3977
+ style: { color: isLocked ? "rgba(255,255,255,0.6)" : systemColor }
3978
+ }
3979
+ )
3980
+ }
3981
+ );
3982
+ }
3983
+ var LOCK_KEY = "koin-controls-locked";
3878
3984
  function VirtualController({
3879
3985
  system,
3880
3986
  isRunning,
@@ -3886,7 +3992,21 @@ function VirtualController({
3886
3992
  const [pressedButtons, setPressedButtons] = useState(/* @__PURE__ */ new Set());
3887
3993
  const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
3888
3994
  const [isFullscreenState, setIsFullscreenState] = useState(false);
3995
+ const [isLocked, setIsLocked] = useState(true);
3889
3996
  const { getPosition, savePosition } = useButtonPositions();
3997
+ useEffect(() => {
3998
+ const stored = localStorage.getItem(LOCK_KEY);
3999
+ if (stored !== null) {
4000
+ setIsLocked(stored === "true");
4001
+ }
4002
+ }, []);
4003
+ const toggleLock = useCallback(() => {
4004
+ setIsLocked((prev) => {
4005
+ const newValue = !prev;
4006
+ localStorage.setItem(LOCK_KEY, String(newValue));
4007
+ return newValue;
4008
+ });
4009
+ }, []);
3890
4010
  const layout = getLayoutForSystem(system);
3891
4011
  const visibleButtons = layout.buttons.filter((btn) => {
3892
4012
  if (isPortrait) {
@@ -4041,6 +4161,14 @@ function VirtualController({
4041
4161
  className: "fixed inset-0 z-30 pointer-events-none",
4042
4162
  style: { touchAction: "none" },
4043
4163
  children: [
4164
+ /* @__PURE__ */ jsx(
4165
+ LockButton,
4166
+ {
4167
+ isLocked,
4168
+ onToggle: toggleLock,
4169
+ systemColor
4170
+ }
4171
+ ),
4044
4172
  /* @__PURE__ */ jsx(
4045
4173
  Dpad_default,
4046
4174
  {
@@ -4053,7 +4181,7 @@ function VirtualController({
4053
4181
  systemColor,
4054
4182
  isLandscape,
4055
4183
  customPosition: getPosition("up", isLandscape),
4056
- onPositionChange: (x, y) => savePosition("up", x, y, isLandscape)
4184
+ onPositionChange: isLocked ? void 0 : (x, y) => savePosition("up", x, y, isLandscape)
4057
4185
  }
4058
4186
  ),
4059
4187
  memoizedButtonElements.filter(({ buttonConfig }) => !DPAD_TYPES.includes(buttonConfig.type)).map(({ buttonConfig, adjustedConfig, customPosition, width, height }) => /* @__PURE__ */ jsx(
@@ -4067,7 +4195,7 @@ function VirtualController({
4067
4195
  containerWidth: width,
4068
4196
  containerHeight: height,
4069
4197
  customPosition,
4070
- onPositionChange: (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
4198
+ onPositionChange: isLocked ? void 0 : (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
4071
4199
  isLandscape,
4072
4200
  console: layout.console
4073
4201
  },
@@ -4387,6 +4515,45 @@ var GameCanvas = memo(function GameCanvas2({
4387
4515
  ] });
4388
4516
  });
4389
4517
  var GameCanvas_default = GameCanvas;
4518
+ function useInputCapture({
4519
+ isOpen,
4520
+ onClose
4521
+ }) {
4522
+ const [listeningFor, setListeningFor] = useState(null);
4523
+ const startListening = useCallback((target) => {
4524
+ setListeningFor(target);
4525
+ }, []);
4526
+ const stopListening = useCallback(() => {
4527
+ setListeningFor(null);
4528
+ }, []);
4529
+ useEffect(() => {
4530
+ if (!isOpen) {
4531
+ setListeningFor(null);
4532
+ }
4533
+ }, [isOpen]);
4534
+ useEffect(() => {
4535
+ if (!isOpen) return;
4536
+ const handleKeyDown = (e) => {
4537
+ if (e.code === "Escape") {
4538
+ if (listeningFor !== null) {
4539
+ e.preventDefault();
4540
+ e.stopPropagation();
4541
+ setListeningFor(null);
4542
+ } else {
4543
+ onClose();
4544
+ }
4545
+ }
4546
+ };
4547
+ window.addEventListener("keydown", handleKeyDown);
4548
+ return () => window.removeEventListener("keydown", handleKeyDown);
4549
+ }, [isOpen, listeningFor, onClose]);
4550
+ return {
4551
+ listeningFor,
4552
+ startListening,
4553
+ stopListening,
4554
+ isListening: listeningFor !== null
4555
+ };
4556
+ }
4390
4557
  function getFilteredGroups(activeButtons) {
4391
4558
  return BUTTON_GROUPS.map((group) => ({
4392
4559
  ...group,
@@ -4402,7 +4569,10 @@ function ControlMapper({
4402
4569
  }) {
4403
4570
  const t = useKoinTranslation();
4404
4571
  const [localControls, setLocalControls] = useState(controls);
4405
- const [listeningFor, setListeningFor] = useState(null);
4572
+ const { listeningFor, startListening, stopListening, isListening } = useInputCapture({
4573
+ isOpen,
4574
+ onClose
4575
+ });
4406
4576
  const activeButtons = useMemo(() => {
4407
4577
  return getConsoleButtons(system || "SNES");
4408
4578
  }, [system]);
@@ -4418,27 +4588,20 @@ function ControlMapper({
4418
4588
  }
4419
4589
  }, [isOpen, controls]);
4420
4590
  useEffect(() => {
4421
- if (!isOpen) {
4422
- setListeningFor(null);
4423
- return;
4424
- }
4591
+ if (!isOpen || !listeningFor) return;
4425
4592
  const handleKeyDown = (e) => {
4426
- if (!listeningFor) return;
4593
+ if (e.code === "Escape") return;
4427
4594
  e.preventDefault();
4428
4595
  e.stopPropagation();
4429
- if (e.code === "Escape") {
4430
- setListeningFor(null);
4431
- return;
4432
- }
4433
4596
  setLocalControls((prev) => ({
4434
4597
  ...prev,
4435
4598
  [listeningFor]: e.code
4436
4599
  }));
4437
- setListeningFor(null);
4600
+ stopListening();
4438
4601
  };
4439
4602
  window.addEventListener("keydown", handleKeyDown);
4440
4603
  return () => window.removeEventListener("keydown", handleKeyDown);
4441
- }, [isOpen, listeningFor]);
4604
+ }, [isOpen, listeningFor, stopListening]);
4442
4605
  const handleReset = () => {
4443
4606
  setLocalControls(defaultControls);
4444
4607
  };
@@ -4446,52 +4609,58 @@ function ControlMapper({
4446
4609
  onSave(localControls);
4447
4610
  onClose();
4448
4611
  };
4449
- if (!isOpen) return null;
4450
- return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
4451
- /* @__PURE__ */ jsx(
4452
- "div",
4453
- {
4454
- className: "absolute inset-0 bg-black/80 backdrop-blur-sm",
4455
- onClick: onClose
4456
- }
4457
- ),
4458
- /* @__PURE__ */ jsxs("div", { className: "relative bg-gray-900 border border-retro-primary/30 rounded-xl shadow-2xl w-full max-w-lg mx-4 overflow-hidden", children: [
4459
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/10 bg-black/50", children: [
4460
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
4461
- /* @__PURE__ */ jsx(Gamepad2, { className: "text-retro-primary", size: 24 }),
4462
- /* @__PURE__ */ jsxs("div", { children: [
4463
- /* @__PURE__ */ jsx("h2", { className: "text-lg font-bold text-white", children: t.modals.controls.title }),
4464
- /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: t.modals.controls.description })
4465
- ] })
4466
- ] }),
4467
- /* @__PURE__ */ jsx(
4612
+ return /* @__PURE__ */ jsx(
4613
+ ModalShell,
4614
+ {
4615
+ isOpen,
4616
+ onClose,
4617
+ title: t.modals.controls.title,
4618
+ subtitle: t.modals.controls.description,
4619
+ icon: /* @__PURE__ */ jsx(Gamepad2, { className: "text-retro-primary", size: 24 }),
4620
+ closeOnBackdrop: !isListening,
4621
+ footer: /* @__PURE__ */ jsxs(Fragment, { children: [
4622
+ /* @__PURE__ */ jsxs(
4623
+ "button",
4624
+ {
4625
+ onClick: handleReset,
4626
+ className: "flex items-center gap-2 px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-white hover:bg-white/10 transition-colors",
4627
+ children: [
4628
+ /* @__PURE__ */ jsx(RotateCcw, { size: 16 }),
4629
+ t.modals.controls.reset
4630
+ ]
4631
+ }
4632
+ ),
4633
+ /* @__PURE__ */ jsxs(
4468
4634
  "button",
4469
4635
  {
4470
- onClick: onClose,
4471
- className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
4472
- children: /* @__PURE__ */ jsx(X, { size: 20 })
4636
+ onClick: handleSave,
4637
+ className: "flex items-center gap-2 px-6 py-2 rounded-lg bg-retro-primary text-black font-bold text-sm hover:bg-retro-primary/90 transition-colors",
4638
+ children: [
4639
+ /* @__PURE__ */ jsx(Check, { size: 16 }),
4640
+ t.modals.controls.save
4641
+ ]
4473
4642
  }
4474
4643
  )
4475
4644
  ] }),
4476
- /* @__PURE__ */ jsx("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: controlGroups.map((group) => /* @__PURE__ */ jsxs("div", { children: [
4645
+ children: /* @__PURE__ */ jsx("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: controlGroups.map((group) => /* @__PURE__ */ jsxs("div", { children: [
4477
4646
  /* @__PURE__ */ jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
4478
4647
  /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxs(
4479
4648
  "button",
4480
4649
  {
4481
- onClick: () => setListeningFor(btn),
4650
+ onClick: () => startListening(btn),
4482
4651
  className: `
4483
- flex items-center justify-between px-4 py-3 rounded-lg border transition-all
4484
- ${listeningFor === btn ? "border-retro-primary bg-retro-primary/20 ring-2 ring-retro-primary/50" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
4485
- `,
4652
+ flex items-center justify-between px-4 py-3 rounded-lg border transition-all
4653
+ ${listeningFor === btn ? "border-retro-primary bg-retro-primary/20 ring-2 ring-retro-primary/50" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
4654
+ `,
4486
4655
  children: [
4487
4656
  /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
4488
4657
  /* @__PURE__ */ jsx(
4489
4658
  "span",
4490
4659
  {
4491
4660
  className: `
4492
- px-2 py-1 rounded text-xs font-mono
4493
- ${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
4494
- `,
4661
+ px-2 py-1 rounded text-xs font-mono
4662
+ ${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
4663
+ `,
4495
4664
  children: listeningFor === btn ? t.modals.controls.pressKey : formatKeyCode(localControls[btn] || "")
4496
4665
  }
4497
4666
  )
@@ -4499,199 +4668,10 @@ function ControlMapper({
4499
4668
  },
4500
4669
  btn
4501
4670
  )) })
4502
- ] }, group.label)) }),
4503
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4 bg-black/30 border-t border-white/10", children: [
4504
- /* @__PURE__ */ jsxs(
4505
- "button",
4506
- {
4507
- onClick: handleReset,
4508
- className: "flex items-center gap-2 px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-white hover:bg-white/10 transition-colors",
4509
- children: [
4510
- /* @__PURE__ */ jsx(RotateCcw, { size: 16 }),
4511
- t.modals.controls.reset
4512
- ]
4513
- }
4514
- ),
4515
- /* @__PURE__ */ jsxs(
4516
- "button",
4517
- {
4518
- onClick: handleSave,
4519
- className: "flex items-center gap-2 px-6 py-2 rounded-lg bg-retro-primary text-black font-bold text-sm hover:bg-retro-primary/90 transition-colors",
4520
- children: [
4521
- /* @__PURE__ */ jsx(Check, { size: 16 }),
4522
- t.modals.controls.save
4523
- ]
4524
- }
4525
- )
4526
- ] })
4527
- ] })
4528
- ] });
4529
- }
4530
- function getDisplayName(id) {
4531
- let name = id;
4532
- name = name.replace(/^[0-9a-f]{4}-[0-9a-f]{4}-/i, "");
4533
- name = name.replace(/\s*\(.*\)\s*$/i, "");
4534
- name = name.replace(/\s*STANDARD GAMEPAD\s*/i, "");
4535
- if (/xbox/i.test(name)) {
4536
- if (/series/i.test(name)) return "Xbox Series Controller";
4537
- if (/one/i.test(name)) return "Xbox One Controller";
4538
- if (/360/i.test(name)) return "Xbox 360 Controller";
4539
- return "Xbox Controller";
4540
- }
4541
- if (/dualsense/i.test(name)) return "DualSense";
4542
- if (/dualshock\s*4/i.test(name)) return "DualShock 4";
4543
- if (/dualshock/i.test(name)) return "DualShock";
4544
- if (/playstation/i.test(name) || /sony/i.test(name)) return "PlayStation Controller";
4545
- if (/pro\s*controller/i.test(name)) return "Switch Pro Controller";
4546
- if (/joy-?con/i.test(name)) return "Joy-Con";
4547
- if (/nintendo/i.test(name)) return "Nintendo Controller";
4548
- return name.trim() || "Gamepad";
4549
- }
4550
- function detectControllerBrand(id) {
4551
- const lowerId = id.toLowerCase();
4552
- if (/xbox|xinput|microsoft/i.test(lowerId)) return "xbox";
4553
- if (/playstation|sony|dualshock|dualsense/i.test(lowerId)) return "playstation";
4554
- if (/nintendo|switch|joy-?con|pro controller/i.test(lowerId)) return "nintendo";
4555
- return "generic";
4556
- }
4557
- function toGamepadInfo(gamepad) {
4558
- return {
4559
- index: gamepad.index,
4560
- id: gamepad.id,
4561
- name: getDisplayName(gamepad.id),
4562
- connected: gamepad.connected,
4563
- buttons: gamepad.buttons.length,
4564
- axes: gamepad.axes.length,
4565
- mapping: gamepad.mapping
4566
- };
4567
- }
4568
- function useGamepad(options) {
4569
- const { onConnect, onDisconnect } = options || {};
4570
- const [gamepads, setGamepads] = useState([]);
4571
- const rafRef = useRef(null);
4572
- const lastStateRef = useRef("");
4573
- const prevCountRef = useRef(0);
4574
- const onConnectRef = useRef(onConnect);
4575
- const onDisconnectRef = useRef(onDisconnect);
4576
- useEffect(() => {
4577
- onConnectRef.current = onConnect;
4578
- onDisconnectRef.current = onDisconnect;
4579
- }, [onConnect, onDisconnect]);
4580
- const getGamepads = useCallback(() => {
4581
- if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") {
4582
- return [];
4583
- }
4584
- const rawGamepads = navigator.getGamepads() ?? [];
4585
- const connected = [];
4586
- for (let i = 0; i < rawGamepads.length; i++) {
4587
- const gp = rawGamepads[i];
4588
- if (gp && gp.connected) {
4589
- connected.push(toGamepadInfo(gp));
4590
- }
4591
- }
4592
- return connected;
4593
- }, []);
4594
- const getRawGamepad = useCallback((index) => {
4595
- const rawGamepads = navigator.getGamepads?.() ?? [];
4596
- return rawGamepads[index] ?? null;
4597
- }, []);
4598
- const refresh = useCallback(() => {
4599
- setGamepads(getGamepads());
4600
- }, [getGamepads]);
4601
- useEffect(() => {
4602
- if (typeof window === "undefined" || typeof navigator === "undefined") {
4603
- return;
4604
- }
4605
- if (typeof navigator.getGamepads !== "function") {
4606
- console.warn("[useGamepad] Gamepad API not supported in this browser");
4607
- return;
4608
- }
4609
- let isActive = true;
4610
- const poll = () => {
4611
- if (!isActive) return;
4612
- const current = getGamepads();
4613
- let hasChanged = current.length !== prevCountRef.current;
4614
- if (!hasChanged) {
4615
- for (let i = 0; i < current.length; i++) {
4616
- const saved = gamepads[i];
4617
- if (!saved || saved.id !== current[i].id || saved.connected !== current[i].connected) {
4618
- hasChanged = true;
4619
- break;
4620
- }
4621
- }
4622
- }
4623
- if (hasChanged) {
4624
- const prevCount = prevCountRef.current;
4625
- const currentCount = current.length;
4626
- if (currentCount > prevCount && prevCount >= 0 && onConnectRef.current) {
4627
- const newGamepad = current[current.length - 1];
4628
- onConnectRef.current(newGamepad);
4629
- } else if (currentCount < prevCount && prevCount > 0 && onDisconnectRef.current) {
4630
- onDisconnectRef.current();
4631
- }
4632
- prevCountRef.current = currentCount;
4633
- setGamepads(current);
4634
- }
4635
- rafRef.current = requestAnimationFrame(poll);
4636
- };
4637
- const handleConnect = (e) => {
4638
- console.log("[useGamepad] \u{1F3AE} Gamepad connected:", e.gamepad.id);
4639
- const current = getGamepads();
4640
- const prevCount = prevCountRef.current;
4641
- prevCountRef.current = current.length;
4642
- lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
4643
- setGamepads(current);
4644
- if (onConnectRef.current && current.length > prevCount) {
4645
- const newGamepad = current[current.length - 1];
4646
- onConnectRef.current(newGamepad);
4647
- }
4648
- };
4649
- const handleDisconnect = (e) => {
4650
- console.log("[useGamepad] \u{1F3AE} Gamepad disconnected:", e.gamepad.id);
4651
- const current = getGamepads();
4652
- const prevCount = prevCountRef.current;
4653
- prevCountRef.current = current.length;
4654
- lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
4655
- setGamepads(current);
4656
- if (onDisconnectRef.current && current.length < prevCount) {
4657
- onDisconnectRef.current();
4658
- }
4659
- };
4660
- window.addEventListener("gamepadconnected", handleConnect);
4661
- window.addEventListener("gamepaddisconnected", handleDisconnect);
4662
- rafRef.current = requestAnimationFrame(poll);
4663
- const initial = getGamepads();
4664
- if (initial.length > 0) {
4665
- console.log("[useGamepad] Initial gamepads found:", initial.map((g) => g.name).join(", "));
4666
- prevCountRef.current = initial.length;
4667
- setGamepads(initial);
4668
- lastStateRef.current = initial.map((g) => `${g.index}:${g.id}`).join("|");
4669
- } else {
4670
- prevCountRef.current = 0;
4671
+ ] }, group.label)) })
4671
4672
  }
4672
- return () => {
4673
- isActive = false;
4674
- if (rafRef.current) {
4675
- cancelAnimationFrame(rafRef.current);
4676
- }
4677
- window.removeEventListener("gamepadconnected", handleConnect);
4678
- window.removeEventListener("gamepaddisconnected", handleDisconnect);
4679
- };
4680
- }, [getGamepads]);
4681
- return {
4682
- gamepads,
4683
- isAnyConnected: gamepads.length > 0,
4684
- connectedCount: gamepads.length,
4685
- getRawGamepad,
4686
- refresh
4687
- };
4673
+ );
4688
4674
  }
4689
- var STANDARD_AXIS_MAP = {
4690
- leftStickX: 0,
4691
- leftStickY: 1,
4692
- rightStickX: 2,
4693
- rightStickY: 3
4694
- };
4695
4675
  function GamepadMapper({
4696
4676
  isOpen,
4697
4677
  gamepads,
@@ -4702,7 +4682,10 @@ function GamepadMapper({
4702
4682
  const t = useKoinTranslation();
4703
4683
  const [selectedPlayer, setSelectedPlayer] = useState(1);
4704
4684
  const [bindings, setBindings] = useState({});
4705
- const [listeningFor, setListeningFor] = useState(null);
4685
+ const { listeningFor, startListening, stopListening, isListening } = useInputCapture({
4686
+ isOpen,
4687
+ onClose
4688
+ });
4706
4689
  const rafRef = useRef(null);
4707
4690
  useEffect(() => {
4708
4691
  if (isOpen) {
@@ -4737,7 +4720,7 @@ function GamepadMapper({
4737
4720
  [listeningFor]: i
4738
4721
  }
4739
4722
  }));
4740
- setListeningFor(null);
4723
+ stopListening();
4741
4724
  return;
4742
4725
  }
4743
4726
  }
@@ -4750,21 +4733,7 @@ function GamepadMapper({
4750
4733
  cancelAnimationFrame(rafRef.current);
4751
4734
  }
4752
4735
  };
4753
- }, [isOpen, listeningFor, selectedPlayer]);
4754
- useEffect(() => {
4755
- if (!isOpen) return;
4756
- const handleKeyDown = (e) => {
4757
- if (e.code === "Escape") {
4758
- if (listeningFor) {
4759
- setListeningFor(null);
4760
- } else {
4761
- onClose();
4762
- }
4763
- }
4764
- };
4765
- window.addEventListener("keydown", handleKeyDown);
4766
- return () => window.removeEventListener("keydown", handleKeyDown);
4767
- }, [isOpen, listeningFor, onClose]);
4736
+ }, [isOpen, listeningFor, selectedPlayer, stopListening]);
4768
4737
  const handleReset = () => {
4769
4738
  setBindings((prev) => ({
4770
4739
  ...prev,
@@ -4779,127 +4748,19 @@ function GamepadMapper({
4779
4748
  onSave?.(bindings[selectedPlayer], selectedPlayer);
4780
4749
  onClose();
4781
4750
  };
4782
- if (!isOpen) return null;
4783
4751
  const currentBindings = bindings[selectedPlayer] ?? DEFAULT_GAMEPAD;
4784
4752
  const currentGamepad = gamepads.find((g) => g.index === selectedPlayer - 1);
4785
- currentGamepad ? detectControllerBrand(currentGamepad.id) : "generic";
4786
- return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
4787
- /* @__PURE__ */ jsx(
4788
- "div",
4789
- {
4790
- className: "absolute inset-0 bg-black/80 backdrop-blur-sm",
4791
- onClick: () => !listeningFor && onClose()
4792
- }
4793
- ),
4794
- /* @__PURE__ */ jsxs("div", { className: "relative bg-gray-900 border border-retro-primary/30 rounded-xl shadow-2xl w-full max-w-lg mx-4 overflow-hidden", children: [
4795
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/10 bg-black/50", children: [
4796
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
4797
- /* @__PURE__ */ jsx(Joystick, { className: "text-retro-primary", size: 24, style: { color: systemColor } }),
4798
- /* @__PURE__ */ jsxs("div", { children: [
4799
- /* @__PURE__ */ jsx("h2", { className: "text-lg font-bold text-white", children: t.modals.gamepad.title }),
4800
- /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: gamepads.length > 0 ? t.modals.gamepad.connected.replace("{{count}}", gamepads.length.toString()) : t.modals.gamepad.none })
4801
- ] })
4802
- ] }),
4803
- /* @__PURE__ */ jsx(
4804
- "button",
4805
- {
4806
- onClick: onClose,
4807
- className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
4808
- children: /* @__PURE__ */ jsx(X, { size: 20 })
4809
- }
4810
- )
4811
- ] }),
4812
- gamepads.length > 1 && /* @__PURE__ */ jsxs("div", { className: "px-6 py-3 border-b border-white/10 bg-black/30", children: [
4813
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
4814
- /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 font-medium", children: t.modals.gamepad.player }),
4815
- /* @__PURE__ */ jsx("div", { className: "flex gap-1", children: gamepads.map((gp) => /* @__PURE__ */ jsxs(
4816
- "button",
4817
- {
4818
- onClick: () => setSelectedPlayer(gp.index + 1),
4819
- className: `
4820
- flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all
4821
- ${selectedPlayer === gp.index + 1 ? "bg-retro-primary/20 text-retro-primary" : "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"}
4822
- `,
4823
- style: selectedPlayer === gp.index + 1 ? {
4824
- backgroundColor: `${systemColor}20`,
4825
- color: systemColor
4826
- } : {},
4827
- children: [
4828
- /* @__PURE__ */ jsx(User, { size: 14 }),
4829
- "P",
4830
- gp.index + 1
4831
- ]
4832
- },
4833
- gp.index
4834
- )) })
4835
- ] }),
4836
- currentGamepad && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: currentGamepad.name })
4837
- ] }),
4838
- gamepads.length === 1 && currentGamepad && /* @__PURE__ */ jsx("div", { className: "px-6 py-2 border-b border-white/10 bg-black/30", children: /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-400", children: [
4839
- currentGamepad.name,
4840
- " \u2022 Player 1"
4841
- ] }) }),
4842
- gamepads.length === 0 && /* @__PURE__ */ jsxs("div", { className: "px-6 py-10 text-center", children: [
4843
- /* @__PURE__ */ jsxs("div", { className: "relative inline-block mb-4", children: [
4844
- /* @__PURE__ */ jsx(Joystick, { size: 56, className: "text-gray-600 animate-pulse" }),
4845
- /* @__PURE__ */ jsx("div", { className: "absolute inset-0 -m-2 rounded-full border-2 border-dashed border-gray-600 animate-spin", style: { animationDuration: "8s" } })
4846
- ] }),
4847
- /* @__PURE__ */ jsx("p", { className: "text-gray-300 font-medium mb-2", children: t.modals.gamepad.noController }),
4848
- /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500 mb-4", children: t.modals.gamepad.pressAny }),
4849
- /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-white/5 border border-white/10", children: [
4850
- /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400", children: t.modals.gamepad.waiting }),
4851
- /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
4852
- /* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "0ms" } }),
4853
- /* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "150ms" } }),
4854
- /* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "300ms" } })
4855
- ] })
4856
- ] })
4857
- ] }),
4858
- gamepads.length > 0 && /* @__PURE__ */ jsxs("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: [
4859
- listeningFor && /* @__PURE__ */ jsxs("div", { className: "p-4 rounded-lg bg-black/50 border border-retro-primary/50 text-center animate-pulse", style: { borderColor: `${systemColor}50` }, children: [
4860
- /* @__PURE__ */ jsx("p", { className: "text-sm text-white mb-1", children: t.modals.gamepad.pressButton.replace("{{button}}", BUTTON_LABELS[listeningFor]) }),
4861
- /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: t.modals.gamepad.pressEsc })
4862
- ] }),
4863
- BUTTON_GROUPS.map((group) => /* @__PURE__ */ jsxs("div", { children: [
4864
- /* @__PURE__ */ jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
4865
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxs(
4866
- "button",
4867
- {
4868
- onClick: () => setListeningFor(btn),
4869
- disabled: !!listeningFor && listeningFor !== btn,
4870
- className: `
4871
- flex items-center justify-between px-4 py-3 rounded-lg border transition-all
4872
- disabled:opacity-50
4873
- ${listeningFor === btn ? "border-retro-primary bg-retro-primary/20 ring-2 ring-retro-primary/50" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
4874
- `,
4875
- style: listeningFor === btn ? {
4876
- borderColor: systemColor,
4877
- backgroundColor: `${systemColor}20`,
4878
- boxShadow: `0 0 0 2px ${systemColor}50`
4879
- } : {},
4880
- children: [
4881
- /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
4882
- /* @__PURE__ */ jsx(
4883
- "span",
4884
- {
4885
- className: `
4886
- px-2 py-1 rounded text-xs font-mono
4887
- ${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
4888
- `,
4889
- style: listeningFor === btn ? {
4890
- backgroundColor: `${systemColor}30`,
4891
- color: systemColor
4892
- } : {},
4893
- children: listeningFor === btn ? t.controls.press : formatGamepadButton(currentBindings[btn])
4894
- }
4895
- )
4896
- ]
4897
- },
4898
- btn
4899
- )) })
4900
- ] }, group.label))
4901
- ] }),
4902
- gamepads.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4 bg-black/30 border-t border-white/10", children: [
4753
+ return /* @__PURE__ */ jsxs(
4754
+ ModalShell,
4755
+ {
4756
+ isOpen,
4757
+ onClose,
4758
+ title: t.modals.gamepad.title,
4759
+ subtitle: gamepads.length > 0 ? t.modals.gamepad.connected.replace("{{count}}", gamepads.length.toString()) : t.modals.gamepad.none,
4760
+ icon: /* @__PURE__ */ jsx(Joystick, { size: 24, style: { color: systemColor } }),
4761
+ systemColor,
4762
+ closeOnBackdrop: !isListening,
4763
+ footer: gamepads.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
4903
4764
  /* @__PURE__ */ jsxs(
4904
4765
  "button",
4905
4766
  {
@@ -4925,9 +4786,101 @@ function GamepadMapper({
4925
4786
  ]
4926
4787
  }
4927
4788
  )
4928
- ] })
4929
- ] })
4930
- ] });
4789
+ ] }) : void 0,
4790
+ children: [
4791
+ gamepads.length > 1 && /* @__PURE__ */ jsxs("div", { className: "px-6 py-3 border-b border-white/10 bg-black/30", children: [
4792
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
4793
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 font-medium", children: t.modals.gamepad.player }),
4794
+ /* @__PURE__ */ jsx("div", { className: "flex gap-1", children: gamepads.map((gp) => /* @__PURE__ */ jsxs(
4795
+ "button",
4796
+ {
4797
+ onClick: () => setSelectedPlayer(gp.index + 1),
4798
+ className: `
4799
+ flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all
4800
+ ${selectedPlayer === gp.index + 1 ? "bg-retro-primary/20 text-retro-primary" : "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"}
4801
+ `,
4802
+ style: selectedPlayer === gp.index + 1 ? {
4803
+ backgroundColor: `${systemColor}20`,
4804
+ color: systemColor
4805
+ } : {},
4806
+ children: [
4807
+ /* @__PURE__ */ jsx(User, { size: 14 }),
4808
+ "P",
4809
+ gp.index + 1
4810
+ ]
4811
+ },
4812
+ gp.index
4813
+ )) })
4814
+ ] }),
4815
+ currentGamepad && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: currentGamepad.name })
4816
+ ] }),
4817
+ gamepads.length === 1 && currentGamepad && /* @__PURE__ */ jsx("div", { className: "px-6 py-2 border-b border-white/10 bg-black/30", children: /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-400", children: [
4818
+ currentGamepad.name,
4819
+ " \u2022 Player 1"
4820
+ ] }) }),
4821
+ gamepads.length === 0 && /* @__PURE__ */ jsxs("div", { className: "px-6 py-10 text-center", children: [
4822
+ /* @__PURE__ */ jsxs("div", { className: "relative inline-block mb-4", children: [
4823
+ /* @__PURE__ */ jsx(Joystick, { size: 56, className: "text-gray-600 animate-pulse" }),
4824
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 -m-2 rounded-full border-2 border-dashed border-gray-600 animate-spin", style: { animationDuration: "8s" } })
4825
+ ] }),
4826
+ /* @__PURE__ */ jsx("p", { className: "text-gray-300 font-medium mb-2", children: t.modals.gamepad.noController }),
4827
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500 mb-4", children: t.modals.gamepad.pressAny }),
4828
+ /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-white/5 border border-white/10", children: [
4829
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400", children: t.modals.gamepad.waiting }),
4830
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
4831
+ /* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "0ms" } }),
4832
+ /* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "150ms" } }),
4833
+ /* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "300ms" } })
4834
+ ] })
4835
+ ] })
4836
+ ] }),
4837
+ gamepads.length > 0 && /* @__PURE__ */ jsxs("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: [
4838
+ listeningFor && /* @__PURE__ */ jsxs("div", { className: "p-4 rounded-lg bg-black/50 border border-retro-primary/50 text-center animate-pulse", style: { borderColor: `${systemColor}50` }, children: [
4839
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-white mb-1", children: t.modals.gamepad.pressButton.replace("{{button}}", BUTTON_LABELS[listeningFor]) }),
4840
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: t.modals.gamepad.pressEsc })
4841
+ ] }),
4842
+ BUTTON_GROUPS.map((group) => /* @__PURE__ */ jsxs("div", { children: [
4843
+ /* @__PURE__ */ jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
4844
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxs(
4845
+ "button",
4846
+ {
4847
+ onClick: () => startListening(btn),
4848
+ disabled: !!listeningFor && listeningFor !== btn,
4849
+ className: `
4850
+ flex items-center justify-between px-4 py-3 rounded-lg border transition-all
4851
+ disabled:opacity-50
4852
+ ${listeningFor === btn ? "border-retro-primary bg-retro-primary/20 ring-2 ring-retro-primary/50" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
4853
+ `,
4854
+ style: listeningFor === btn ? {
4855
+ borderColor: systemColor,
4856
+ backgroundColor: `${systemColor}20`,
4857
+ boxShadow: `0 0 0 2px ${systemColor}50`
4858
+ } : {},
4859
+ children: [
4860
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
4861
+ /* @__PURE__ */ jsx(
4862
+ "span",
4863
+ {
4864
+ className: `
4865
+ px-2 py-1 rounded text-xs font-mono
4866
+ ${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
4867
+ `,
4868
+ style: listeningFor === btn ? {
4869
+ backgroundColor: `${systemColor}30`,
4870
+ color: systemColor
4871
+ } : {},
4872
+ children: listeningFor === btn ? t.controls.press : formatGamepadButton(currentBindings[btn])
4873
+ }
4874
+ )
4875
+ ]
4876
+ },
4877
+ btn
4878
+ )) })
4879
+ ] }, group.label))
4880
+ ] })
4881
+ ]
4882
+ }
4883
+ );
4931
4884
  }
4932
4885
  function CheatModal({
4933
4886
  isOpen,
@@ -4935,42 +4888,24 @@ function CheatModal({
4935
4888
  activeCheats,
4936
4889
  onToggle,
4937
4890
  onClose
4938
- }) {
4939
- const t = useKoinTranslation();
4940
- const [copiedId, setCopiedId] = React2.useState(null);
4941
- if (!isOpen) return null;
4942
- const handleCopy = async (code, id) => {
4943
- await navigator.clipboard.writeText(code);
4944
- setCopiedId(id);
4945
- setTimeout(() => setCopiedId(null), 2e3);
4946
- };
4947
- return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
4948
- /* @__PURE__ */ jsx(
4949
- "div",
4950
- {
4951
- className: "absolute inset-0 bg-black/80 backdrop-blur-sm",
4952
- onClick: onClose
4953
- }
4954
- ),
4955
- /* @__PURE__ */ jsxs("div", { className: "relative bg-gray-900 border border-retro-primary/30 rounded-xl shadow-2xl w-full max-w-lg mx-4 overflow-hidden", children: [
4956
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/10 bg-black/50", children: [
4957
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
4958
- /* @__PURE__ */ jsx(Code, { className: "text-purple-400", size: 24 }),
4959
- /* @__PURE__ */ jsxs("div", { children: [
4960
- /* @__PURE__ */ jsx("h2", { className: "text-lg font-bold text-white", children: t.modals.cheats.title }),
4961
- /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()) })
4962
- ] })
4963
- ] }),
4964
- /* @__PURE__ */ jsx(
4965
- "button",
4966
- {
4967
- onClick: onClose,
4968
- className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
4969
- children: /* @__PURE__ */ jsx(X, { size: 20 })
4970
- }
4971
- )
4972
- ] }),
4973
- /* @__PURE__ */ jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: cheats.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "text-center py-12 text-gray-500", children: [
4891
+ }) {
4892
+ const t = useKoinTranslation();
4893
+ const [copiedId, setCopiedId] = React2.useState(null);
4894
+ const handleCopy = async (code, id) => {
4895
+ await navigator.clipboard.writeText(code);
4896
+ setCopiedId(id);
4897
+ setTimeout(() => setCopiedId(null), 2e3);
4898
+ };
4899
+ return /* @__PURE__ */ jsx(
4900
+ ModalShell,
4901
+ {
4902
+ isOpen,
4903
+ onClose,
4904
+ title: t.modals.cheats.title,
4905
+ subtitle: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()),
4906
+ icon: /* @__PURE__ */ jsx(Code, { size: 24, className: "text-purple-400" }),
4907
+ footer: /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 text-center w-full", children: activeCheats.size > 0 ? t.modals.cheats.active.replace("{{count}}", activeCheats.size.toString()) : t.modals.cheats.toggleHint }),
4908
+ children: /* @__PURE__ */ jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: cheats.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "text-center py-12 text-gray-500", children: [
4974
4909
  /* @__PURE__ */ jsx(Code, { size: 48, className: "mx-auto mb-3 opacity-50" }),
4975
4910
  /* @__PURE__ */ jsx("p", { className: "font-medium", children: t.modals.cheats.emptyTitle }),
4976
4911
  /* @__PURE__ */ jsx("p", { className: "text-sm mt-1", children: t.modals.cheats.emptyDesc })
@@ -4980,18 +4915,18 @@ function CheatModal({
4980
4915
  "div",
4981
4916
  {
4982
4917
  className: `
4983
- group flex items-start gap-4 p-4 rounded-lg border transition-all cursor-pointer
4984
- ${isActive ? "border-purple-500/50 bg-purple-500/10" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
4985
- `,
4918
+ group flex items-start gap-4 p-4 rounded-lg border transition-all cursor-pointer
4919
+ ${isActive ? "border-purple-500/50 bg-purple-500/10" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
4920
+ `,
4986
4921
  onClick: () => onToggle(cheat.id),
4987
4922
  children: [
4988
4923
  /* @__PURE__ */ jsx(
4989
4924
  "div",
4990
4925
  {
4991
4926
  className: `
4992
- flex-shrink-0 w-6 h-6 rounded border-2 flex items-center justify-center transition-all
4993
- ${isActive ? "border-purple-500 bg-purple-500" : "border-gray-600 group-hover:border-gray-400"}
4994
- `,
4927
+ flex-shrink-0 w-6 h-6 rounded border-2 flex items-center justify-center transition-all
4928
+ ${isActive ? "border-purple-500 bg-purple-500" : "border-gray-600 group-hover:border-gray-400"}
4929
+ `,
4995
4930
  children: isActive && /* @__PURE__ */ jsx(Check, { size: 14, className: "text-white" })
4996
4931
  }
4997
4932
  ),
@@ -5017,10 +4952,9 @@ function CheatModal({
5017
4952
  },
5018
4953
  cheat.id
5019
4954
  );
5020
- }) }),
5021
- /* @__PURE__ */ jsx("div", { className: "px-6 py-3 bg-black/30 border-t border-white/10", children: /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 text-center", children: activeCheats.size > 0 ? t.modals.cheats.active.replace("{{count}}", activeCheats.size.toString()) : t.modals.cheats.toggleHint }) })
5022
- ] })
5023
- ] });
4955
+ }) })
4956
+ }
4957
+ );
5024
4958
  }
5025
4959
  var AUTO_SAVE_SLOT = 5;
5026
4960
  function formatBytes(bytes) {
@@ -5061,7 +4995,6 @@ function SaveSlotModal({
5061
4995
  onUpgrade
5062
4996
  }) {
5063
4997
  const t = useKoinTranslation();
5064
- if (!isOpen) return null;
5065
4998
  const isSaveMode = mode === "save";
5066
4999
  const allSlots = [1, 2, 3, 4, 5];
5067
5000
  const isUnlimited = maxSlots === -1 || maxSlots >= 5;
@@ -5076,33 +5009,17 @@ function SaveSlotModal({
5076
5009
  const getSlotData = (slotNum) => {
5077
5010
  return slots.find((s) => s.slot === slotNum);
5078
5011
  };
5079
- return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
5080
- /* @__PURE__ */ jsx(
5081
- "div",
5082
- {
5083
- className: "absolute inset-0 bg-black/80 backdrop-blur-sm",
5084
- onClick: onClose
5085
- }
5086
- ),
5087
- /* @__PURE__ */ jsxs("div", { className: "relative bg-gray-900 border border-retro-primary/30 rounded-xl shadow-2xl w-full max-w-md mx-4 overflow-hidden", children: [
5088
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/10 bg-black/50", children: [
5089
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
5090
- isSaveMode ? /* @__PURE__ */ jsx(Save, { className: "text-retro-primary", size: 24 }) : /* @__PURE__ */ jsx(Download, { className: "text-retro-primary", size: 24 }),
5091
- /* @__PURE__ */ jsxs("div", { children: [
5092
- /* @__PURE__ */ jsx("h2", { className: "text-lg font-bold text-white", children: isSaveMode ? t.modals.saveSlots.saveTitle : t.modals.saveSlots.loadTitle }),
5093
- /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: isSaveMode ? t.modals.saveSlots.subtitleSave : t.modals.saveSlots.subtitleLoad })
5094
- ] })
5095
- ] }),
5096
- /* @__PURE__ */ jsx(
5097
- "button",
5098
- {
5099
- onClick: onClose,
5100
- className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
5101
- children: /* @__PURE__ */ jsx(X, { size: 20 })
5102
- }
5103
- )
5104
- ] }),
5105
- /* @__PURE__ */ jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: isLoading ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-gray-400", children: [
5012
+ return /* @__PURE__ */ jsx(
5013
+ ModalShell,
5014
+ {
5015
+ isOpen,
5016
+ onClose,
5017
+ title: isSaveMode ? t.modals.saveSlots.saveTitle : t.modals.saveSlots.loadTitle,
5018
+ subtitle: isSaveMode ? t.modals.saveSlots.subtitleSave : t.modals.saveSlots.subtitleLoad,
5019
+ icon: isSaveMode ? /* @__PURE__ */ jsx(Save, { className: "text-retro-primary", size: 24 }) : /* @__PURE__ */ jsx(Download, { className: "text-retro-primary", size: 24 }),
5020
+ maxWidth: "md",
5021
+ footer: /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 text-center w-full", children: isSaveMode ? t.modals.saveSlots.footerSave : t.modals.saveSlots.footerLoad }),
5022
+ children: /* @__PURE__ */ jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: isLoading ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-gray-400", children: [
5106
5023
  /* @__PURE__ */ jsx(Loader2, { className: "w-8 h-8 animate-spin mb-3" }),
5107
5024
  /* @__PURE__ */ jsx("span", { className: "text-sm", children: t.modals.saveSlots.loading })
5108
5025
  ] }) : displaySlots.map((slotNum) => {
@@ -5221,10 +5138,9 @@ function SaveSlotModal({
5221
5138
  },
5222
5139
  slotNum
5223
5140
  );
5224
- }) }),
5225
- /* @__PURE__ */ jsx("div", { className: "px-6 py-3 bg-black/30 border-t border-white/10", children: /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 text-center", children: isSaveMode ? t.modals.saveSlots.footerSave : t.modals.saveSlots.footerLoad }) })
5226
- ] })
5227
- ] });
5141
+ }) })
5142
+ }
5143
+ );
5228
5144
  }
5229
5145
  function BiosSelectionModal({
5230
5146
  isOpen,
@@ -5371,36 +5287,28 @@ function SettingsModal({
5371
5287
  systemColor = "#00FF41"
5372
5288
  }) {
5373
5289
  const t = useKoinTranslation();
5374
- if (!isOpen) return null;
5375
5290
  const languages = [
5376
5291
  { code: "en", name: "English" },
5377
5292
  { code: "es", name: "Espa\xF1ol" },
5378
5293
  { code: "fr", name: "Fran\xE7ais" }
5379
5294
  ];
5380
- return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
5381
- /* @__PURE__ */ jsx(
5382
- "div",
5383
- {
5384
- className: "absolute inset-0 bg-black/80 backdrop-blur-sm",
5385
- onClick: onClose
5386
- }
5387
- ),
5388
- /* @__PURE__ */ jsxs("div", { className: "relative bg-gray-900 border border-white/10 rounded-xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden animate-in fade-in zoom-in-95 duration-200", children: [
5389
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/10 bg-black/50", children: [
5390
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
5391
- /* @__PURE__ */ jsx(Settings, { className: "text-white", size: 20 }),
5392
- /* @__PURE__ */ jsx("h2", { className: "text-lg font-bold text-white", children: t.settings.title })
5393
- ] }),
5394
- /* @__PURE__ */ jsx(
5395
- "button",
5396
- {
5397
- onClick: onClose,
5398
- className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
5399
- children: /* @__PURE__ */ jsx(X, { size: 20 })
5400
- }
5401
- )
5402
- ] }),
5403
- /* @__PURE__ */ jsx("div", { className: "p-6 space-y-6", children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
5295
+ return /* @__PURE__ */ jsx(
5296
+ ModalShell,
5297
+ {
5298
+ isOpen,
5299
+ onClose,
5300
+ title: t.settings.title,
5301
+ icon: /* @__PURE__ */ jsx(Settings, { size: 20, className: "text-white" }),
5302
+ maxWidth: "sm",
5303
+ footer: /* @__PURE__ */ jsx(
5304
+ "button",
5305
+ {
5306
+ onClick: onClose,
5307
+ className: "text-sm text-gray-500 hover:text-white transition-colors w-full text-center",
5308
+ children: t.modals.shortcuts.pressEsc
5309
+ }
5310
+ ),
5311
+ children: /* @__PURE__ */ jsx("div", { className: "p-6 space-y-6", children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
5404
5312
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-400", children: [
5405
5313
  /* @__PURE__ */ jsx(Globe, { size: 16 }),
5406
5314
  /* @__PURE__ */ jsx("span", { children: t.settings.language })
@@ -5412,9 +5320,9 @@ function SettingsModal({
5412
5320
  {
5413
5321
  onClick: () => onLanguageChange(lang.code),
5414
5322
  className: `
5415
- flex items-center justify-between px-4 py-3 rounded-lg border transition-all
5416
- ${isActive ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
5417
- `,
5323
+ flex items-center justify-between px-4 py-3 rounded-lg border transition-all
5324
+ ${isActive ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
5325
+ `,
5418
5326
  children: [
5419
5327
  /* @__PURE__ */ jsx("span", { children: lang.name }),
5420
5328
  isActive && /* @__PURE__ */ jsx(Check, { size: 16, style: { color: systemColor } })
@@ -5423,17 +5331,9 @@ function SettingsModal({
5423
5331
  lang.code
5424
5332
  );
5425
5333
  }) })
5426
- ] }) }),
5427
- /* @__PURE__ */ jsx("div", { className: "px-6 py-4 bg-black/30 border-t border-white/10 text-center", children: /* @__PURE__ */ jsx(
5428
- "button",
5429
- {
5430
- onClick: onClose,
5431
- className: "text-sm text-gray-500 hover:text-white transition-colors",
5432
- children: t.modals.shortcuts.pressEsc
5433
- }
5434
- ) })
5435
- ] })
5436
- ] });
5334
+ ] }) })
5335
+ }
5336
+ );
5437
5337
  }
5438
5338
  function GameModals({
5439
5339
  controlsModalOpen,
@@ -7644,6 +7544,171 @@ var useNostalgist = ({
7644
7544
  ]);
7645
7545
  return hookReturn;
7646
7546
  };
7547
+ function getDisplayName(id) {
7548
+ let name = id;
7549
+ name = name.replace(/^[0-9a-f]{4}-[0-9a-f]{4}-/i, "");
7550
+ name = name.replace(/\s*\(.*\)\s*$/i, "");
7551
+ name = name.replace(/\s*STANDARD GAMEPAD\s*/i, "");
7552
+ if (/xbox/i.test(name)) {
7553
+ if (/series/i.test(name)) return "Xbox Series Controller";
7554
+ if (/one/i.test(name)) return "Xbox One Controller";
7555
+ if (/360/i.test(name)) return "Xbox 360 Controller";
7556
+ return "Xbox Controller";
7557
+ }
7558
+ if (/dualsense/i.test(name)) return "DualSense";
7559
+ if (/dualshock\s*4/i.test(name)) return "DualShock 4";
7560
+ if (/dualshock/i.test(name)) return "DualShock";
7561
+ if (/playstation/i.test(name) || /sony/i.test(name)) return "PlayStation Controller";
7562
+ if (/pro\s*controller/i.test(name)) return "Switch Pro Controller";
7563
+ if (/joy-?con/i.test(name)) return "Joy-Con";
7564
+ if (/nintendo/i.test(name)) return "Nintendo Controller";
7565
+ return name.trim() || "Gamepad";
7566
+ }
7567
+ function detectControllerBrand(id) {
7568
+ const lowerId = id.toLowerCase();
7569
+ if (/xbox|xinput|microsoft/i.test(lowerId)) return "xbox";
7570
+ if (/playstation|sony|dualshock|dualsense/i.test(lowerId)) return "playstation";
7571
+ if (/nintendo|switch|joy-?con|pro controller/i.test(lowerId)) return "nintendo";
7572
+ return "generic";
7573
+ }
7574
+ function toGamepadInfo(gamepad) {
7575
+ return {
7576
+ index: gamepad.index,
7577
+ id: gamepad.id,
7578
+ name: getDisplayName(gamepad.id),
7579
+ connected: gamepad.connected,
7580
+ buttons: gamepad.buttons.length,
7581
+ axes: gamepad.axes.length,
7582
+ mapping: gamepad.mapping
7583
+ };
7584
+ }
7585
+ function useGamepad(options) {
7586
+ const { onConnect, onDisconnect } = options || {};
7587
+ const [gamepads, setGamepads] = useState([]);
7588
+ const rafRef = useRef(null);
7589
+ const lastStateRef = useRef("");
7590
+ const prevCountRef = useRef(0);
7591
+ const onConnectRef = useRef(onConnect);
7592
+ const onDisconnectRef = useRef(onDisconnect);
7593
+ useEffect(() => {
7594
+ onConnectRef.current = onConnect;
7595
+ onDisconnectRef.current = onDisconnect;
7596
+ }, [onConnect, onDisconnect]);
7597
+ const getGamepads = useCallback(() => {
7598
+ if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") {
7599
+ return [];
7600
+ }
7601
+ const rawGamepads = navigator.getGamepads() ?? [];
7602
+ const connected = [];
7603
+ for (let i = 0; i < rawGamepads.length; i++) {
7604
+ const gp = rawGamepads[i];
7605
+ if (gp && gp.connected) {
7606
+ connected.push(toGamepadInfo(gp));
7607
+ }
7608
+ }
7609
+ return connected;
7610
+ }, []);
7611
+ const getRawGamepad = useCallback((index) => {
7612
+ const rawGamepads = navigator.getGamepads?.() ?? [];
7613
+ return rawGamepads[index] ?? null;
7614
+ }, []);
7615
+ const refresh = useCallback(() => {
7616
+ setGamepads(getGamepads());
7617
+ }, [getGamepads]);
7618
+ useEffect(() => {
7619
+ if (typeof window === "undefined" || typeof navigator === "undefined") {
7620
+ return;
7621
+ }
7622
+ if (typeof navigator.getGamepads !== "function") {
7623
+ console.warn("[useGamepad] Gamepad API not supported in this browser");
7624
+ return;
7625
+ }
7626
+ let isActive = true;
7627
+ const poll = () => {
7628
+ if (!isActive) return;
7629
+ const current = getGamepads();
7630
+ let hasChanged = current.length !== prevCountRef.current;
7631
+ if (!hasChanged) {
7632
+ for (let i = 0; i < current.length; i++) {
7633
+ const saved = gamepads[i];
7634
+ if (!saved || saved.id !== current[i].id || saved.connected !== current[i].connected) {
7635
+ hasChanged = true;
7636
+ break;
7637
+ }
7638
+ }
7639
+ }
7640
+ if (hasChanged) {
7641
+ const prevCount = prevCountRef.current;
7642
+ const currentCount = current.length;
7643
+ if (currentCount > prevCount && prevCount >= 0 && onConnectRef.current) {
7644
+ const newGamepad = current[current.length - 1];
7645
+ onConnectRef.current(newGamepad);
7646
+ } else if (currentCount < prevCount && prevCount > 0 && onDisconnectRef.current) {
7647
+ onDisconnectRef.current();
7648
+ }
7649
+ prevCountRef.current = currentCount;
7650
+ setGamepads(current);
7651
+ }
7652
+ rafRef.current = requestAnimationFrame(poll);
7653
+ };
7654
+ const handleConnect = (e) => {
7655
+ console.log("[useGamepad] \u{1F3AE} Gamepad connected:", e.gamepad.id);
7656
+ const current = getGamepads();
7657
+ const prevCount = prevCountRef.current;
7658
+ prevCountRef.current = current.length;
7659
+ lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
7660
+ setGamepads(current);
7661
+ if (onConnectRef.current && current.length > prevCount) {
7662
+ const newGamepad = current[current.length - 1];
7663
+ onConnectRef.current(newGamepad);
7664
+ }
7665
+ };
7666
+ const handleDisconnect = (e) => {
7667
+ console.log("[useGamepad] \u{1F3AE} Gamepad disconnected:", e.gamepad.id);
7668
+ const current = getGamepads();
7669
+ const prevCount = prevCountRef.current;
7670
+ prevCountRef.current = current.length;
7671
+ lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
7672
+ setGamepads(current);
7673
+ if (onDisconnectRef.current && current.length < prevCount) {
7674
+ onDisconnectRef.current();
7675
+ }
7676
+ };
7677
+ window.addEventListener("gamepadconnected", handleConnect);
7678
+ window.addEventListener("gamepaddisconnected", handleDisconnect);
7679
+ rafRef.current = requestAnimationFrame(poll);
7680
+ const initial = getGamepads();
7681
+ if (initial.length > 0) {
7682
+ console.log("[useGamepad] Initial gamepads found:", initial.map((g) => g.name).join(", "));
7683
+ prevCountRef.current = initial.length;
7684
+ setGamepads(initial);
7685
+ lastStateRef.current = initial.map((g) => `${g.index}:${g.id}`).join("|");
7686
+ } else {
7687
+ prevCountRef.current = 0;
7688
+ }
7689
+ return () => {
7690
+ isActive = false;
7691
+ if (rafRef.current) {
7692
+ cancelAnimationFrame(rafRef.current);
7693
+ }
7694
+ window.removeEventListener("gamepadconnected", handleConnect);
7695
+ window.removeEventListener("gamepaddisconnected", handleDisconnect);
7696
+ };
7697
+ }, [getGamepads]);
7698
+ return {
7699
+ gamepads,
7700
+ isAnyConnected: gamepads.length > 0,
7701
+ connectedCount: gamepads.length,
7702
+ getRawGamepad,
7703
+ refresh
7704
+ };
7705
+ }
7706
+ var STANDARD_AXIS_MAP = {
7707
+ leftStickX: 0,
7708
+ leftStickY: 1,
7709
+ rightStickX: 2,
7710
+ rightStickY: 3
7711
+ };
7647
7712
  function useVolume({
7648
7713
  setVolume: setVolumeInHook,
7649
7714
  toggleMute: toggleMuteInHook
@@ -9531,27 +9596,15 @@ function AchievementPopup({
9531
9596
  onDismiss,
9532
9597
  autoDismissMs = 5e3
9533
9598
  }) {
9534
- const [isVisible, setIsVisible] = useState(false);
9535
- const [isExiting, setIsExiting] = useState(false);
9536
- useEffect(() => {
9537
- requestAnimationFrame(() => {
9538
- setIsVisible(true);
9539
- });
9540
- const timer = setTimeout(() => {
9541
- handleDismiss();
9542
- }, autoDismissMs);
9543
- return () => clearTimeout(timer);
9544
- }, [autoDismissMs]);
9545
- const handleDismiss = () => {
9546
- setIsExiting(true);
9547
- setTimeout(() => {
9548
- onDismiss();
9549
- }, 300);
9550
- };
9599
+ const { slideInRightClasses, triggerExit } = useAnimatedVisibility({
9600
+ exitDuration: 300,
9601
+ onExit: onDismiss,
9602
+ autoDismissMs
9603
+ });
9551
9604
  return /* @__PURE__ */ jsxs(
9552
9605
  "div",
9553
9606
  {
9554
- className: `fixed top-4 right-4 z-[100] transition-all duration-300 ${isVisible && !isExiting ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"}`,
9607
+ className: `fixed top-4 right-4 z-[100] transition-all duration-300 ${slideInRightClasses}`,
9555
9608
  children: [
9556
9609
  /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-gradient-to-r from-yellow-500 to-orange-500 blur-lg opacity-50 animate-pulse" }),
9557
9610
  /* @__PURE__ */ jsx("div", { className: "relative bg-gradient-to-r from-yellow-500 to-orange-500 p-[2px] rounded-lg", children: /* @__PURE__ */ jsxs("div", { className: "bg-gray-900 rounded-lg p-4 flex items-center gap-4 min-w-[320px]", children: [
@@ -9588,7 +9641,7 @@ function AchievementPopup({
9588
9641
  /* @__PURE__ */ jsx(
9589
9642
  "button",
9590
9643
  {
9591
- onClick: handleDismiss,
9644
+ onClick: triggerExit,
9592
9645
  className: "flex-shrink-0 text-gray-500 hover:text-white transition-colors",
9593
9646
  children: /* @__PURE__ */ jsx(X, { size: 18 })
9594
9647
  }