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