koin.js 1.0.14 → 1.0.15

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
@@ -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,
@@ -3667,21 +3739,12 @@ var Dpad = React2__default.default.memo(function Dpad2({
3667
3739
  }
3668
3740
  }
3669
3741
  }, [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]);
3742
+ useTouchEvents(dpadRef, {
3743
+ onTouchStart: handleTouchStart,
3744
+ onTouchMove: handleTouchMove,
3745
+ onTouchEnd: handleTouchEnd,
3746
+ onTouchCancel: handleTouchEnd
3747
+ }, { cleanup: drag.clearDragTimer });
3685
3748
  const leftPx = displayX / 100 * containerWidth - size / 2;
3686
3749
  const topPx = displayY / 100 * containerHeight - size / 2;
3687
3750
  const dUp = "M 35,5 L 65,5 L 65,35 L 50,50 L 35,35 Z";
@@ -3881,6 +3944,33 @@ function ControlsHint({ isVisible }) {
3881
3944
  }
3882
3945
  );
3883
3946
  }
3947
+ function LockButton({
3948
+ isLocked,
3949
+ onToggle,
3950
+ systemColor = "#00FF41"
3951
+ }) {
3952
+ const Icon = isLocked ? lucideReact.Lock : lucideReact.Unlock;
3953
+ return /* @__PURE__ */ jsxRuntime.jsx(
3954
+ "button",
3955
+ {
3956
+ onClick: onToggle,
3957
+ 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",
3958
+ style: {
3959
+ backgroundColor: isLocked ? "rgba(0,0,0,0.6)" : `${systemColor}20`,
3960
+ border: `1px solid ${isLocked ? "rgba(255,255,255,0.2)" : systemColor}`
3961
+ },
3962
+ "aria-label": isLocked ? "Unlock controls for repositioning" : "Lock controls",
3963
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3964
+ Icon,
3965
+ {
3966
+ size: 18,
3967
+ style: { color: isLocked ? "rgba(255,255,255,0.6)" : systemColor }
3968
+ }
3969
+ )
3970
+ }
3971
+ );
3972
+ }
3973
+ var LOCK_KEY = "koin-controls-locked";
3884
3974
  function VirtualController({
3885
3975
  system,
3886
3976
  isRunning,
@@ -3892,7 +3982,21 @@ function VirtualController({
3892
3982
  const [pressedButtons, setPressedButtons] = React2.useState(/* @__PURE__ */ new Set());
3893
3983
  const [containerSize, setContainerSize] = React2.useState({ width: 0, height: 0 });
3894
3984
  const [isFullscreenState, setIsFullscreenState] = React2.useState(false);
3985
+ const [isLocked, setIsLocked] = React2.useState(true);
3895
3986
  const { getPosition, savePosition } = useButtonPositions();
3987
+ React2.useEffect(() => {
3988
+ const stored = localStorage.getItem(LOCK_KEY);
3989
+ if (stored !== null) {
3990
+ setIsLocked(stored === "true");
3991
+ }
3992
+ }, []);
3993
+ const toggleLock = React2.useCallback(() => {
3994
+ setIsLocked((prev) => {
3995
+ const newValue = !prev;
3996
+ localStorage.setItem(LOCK_KEY, String(newValue));
3997
+ return newValue;
3998
+ });
3999
+ }, []);
3896
4000
  const layout = getLayoutForSystem(system);
3897
4001
  const visibleButtons = layout.buttons.filter((btn) => {
3898
4002
  if (isPortrait) {
@@ -4047,6 +4151,14 @@ function VirtualController({
4047
4151
  className: "fixed inset-0 z-30 pointer-events-none",
4048
4152
  style: { touchAction: "none" },
4049
4153
  children: [
4154
+ /* @__PURE__ */ jsxRuntime.jsx(
4155
+ LockButton,
4156
+ {
4157
+ isLocked,
4158
+ onToggle: toggleLock,
4159
+ systemColor
4160
+ }
4161
+ ),
4050
4162
  /* @__PURE__ */ jsxRuntime.jsx(
4051
4163
  Dpad_default,
4052
4164
  {
@@ -4059,7 +4171,7 @@ function VirtualController({
4059
4171
  systemColor,
4060
4172
  isLandscape,
4061
4173
  customPosition: getPosition("up", isLandscape),
4062
- onPositionChange: (x, y) => savePosition("up", x, y, isLandscape)
4174
+ onPositionChange: isLocked ? void 0 : (x, y) => savePosition("up", x, y, isLandscape)
4063
4175
  }
4064
4176
  ),
4065
4177
  memoizedButtonElements.filter(({ buttonConfig }) => !DPAD_TYPES.includes(buttonConfig.type)).map(({ buttonConfig, adjustedConfig, customPosition, width, height }) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -4073,7 +4185,7 @@ function VirtualController({
4073
4185
  containerWidth: width,
4074
4186
  containerHeight: height,
4075
4187
  customPosition,
4076
- onPositionChange: (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
4188
+ onPositionChange: isLocked ? void 0 : (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
4077
4189
  isLandscape,
4078
4190
  console: layout.console
4079
4191
  },
@@ -4393,6 +4505,45 @@ var GameCanvas = React2.memo(function GameCanvas2({
4393
4505
  ] });
4394
4506
  });
4395
4507
  var GameCanvas_default = GameCanvas;
4508
+ function useInputCapture({
4509
+ isOpen,
4510
+ onClose
4511
+ }) {
4512
+ const [listeningFor, setListeningFor] = React2.useState(null);
4513
+ const startListening = React2.useCallback((target) => {
4514
+ setListeningFor(target);
4515
+ }, []);
4516
+ const stopListening = React2.useCallback(() => {
4517
+ setListeningFor(null);
4518
+ }, []);
4519
+ React2.useEffect(() => {
4520
+ if (!isOpen) {
4521
+ setListeningFor(null);
4522
+ }
4523
+ }, [isOpen]);
4524
+ React2.useEffect(() => {
4525
+ if (!isOpen) return;
4526
+ const handleKeyDown = (e) => {
4527
+ if (e.code === "Escape") {
4528
+ if (listeningFor !== null) {
4529
+ e.preventDefault();
4530
+ e.stopPropagation();
4531
+ setListeningFor(null);
4532
+ } else {
4533
+ onClose();
4534
+ }
4535
+ }
4536
+ };
4537
+ window.addEventListener("keydown", handleKeyDown);
4538
+ return () => window.removeEventListener("keydown", handleKeyDown);
4539
+ }, [isOpen, listeningFor, onClose]);
4540
+ return {
4541
+ listeningFor,
4542
+ startListening,
4543
+ stopListening,
4544
+ isListening: listeningFor !== null
4545
+ };
4546
+ }
4396
4547
  function getFilteredGroups(activeButtons) {
4397
4548
  return BUTTON_GROUPS.map((group) => ({
4398
4549
  ...group,
@@ -4408,7 +4559,10 @@ function ControlMapper({
4408
4559
  }) {
4409
4560
  const t = useKoinTranslation();
4410
4561
  const [localControls, setLocalControls] = React2.useState(controls);
4411
- const [listeningFor, setListeningFor] = React2.useState(null);
4562
+ const { listeningFor, startListening, stopListening, isListening } = useInputCapture({
4563
+ isOpen,
4564
+ onClose
4565
+ });
4412
4566
  const activeButtons = React2.useMemo(() => {
4413
4567
  return getConsoleButtons(system || "SNES");
4414
4568
  }, [system]);
@@ -4424,27 +4578,20 @@ function ControlMapper({
4424
4578
  }
4425
4579
  }, [isOpen, controls]);
4426
4580
  React2.useEffect(() => {
4427
- if (!isOpen) {
4428
- setListeningFor(null);
4429
- return;
4430
- }
4581
+ if (!isOpen || !listeningFor) return;
4431
4582
  const handleKeyDown = (e) => {
4432
- if (!listeningFor) return;
4583
+ if (e.code === "Escape") return;
4433
4584
  e.preventDefault();
4434
4585
  e.stopPropagation();
4435
- if (e.code === "Escape") {
4436
- setListeningFor(null);
4437
- return;
4438
- }
4439
4586
  setLocalControls((prev) => ({
4440
4587
  ...prev,
4441
4588
  [listeningFor]: e.code
4442
4589
  }));
4443
- setListeningFor(null);
4590
+ stopListening();
4444
4591
  };
4445
4592
  window.addEventListener("keydown", handleKeyDown);
4446
4593
  return () => window.removeEventListener("keydown", handleKeyDown);
4447
- }, [isOpen, listeningFor]);
4594
+ }, [isOpen, listeningFor, stopListening]);
4448
4595
  const handleReset = () => {
4449
4596
  setLocalControls(defaultControls);
4450
4597
  };
@@ -4452,52 +4599,58 @@ function ControlMapper({
4452
4599
  onSave(localControls);
4453
4600
  onClose();
4454
4601
  };
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(
4602
+ return /* @__PURE__ */ jsxRuntime.jsx(
4603
+ ModalShell,
4604
+ {
4605
+ isOpen,
4606
+ onClose,
4607
+ title: t.modals.controls.title,
4608
+ subtitle: t.modals.controls.description,
4609
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Gamepad2, { className: "text-retro-primary", size: 24 }),
4610
+ closeOnBackdrop: !isListening,
4611
+ footer: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4612
+ /* @__PURE__ */ jsxRuntime.jsxs(
4613
+ "button",
4614
+ {
4615
+ onClick: handleReset,
4616
+ 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",
4617
+ children: [
4618
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { size: 16 }),
4619
+ t.modals.controls.reset
4620
+ ]
4621
+ }
4622
+ ),
4623
+ /* @__PURE__ */ jsxRuntime.jsxs(
4474
4624
  "button",
4475
4625
  {
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 })
4626
+ onClick: handleSave,
4627
+ 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",
4628
+ children: [
4629
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 16 }),
4630
+ t.modals.controls.save
4631
+ ]
4479
4632
  }
4480
4633
  )
4481
4634
  ] }),
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: [
4635
+ 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
4636
  /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
4484
4637
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxRuntime.jsxs(
4485
4638
  "button",
4486
4639
  {
4487
- onClick: () => setListeningFor(btn),
4640
+ onClick: () => startListening(btn),
4488
4641
  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
- `,
4642
+ flex items-center justify-between px-4 py-3 rounded-lg border transition-all
4643
+ ${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"}
4644
+ `,
4492
4645
  children: [
4493
4646
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
4494
4647
  /* @__PURE__ */ jsxRuntime.jsx(
4495
4648
  "span",
4496
4649
  {
4497
4650
  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
- `,
4651
+ px-2 py-1 rounded text-xs font-mono
4652
+ ${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
4653
+ `,
4501
4654
  children: listeningFor === btn ? t.modals.controls.pressKey : formatKeyCode(localControls[btn] || "")
4502
4655
  }
4503
4656
  )
@@ -4505,199 +4658,10 @@ function ControlMapper({
4505
4658
  },
4506
4659
  btn
4507
4660
  )) })
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 [];
4661
+ ] }, group.label)) })
4589
4662
  }
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
- }
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
- };
4663
+ );
4694
4664
  }
4695
- var STANDARD_AXIS_MAP = {
4696
- leftStickX: 0,
4697
- leftStickY: 1,
4698
- rightStickX: 2,
4699
- rightStickY: 3
4700
- };
4701
4665
  function GamepadMapper({
4702
4666
  isOpen,
4703
4667
  gamepads,
@@ -4708,7 +4672,10 @@ function GamepadMapper({
4708
4672
  const t = useKoinTranslation();
4709
4673
  const [selectedPlayer, setSelectedPlayer] = React2.useState(1);
4710
4674
  const [bindings, setBindings] = React2.useState({});
4711
- const [listeningFor, setListeningFor] = React2.useState(null);
4675
+ const { listeningFor, startListening, stopListening, isListening } = useInputCapture({
4676
+ isOpen,
4677
+ onClose
4678
+ });
4712
4679
  const rafRef = React2.useRef(null);
4713
4680
  React2.useEffect(() => {
4714
4681
  if (isOpen) {
@@ -4743,7 +4710,7 @@ function GamepadMapper({
4743
4710
  [listeningFor]: i
4744
4711
  }
4745
4712
  }));
4746
- setListeningFor(null);
4713
+ stopListening();
4747
4714
  return;
4748
4715
  }
4749
4716
  }
@@ -4756,21 +4723,7 @@ function GamepadMapper({
4756
4723
  cancelAnimationFrame(rafRef.current);
4757
4724
  }
4758
4725
  };
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]);
4726
+ }, [isOpen, listeningFor, selectedPlayer, stopListening]);
4774
4727
  const handleReset = () => {
4775
4728
  setBindings((prev) => ({
4776
4729
  ...prev,
@@ -4785,127 +4738,19 @@ function GamepadMapper({
4785
4738
  onSave?.(bindings[selectedPlayer], selectedPlayer);
4786
4739
  onClose();
4787
4740
  };
4788
- if (!isOpen) return null;
4789
4741
  const currentBindings = bindings[selectedPlayer] ?? DEFAULT_GAMEPAD;
4790
4742
  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: [
4743
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4744
+ ModalShell,
4745
+ {
4746
+ isOpen,
4747
+ onClose,
4748
+ title: t.modals.gamepad.title,
4749
+ subtitle: gamepads.length > 0 ? t.modals.gamepad.connected.replace("{{count}}", gamepads.length.toString()) : t.modals.gamepad.none,
4750
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Joystick, { size: 24, style: { color: systemColor } }),
4751
+ systemColor,
4752
+ closeOnBackdrop: !isListening,
4753
+ footer: gamepads.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4909
4754
  /* @__PURE__ */ jsxRuntime.jsxs(
4910
4755
  "button",
4911
4756
  {
@@ -4931,9 +4776,101 @@ function GamepadMapper({
4931
4776
  ]
4932
4777
  }
4933
4778
  )
4934
- ] })
4935
- ] })
4936
- ] });
4779
+ ] }) : void 0,
4780
+ children: [
4781
+ gamepads.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-3 border-b border-white/10 bg-black/30", children: [
4782
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
4783
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400 font-medium", children: t.modals.gamepad.player }),
4784
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-1", children: gamepads.map((gp) => /* @__PURE__ */ jsxRuntime.jsxs(
4785
+ "button",
4786
+ {
4787
+ onClick: () => setSelectedPlayer(gp.index + 1),
4788
+ className: `
4789
+ flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all
4790
+ ${selectedPlayer === gp.index + 1 ? "bg-retro-primary/20 text-retro-primary" : "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"}
4791
+ `,
4792
+ style: selectedPlayer === gp.index + 1 ? {
4793
+ backgroundColor: `${systemColor}20`,
4794
+ color: systemColor
4795
+ } : {},
4796
+ children: [
4797
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.User, { size: 14 }),
4798
+ "P",
4799
+ gp.index + 1
4800
+ ]
4801
+ },
4802
+ gp.index
4803
+ )) })
4804
+ ] }),
4805
+ currentGamepad && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: currentGamepad.name })
4806
+ ] }),
4807
+ 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: [
4808
+ currentGamepad.name,
4809
+ " \u2022 Player 1"
4810
+ ] }) }),
4811
+ gamepads.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-10 text-center", children: [
4812
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative inline-block mb-4", children: [
4813
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Joystick, { size: 56, className: "text-gray-600 animate-pulse" }),
4814
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 -m-2 rounded-full border-2 border-dashed border-gray-600 animate-spin", style: { animationDuration: "8s" } })
4815
+ ] }),
4816
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-300 font-medium mb-2", children: t.modals.gamepad.noController }),
4817
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 mb-4", children: t.modals.gamepad.pressAny }),
4818
+ /* @__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: [
4819
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400", children: t.modals.gamepad.waiting }),
4820
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1", children: [
4821
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "0ms" } }),
4822
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "150ms" } }),
4823
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "300ms" } })
4824
+ ] })
4825
+ ] })
4826
+ ] }),
4827
+ gamepads.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: [
4828
+ 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: [
4829
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-white mb-1", children: t.modals.gamepad.pressButton.replace("{{button}}", BUTTON_LABELS[listeningFor]) }),
4830
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: t.modals.gamepad.pressEsc })
4831
+ ] }),
4832
+ BUTTON_GROUPS.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4833
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
4834
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxRuntime.jsxs(
4835
+ "button",
4836
+ {
4837
+ onClick: () => startListening(btn),
4838
+ disabled: !!listeningFor && listeningFor !== btn,
4839
+ className: `
4840
+ flex items-center justify-between px-4 py-3 rounded-lg border transition-all
4841
+ disabled:opacity-50
4842
+ ${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"}
4843
+ `,
4844
+ style: listeningFor === btn ? {
4845
+ borderColor: systemColor,
4846
+ backgroundColor: `${systemColor}20`,
4847
+ boxShadow: `0 0 0 2px ${systemColor}50`
4848
+ } : {},
4849
+ children: [
4850
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
4851
+ /* @__PURE__ */ jsxRuntime.jsx(
4852
+ "span",
4853
+ {
4854
+ className: `
4855
+ px-2 py-1 rounded text-xs font-mono
4856
+ ${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
4857
+ `,
4858
+ style: listeningFor === btn ? {
4859
+ backgroundColor: `${systemColor}30`,
4860
+ color: systemColor
4861
+ } : {},
4862
+ children: listeningFor === btn ? t.controls.press : formatGamepadButton(currentBindings[btn])
4863
+ }
4864
+ )
4865
+ ]
4866
+ },
4867
+ btn
4868
+ )) })
4869
+ ] }, group.label))
4870
+ ] })
4871
+ ]
4872
+ }
4873
+ );
4937
4874
  }
