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.js CHANGED
@@ -827,7 +827,7 @@ var SaveLoadControls = React2.memo(function SaveLoadControls2({
827
827
  {
828
828
  progress: autoSaveProgress,
829
829
  state: autoSavePaused ? "idle" : autoSaveState,
830
- intervalSeconds: 20,
830
+ intervalSeconds: 60,
831
831
  isPaused: autoSavePaused,
832
832
  onClick: onAutoSaveToggle
833
833
  }
@@ -1447,6 +1447,40 @@ var PlayerControls = React2.memo(function PlayerControls2({
1447
1447
  ] });
1448
1448
  });
1449
1449
  var PlayerControls_default = PlayerControls;
1450
+ function useAnimatedVisibility({
1451
+ exitDuration = 200,
1452
+ onExit,
1453
+ autoDismissMs
1454
+ } = {}) {
1455
+ const [isVisible, setIsVisible] = React2.useState(false);
1456
+ const [isExiting, setIsExiting] = React2.useState(false);
1457
+ React2.useEffect(() => {
1458
+ requestAnimationFrame(() => {
1459
+ setIsVisible(true);
1460
+ });
1461
+ }, []);
1462
+ React2.useEffect(() => {
1463
+ if (!autoDismissMs) return;
1464
+ const timer = setTimeout(() => {
1465
+ triggerExit();
1466
+ }, autoDismissMs);
1467
+ return () => clearTimeout(timer);
1468
+ }, [autoDismissMs]);
1469
+ const triggerExit = React2.useCallback(() => {
1470
+ if (isExiting) return;
1471
+ setIsExiting(true);
1472
+ setTimeout(() => {
1473
+ onExit?.();
1474
+ }, exitDuration);
1475
+ }, [isExiting, exitDuration, onExit]);
1476
+ const slideInRightClasses = isVisible && !isExiting ? "translate-x-0 opacity-100" : "translate-x-full opacity-0";
1477
+ return {
1478
+ isVisible,
1479
+ isExiting,
1480
+ triggerExit,
1481
+ slideInRightClasses
1482
+ };
1483
+ }
1450
1484
  var TOAST_CONFIGS = {
1451
1485
  success: {
1452
1486
  icon: lucideReact.CheckCircle,
@@ -1491,32 +1525,20 @@ var TOAST_CONFIGS = {
1491
1525
  }
1492
1526
  };
1493
1527
  function ToastItem({ toast, onDismiss }) {
1494
- const [isVisible, setIsVisible] = React2.useState(false);
1495
- const [isExiting, setIsExiting] = React2.useState(false);
1528
+ const { slideInRightClasses, triggerExit } = useAnimatedVisibility({
1529
+ exitDuration: 200,
1530
+ onExit: () => onDismiss?.(toast.id)
1531
+ });
1496
1532
  const config = TOAST_CONFIGS[toast.type];
1497
1533
  const IconComponent = config.icon;
1498
- React2.useEffect(() => {
1499
- requestAnimationFrame(() => {
1500
- setIsVisible(true);
1501
- });
1502
- }, []);
1503
- const handleDismiss = () => {
1504
- setIsExiting(true);
1505
- setTimeout(() => {
1506
- onDismiss?.(toast.id);
1507
- }, 200);
1508
- };
1509
1534
  return /* @__PURE__ */ jsxRuntime.jsx(
1510
1535
  "div",
1511
1536
  {
1512
- className: `
1513
- relative transition-all duration-300 ease-out
1514
- ${isVisible && !isExiting ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"}
1515
- `,
1537
+ className: `relative transition-all duration-300 ease-out ${slideInRightClasses}`,
1516
1538
  children: /* @__PURE__ */ jsxRuntime.jsxs(
1517
1539
  "div",
1518
1540
  {
1519
- className: "relative w-[320px]",
1541
+ className: "relative w-[320px] pointer-events-auto",
1520
1542
  style: {
1521
1543
  backgroundColor: config.bgColor,
1522
1544
  border: `2px solid ${config.borderColor}`,
@@ -1551,7 +1573,7 @@ function ToastItem({ toast, onDismiss }) {
1551
1573
  {
1552
1574
  onClick: () => {
1553
1575
  toast.action?.onClick();
1554
- handleDismiss();
1576
+ triggerExit();
1555
1577
  },
1556
1578
  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",
1557
1579
  style: {
@@ -1565,7 +1587,7 @@ function ToastItem({ toast, onDismiss }) {
1565
1587
  /* @__PURE__ */ jsxRuntime.jsx(
1566
1588
  "button",
1567
1589
  {
1568
- onClick: handleDismiss,
1590
+ onClick: triggerExit,
1569
1591
  className: "flex-shrink-0 p-0.5 text-gray-500 hover:text-white transition-colors",
1570
1592
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 14 })
1571
1593
  }
@@ -1938,6 +1960,67 @@ var RecordingIndicator = React2.memo(function RecordingIndicator2({
1938
1960
  );
1939
1961
  });
1940
1962
  var RecordingIndicator_default = RecordingIndicator;
1963
+ var MAX_WIDTH_CLASSES = {
1964
+ sm: "max-w-sm",
1965
+ md: "max-w-md",
1966
+ lg: "max-w-lg"
1967
+ };
1968
+ function ModalShell({
1969
+ isOpen,
1970
+ onClose,
1971
+ title,
1972
+ subtitle,
1973
+ icon,
1974
+ children,
1975
+ footer,
1976
+ maxWidth = "lg",
1977
+ systemColor,
1978
+ closeOnBackdrop = true
1979
+ }) {
1980
+ if (!isOpen) return null;
1981
+ const handleBackdropClick = () => {
1982
+ if (closeOnBackdrop) {
1983
+ onClose();
1984
+ }
1985
+ };
1986
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
1987
+ /* @__PURE__ */ jsxRuntime.jsx(
1988
+ "div",
1989
+ {
1990
+ className: "absolute inset-0 bg-black/80 backdrop-blur-sm",
1991
+ onClick: handleBackdropClick
1992
+ }
1993
+ ),
1994
+ /* @__PURE__ */ jsxRuntime.jsxs(
1995
+ "div",
1996
+ {
1997
+ className: `relative bg-gray-900 border border-retro-primary/30 rounded-xl shadow-2xl w-full ${MAX_WIDTH_CLASSES[maxWidth]} mx-4 overflow-hidden`,
1998
+ style: systemColor ? { borderColor: `${systemColor}30` } : void 0,
1999
+ children: [
2000
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/10 bg-black/50", children: [
2001
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
2002
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: systemColor ? { color: systemColor } : void 0, children: icon }),
2003
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2004
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: title }),
2005
+ subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: subtitle })
2006
+ ] })
2007
+ ] }),
2008
+ /* @__PURE__ */ jsxRuntime.jsx(
2009
+ "button",
2010
+ {
2011
+ onClick: onClose,
2012
+ className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
2013
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 20 })
2014
+ }
2015
+ )
2016
+ ] }),
2017
+ children,
2018
+ footer && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-between px-6 py-4 bg-black/30 border-t border-white/10", children: footer })
2019
+ ]
2020
+ }
2021
+ )
2022
+ ] });
2023
+ }
1941
2024
  var ShortcutsModal = React2.memo(function ShortcutsModal2({
1942
2025
  isOpen,
1943
2026
  onClose,
@@ -1966,90 +2049,55 @@ var ShortcutsModal = React2.memo(function ShortcutsModal2({
1966
2049
  ]
1967
2050
  }
1968
2051
  ], [t]);
1969
- if (!isOpen) return null;
1970
2052
  return /* @__PURE__ */ jsxRuntime.jsx(
1971
- "div",
2053
+ ModalShell,
1972
2054
  {
1973
- className: "absolute inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm",
1974
- onClick: onClose,
1975
- children: /* @__PURE__ */ jsxRuntime.jsxs(
1976
- "div",
1977
- {
1978
- className: "max-w-sm w-full mx-4 bg-black/95 border rounded-lg overflow-hidden",
1979
- style: { borderColor: `${systemColor}40` },
1980
- onClick: (e) => e.stopPropagation(),
1981
- children: [
1982
- /* @__PURE__ */ jsxRuntime.jsxs(
1983
- "div",
1984
- {
1985
- className: "flex items-center justify-between px-4 py-3 border-b",
1986
- style: { borderColor: `${systemColor}20`, backgroundColor: `${systemColor}10` },
1987
- children: [
1988
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1989
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Keyboard, { size: 18, style: { color: systemColor } }),
1990
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-bold text-white", children: t.modals.shortcuts.playerShortcuts })
1991
- ] }),
1992
- /* @__PURE__ */ jsxRuntime.jsx(
1993
- "button",
1994
- {
1995
- onClick: onClose,
1996
- className: "p-1 rounded hover:bg-white/10 transition-colors",
1997
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 18, className: "text-white/60 hover:text-white" })
1998
- }
1999
- )
2000
- ]
2001
- }
2002
- ),
2003
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 space-y-3", children: [
2004
- shortcuts.map(({ section, items }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2055
+ isOpen,
2056
+ onClose,
2057
+ title: t.modals.shortcuts.playerShortcuts,
2058
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Keyboard, { size: 20, style: { color: systemColor } }),
2059
+ maxWidth: "sm",
2060
+ systemColor,
2061
+ footer: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-500 w-full text-center", children: t.modals.shortcuts.pressEsc }),
2062
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 space-y-3", children: [
2063
+ shortcuts.map(({ section, items }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2064
+ /* @__PURE__ */ jsxRuntime.jsx(
2065
+ "h3",
2066
+ {
2067
+ className: "text-[10px] font-bold uppercase tracking-wider mb-1.5 opacity-60",
2068
+ style: { color: systemColor },
2069
+ children: section
2070
+ }
2071
+ ),
2072
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1", children: items.map(({ key, description }) => /* @__PURE__ */ jsxRuntime.jsxs(
2073
+ "div",
2074
+ {
2075
+ className: "flex items-center justify-between text-sm",
2076
+ children: [
2077
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-white/70", children: description }),
2005
2078
  /* @__PURE__ */ jsxRuntime.jsx(
2006
- "h3",
2079
+ "kbd",
2007
2080
  {
2008
- className: "text-[10px] font-bold uppercase tracking-wider mb-1.5 opacity-60",
2009
- style: { color: systemColor },
2010
- children: section
2081
+ className: "px-2 py-0.5 rounded text-xs font-mono font-bold",
2082
+ style: {
2083
+ backgroundColor: `${systemColor}20`,
2084
+ color: systemColor,
2085
+ border: `1px solid ${systemColor}40`
2086
+ },
2087
+ children: key
2011
2088
  }
2012
- ),
2013
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1", children: items.map(({ key, description }) => /* @__PURE__ */ jsxRuntime.jsxs(
2014
- "div",
2015
- {
2016
- className: "flex items-center justify-between text-sm",
2017
- children: [
2018
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-white/70", children: description }),
2019
- /* @__PURE__ */ jsxRuntime.jsx(
2020
- "kbd",
2021
- {
2022
- className: "px-2 py-0.5 rounded text-xs font-mono font-bold",
2023
- style: {
2024
- backgroundColor: `${systemColor}20`,
2025
- color: systemColor,
2026
- border: `1px solid ${systemColor}40`
2027
- },
2028
- children: key
2029
- }
2030
- )
2031
- ]
2032
- },
2033
- key
2034
- )) })
2035
- ] }, section)),
2036
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pt-2 border-t border-white/10 text-xs text-white/40", children: [
2037
- "Game controls can be configured in ",
2038
- /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white/60", children: t.controls.keys }),
2039
- " settings."
2040
- ] })
2041
- ] }),
2042
- /* @__PURE__ */ jsxRuntime.jsx(
2043
- "div",
2044
- {
2045
- className: "px-4 py-2 text-center text-xs text-white/40 border-t",
2046
- style: { borderColor: `${systemColor}20` },
2047
- children: t.modals.shortcuts.pressEsc
2048
- }
2049
- )
2050
- ]
2051
- }
2052
- )
2089
+ )
2090
+ ]
2091
+ },
2092
+ key
2093
+ )) })
2094
+ ] }, section)),
2095
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pt-2 border-t border-white/10 text-xs text-white/40", children: [
2096
+ "Game controls can be configured in ",
2097
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white/60", children: t.controls.keys }),
2098
+ " settings."
2099
+ ] })
2100
+ ] })
2053
2101
  }
2054
2102
  );
2055
2103
  });
@@ -2646,6 +2694,39 @@ function useTouchHandlers({
2646
2694
  cleanup
2647
2695
  };
2648
2696
  }
2697
+ function useTouchEvents(ref, handlers, options = {}) {
2698
+ const { cleanup, passive = false } = options;
2699
+ const handlersRef = React2.useRef(handlers);
2700
+ handlersRef.current = handlers;
2701
+ const handleTouchStart = React2.useCallback((e) => {
2702
+ handlersRef.current.onTouchStart?.(e);
2703
+ }, []);
2704
+ const handleTouchMove = React2.useCallback((e) => {
2705
+ handlersRef.current.onTouchMove?.(e);
2706
+ }, []);
2707
+ const handleTouchEnd = React2.useCallback((e) => {
2708
+ handlersRef.current.onTouchEnd?.(e);
2709
+ }, []);
2710
+ const handleTouchCancel = React2.useCallback((e) => {
2711
+ handlersRef.current.onTouchCancel?.(e);
2712
+ }, []);
2713
+ React2.useEffect(() => {
2714
+ const element = ref.current;
2715
+ if (!element) return;
2716
+ const listenerOptions = { passive };
2717
+ element.addEventListener("touchstart", handleTouchStart, listenerOptions);
2718
+ element.addEventListener("touchmove", handleTouchMove, listenerOptions);
2719
+ element.addEventListener("touchend", handleTouchEnd, listenerOptions);
2720
+ element.addEventListener("touchcancel", handleTouchCancel, listenerOptions);
2721
+ return () => {
2722
+ element.removeEventListener("touchstart", handleTouchStart);
2723
+ element.removeEventListener("touchmove", handleTouchMove);
2724
+ element.removeEventListener("touchend", handleTouchEnd);
2725
+ element.removeEventListener("touchcancel", handleTouchCancel);
2726
+ cleanup?.();
2727
+ };
2728
+ }, [ref, passive, cleanup, handleTouchStart, handleTouchMove, handleTouchEnd, handleTouchCancel]);
2729
+ }
2649
2730
 
2650
2731
  // src/components/VirtualController/utils/buttonStyles.ts