4938
4875
  function CheatModal({
4939
4876
  isOpen,
@@ -4941,42 +4878,24 @@ function CheatModal({
4941
4878
  activeCheats,
4942
4879
  onToggle,
4943
4880
  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: [
4881
+ }) {
4882
+ const t = useKoinTranslation();
4883
+ const [copiedId, setCopiedId] = React2__default.default.useState(null);
4884
+ const handleCopy = async (code, id) => {
4885
+ await navigator.clipboard.writeText(code);
4886
+ setCopiedId(id);
4887
+ setTimeout(() => setCopiedId(null), 2e3);
4888
+ };
4889
+ return /* @__PURE__ */ jsxRuntime.jsx(
4890
+ ModalShell,
4891
+ {
4892
+ isOpen,
4893
+ onClose,
4894
+ title: t.modals.cheats.title,
4895
+ subtitle: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()),
4896
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 24, className: "text-purple-400" }),
4897
+ 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 }),
4898
+ 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
4899
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 48, className: "mx-auto mb-3 opacity-50" }),
4981
4900
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: t.modals.cheats.emptyTitle }),
4982
4901
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm mt-1", children: t.modals.cheats.emptyDesc })
@@ -4986,18 +4905,18 @@ function CheatModal({
4986
4905
  "div",
4987
4906
  {
4988
4907
  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
- `,
4908
+ group flex items-start gap-4 p-4 rounded-lg border transition-all cursor-pointer
4909
+ ${isActive ? "border-purple-500/50 bg-purple-500/10" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
4910
+ `,
4992
4911
  onClick: () => onToggle(cheat.id),
4993
4912
  children: [
4994
4913
  /* @__PURE__ */ jsxRuntime.jsx(
4995
4914
  "div",
4996
4915
  {
4997
4916
  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
- `,
4917
+ flex-shrink-0 w-6 h-6 rounded border-2 flex items-center justify-center transition-all
4918
+ ${isActive ? "border-purple-500 bg-purple-500" : "border-gray-600 group-hover:border-gray-400"}
4919
+ `,
5001
4920
  children: isActive && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 14, className: "text-white" })
5002
4921
  }
5003
4922
  ),
@@ -5023,10 +4942,9 @@ function CheatModal({
5023
4942
  },
5024
4943
  cheat.id
5025
4944
  );
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
- ] });
4945
+ }) })
4946
+ }
4947
+ );
5030
4948
  }