2651
2732
  var DEFAULT_FACE = {
@@ -2803,21 +2884,12 @@ var VirtualButton = React2__default.default.memo(function VirtualButton2({
2803
2884
  onRelease,
2804
2885
  onPositionChange
2805
2886
  });
2806
- React2.useEffect(() => {
2807
- const button = buttonRef.current;
2808
- if (!button) return;
2809
- button.addEventListener("touchstart", handleTouchStart, { passive: false });
2810
- button.addEventListener("touchmove", handleTouchMove, { passive: false });
2811
- button.addEventListener("touchend", handleTouchEnd, { passive: false });
2812
- button.addEventListener("touchcancel", handleTouchCancel, { passive: false });
2813
- return () => {
2814
- button.removeEventListener("touchstart", handleTouchStart);
2815
- button.removeEventListener("touchmove", handleTouchMove);
2816
- button.removeEventListener("touchend", handleTouchEnd);
2817
- button.removeEventListener("touchcancel", handleTouchCancel);
2818
- cleanup();
2819
- };
2820
- }, [handleTouchStart, handleTouchMove, handleTouchEnd, handleTouchCancel, cleanup]);
2887
+ useTouchEvents(buttonRef, {
2888
+ onTouchStart: handleTouchStart,
2889
+ onTouchMove: handleTouchMove,
2890
+ onTouchEnd: handleTouchEnd,
2891
+ onTouchCancel: handleTouchCancel
2892
+ }, { cleanup });
2821
2893
  const leftPercent = displayX / 100 * containerWidth - config.size / 2;
2822
2894
  const topPercent = displayY / 100 * containerHeight - config.size / 2;
2823
2895
  const transform = `translate3d(${leftPercent.toFixed(1)}px, ${topPercent.toFixed(1)}px, 0)`;
@@ -3493,7 +3565,7 @@ function dispatchKeyboardEvent(type, code) {
3493
3565
  canvas.dispatchEvent(event);
3494
3566
  return true;
3495
3567
  }
3496
- var CENTER_TOUCH_RADIUS = 0.25;
3568
+ var CENTER_TOUCH_RADIUS = 0.35;
3497
3569
  var Dpad = React2__default.default.memo(function Dpad2({
3498
3570
  size = 180,
3499
3571
  x,
@@ -3635,14 +3707,22 @@ var Dpad = React2__default.default.memo(function Dpad2({
3635
3707
  if (!touch) return;
3636
3708
  if (drag.isDragging) {
3637
3709
  drag.handleDragMove(touch.clientX, touch.clientY);
3710
+ } else if (onPositionChange) {
3711
+ const startedDrag = drag.checkMoveThreshold(touch.clientX, touch.clientY);
3712
+ if (!startedDrag) {
3713
+ drag.clearDragTimer();
3714
+ const rect = dpadRef.current?.getBoundingClientRect();
3715
+ if (rect) {
3716
+ updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3717
+ }
3718
+ }
3638
3719
  } else {
3639
3720
  const rect = dpadRef.current?.getBoundingClientRect();
3640
3721
  if (rect) {
3641
- drag.clearDragTimer();
3642
3722
  updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3643
3723
  }
3644
3724
  }
3645
- }, [drag, getDirectionsFromTouch, updateDirections]);
3725
+ }, [drag, getDirectionsFromTouch, updateDirections, onPositionChange]);
3646
3726
  const handleTouchEnd = React2.useCallback((e) => {
3647
3727
  e.preventDefault();
3648
3728
  drag.clearDragTimer();
@@ -3667,21 +3747,12 @@ var Dpad = React2__default.default.memo(function Dpad2({
3667
3747
  }
3668
3748
  }
3669
3749
  }, [getKeyCode, updateVisuals, drag]);
3670
- React2.useEffect(() => {
3671
- const dpad = dpadRef.current;
3672
- if (!dpad) return;
3673
- dpad.addEventListener("touchstart", handleTouchStart, { passive: false });
3674
- dpad.addEventListener("touchmove", handleTouchMove, { passive: false });
3675
- dpad.addEventListener("touchend", handleTouchEnd, { passive: false });
3676
- dpad.addEventListener("touchcancel", handleTouchEnd, { passive: false });
3677
- return () => {
3678
- dpad.removeEventListener("touchstart", handleTouchStart);
3679
- dpad.removeEventListener("touchmove", handleTouchMove);
3680
- dpad.removeEventListener("touchend", handleTouchEnd);
3681
- dpad.removeEventListener("touchcancel", handleTouchEnd);
3682
- drag.clearDragTimer();
3683
- };
3684
- }, [handleTouchStart, handleTouchMove, handleTouchEnd, drag]);
3750
+ useTouchEvents(dpadRef, {
3751
+ onTouchStart: handleTouchStart,
3752
+ onTouchMove: handleTouchMove,
3753
+ onTouchEnd: handleTouchEnd,
3754
+ onTouchCancel: handleTouchEnd
3755
+ }, { cleanup: drag.clearDragTimer });
3685
3756
  const leftPx = displayX / 100 * containerWidth - size / 2;
3686
3757
  const topPx = displayY / 100 * containerHeight - size / 2;
3687
3758
  const dUp = "M 35,5 L 65,5 L 65,35 L 50,50 L 35,35 Z";
@@ -3852,8 +3923,16 @@ function ControlsHint({ isVisible }) {
3852
3923
  children: [
3853
3924
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center mb-4", children: /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(lucideReact.Move, { size: 32, className: "text-green-400" }) }) }),
3854
3925
  /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-white text-lg font-bold mb-2", children: "Customize Your Controls" }),
3855
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-white/70 text-sm mb-4", children: [
3856
- /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white", children: "Long-press" }),
3926
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-white/70 text-sm mb-3", children: [
3927
+ "Use the ",
3928
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Lock, { size: 12, className: "inline mx-1 text-white" }),
3929
+ " ",
3930
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white", children: "lock icon" }),
3931
+ " at the top to unlock controls for repositioning."
3932
+ ] }),
3933
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-white/70 text-sm mb-3", children: [
3934
+ "When unlocked, ",
3935
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white", children: "long-press" }),
3857
3936
  " any button or the ",
3858
3937
  /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white", children: "D-pad center" }),
3859
3938
  " to drag and reposition it."
@@ -3881,6 +3960,33 @@ function ControlsHint({ isVisible }) {
3881
3960
  }
3882
3961
  );
3883
3962
  }
3963
+ function LockButton({
3964
+ isLocked,
3965
+ onToggle,
3966
+ systemColor = "#00FF41"
3967
+ }) {
3968
+ const Icon = isLocked ? lucideReact.Lock : lucideReact.Unlock;
3969
+ return /* @__PURE__ */ jsxRuntime.jsx(
3970
+ "button",
3971
+ {
3972
+ onClick: onToggle,
3973
+ 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",
3974
+ style: {
3975
+ backgroundColor: isLocked ? "rgba(0,0,0,0.6)" : `${systemColor}20`,
3976
+ border: `1px solid ${isLocked ? "rgba(255,255,255,0.2)" : systemColor}`
3977
+ },
3978
+ "aria-label": isLocked ? "Unlock controls for repositioning" : "Lock controls",
3979
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3980
+ Icon,
3981
+ {
3982
+ size: 18,
3983
+ style: { color: isLocked ? "rgba(255,255,255,0.6)" : systemColor }
3984
+ }
3985
+ )
3986
+ }
3987
+ );
3988
+ }
3989
+ var LOCK_KEY = "koin-controls-locked";
3884
3990
  function VirtualController({
3885
3991
  system,
3886
3992
  isRunning,
@@ -3892,7 +3998,21 @@ function VirtualController({
3892
3998
  const [pressedButtons, setPressedButtons] = React2.useState(/* @__PURE__ */ new Set());
3893
3999
  const [containerSize, setContainerSize] = React2.useState({ width: 0, height: 0 });
3894
4000
  const [isFullscreenState, setIsFullscreenState] = React2.useState(false);
4001
+ const [isLocked, setIsLocked] = React2.useState(true);
3895
4002
  const { getPosition, savePosition } = useButtonPositions();
4003
+ React2.useEffect(() => {
4004
+ const stored = localStorage.getItem(LOCK_KEY);
4005
+ if (stored !== null) {
4006
+ setIsLocked(stored === "true");
4007
+ }
4008
+ }, []);
4009
+ const toggleLock = React2.useCallback(() => {
4010
+ setIsLocked((prev) => {
4011
+ const newValue = !prev;
4012
+ localStorage.setItem(LOCK_KEY, String(newValue));
4013
+ return newValue;
4014
+ });
4015
+ }, []);
3896
4016
  const layout = getLayoutForSystem(system);
3897
4017
  const visibleButtons = layout.buttons.filter((btn) => {
3898
4018
  if (isPortrait) {
@@ -4047,6 +4167,14 @@ function VirtualController({
4047
4167
  className: "fixed inset-0 z-30 pointer-events-none",
4048
4168
  style: { touchAction: "none" },
4049
4169
  children: [
4170
+ /* @__PURE__ */ jsxRuntime.jsx(
4171
+ LockButton,
4172
+ {
4173
+ isLocked,
4174
+ onToggle: toggleLock,
4175
+ systemColor
4176
+ }
4177
+ ),
4050
4178
  /* @__PURE__ */ jsxRuntime.jsx(
4051
4179
  Dpad_default,
4052
4180
  {
@@ -4059,7 +4187,7 @@ function VirtualController({
4059
4187
  systemColor,
4060
4188
  isLandscape,
4061
4189
  customPosition: getPosition("up", isLandscape),
4062
- onPositionChange: (x, y) => savePosition("up", x, y, isLandscape)
4190
+ onPositionChange: isLocked ? void 0 : (x, y) => savePosition("up", x, y, isLandscape)
4063
4191
  }
4064
4192
  ),
4065
4193
  memoizedButtonElements.filter(({ buttonConfig }) => !DPAD_TYPES.includes(buttonConfig.type)).map(({ buttonConfig, adjustedConfig, customPosition, width, height }) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -4073,7 +4201,7 @@ function VirtualController({
4073
4201
  containerWidth: width,
4074
4202
  containerHeight: height,
4075
4203
  customPosition,
4076
- onPositionChange: (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
4204
+ onPositionChange: isLocked ? void 0 : (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
4077
4205
  isLandscape,
4078
4206
  console: layout.console
4079
4207
  },
@@ -4393,6 +4521,45 @@ var GameCanvas = React2.memo(function GameCanvas2({
4393
4521
  ] });
4394
4522
  });
4395
4523
  var GameCanvas_default = GameCanvas;
4524
+ function useInputCapture({
4525
+ isOpen,
4526
+ onClose
4527
+ }) {
4528
+ const [listeningFor, setListeningFor] = React2.useState(null);
4529
+ const startListening = React2.useCallback((target) => {
4530
+ setListeningFor(target);
4531
+ }, []);
4532
+ const stopListening = React2.useCallback(() => {
4533
+ setListeningFor(null);
4534
+ }, []);
4535
+ React2.useEffect(() => {
4536
+ if (!isOpen) {
4537
+ setListeningFor(null);
4538
+ }
4539
+ }, [isOpen]);
4540
+ React2.useEffect(() => {
4541
+ if (!isOpen) return;
4542
+ const handleKeyDown = (e) => {
4543
+ if (e.code === "Escape") {
4544
+ if (listeningFor !== null) {
4545
+ e.preventDefault();
4546
+ e.stopPropagation();
4547
+ setListeningFor(null);
4548
+ } else {
4549
+ onClose();
4550
+ }
4551
+ }
4552
+ };
4553
+ window.addEventListener("keydown", handleKeyDown);
4554
+ return () => window.removeEventListener("keydown", handleKeyDown);
4555
+ }, [isOpen, listeningFor, onClose]);
4556
+ return {
4557
+ listeningFor,
4558
+ startListening,
4559
+ stopListening,
4560
+ isListening: listeningFor !== null
4561
+ };
4562
+ }
4396
4563
  function getFilteredGroups(activeButtons) {
4397
4564
  return BUTTON_GROUPS.map((group) => ({
4398
4565
  ...group,
@@ -4408,7 +4575,10 @@ function ControlMapper({
4408
4575
  }) {
4409
4576
  const t = useKoinTranslation();
4410
4577
  const [localControls, setLocalControls] = React2.useState(controls);
4411
- const [listeningFor, setListeningFor] = React2.useState(null);
4578
+ const { listeningFor, startListening, stopListening, isListening } = useInputCapture({
4579
+ isOpen,
4580
+ onClose
4581
+ });
4412
4582
  const activeButtons = React2.useMemo(() => {
4413
4583
  return getConsoleButtons(system || "SNES");
4414
4584
  }, [system]);
@@ -4424,27 +4594,20 @@ function ControlMapper({
4424
4594
  }
4425
4595
  }, [isOpen, controls]);
4426
4596
  React2.useEffect(() => {
4427
- if (!isOpen) {
4428
- setListeningFor(null);
4429
- return;
4430
- }
4597
+ if (!isOpen || !listeningFor) return;
4431
4598
  const handleKeyDown = (e) => {
4432
- if (!listeningFor) return;
4599
+ if (e.code === "Escape") return;
4433
4600
  e.preventDefault();
4434
4601
  e.stopPropagation();
4435
- if (e.code === "Escape") {
4436
- setListeningFor(null);
4437
- return;
4438
- }
4439
4602
  setLocalControls((prev) => ({
4440
4603
  ...prev,
4441
4604
  [listeningFor]: e.code
4442
4605
  }));
4443
- setListeningFor(null);
4606
+ stopListening();
4444
4607
  };
4445
4608
  window.addEventListener("keydown", handleKeyDown);
4446
4609
  return () => window.removeEventListener("keydown", handleKeyDown);
4447
- }, [isOpen, listeningFor]);
4610
+ }, [isOpen, listeningFor, stopListening]);
4448
4611
  const handleReset = () => {
4449
4612
  setLocalControls(defaultControls);
4450
4613
  };
@@ -4452,52 +4615,58 @@ function ControlMapper({
4452
4615
  onSave(localControls);
4453
4616
  onClose();
4454
4617
  };
4455
- if (!isOpen) return null;
4456
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
4457
- /* @__PURE__ */ jsxRuntime.jsx(
4458
- "div",
4459
- {
4460
- className: "absolute inset-0 bg-black/80 backdrop-blur-sm",
4461
- onClick: onClose
4462
- }
4463
- ),
4464
- /* @__PURE__ */ jsxRuntime.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: [
4465
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/10 bg-black/50", children: [
4466
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
4467
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Gamepad2, { className: "text-retro-primary", size: 24 }),
4468
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4469
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: t.modals.controls.title }),
4470
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: t.modals.controls.description })
4471
- ] })
4472
- ] }),
4473
- /* @__PURE__ */ jsxRuntime.jsx(
4618
+ return /* @__PURE__ */ jsxRuntime.jsx(
4619
+ ModalShell,
4620
+ {
4621
+ isOpen,
4622
+ onClose,
4623
+ title: t.modals.controls.title,
4624
+ subtitle: t.modals.controls.description,
4625
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Gamepad2, { className: "text-retro-primary", size: 24 }),
4626
+ closeOnBackdrop: !isListening,
4627
+ footer: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4628
+ /* @__PURE__ */ jsxRuntime.jsxs(
4629
+ "button",
4630
+ {
4631
+ onClick: handleReset,
4632
+ 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",
4633
+ children: [
4634
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { size: 16 }),
4635
+ t.modals.controls.reset
4636
+ ]
4637
+ }
4638
+ ),
4639
+ /* @__PURE__ */ jsxRuntime.jsxs(
4474
4640
  "button",
4475
4641
  {
4476
- onClick: onClose,
4477
- className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
4478
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 20 })
4642
+ onClick: handleSave,
4643
+ 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",
4644
+ children: [
4645
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 16 }),
4646
+ t.modals.controls.save
4647
+ ]
4479
4648
  }
4480
4649
  )
4481
4650
  ] }),
4482
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: controlGroups.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4651
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: controlGroups.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4483
4652
  /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
4484
4653
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxRuntime.jsxs(
4485
4654
  "button",
4486
4655
  {
4487
- onClick: () => setListeningFor(btn),
4656
+ onClick: () => startListening(btn),
4488
4657
  className: `
4489
- flex items-center justify-between px-4 py-3 rounded-lg border transition-all
4490
- ${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"}
4491
- `,
4658
+ flex items-center justify-between px-4 py-3 rounded-lg border transition-all
4659
+ ${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"}
4660
+ `,
4492
4661
  children: [
4493
4662
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
4494
4663
  /* @__PURE__ */ jsxRuntime.jsx(
4495
4664
  "span",
4496
4665
  {
4497
4666
  className: `
4498
- px-2 py-1 rounded text-xs font-mono
4499
- ${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
4500
- `,
4667
+ px-2 py-1 rounded text-xs font-mono
4668
+ ${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
4669
+ `,
4501
4670
  children: listeningFor === btn ? t.modals.controls.pressKey : formatKeyCode(localControls[btn] || "")
4502
4671
  }
4503
4672
  )
@@ -4505,199 +4674,10 @@ function ControlMapper({
4505
4674
  },
4506
4675
  btn
4507
4676
  )) })
4508
- ] }, group.label)) }),
4509
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4 bg-black/30 border-t border-white/10", children: [
4510
- /* @__PURE__ */ jsxRuntime.jsxs(
4511
- "button",
4512
- {
4513
- onClick: handleReset,
4514
- 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",
4515
- children: [
4516
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { size: 16 }),
4517
- t.modals.controls.reset
4518
- ]
4519
- }
4520
- ),
4521
- /* @__PURE__ */ jsxRuntime.jsxs(
4522
- "button",
4523
- {
4524
- onClick: handleSave,
4525
- 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",
4526
- children: [
4527
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 16 }),
4528
- t.modals.controls.save
4529
- ]
4530
- }
4531
- )
4532
- ] })
4533
- ] })
4534
- ] });
4535
- }
4536
- function getDisplayName(id) {
4537
- let name = id;
4538
- name = name.replace(/^[0-9a-f]{4}-[0-9a-f]{4}-/i, "");
4539
- name = name.replace(/\s*\(.*\)\s*$/i, "");
4540
- name = name.replace(/\s*STANDARD GAMEPAD\s*/i, "");
4541
- if (/xbox/i.test(name)) {
4542
- if (/series/i.test(name)) return "Xbox Series Controller";
4543
- if (/one/i.test(name)) return "Xbox One Controller";
4544
- if (/360/i.test(name)) return "Xbox 360 Controller";
4545
- return "Xbox Controller";
4546
- }
4547
- if (/dualsense/i.test(name)) return "DualSense";
4548
- if (/dualshock\s*4/i.test(name)) return "DualShock 4";
4549
- if (/dualshock/i.test(name)) return "DualShock";
4550
- if (/playstation/i.test(name) || /sony/i.test(name)) return "PlayStation Controller";
4551
- if (/pro\s*controller/i.test(name)) return "Switch Pro Controller";
4552
- if (/joy-?con/i.test(name)) return "Joy-Con";
4553
- if (/nintendo/i.test(name)) return "Nintendo Controller";
4554
- return name.trim() || "Gamepad";
4555
- }
4556
- function detectControllerBrand(id) {
4557
- const lowerId = id.toLowerCase();
4558
- if (/xbox|xinput|microsoft/i.test(lowerId)) return "xbox";
4559
- if (/playstation|sony|dualshock|dualsense/i.test(lowerId)) return "playstation";
4560
- if (/nintendo|switch|joy-?con|pro controller/i.test(lowerId)) return "nintendo";
4561
- return "generic";
4562
- }
4563
- function toGamepadInfo(gamepad) {
4564
- return {
4565
- index: gamepad.index,
4566
- id: gamepad.id,
4567
- name: getDisplayName(gamepad.id),
4568
- connected: gamepad.connected,
4569
- buttons: gamepad.buttons.length,
4570
- axes: gamepad.axes.length,
4571
- mapping: gamepad.mapping
4572
- };
4573
- }
4574
- function useGamepad(options) {
4575
- const { onConnect, onDisconnect } = options || {};
4576
- const [gamepads, setGamepads] = React2.useState([]);
4577
- const rafRef = React2.useRef(null);
4578
- const lastStateRef = React2.useRef("");
4579
- const prevCountRef = React2.useRef(0);
4580
- const onConnectRef = React2.useRef(onConnect);
4581
- const onDisconnectRef = React2.useRef(onDisconnect);
4582
- React2.useEffect(() => {
4583
- onConnectRef.current = onConnect;
4584
- onDisconnectRef.current = onDisconnect;
4585
- }, [onConnect, onDisconnect]);
4586
- const getGamepads = React2.useCallback(() => {
4587
- if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") {
4588
- return [];
4589
- }
4590
- const rawGamepads = navigator.getGamepads() ?? [];
4591
- const connected = [];
4592
- for (let i = 0; i < rawGamepads.length; i++) {
4593
- const gp = rawGamepads[i];
4594
- if (gp && gp.connected) {
4595
- connected.push(toGamepadInfo(gp));
4596
- }
4597
- }
4598
- return connected;
4599
- }, []);
4600
- const getRawGamepad = React2.useCallback((index) => {
4601
- const rawGamepads = navigator.getGamepads?.() ?? [];
4602
- return rawGamepads[index] ?? null;
4603
- }, []);
4604
- const refresh = React2.useCallback(() => {
4605
- setGamepads(getGamepads());
4606
- }, [getGamepads]);
4607
- React2.useEffect(() => {
4608
- if (typeof window === "undefined" || typeof navigator === "undefined") {
4609
- return;
4610
- }
4611
- if (typeof navigator.getGamepads !== "function") {
4612
- console.warn("[useGamepad] Gamepad API not supported in this browser");
4613
- return;
4614
- }
4615
- let isActive = true;
4616
- const poll = () => {
4617
- if (!isActive) return;
4618
- const current = getGamepads();
4619
- let hasChanged = current.length !== prevCountRef.current;
4620
- if (!hasChanged) {
4621
- for (let i = 0; i < current.length; i++) {
4622
- const saved = gamepads[i];
4623
- if (!saved || saved.id !== current[i].id || saved.connected !== current[i].connected) {
4624
- hasChanged = true;
4625
- break;
4626
- }
4627
- }
4628
- }
4629
- if (hasChanged) {
4630
- const prevCount = prevCountRef.current;
4631
- const currentCount = current.length;
4632
- if (currentCount > prevCount && prevCount >= 0 && onConnectRef.current) {
4633
- const newGamepad = current[current.length - 1];
4634
- onConnectRef.current(newGamepad);
4635
- } else if (currentCount < prevCount && prevCount > 0 && onDisconnectRef.current) {
4636
- onDisconnectRef.current();
4637
- }
4638
- prevCountRef.current = currentCount;
4639
- setGamepads(current);
4640
- }
4641
- rafRef.current = requestAnimationFrame(poll);
4642
- };
4643
- const handleConnect = (e) => {
4644
- console.log("[useGamepad] \u{1F3AE} Gamepad connected:", e.gamepad.id);
4645
- const current = getGamepads();
4646
- const prevCount = prevCountRef.current;
4647
- prevCountRef.current = current.length;
4648
- lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
4649
- setGamepads(current);
4650
- if (onConnectRef.current && current.length > prevCount) {
4651
- const newGamepad = current[current.length - 1];
4652
- onConnectRef.current(newGamepad);
4653
- }
4654
- };
4655
- const handleDisconnect = (e) => {
4656
- console.log("[useGamepad] \u{1F3AE} Gamepad disconnected:", e.gamepad.id);
4657
- const current = getGamepads();
4658
- const prevCount = prevCountRef.current;
4659
- prevCountRef.current = current.length;
4660
- lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
4661
- setGamepads(current);
4662
- if (onDisconnectRef.current && current.length < prevCount) {
4663
- onDisconnectRef.current();
4664
- }
4665
- };
4666
- window.addEventListener("gamepadconnected", handleConnect);
4667
- window.addEventListener("gamepaddisconnected", handleDisconnect);
4668
- rafRef.current = requestAnimationFrame(poll);
4669
- const initial = getGamepads();
4670
- if (initial.length > 0) {
4671
- console.log("[useGamepad] Initial gamepads found:", initial.map((g) => g.name).join(", "));
4672
- prevCountRef.current = initial.length;
4673
- setGamepads(initial);
4674
- lastStateRef.current = initial.map((g) => `${g.index}:${g.id}`).join("|");
4675
- } else {
4676
- prevCountRef.current = 0;
4677
+ ] }, group.label)) })
4677
4678
  }