5031
4949
  var AUTO_SAVE_SLOT = 5;
5032
4950
  function formatBytes(bytes) {
@@ -5067,7 +4985,6 @@ function SaveSlotModal({
5067
4985
  onUpgrade
5068
4986
  }) {
5069
4987
  const t = useKoinTranslation();
5070
- if (!isOpen) return null;
5071
4988
  const isSaveMode = mode === "save";
5072
4989
  const allSlots = [1, 2, 3, 4, 5];
5073
4990
  const isUnlimited = maxSlots === -1 || maxSlots >= 5;
@@ -5082,33 +4999,17 @@ function SaveSlotModal({
5082
4999
  const getSlotData = (slotNum) => {
5083
5000
  return slots.find((s) => s.slot === slotNum);
5084
5001
  };
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: [
5002
+ return /* @__PURE__ */ jsxRuntime.jsx(
5003
+ ModalShell,
5004
+ {
5005
+ isOpen,
5006
+ onClose,
5007
+ title: isSaveMode ? t.modals.saveSlots.saveTitle : t.modals.saveSlots.loadTitle,
5008
+ subtitle: isSaveMode ? t.modals.saveSlots.subtitleSave : t.modals.saveSlots.subtitleLoad,
5009
+ icon: isSaveMode ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { className: "text-retro-primary", size: 24 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { className: "text-retro-primary", size: 24 }),
5010
+ maxWidth: "md",
5011
+ 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 }),
5012
+ 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
5013
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "w-8 h-8 animate-spin mb-3" }),
5113
5014
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm", children: t.modals.saveSlots.loading })
5114
5015
  ] }) : displaySlots.map((slotNum) => {
@@ -5227,10 +5128,9 @@ function SaveSlotModal({
5227
5128
  },
5228
5129
  slotNum
5229
5130
  );
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
- ] });
5131
+ }) })
5132
+ }
5133
+ );
5234
5134
  }