4678
- return () => {
4679
- isActive = false;
4680
- if (rafRef.current) {
4681
- cancelAnimationFrame(rafRef.current);
4682
- }
4683
- window.removeEventListener("gamepadconnected", handleConnect);
4684
- window.removeEventListener("gamepaddisconnected", handleDisconnect);
4685
- };
4686
- }, [getGamepads]);
4687
- return {
4688
- gamepads,
4689
- isAnyConnected: gamepads.length > 0,
4690
- connectedCount: gamepads.length,
4691
- getRawGamepad,
4692
- refresh
4693
- };
4679
+ );
4694
4680
  }
4695
- var STANDARD_AXIS_MAP = {
4696
- leftStickX: 0,
4697
- leftStickY: 1,
4698
- rightStickX: 2,
4699
- rightStickY: 3
4700
- };
4701
4681
  function GamepadMapper({
4702
4682
  isOpen,
4703
4683
  gamepads,
@@ -4708,7 +4688,10 @@ function GamepadMapper({
4708
4688
  const t = useKoinTranslation();
4709
4689
  const [selectedPlayer, setSelectedPlayer] = React2.useState(1);
4710
4690
  const [bindings, setBindings] = React2.useState({});
4711
- const [listeningFor, setListeningFor] = React2.useState(null);
4691
+ const { listeningFor, startListening, stopListening, isListening } = useInputCapture({
4692
+ isOpen,
4693
+ onClose
4694
+ });
4712
4695
  const rafRef = React2.useRef(null);
4713
4696
  React2.useEffect(() => {
4714
4697
  if (isOpen) {
@@ -4743,7 +4726,7 @@ function GamepadMapper({
4743
4726
  [listeningFor]: i
4744
4727
  }
4745
4728
  }));
4746
- setListeningFor(null);
4729
+ stopListening();
4747
4730
  return;
4748
4731
  }
4749
4732
  }
@@ -4756,21 +4739,7 @@ function GamepadMapper({
4756
4739
  cancelAnimationFrame(rafRef.current);
4757
4740
  }
4758
4741
  };
4759
- }, [isOpen, listeningFor, selectedPlayer]);
4760
- React2.useEffect(() => {
4761
- if (!isOpen) return;
4762
- const handleKeyDown = (e) => {
4763
- if (e.code === "Escape") {
4764
- if (listeningFor) {
4765
- setListeningFor(null);
4766
- } else {
4767
- onClose();
4768
- }
4769
- }
4770
- };
4771
- window.addEventListener("keydown", handleKeyDown);
4772
- return () => window.removeEventListener("keydown", handleKeyDown);
4773
- }, [isOpen, listeningFor, onClose]);
4742
+ }, [isOpen, listeningFor, selectedPlayer, stopListening]);
4774
4743
  const handleReset = () => {
4775
4744
  setBindings((prev) => ({
4776
4745
  ...prev,
@@ -4785,127 +4754,19 @@ function GamepadMapper({
4785
4754
  onSave?.(bindings[selectedPlayer], selectedPlayer);
4786
4755
  onClose();
4787
4756
  };
4788
- if (!isOpen) return null;
4789
4757
  const currentBindings = bindings[selectedPlayer] ?? DEFAULT_GAMEPAD;
4790
4758
  const currentGamepad = gamepads.find((g) => g.index === selectedPlayer - 1);
4791
- currentGamepad ? detectControllerBrand(currentGamepad.id) : "generic";
4792
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
4793
- /* @__PURE__ */ jsxRuntime.jsx(
4794
- "div",
4795
- {
4796
- className: "absolute inset-0 bg-black/80 backdrop-blur-sm",
4797
- onClick: () => !listeningFor && onClose()
4798
- }
4799
- ),
4800
- /* @__PURE__ */ jsxRuntime.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: [
4801
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/10 bg-black/50", children: [
4802
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
4803
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Joystick, { className: "text-retro-primary", size: 24, style: { color: systemColor } }),
4804
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4805
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: t.modals.gamepad.title }),
4806
- /* @__PURE__ */ jsxRuntime.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 })
4807
- ] })
4808
- ] }),
4809
- /* @__PURE__ */ jsxRuntime.jsx(
4810
- "button",
4811
- {
4812
- onClick: onClose,
4813
- className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
4814
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 20 })
4815
- }
4816
- )
4817
- ] }),
4818
- gamepads.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-3 border-b border-white/10 bg-black/30", children: [
4819
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
4820
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400 font-medium", children: t.modals.gamepad.player }),
4821
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-1", children: gamepads.map((gp) => /* @__PURE__ */ jsxRuntime.jsxs(
4822
- "button",
4823
- {
4824
- onClick: () => setSelectedPlayer(gp.index + 1),
4825
- className: `
4826
- flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all
4827
- ${selectedPlayer === gp.index + 1 ? "bg-retro-primary/20 text-retro-primary" : "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"}
4828
- `,
4829
- style: selectedPlayer === gp.index + 1 ? {
4830
- backgroundColor: `${systemColor}20`,
4831
- color: systemColor
4832
- } : {},
4833
- children: [
4834
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.User, { size: 14 }),
4835
- "P",
4836
- gp.index + 1
4837
- ]
4838
- },
4839
- gp.index
4840
- )) })
4841
- ] }),
4842
- currentGamepad && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: currentGamepad.name })
4843
- ] }),
4844
- gamepads.length === 1 && currentGamepad && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-2 border-b border-white/10 bg-black/30", children: /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-400", children: [
4845
- currentGamepad.name,
4846
- " \u2022 Player 1"
4847
- ] }) }),
4848
- gamepads.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-10 text-center", children: [
4849
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative inline-block mb-4", children: [
4850
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Joystick, { size: 56, className: "text-gray-600 animate-pulse" }),
4851
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 -m-2 rounded-full border-2 border-dashed border-gray-600 animate-spin", style: { animationDuration: "8s" } })
4852
- ] }),
4853
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-300 font-medium mb-2", children: t.modals.gamepad.noController }),
4854
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 mb-4", children: t.modals.gamepad.pressAny }),
4855
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-white/5 border border-white/10", children: [
4856
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400", children: t.modals.gamepad.waiting }),
4857
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1", children: [
4858
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "0ms" } }),
4859
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "150ms" } }),
4860
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "300ms" } })
4861
- ] })
4862
- ] })
4863
- ] }),
4864
- gamepads.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: [
4865
- listeningFor && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 rounded-lg bg-black/50 border border-retro-primary/50 text-center animate-pulse", style: { borderColor: `${systemColor}50` }, children: [
4866
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-white mb-1", children: t.modals.gamepad.pressButton.replace("{{button}}", BUTTON_LABELS[listeningFor]) }),
4867
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: t.modals.gamepad.pressEsc })
4868
- ] }),
4869
- BUTTON_GROUPS.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4870
- /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
4871
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxRuntime.jsxs(
4872
- "button",
4873
- {
4874
- onClick: () => setListeningFor(btn),
4875
- disabled: !!listeningFor && listeningFor !== btn,
4876
- className: `
4877
- flex items-center justify-between px-4 py-3 rounded-lg border transition-all
4878
- disabled:opacity-50
4879
- ${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"}
4880
- `,
4881
- style: listeningFor === btn ? {
4882
- borderColor: systemColor,
4883
- backgroundColor: `${systemColor}20`,
4884
- boxShadow: `0 0 0 2px ${systemColor}50`
4885
- } : {},
4886
- children: [
4887
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
4888
- /* @__PURE__ */ jsxRuntime.jsx(
4889
- "span",
4890
- {
4891
- className: `
4892
- px-2 py-1 rounded text-xs font-mono
4893
- ${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
4894
- `,
4895
- style: listeningFor === btn ? {
4896
- backgroundColor: `${systemColor}30`,
4897
- color: systemColor
4898
- } : {},
4899
- children: listeningFor === btn ? t.controls.press : formatGamepadButton(currentBindings[btn])
4900
- }
4901
- )
4902
- ]
4903
- },
4904
- btn
4905
- )) })
4906
- ] }, group.label))
4907
- ] }),
4908
- gamepads.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4 bg-black/30 border-t border-white/10", children: [
4759
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4760
+ ModalShell,
4761
+ {
4762
+ isOpen,
4763
+ onClose,
4764
+ title: t.modals.gamepad.title,
4765
+ subtitle: gamepads.length > 0 ? t.modals.gamepad.connected.replace("{{count}}", gamepads.length.toString()) : t.modals.gamepad.none,
4766
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Joystick, { size: 24, style: { color: systemColor } }),
4767
+ systemColor,
4768
+ closeOnBackdrop: !isListening,
4769
+ footer: gamepads.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4909
4770
  /* @__PURE__ */ jsxRuntime.jsxs(
4910
4771
  "button",
4911
4772
  {
@@ -4931,9 +4792,101 @@ function GamepadMapper({
4931
4792
  ]
4932
4793
  }
4933
4794
  )
4934
- ] })
4935
- ] })
4936
- ] });
4795
+ ] }) : void 0,
4796
+ children: [
4797
+ gamepads.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-3 border-b border-white/10 bg-black/30", children: [
4798
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
4799
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400 font-medium", children: t.modals.gamepad.player }),
4800
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-1", children: gamepads.map((gp) => /* @__PURE__ */ jsxRuntime.jsxs(
4801
+ "button",
4802
+ {
4803
+ onClick: () => setSelectedPlayer(gp.index + 1),
4804
+ className: `
4805
+ flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all
4806
+ ${selectedPlayer === gp.index + 1 ? "bg-retro-primary/20 text-retro-primary" : "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"}
4807
+ `,
4808
+ style: selectedPlayer === gp.index + 1 ? {
4809
+ backgroundColor: `${systemColor}20`,
4810
+ color: systemColor
4811
+ } : {},
4812
+ children: [
4813
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.User, { size: 14 }),
4814
+ "P",
4815
+ gp.index + 1
4816
+ ]
4817
+ },
4818
+ gp.index
4819
+ )) })
4820
+ ] }),
4821
+ currentGamepad && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: currentGamepad.name })
4822
+ ] }),
4823
+ gamepads.length === 1 && currentGamepad && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-2 border-b border-white/10 bg-black/30", children: /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-400", children: [
4824
+ currentGamepad.name,
4825
+ " \u2022 Player 1"
4826
+ ] }) }),
4827
+ gamepads.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-10 text-center", children: [
4828
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative inline-block mb-4", children: [
4829
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Joystick, { size: 56, className: "text-gray-600 animate-pulse" }),
4830
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 -m-2 rounded-full border-2 border-dashed border-gray-600 animate-spin", style: { animationDuration: "8s" } })
4831
+ ] }),
4832
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-300 font-medium mb-2", children: t.modals.gamepad.noController }),
4833
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 mb-4", children: t.modals.gamepad.pressAny }),
4834
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-white/5 border border-white/10", children: [
4835
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400", children: t.modals.gamepad.waiting }),
4836
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1", children: [
4837
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "0ms" } }),
4838
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "150ms" } }),
4839
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "300ms" } })
4840
+ ] })
4841
+ ] })
4842
+ ] }),
4843
+ gamepads.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: [
4844
+ listeningFor && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 rounded-lg bg-black/50 border border-retro-primary/50 text-center animate-pulse", style: { borderColor: `${systemColor}50` }, children: [
4845
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-white mb-1", children: t.modals.gamepad.pressButton.replace("{{button}}", BUTTON_LABELS[listeningFor]) }),
4846
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: t.modals.gamepad.pressEsc })
4847
+ ] }),
4848
+ BUTTON_GROUPS.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4849
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
4850
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxRuntime.jsxs(
4851
+ "button",
4852
+ {
4853
+ onClick: () => startListening(btn),
4854
+ disabled: !!listeningFor && listeningFor !== btn,
4855
+ className: `
4856
+ flex items-center justify-between px-4 py-3 rounded-lg border transition-all
4857
+ disabled:opacity-50
4858
+ ${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"}
4859
+ `,
4860
+ style: listeningFor === btn ? {
4861
+ borderColor: systemColor,
4862
+ backgroundColor: `${systemColor}20`,
4863
+ boxShadow: `0 0 0 2px ${systemColor}50`
4864
+ } : {},
4865
+ children: [
4866
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
4867
+ /* @__PURE__ */ jsxRuntime.jsx(
4868
+ "span",
4869
+ {
4870
+ className: `
4871
+ px-2 py-1 rounded text-xs font-mono
4872
+ ${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
4873
+ `,
4874
+ style: listeningFor === btn ? {
4875
+ backgroundColor: `${systemColor}30`,
4876
+ color: systemColor
4877
+ } : {},
4878
+ children: listeningFor === btn ? t.controls.press : formatGamepadButton(currentBindings[btn])
4879
+ }
4880
+ )
4881
+ ]
4882
+ },
4883
+ btn
4884
+ )) })
4885
+ ] }, group.label))
4886
+ ] })
4887
+ ]
4888
+ }
4889
+ );
4937
4890
  }
4938
4891
  function CheatModal({
4939
4892
  isOpen,
@@ -4941,42 +4894,24 @@ function CheatModal({
4941
4894
  activeCheats,
4942
4895
  onToggle,
4943
4896
  onClose
4944
- }) {
4945
- const t = useKoinTranslation();
4946
- const [copiedId, setCopiedId] = React2__default.default.useState(null);
4947
- if (!isOpen) return null;
4948
- const handleCopy = async (code, id) => {
4949
- await navigator.clipboard.writeText(code);
4950
- setCopiedId(id);
4951
- setTimeout(() => setCopiedId(null), 2e3);
4952
- };
4953
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
4954
- /* @__PURE__ */ jsxRuntime.jsx(
4955
- "div",
4956
- {
4957
- className: "absolute inset-0 bg-black/80 backdrop-blur-sm",
4958
- onClick: onClose
4959
- }
4960
- ),
4961
- /* @__PURE__ */ jsxRuntime.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: [
4962
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/10 bg-black/50", children: [
4963
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
4964
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { className: "text-purple-400", size: 24 }),
4965
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4966
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: t.modals.cheats.title }),
4967
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()) })
4968
- ] })
4969
- ] }),
4970
- /* @__PURE__ */ jsxRuntime.jsx(
4971
- "button",
4972
- {
4973
- onClick: onClose,
4974
- className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
4975
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 20 })
4976
- }
4977
- )
4978
- ] }),
4979
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: cheats.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center py-12 text-gray-500", children: [
4897
+ }) {
4898
+ const t = useKoinTranslation();
4899
+ const [copiedId, setCopiedId] = React2__default.default.useState(null);
4900
+ const handleCopy = async (code, id) => {
4901
+ await navigator.clipboard.writeText(code);
4902
+ setCopiedId(id);
4903
+ setTimeout(() => setCopiedId(null), 2e3);
4904
+ };
4905
+ return /* @__PURE__ */ jsxRuntime.jsx(
4906
+ ModalShell,
4907
+ {
4908
+ isOpen,
4909
+ onClose,
4910
+ title: t.modals.cheats.title,
4911
+ subtitle: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()),
4912
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 24, className: "text-purple-400" }),
4913
+ footer: /* @__PURE__ */ jsxRuntime.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 }),
4914
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: cheats.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center py-12 text-gray-500", children: [
4980
4915
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 48, className: "mx-auto mb-3 opacity-50" }),
4981
4916
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: t.modals.cheats.emptyTitle }),
4982
4917
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm mt-1", children: t.modals.cheats.emptyDesc })
@@ -4986,18 +4921,18 @@ function CheatModal({
4986
4921
  "div",
4987
4922
  {
4988
4923
  className: `
4989
- group flex items-start gap-4 p-4 rounded-lg border transition-all cursor-pointer
4990
- ${isActive ? "border-purple-500/50 bg-purple-500/10" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
4991
- `,
4924
+ group flex items-start gap-4 p-4 rounded-lg border transition-all cursor-pointer
4925
+ ${isActive ? "border-purple-500/50 bg-purple-500/10" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
4926
+ `,
4992
4927
  onClick: () => onToggle(cheat.id),
4993
4928
  children: [
4994
4929
  /* @__PURE__ */ jsxRuntime.jsx(
4995
4930
  "div",
4996
4931
  {
4997
4932
  className: `
4998
- flex-shrink-0 w-6 h-6 rounded border-2 flex items-center justify-center transition-all
4999
- ${isActive ? "border-purple-500 bg-purple-500" : "border-gray-600 group-hover:border-gray-400"}
5000
- `,
4933
+ flex-shrink-0 w-6 h-6 rounded border-2 flex items-center justify-center transition-all
4934
+ ${isActive ? "border-purple-500 bg-purple-500" : "border-gray-600 group-hover:border-gray-400"}
4935
+ `,
5001
4936
  children: isActive && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 14, className: "text-white" })
5002
4937
  }
5003
4938
  ),