5235
5135
  function BiosSelectionModal({
5236
5136
  isOpen,
@@ -5377,36 +5277,28 @@ function SettingsModal({
5377
5277
  systemColor = "#00FF41"
5378
5278
  }) {
5379
5279
  const t = useKoinTranslation();
5380
- if (!isOpen) return null;
5381
5280
  const languages = [
5382
5281
  { code: "en", name: "English" },
5383
5282
  { code: "es", name: "Espa\xF1ol" },
5384
5283
  { code: "fr", name: "Fran\xE7ais" }
5385
5284
  ];
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: [
5285
+ return /* @__PURE__ */ jsxRuntime.jsx(
5286
+ ModalShell,
5287
+ {
5288
+ isOpen,
5289
+ onClose,
5290
+ title: t.settings.title,
5291
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings, { size: 20, className: "text-white" }),
5292
+ maxWidth: "sm",
5293
+ footer: /* @__PURE__ */ jsxRuntime.jsx(
5294
+ "button",
5295
+ {
5296
+ onClick: onClose,
5297
+ className: "text-sm text-gray-500 hover:text-white transition-colors w-full text-center",
5298
+ children: t.modals.shortcuts.pressEsc
5299
+ }
5300
+ ),
5301
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6 space-y-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
5410
5302
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-400", children: [
5411
5303
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Globe, { size: 16 }),
5412
5304
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: t.settings.language })
@@ -5418,9 +5310,9 @@ function SettingsModal({
5418
5310
  {
5419
5311
  onClick: () => onLanguageChange(lang.code),
5420
5312
  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
- `,
5313
+ flex items-center justify-between px-4 py-3 rounded-lg border transition-all
5314
+ ${isActive ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
5315
+ `,
5424
5316
  children: [
5425
5317
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: lang.name }),
5426
5318
  isActive && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 16, style: { color: systemColor } })
@@ -5429,17 +5321,9 @@ function SettingsModal({
5429
5321
  lang.code
5430
5322
  );
5431
5323
  }) })
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
- ] });
5324
+ ] }) })
5325
+ }
5326
+ );
5443
5327
  }