@@ -5023,10 +4958,9 @@ function CheatModal({
5023
4958
  },
5024
4959
  cheat.id
5025
4960
  );
5026
- }) }),
5027
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-3 bg-black/30 border-t border-white/10", children: /* @__PURE__ */ jsxRuntime.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 }) })
5028
- ] })
5029
- ] });
4961
+ }) })
4962
+ }
4963
+ );
5030
4964
  }
5031
4965
  var AUTO_SAVE_SLOT = 5;
5032
4966
  function formatBytes(bytes) {
@@ -5067,7 +5001,6 @@ function SaveSlotModal({
5067
5001
  onUpgrade
5068
5002
  }) {
5069
5003
  const t = useKoinTranslation();
5070
- if (!isOpen) return null;
5071
5004
  const isSaveMode = mode === "save";
5072
5005
  const allSlots = [1, 2, 3, 4, 5];
5073
5006
  const isUnlimited = maxSlots === -1 || maxSlots >= 5;
@@ -5082,33 +5015,17 @@ function SaveSlotModal({
5082
5015
  const getSlotData = (slotNum) => {
5083
5016
  return slots.find((s) => s.slot === slotNum);
5084
5017
  };
5085
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
5086
- /* @__PURE__ */ jsxRuntime.jsx(
5087
- "div",
5088
- {
5089
- className: "absolute inset-0 bg-black/80 backdrop-blur-sm",
5090
- onClick: onClose
5091
- }
5092
- ),
5093
- /* @__PURE__ */ jsxRuntime.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: [
5094
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/10 bg-black/50", children: [
5095
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
5096
- isSaveMode ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { className: "text-retro-primary", size: 24 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { className: "text-retro-primary", size: 24 }),
5097
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
5098
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: isSaveMode ? t.modals.saveSlots.saveTitle : t.modals.saveSlots.loadTitle }),
5099
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: isSaveMode ? t.modals.saveSlots.subtitleSave : t.modals.saveSlots.subtitleLoad })
5100
- ] })
5101
- ] }),
5102
- /* @__PURE__ */ jsxRuntime.jsx(
5103
- "button",
5104
- {
5105
- onClick: onClose,
5106
- className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
5107
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 20 })
5108
- }
5109
- )
5110
- ] }),
5111
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: isLoading ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-gray-400", children: [
5018
+ return /* @__PURE__ */ jsxRuntime.jsx(
5019
+ ModalShell,
5020
+ {
5021
+ isOpen,
5022
+ onClose,
5023
+ title: isSaveMode ? t.modals.saveSlots.saveTitle : t.modals.saveSlots.loadTitle,
5024
+ subtitle: isSaveMode ? t.modals.saveSlots.subtitleSave : t.modals.saveSlots.subtitleLoad,
5025
+ icon: isSaveMode ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { className: "text-retro-primary", size: 24 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { className: "text-retro-primary", size: 24 }),
5026
+ maxWidth: "md",
5027
+ footer: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 text-center w-full", children: isSaveMode ? t.modals.saveSlots.footerSave : t.modals.saveSlots.footerLoad }),
5028
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: isLoading ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-gray-400", children: [
5112
5029
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "w-8 h-8 animate-spin mb-3" }),
5113
5030
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm", children: t.modals.saveSlots.loading })
5114
5031
  ] }) : displaySlots.map((slotNum) => {
@@ -5227,10 +5144,9 @@ function SaveSlotModal({
5227
5144
  },
5228
5145
  slotNum
5229
5146
  );
5230
- }) }),
5231
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-3 bg-black/30 border-t border-white/10", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 text-center", children: isSaveMode ? t.modals.saveSlots.footerSave : t.modals.saveSlots.footerLoad }) })
5232
- ] })
5233
- ] });
5147
+ }) })
5148
+ }
5149
+ );
5234
5150
  }
5235
5151
  function BiosSelectionModal({
5236
5152
  isOpen,
@@ -5377,36 +5293,28 @@ function SettingsModal({
5377
5293
  systemColor = "#00FF41"
5378
5294
  }) {
5379
5295
  const t = useKoinTranslation();
5380
- if (!isOpen) return null;
5381
5296
  const languages = [
5382
5297
  { code: "en", name: "English" },
5383
5298
  { code: "es", name: "Espa\xF1ol" },
5384
5299
  { code: "fr", name: "Fran\xE7ais" }
5385
5300
  ];
5386
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
5387
- /* @__PURE__ */ jsxRuntime.jsx(
5388
- "div",
5389
- {
5390
- className: "absolute inset-0 bg-black/80 backdrop-blur-sm",
5391
- onClick: onClose
5392
- }
5393
- ),
5394
- /* @__PURE__ */ jsxRuntime.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: [
5395
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/10 bg-black/50", children: [
5396
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
5397
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings, { className: "text-white", size: 20 }),
5398
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: t.settings.title })
5399
- ] }),
5400
- /* @__PURE__ */ jsxRuntime.jsx(
5401
- "button",
5402
- {
5403
- onClick: onClose,
5404
- className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
5405
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 20 })
5406
- }
5407
- )
5408
- ] }),
5409
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6 space-y-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
5301
+ return /* @__PURE__ */ jsxRuntime.jsx(
5302
+ ModalShell,
5303
+ {
5304
+ isOpen,
5305
+ onClose,
5306
+ title: t.settings.title,
5307
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings, { size: 20, className: "text-white" }),
5308
+ maxWidth: "sm",
5309
+ footer: /* @__PURE__ */ jsxRuntime.jsx(
5310
+ "button",
5311
+ {
5312
+ onClick: onClose,
5313
+ className: "text-sm text-gray-500 hover:text-white transition-colors w-full text-center",
5314
+ children: t.modals.shortcuts.pressEsc
5315
+ }
5316
+ ),
5317
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6 space-y-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
5410
5318
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-400", children: [
5411
5319
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Globe, { size: 16 }),
5412
5320
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: t.settings.language })
@@ -5418,9 +5326,9 @@ function SettingsModal({
5418
5326
  {
5419
5327
  onClick: () => onLanguageChange(lang.code),
5420
5328
  className: `
5421
- flex items-center justify-between px-4 py-3 rounded-lg border transition-all
5422
- ${isActive ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
5423
- `,
5329
+ flex items-center justify-between px-4 py-3 rounded-lg border transition-all
5330
+ ${isActive ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
5331
+ `,
5424
5332
  children: [
5425
5333
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: lang.name }),
5426
5334
  isActive && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 16, style: { color: systemColor } })
@@ -5429,17 +5337,9 @@ function SettingsModal({
5429
5337
  lang.code
5430
5338
  );
5431
5339
  }) })
5432
- ] }) }),
5433
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 bg-black/30 border-t border-white/10 text-center", children: /* @__PURE__ */ jsxRuntime.jsx(
5434
- "button",
5435
- {
5436
- onClick: onClose,
5437
- className: "text-sm text-gray-500 hover:text-white transition-colors",
5438
- children: t.modals.shortcuts.pressEsc
5439
- }
5440
- ) })
5441
- ] })
5442
- ] });
5340
+ ] }) })
5341
+ }
5342
+ );
5443
5343
  }
5444
5344
  function GameModals({
5445
5345
  controlsModalOpen,
@@ -7650,6 +7550,171 @@ var useNostalgist = ({
7650
7550
  ]);
7651
7551
  return hookReturn;
7652
7552
  };
7553
+ function getDisplayName(id) {
7554
+ let name = id;
7555
+ name = name.replace(/^[0-9a-f]{4}-[0-9a-f]{4}-/i, "");
7556
+ name = name.replace(/\s*\(.*\)\s*$/i, "");
7557
+ name = name.replace(/\s*STANDARD GAMEPAD\s*/i, "");
7558
+ if (/xbox/i.test(name)) {
7559
+ if (/series/i.test(name)) return "Xbox Series Controller";
7560
+ if (/one/i.test(name)) return "Xbox One Controller";
7561
+ if (/360/i.test(name)) return "Xbox 360 Controller";
7562
+ return "Xbox Controller";
7563
+ }
7564
+ if (/dualsense/i.test(name)) return "DualSense";
7565
+ if (/dualshock\s*4/i.test(name)) return "DualShock 4";
7566
+ if (/dualshock/i.test(name)) return "DualShock";
7567
+ if (/playstation/i.test(name) || /sony/i.test(name)) return "PlayStation Controller";
7568
+ if (/pro\s*controller/i.test(name)) return "Switch Pro Controller";
7569
+ if (/joy-?con/i.test(name)) return "Joy-Con";
7570
+ if (/nintendo/i.test(name)) return "Nintendo Controller";
7571
+ return name.trim() || "Gamepad";
7572
+ }
7573
+ function detectControllerBrand(id) {
7574
+ const lowerId = id.toLowerCase();
7575
+ if (/xbox|xinput|microsoft/i.test(lowerId)) return "xbox";
7576
+ if (/playstation|sony|dualshock|dualsense/i.test(lowerId)) return "playstation";
7577
+ if (/nintendo|switch|joy-?con|pro controller/i.test(lowerId)) return "nintendo";
7578
+ return "generic";
7579
+ }
7580
+ function toGamepadInfo(gamepad) {
7581
+ return {
7582
+ index: gamepad.index,
7583
+ id: gamepad.id,
7584
+ name: getDisplayName(gamepad.id),
7585
+ connected: gamepad.connected,
7586
+ buttons: gamepad.buttons.length,
7587
+ axes: gamepad.axes.length,
7588
+ mapping: gamepad.mapping
7589
+ };
7590
+ }
7591
+ function useGamepad(options) {
7592
+ const { onConnect, onDisconnect } = options || {};
7593
+ const [gamepads, setGamepads] = React2.useState([]);
7594
+ const rafRef = React2.useRef(null);
7595
+ const lastStateRef = React2.useRef("");
7596
+ const prevCountRef = React2.useRef(0);
7597
+ const onConnectRef = React2.useRef(onConnect);
7598
+ const onDisconnectRef = React2.useRef(onDisconnect);
7599
+ React2.useEffect(() => {
7600
+ onConnectRef.current = onConnect;
7601
+ onDisconnectRef.current = onDisconnect;
7602
+ }, [onConnect, onDisconnect]);
7603
+ const getGamepads = React2.useCallback(() => {
7604
+ if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") {
7605
+ return [];
7606
+ }
7607
+ const rawGamepads = navigator.getGamepads() ?? [];
7608
+ const connected = [];
7609
+ for (let i = 0; i < rawGamepads.length; i++) {
7610
+ const gp = rawGamepads[i];
7611
+ if (gp && gp.connected) {
7612
+ connected.push(toGamepadInfo(gp));
7613
+ }
7614
+ }
7615
+ return connected;
7616
+ }, []);
7617
+ const getRawGamepad = React2.useCallback((index) => {
7618
+ const rawGamepads = navigator.getGamepads?.() ?? [];
7619
+ return rawGamepads[index] ?? null;
7620
+ }, []);
7621
+ const refresh = React2.useCallback(() => {
7622
+ setGamepads(getGamepads());
7623
+ }, [getGamepads]);
7624
+ React2.useEffect(() => {
7625
+ if (typeof window === "undefined" || typeof navigator === "undefined") {
7626
+ return;
7627
+ }
7628
+ if (typeof navigator.getGamepads !== "function") {
7629
+ console.warn("[useGamepad] Gamepad API not supported in this browser");
7630
+ return;
7631
+ }
7632
+ let isActive = true;
7633
+ const poll = () => {
7634
+ if (!isActive) return;
7635
+ const current = getGamepads();
7636
+ let hasChanged = current.length !== prevCountRef.current;
7637
+ if (!hasChanged) {
7638
+ for (let i = 0; i < current.length; i++) {
7639
+ const saved = gamepads[i];
7640
+ if (!saved || saved.id !== current[i].id || saved.connected !== current[i].connected) {
7641
+ hasChanged = true;
7642
+ break;
7643
+ }
7644
+ }
7645
+ }
7646
+ if (hasChanged) {
7647
+ const prevCount = prevCountRef.current;
7648
+ const currentCount = current.length;
7649
+ if (currentCount > prevCount && prevCount >= 0 && onConnectRef.current) {
7650
+ const newGamepad = current[current.length - 1];
7651
+ onConnectRef.current(newGamepad);
7652
+ } else if (currentCount < prevCount && prevCount > 0 && onDisconnectRef.current) {
7653
+ onDisconnectRef.current();
7654
+ }
7655
+ prevCountRef.current = currentCount;
7656
+ setGamepads(current);
7657
+ }
7658
+ rafRef.current = requestAnimationFrame(poll);
7659
+ };
7660
+ const handleConnect = (e) => {
7661
+ console.log("[useGamepad] \u{1F3AE} Gamepad connected:", e.gamepad.id);
7662
+ const current = getGamepads();
7663
+ const prevCount = prevCountRef.current;
7664
+ prevCountRef.current = current.length;
7665
+ lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
7666
+ setGamepads(current);
7667
+ if (onConnectRef.current && current.length > prevCount) {
7668
+ const newGamepad = current[current.length - 1];
7669
+ onConnectRef.current(newGamepad);
7670
+ }
7671
+ };
7672
+ const handleDisconnect = (e) => {
7673
+ console.log("[useGamepad] \u{1F3AE} Gamepad disconnected:", e.gamepad.id);
7674
+ const current = getGamepads();
7675
+ const prevCount = prevCountRef.current;
7676
+ prevCountRef.current = current.length;
7677
+ lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
7678
+ setGamepads(current);
7679
+ if (onDisconnectRef.current && current.length < prevCount) {
7680
+ onDisconnectRef.current();
7681
+ }
7682
+ };
7683
+ window.addEventListener("gamepadconnected", handleConnect);
7684
+ window.addEventListener("gamepaddisconnected", handleDisconnect);
7685
+ rafRef.current = requestAnimationFrame(poll);
7686
+ const initial = getGamepads();
7687
+ if (initial.length > 0) {
7688
+ console.log("[useGamepad] Initial gamepads found:", initial.map((g) => g.name).join(", "));
7689
+ prevCountRef.current = initial.length;
7690
+ setGamepads(initial);
7691
+ lastStateRef.current = initial.map((g) => `${g.index}:${g.id}`).join("|");
7692
+ } else {
7693
+ prevCountRef.current = 0;
7694
+ }
7695
+ return () => {
7696
+ isActive = false;
7697
+ if (rafRef.current) {
7698
+ cancelAnimationFrame(rafRef.current);
7699
+ }
7700
+ window.removeEventListener("gamepadconnected", handleConnect);
7701
+ window.removeEventListener("gamepaddisconnected", handleDisconnect);
7702
+ };
7703
+ }, [getGamepads]);
7704
+ return {
7705
+ gamepads,
7706
+ isAnyConnected: gamepads.length > 0,
7707
+ connectedCount: gamepads.length,
7708
+ getRawGamepad,
7709
+ refresh
7710
+ };
7711
+ }
7712
+ var STANDARD_AXIS_MAP = {
7713
+ leftStickX: 0,
7714
+ leftStickY: 1,
7715
+ rightStickX: 2,
7716
+ rightStickY: 3
7717
+ };
7653
7718
  function useVolume({
7654
7719
  setVolume: setVolumeInHook,
7655
7720
  toggleMute: toggleMuteInHook
@@ -9537,27 +9602,15 @@ function AchievementPopup({
9537
9602
  onDismiss,
9538
9603
  autoDismissMs = 5e3
9539
9604
  }) {
9540
- const [isVisible, setIsVisible] = React2.useState(false);
9541
- const [isExiting, setIsExiting] = React2.useState(false);
9542
- React2.useEffect(() => {
9543
- requestAnimationFrame(() => {
9544
- setIsVisible(true);
9545
- });
9546
- const timer = setTimeout(() => {
9547
- handleDismiss();
9548
- }, autoDismissMs);
9549
- return () => clearTimeout(timer);
9550
- }, [autoDismissMs]);
9551
- const handleDismiss = () => {
9552
- setIsExiting(true);
9553
- setTimeout(() => {
9554
- onDismiss();
9555
- }, 300);
9556
- };
9605
+ const { slideInRightClasses, triggerExit } = useAnimatedVisibility({
9606
+ exitDuration: 300,
9607
+ onExit: onDismiss,
9608
+ autoDismissMs
9609
+ });
9557
9610
  return /* @__PURE__ */ jsxRuntime.jsxs(
9558
9611
  "div",
9559
9612
  {
9560
- className: `fixed top-4 right-4 z-[100] transition-all duration-300 ${isVisible && !isExiting ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"}`,
9613
+ className: `fixed top-4 right-4 z-[100] transition-all duration-300 ${slideInRightClasses}`,
9561
9614
  children: [
9562
9615
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-gradient-to-r from-yellow-500 to-orange-500 blur-lg opacity-50 animate-pulse" }),
9563
9616
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative bg-gradient-to-r from-yellow-500 to-orange-500 p-[2px] rounded-lg", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-gray-900 rounded-lg p-4 flex items-center gap-4 min-w-[320px]", children: [
@@ -9594,7 +9647,7 @@ function AchievementPopup({
9594
9647
  /* @__PURE__ */ jsxRuntime.jsx(
9595
9648
  "button",
9596
9649
  {
9597
- onClick: handleDismiss,
9650
+ onClick: triggerExit,
9598
9651
  className: "flex-shrink-0 text-gray-500 hover:text-white transition-colors",
9599
9652
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 18 })
9600
9653
  }