5444
5328
  function GameModals({
5445
5329
  controlsModalOpen,
@@ -7650,6 +7534,171 @@ var useNostalgist = ({
7650
7534
  ]);
7651
7535
  return hookReturn;
7652
7536
  };
7537
+ function getDisplayName(id) {
7538
+ let name = id;
7539
+ name = name.replace(/^[0-9a-f]{4}-[0-9a-f]{4}-/i, "");
7540
+ name = name.replace(/\s*\(.*\)\s*$/i, "");
7541
+ name = name.replace(/\s*STANDARD GAMEPAD\s*/i, "");
7542
+ if (/xbox/i.test(name)) {
7543
+ if (/series/i.test(name)) return "Xbox Series Controller";
7544
+ if (/one/i.test(name)) return "Xbox One Controller";
7545
+ if (/360/i.test(name)) return "Xbox 360 Controller";
7546
+ return "Xbox Controller";
7547
+ }
7548
+ if (/dualsense/i.test(name)) return "DualSense";
7549
+ if (/dualshock\s*4/i.test(name)) return "DualShock 4";
7550
+ if (/dualshock/i.test(name)) return "DualShock";
7551
+ if (/playstation/i.test(name) || /sony/i.test(name)) return "PlayStation Controller";
7552
+ if (/pro\s*controller/i.test(name)) return "Switch Pro Controller";
7553
+ if (/joy-?con/i.test(name)) return "Joy-Con";
7554
+ if (/nintendo/i.test(name)) return "Nintendo Controller";
7555
+ return name.trim() || "Gamepad";
7556
+ }
7557
+ function detectControllerBrand(id) {
7558
+ const lowerId = id.toLowerCase();
7559
+ if (/xbox|xinput|microsoft/i.test(lowerId)) return "xbox";
7560
+ if (/playstation|sony|dualshock|dualsense/i.test(lowerId)) return "playstation";
7561
+ if (/nintendo|switch|joy-?con|pro controller/i.test(lowerId)) return "nintendo";
7562
+ return "generic";
7563
+ }
7564
+ function toGamepadInfo(gamepad) {
7565
+ return {
7566
+ index: gamepad.index,
7567
+ id: gamepad.id,
7568
+ name: getDisplayName(gamepad.id),
7569
+ connected: gamepad.connected,
7570
+ buttons: gamepad.buttons.length,
7571
+ axes: gamepad.axes.length,
7572
+ mapping: gamepad.mapping
7573
+ };
7574
+ }
7575
+ function useGamepad(options) {
7576
+ const { onConnect, onDisconnect } = options || {};
7577
+ const [gamepads, setGamepads] = React2.useState([]);
7578
+ const rafRef = React2.useRef(null);
7579
+ const lastStateRef = React2.useRef("");
7580
+ const prevCountRef = React2.useRef(0);
7581
+ const onConnectRef = React2.useRef(onConnect);
7582
+ const onDisconnectRef = React2.useRef(onDisconnect);
7583
+ React2.useEffect(() => {
7584
+ onConnectRef.current = onConnect;
7585
+ onDisconnectRef.current = onDisconnect;
7586
+ }, [onConnect, onDisconnect]);
7587
+ const getGamepads = React2.useCallback(() => {
7588
+ if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") {
7589
+ return [];
7590
+ }
7591
+ const rawGamepads = navigator.getGamepads() ?? [];
7592
+ const connected = [];
7593
+ for (let i = 0; i < rawGamepads.length; i++) {
7594
+ const gp = rawGamepads[i];
7595
+ if (gp && gp.connected) {
7596
+ connected.push(toGamepadInfo(gp));
7597
+ }
7598
+ }
7599
+ return connected;
7600
+ }, []);
7601
+ const getRawGamepad = React2.useCallback((index) => {
7602
+ const rawGamepads = navigator.getGamepads?.() ?? [];
7603
+ return rawGamepads[index] ?? null;
7604
+ }, []);
7605
+ const refresh = React2.useCallback(() => {
7606
+ setGamepads(getGamepads());
7607
+ }, [getGamepads]);
7608
+ React2.useEffect(() => {
7609
+ if (typeof window === "undefined" || typeof navigator === "undefined") {
7610
+ return;
7611
+ }
7612
+ if (typeof navigator.getGamepads !== "function") {
7613
+ console.warn("[useGamepad] Gamepad API not supported in this browser");
7614
+ return;
7615
+ }
7616
+ let isActive = true;
7617
+ const poll = () => {
7618
+ if (!isActive) return;
7619
+ const current = getGamepads();
7620
+ let hasChanged = current.length !== prevCountRef.current;
7621
+ if (!hasChanged) {
7622
+ for (let i = 0; i < current.length; i++) {
7623
+ const saved = gamepads[i];
7624
+ if (!saved || saved.id !== current[i].id || saved.connected !== current[i].connected) {
7625
+ hasChanged = true;
7626
+ break;
7627
+ }
7628
+ }
7629
+ }
7630
+ if (hasChanged) {
7631
+ const prevCount = prevCountRef.current;
7632
+ const currentCount = current.length;
7633
+ if (currentCount > prevCount && prevCount >= 0 && onConnectRef.current) {
7634
+ const newGamepad = current[current.length - 1];
7635
+ onConnectRef.current(newGamepad);
7636
+ } else if (currentCount < prevCount && prevCount > 0 && onDisconnectRef.current) {
7637
+ onDisconnectRef.current();
7638
+ }
7639
+ prevCountRef.current = currentCount;
7640
+ setGamepads(current);
7641
+ }
7642
+ rafRef.current = requestAnimationFrame(poll);
7643
+ };
7644
+ const handleConnect = (e) => {
7645
+ console.log("[useGamepad] \u{1F3AE} Gamepad connected:", e.gamepad.id);
7646
+ const current = getGamepads();
7647
+ const prevCount = prevCountRef.current;
7648
+ prevCountRef.current = current.length;
7649
+ lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
7650
+ setGamepads(current);
7651
+ if (onConnectRef.current && current.length > prevCount) {
7652
+ const newGamepad = current[current.length - 1];
7653
+ onConnectRef.current(newGamepad);
7654
+ }
7655
+ };
7656
+ const handleDisconnect = (e) => {
7657
+ console.log("[useGamepad] \u{1F3AE} Gamepad disconnected:", e.gamepad.id);
7658
+ const current = getGamepads();
7659
+ const prevCount = prevCountRef.current;
7660
+ prevCountRef.current = current.length;
7661
+ lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
7662
+ setGamepads(current);
7663
+ if (onDisconnectRef.current && current.length < prevCount) {
7664
+ onDisconnectRef.current();
7665
+ }
7666
+ };
7667
+ window.addEventListener("gamepadconnected", handleConnect);
7668
+ window.addEventListener("gamepaddisconnected", handleDisconnect);
7669
+ rafRef.current = requestAnimationFrame(poll);
7670
+ const initial = getGamepads();
7671
+ if (initial.length > 0) {
7672
+ console.log("[useGamepad] Initial gamepads found:", initial.map((g) => g.name).join(", "));
7673
+ prevCountRef.current = initial.length;
7674
+ setGamepads(initial);
7675
+ lastStateRef.current = initial.map((g) => `${g.index}:${g.id}`).join("|");
7676
+ } else {
7677
+ prevCountRef.current = 0;
7678
+ }
7679
+ return () => {
7680
+ isActive = false;
7681
+ if (rafRef.current) {
7682
+ cancelAnimationFrame(rafRef.current);
7683
+ }
7684
+ window.removeEventListener("gamepadconnected", handleConnect);
7685
+ window.removeEventListener("gamepaddisconnected", handleDisconnect);
7686
+ };
7687
+ }, [getGamepads]);
7688
+ return {
7689
+ gamepads,
7690
+ isAnyConnected: gamepads.length > 0,
7691
+ connectedCount: gamepads.length,
7692
+ getRawGamepad,
7693
+ refresh
7694
+ };
7695
+ }
7696
+ var STANDARD_AXIS_MAP = {
7697
+ leftStickX: 0,
7698
+ leftStickY: 1,
7699
+ rightStickX: 2,
7700
+ rightStickY: 3
7701
+ };
7653
7702
  function useVolume({
7654
7703
  setVolume: setVolumeInHook,
7655
7704
  toggleMute: toggleMuteInHook
@@ -9537,27 +9586,15 @@ function AchievementPopup({
9537
9586
  onDismiss,
9538
9587
  autoDismissMs = 5e3
9539
9588
  }) {
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
- };
9589
+ const { slideInRightClasses, triggerExit } = useAnimatedVisibility({
9590
+ exitDuration: 300,
9591
+ onExit: onDismiss,
9592
+ autoDismissMs
9593
+ });
9557
9594
  return /* @__PURE__ */ jsxRuntime.jsxs(
9558
9595
  "div",
9559
9596
  {
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"}`,
9597
+ className: `fixed top-4 right-4 z-[100] transition-all duration-300 ${slideInRightClasses}`,
9561
9598
  children: [
9562
9599
  /* @__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
9600
  /* @__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 +9631,7 @@ function AchievementPopup({
9594
9631
  /* @__PURE__ */ jsxRuntime.jsx(
9595
9632
  "button",
9596
9633
  {
9597
- onClick: handleDismiss,
9634
+ onClick: triggerExit,
9598
9635
  className: "flex-shrink-0 text-gray-500 hover:text-white transition-colors",
9599
9636
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 18 })
9600
9637
  }