@tastic/shell 0.6.0 → 0.7.0

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
@@ -35,7 +35,6 @@ __export(index_exports, {
35
35
  GUEST_PLAYER_ID: () => GUEST_PLAYER_ID,
36
36
  HomeScreen: () => HomeScreen,
37
37
  LoadoutRoute: () => LoadoutRoute,
38
- MATCH_CHIPS_DESKTOP_MIN_WIDTH: () => MATCH_CHIPS_DESKTOP_MIN_WIDTH,
39
38
  MATCH_CHIPS_FADE_MS: () => MATCH_CHIPS_FADE_MS,
40
39
  MatchChips: () => MatchChips,
41
40
  PREVIEW_ASPECT: () => PREVIEW_ASPECT,
@@ -67,12 +66,13 @@ __export(index_exports, {
67
66
  profileSelectionActions: () => profileSelectionActions,
68
67
  profileSelectionReducer: () => profileSelectionSlice_default,
69
68
  roundModeFor: () => roundModeFor,
70
- safeBack: () => import_core7.safeBack,
69
+ safeBack: () => import_core8.safeBack,
71
70
  seatColorsActions: () => seatColorsActions,
72
71
  seatPlayerKey: () => seatPlayerKey,
73
72
  seatTwoFor: () => seatTwoFor,
74
73
  useActiveRound: () => useActiveRound,
75
74
  useAppSettings: () => useAppSettings,
75
+ useAtComputer: () => useAtComputer,
76
76
  useGameGuide: () => useGameGuide,
77
77
  useOpponent: () => useOpponent,
78
78
  usePack: () => usePack,
@@ -525,7 +525,7 @@ var styles = import_react_native2.StyleSheet.create({
525
525
  // src/screens/GameRoute.tsx
526
526
  var import_expo_router5 = require("expo-router");
527
527
  var import_react14 = require("react");
528
- var import_react_native5 = require("react-native");
528
+ var import_react_native6 = require("react-native");
529
529
 
530
530
  // src/routes.ts
531
531
  var playHref = (game) => ({ pathname: "/play/[game]", params: { game } });
@@ -535,91 +535,66 @@ var PROFILES_HREF = "/profiles";
535
535
 
536
536
  // src/screens/LoadoutScreen.tsx
537
537
  var import_auto_paper4 = require("@rific/auto-paper");
538
- var import_core9 = require("@tastic/core");
538
+ var import_core10 = require("@tastic/core");
539
539
  var import_chrome4 = require("@tastic/hud/chrome");
540
540
  var import_lobby2 = require("@tastic/hud/lobby");
541
541
  var import_split_screen = require("@tastic/split-screen");
542
542
  var import_expo_router4 = require("expo-router");
543
543
  var import_react13 = require("react");
544
- var import_react_native4 = require("react-native");
544
+ var import_react_native5 = require("react-native");
545
545
  var import_react_native_reanimated2 = __toESM(require("react-native-reanimated"));
546
546
  var import_react_native_safe_area_context2 = require("react-native-safe-area-context");
547
547
  var import_react_redux9 = require("react-redux");
548
548
 
549
- // src/components/SeatTwoPanel.tsx
549
+ // src/components/OpponentPicker.tsx
550
550
  var import_lobby = require("@tastic/hud/lobby");
551
551
  var import_profile4 = require("@tastic/profile");
552
552
  var import_react9 = require("react");
553
553
  var import_react_native3 = require("react-native");
554
554
  var import_jsx_runtime10 = require("react/jsx-runtime");
555
555
  var CPU_PREFIX = "cpu:";
556
- var PROFILE_PREFIX = "profile:";
557
556
  var NOBODY = "nobody";
558
- var GUEST = "guest";
559
- var GUEST_ICON = "account-outline";
557
+ var PERSON = "person";
558
+ var PERSON_ICON = "account-outline";
560
559
  var NOBODY_ICON = "account-off-outline";
561
560
  var CPU_ICON = "robot";
562
- function getSeatTwoPopoverId(idPrefix) {
563
- return `${idPrefix}-seat`;
561
+ function getOpponentPopoverId(idPrefix) {
562
+ return `${idPrefix}-opponent`;
564
563
  }
565
- function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDifficulty, profiles, selectedProfileId, onProfileSelect, color, onColorChange, swatches, takenColor, allowSwapTaken, colorCaption, dark, secondPicker, secondPickerId, ready, onToggleReady, showReadyButton = true, previewValue, triggerStyle, usePickerAlign }) {
566
- const isPerson = seatTwo === "person";
564
+ function OpponentPicker({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDifficulty, color, onColorChange, swatches, takenColor, allowSwapTaken, dark, previewValue, triggerStyle, usePickerAlign }) {
567
565
  const drawn = previewValue ? previewValue(color) : color;
568
- const selectedProfile = isPerson ? profiles.find((p) => p.id === selectedProfileId) ?? null : null;
569
566
  const cpuOptions = cpuDifficulty?.options;
570
567
  const options = (0, import_react9.useMemo)(() => {
571
568
  const list = [];
572
569
  if (choices.includes("cpu")) list.push(...cpuOptions ? cpuOptions.map((o) => ({ value: `${CPU_PREFIX}${o.value}`, label: o.label, icon: o.icon })) : [{ value: CPU_PREFIX, label: "CPU", icon: CPU_ICON }]);
573
- if (choices.includes("nobody")) list.push({ value: NOBODY, label: "Nobody", icon: NOBODY_ICON });
574
- if (choices.includes("person")) {
575
- list.push({ value: GUEST, label: "Player 2", icon: GUEST_ICON });
576
- list.push(...[...profiles].sort((a, b) => a.name.localeCompare(b.name)).map((p) => ({ value: `${PROFILE_PREFIX}${p.id}`, label: p.name, icon: "account", color: previewValue ? previewValue(p.color) : p.color })));
577
- }
570
+ if (choices.includes("nobody")) list.push({ value: NOBODY, label: "Solo", icon: NOBODY_ICON, dividerBefore: true });
571
+ if (choices.includes("person")) list.push({ value: PERSON, label: "Player", icon: PERSON_ICON, dividerBefore: true });
578
572
  return list;
579
- }, [choices, cpuOptions, profiles, previewValue]);
580
- const value = seatTwo === "cpu" ? `${CPU_PREFIX}${cpuDifficulty?.value ?? ""}` : seatTwo === "nobody" ? NOBODY : selectedProfile ? `${PROFILE_PREFIX}${selectedProfile.id}` : GUEST;
581
- const icon = seatTwo === "cpu" ? CPU_ICON : seatTwo === "nobody" ? NOBODY_ICON : selectedProfile ? "account" : GUEST_ICON;
573
+ }, [choices, cpuOptions]);
574
+ const value = seatTwo === "cpu" ? `${CPU_PREFIX}${cpuDifficulty?.value ?? ""}` : seatTwo === "nobody" ? NOBODY : PERSON;
575
+ const icon = seatTwo === "cpu" ? CPU_ICON : seatTwo === "nobody" ? NOBODY_ICON : PERSON_ICON;
582
576
  const handleChange = (next) => {
583
577
  if (next.startsWith(CPU_PREFIX)) {
584
578
  cpuDifficulty?.onChange(next.slice(CPU_PREFIX.length));
585
579
  onSeatTwoChange("cpu");
586
- } else if (next === NOBODY) {
587
- onSeatTwoChange("nobody");
588
580
  } else {
589
- onProfileSelect(next.startsWith(PROFILE_PREFIX) ? next.slice(PROFILE_PREFIX.length) : null);
590
- onSeatTwoChange("person");
581
+ onSeatTwoChange(next === NOBODY ? "nobody" : "person");
591
582
  }
592
583
  };
593
- const colorSection = seatTwo === "nobody" ? void 0 : { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, caption: selectedProfile ? colorCaption : void 0, previewValue };
594
- const pickerId = getSeatTwoPopoverId(idPrefix);
595
- const size = (0, import_profile4.getOptionPickerContentSize)(options.length, colorSection);
584
+ const colorSection = seatTwo === "cpu" ? { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, previewValue } : void 0;
585
+ const pickerId = getOpponentPopoverId(idPrefix);
586
+ const size = (0, import_profile4.getOptionPickerContentSize)(options, colorSection);
596
587
  const useAlign = usePickerAlign ?? import_lobby.useZoneClampedAlign;
597
588
  const align = useAlign(host.openId === pickerId, size.width, size.height);
598
- const ownPopoverOpen = host.openId?.startsWith(`${idPrefix}-`) ?? false;
599
- const pickerRowOpen = secondPickerId !== void 0 && host.openId === secondPickerId;
600
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native3.View, { style: [styles2.panel, ownPopoverOpen && styles2.panelOpen], children: [
601
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_profile4.OptionPicker, { id: pickerId, host, options, value, onChange: handleChange, color: drawn, dark, icon, colorSection, alignOverride: align, triggerStyle }),
602
- isPerson && secondPicker && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native3.View, { style: [styles2.pickerRow, pickerRowOpen && styles2.pickerRowOpen], children: secondPicker }),
603
- isPerson && showReadyButton && onToggleReady && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lobby.ReadyButton, { color: drawn, ready: ready ?? false, onToggleReady })
604
- ] });
589
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native3.View, { style: [styles2.button, host.openId === pickerId && styles2.buttonOpen], children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_profile4.OptionPicker, { id: pickerId, host, options, value, onChange: handleChange, color: drawn, dark, icon, colorSection, alignOverride: align, triggerStyle }) });
605
590
  }
606
591
  var styles2 = import_react_native3.StyleSheet.create({
607
- panel: {
608
- alignItems: "center",
609
- gap: 12
610
- },
611
- // A popover escaping this panel needs the panel itself elevated to paint above a later sibling.
612
- panelOpen: {
613
- zIndex: 100
614
- },
615
- pickerRow: {
616
- alignItems: "center",
617
- flexDirection: "row",
618
- gap: 12
592
+ // Its own column, so a row of seat buttons doesn't stretch the pill to the tallest of them.
593
+ button: {
594
+ alignItems: "center"
619
595
  },
620
- // Lifts the controls picker's popover above the Ready button below it (each View is its own
621
- // stacking context on the web).
622
- pickerRowOpen: {
596
+ // A popover escaping this button needs the button itself elevated to paint above a later sibling.
597
+ buttonOpen: {
623
598
  zIndex: 100
624
599
  }
625
600
  });
@@ -671,14 +646,22 @@ function SettingsDialog({ visible, onDismiss, onShowHowToPlay, children }) {
671
646
  ] });
672
647
  }
673
648
 
649
+ // src/hooks/useAtComputer.ts
650
+ var import_core6 = require("@tastic/core");
651
+ var import_react_native4 = require("react-native");
652
+ function useAtComputer() {
653
+ const touchPrimary = (0, import_core6.useIsTouchPrimaryDevice)();
654
+ return import_react_native4.Platform.OS === "web" && !touchPrimary;
655
+ }
656
+
674
657
  // src/hooks/useOpponent.ts
675
658
  var import_react11 = require("react");
676
659
  var import_react_redux7 = require("react-redux");
677
660
 
678
661
  // src/redux/opponentSlice.ts
679
- var import_core6 = require("@rific/core");
662
+ var import_core7 = require("@rific/core");
680
663
  var DEFAULT_OPPONENT = { seatTwo: "cpu" };
681
- var { actions: actions4, reducer: reducer4 } = (0, import_core6.createSettingsSlice)("opponent", {
664
+ var { actions: actions4, reducer: reducer4 } = (0, import_core7.createSettingsSlice)("opponent", {
682
665
  initialState: DEFAULT_OPPONENT
683
666
  });
684
667
  var opponentActions = actions4;
@@ -811,13 +794,13 @@ function useSeatColors(p2IsHuman) {
811
794
  }
812
795
 
813
796
  // src/navigation.ts
814
- var import_core7 = require("@rific/core");
797
+ var import_core8 = require("@rific/core");
815
798
  var import_expo_router3 = require("expo-router");
816
- (0, import_core7.configureNavigation)({ router: import_expo_router3.router });
799
+ (0, import_core8.configureNavigation)({ router: import_expo_router3.router });
817
800
 
818
801
  // src/redux/liveplaySlice.ts
819
- var import_core8 = require("@rific/core");
820
- var { actions: actions5, reducer: reducer5 } = (0, import_core8.createSettingsSlice)("liveplay", {
802
+ var import_core9 = require("@rific/core");
803
+ var { actions: actions5, reducer: reducer5 } = (0, import_core9.createSettingsSlice)("liveplay", {
821
804
  initialState: { activelyPlaying: false, activeRound: null }
822
805
  });
823
806
  var liveplayActions = actions5;
@@ -826,6 +809,8 @@ var liveplaySlice_default = reducer5;
826
809
  // src/screens/LoadoutScreen.tsx
827
810
  var import_jsx_runtime12 = require("react/jsx-runtime");
828
811
  var PANEL_SWAP_FADE_MS = 180;
812
+ var P1_CONTROL_ID = "p1-control";
813
+ var P2_CONTROL_ID = "p2-control";
829
814
  function LoadoutScreen({ game }) {
830
815
  const pack = usePack();
831
816
  const { seatTwo: savedSeatTwo, setSeatTwo } = useOpponent();
@@ -837,8 +822,10 @@ function LoadoutScreen({ game }) {
837
822
  const loadout = game.useLoadout(mode);
838
823
  const { openFor } = useGameGuide();
839
824
  const { profiles, lastSelected, selectProfile } = useProfiles();
840
- const { orientationMode } = (0, import_core9.useOrientationState)();
841
- const panelMode = p2IsHuman && !game.sideBySideWhenWide ? "faceToFace" : orientationMode;
825
+ const { orientationMode } = (0, import_core10.useOrientationState)();
826
+ const atComputer = useAtComputer();
827
+ const twoPlayerMode = game.sideBySideAtComputer && atComputer ? "sideBySide" : game.sideBySideWhenWide ? orientationMode : "faceToFace";
828
+ const panelMode = p2IsHuman ? twoPlayerMode : orientationMode;
842
829
  const { panelLayout, panelFadeStyle } = (0, import_split_screen.useDualZoneLayout)(panelMode, false, true, false, PANEL_SWAP_FADE_MS);
843
830
  const playersRowFade = panelFadeStyle;
844
831
  const [settingsOpen, setSettingsOpen] = (0, import_react13.useState)(false);
@@ -847,7 +834,7 @@ function LoadoutScreen({ game }) {
847
834
  const { p1Color, p2Color, handleP1ColorChange, handleP2ColorChange } = useSeatColors(p2IsHuman);
848
835
  const dispatch = (0, import_react_redux9.useDispatch)();
849
836
  const handleP1ProfileSelect = (0, import_react13.useCallback)((profile) => selectProfile(1, profile?.id ?? null), [selectProfile]);
850
- const handleP2ProfileSelect = (0, import_react13.useCallback)((profileId) => selectProfile(2, profileId), [selectProfile]);
837
+ const handleP2ProfileSelect = (0, import_react13.useCallback)((profile) => selectProfile(2, profile?.id ?? null), [selectProfile]);
851
838
  const [ready, setReady] = (0, import_react13.useState)({ 1: false, 2: false });
852
839
  (0, import_expo_router4.useFocusEffect)(
853
840
  (0, import_react13.useCallback)(() => {
@@ -878,16 +865,16 @@ function LoadoutScreen({ game }) {
878
865
  const p1Drawn = seatPreview ? seatPreview(p1Color) : p1Color;
879
866
  const p2Drawn = seatPreview ? seatPreview(p2Color) : p2Color;
880
867
  const triggerStyle = (0, import_react13.useMemo)(() => pack.Backdrop ? [styles3.pill, { backgroundColor: themeColors.surface }] : void 0, [pack.Backdrop, themeColors.surface]);
868
+ const readySurface = pack.Backdrop ? themeColors.surface : void 0;
881
869
  const isPortrait = panelLayout.orientationMode === "faceToFace";
882
870
  const showMetaInSharedBand = (0, import_split_screen.needsSharedNeutralZone)(panelLayout.orientationMode, humanSeats.length);
883
871
  const p2ZoneStyle = p2IsHuman ? isPortrait ? styles3.p2ZoneTop : styles3.p2ZoneRight : null;
884
- const showReadyButton = p2IsHuman;
885
872
  const seatPickerAlign = p2IsHuman ? void 0 : pack.usePickerAlign;
886
873
  const layoutKey = p2IsHuman ? "zones" : "row";
887
874
  const controlsHost = (0, import_chrome4.usePopoverHost)();
888
- const p2PanelHost = (0, import_chrome4.usePopoverHost)();
889
- const p2Host = p2IsHuman ? p2PanelHost : controlsHost;
890
- const anyPlayerPickerOpen = !!(controlsHost.openId?.startsWith("p1-") || controlsHost.openId?.startsWith("p2-"));
875
+ const p2Host = (0, import_chrome4.usePopoverHost)();
876
+ const p1PickerOpen = controlsHost.openId?.startsWith("p1-") ?? false;
877
+ const p1ControlOpen = controlsHost.openId === P1_CONTROL_ID;
891
878
  const optionsRow = loadout.options({
892
879
  host: controlsHost,
893
880
  accentColor: themeColors.tertiary,
@@ -899,36 +886,44 @@ function LoadoutScreen({ game }) {
899
886
  onReset: showMetaInSharedBand ? void 0 : loadout.reset
900
887
  });
901
888
  const optionsPopoverOpen = controlsHost.openId !== null && loadout.optionIds.includes(controlsHost.openId);
902
- const sharedControls = showMetaInSharedBand ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lobby2.SharedActionBand, { onBack: import_core7.safeBack, onSettings: () => setSettingsOpen(true), onRandomize: loadout.randomize, onReset: loadout.reset, fg, popoverOpen: optionsPopoverOpen, children: optionsRow }) : optionsRow;
889
+ const sharedControls = showMetaInSharedBand ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lobby2.SharedActionBand, { onBack: import_core8.safeBack, onSettings: () => setSettingsOpen(true), onRandomize: loadout.randomize, onReset: loadout.reset, fg, popoverOpen: optionsPopoverOpen, children: optionsRow }) : optionsRow;
903
890
  const controlLook = { accentColor: themeColors.tertiary, mutedColor: fgMuted, dark };
904
891
  const p1Control = loadout.seatControl(1, controlsHost, { ...controlLook, seatColor: p1Drawn });
905
- const p1Panel = /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lobby2.PlayerSetupPanel, { idPrefix: "p1", host: controlsHost, color: p1Color, onColorChange: handleP1ColorChange, swatches: import_auto_paper4.defaultColors, takenColor: p2Exists ? p2Color : void 0, allowSwapTaken: mode === "vsCpu", colorCaption: pack.colorCaption, isHuman: true, ready: ready[1], onToggleReady: () => setReady((r) => ({ ...r, 1: !r[1] })), dark, showReadyButton, secondPicker: p1Control, secondPickerId: p1Control ? "p1-control" : void 0, profiles, selectedProfileId: lastSelected[1], takenProfileId: p2IsHuman ? lastSelected[2] : null, guestLabel: pack.guestLabel ?? "P1", nullLabel: pack.nullLabel, onProfileSelect: handleP1ProfileSelect, onManageProfiles: () => import_expo_router4.router.push(PROFILES_HREF), previewValue: seatPreview, triggerStyle, useSeatPickerAlign: seatPickerAlign }, `p1-${layoutKey}`);
892
+ const p1Panel = /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lobby2.PlayerSetupPanel, { idPrefix: "p1", host: controlsHost, color: p1Color, onColorChange: handleP1ColorChange, swatches: import_auto_paper4.defaultColors, takenColor: p2Exists ? p2Color : void 0, allowSwapTaken: mode === "vsCpu", colorCaption: pack.colorCaption, isHuman: true, dark, showReadyButton: false, profiles, selectedProfileId: lastSelected[1], takenProfileId: p2IsHuman ? lastSelected[2] : null, guestLabel: pack.guestLabel ?? "P1", nullLabel: pack.nullLabel, onProfileSelect: handleP1ProfileSelect, onManageProfiles: () => import_expo_router4.router.push(PROFILES_HREF), previewValue: seatPreview, triggerStyle, useSeatPickerAlign: seatPickerAlign }, `p1-${layoutKey}`);
893
+ const opponentPicker = game.seatTwo && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(OpponentPicker, { idPrefix: "p1", host: controlsHost, choices: game.seatTwo, seatTwo, onSeatTwoChange: changeSeatTwo, cpuDifficulty: loadout.cpuDifficulty, color: p2Color, onColorChange: handleP2ColorChange, swatches: import_auto_paper4.defaultColors, takenColor: p1Color, allowSwapTaken: mode === "vsCpu", dark, previewValue: seatPreview, triggerStyle, usePickerAlign: seatPickerAlign }, `opponent-${layoutKey}`);
894
+ const toggleP1Ready = () => setReady((r) => ({ ...r, 1: !r[1] }));
895
+ const seatOne = /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native5.View, { style: [styles3.seatOne, !p2IsHuman && styles3.seatOneAlone, p1PickerOpen && styles3.raised], children: [
896
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native5.View, { style: [styles3.seatOneButtons, p1PickerOpen && styles3.raised], children: [
897
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native_reanimated2.default.View, { style: [styles3.playersRow, p1PickerOpen && styles3.raised, !p2IsHuman && playersRowFade], children: [
898
+ p1Panel,
899
+ opponentPicker
900
+ ] }),
901
+ p1Control && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native5.View, { style: [styles3.controlRow, p1ControlOpen && styles3.raised], children: p1Control })
902
+ ] }),
903
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lobby2.ReadyButton, { color: p1Drawn, ready: ready[1], onToggleReady: toggleP1Ready, surfaceColor: readySurface, dark, style: !p2IsHuman && p1PickerOpen && styles3.readyButtonHidden })
904
+ ] });
906
905
  const p2Control = p2IsHuman ? loadout.seatControl(2, p2Host, { ...controlLook, seatColor: p2Drawn }) : void 0;
907
- const p2Panel = game.seatTwo && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SeatTwoPanel, { idPrefix: "p2", host: p2Host, choices: game.seatTwo, seatTwo, onSeatTwoChange: changeSeatTwo, cpuDifficulty: loadout.cpuDifficulty, profiles, selectedProfileId: lastSelected[2], onProfileSelect: handleP2ProfileSelect, color: p2Color, onColorChange: handleP2ColorChange, swatches: import_auto_paper4.defaultColors, takenColor: p1Color, allowSwapTaken: mode === "vsCpu", colorCaption: pack.colorCaption, dark, secondPicker: p2Control, secondPickerId: p2Control ? "p2-control" : void 0, ready: ready[2], onToggleReady: () => setReady((r) => ({ ...r, 2: !r[2] })), showReadyButton, previewValue: seatPreview, triggerStyle, usePickerAlign: seatPickerAlign }, `p2-${layoutKey}`);
908
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native4.View, { style: [styles3.container, { backgroundColor: bg }], children: [
909
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native4.View, { style: styles3.frame, children: [
906
+ const p2Panel = p2IsHuman && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lobby2.PlayerSetupPanel, { idPrefix: "p2", host: p2Host, color: p2Color, onColorChange: handleP2ColorChange, swatches: import_auto_paper4.defaultColors, takenColor: p1Color, colorCaption: pack.colorCaption, isHuman: true, ready: ready[2], onToggleReady: () => setReady((r) => ({ ...r, 2: !r[2] })), dark, secondPicker: p2Control, secondPickerId: p2Control ? P2_CONTROL_ID : void 0, profiles, selectedProfileId: lastSelected[2], takenProfileId: lastSelected[1], guestLabel: pack.guestLabel ?? "P2", nullLabel: pack.nullLabel, onProfileSelect: handleP2ProfileSelect, previewValue: seatPreview, triggerStyle, readySurfaceColor: readySurface }, "p2-zones");
907
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native5.View, { style: [styles3.container, { backgroundColor: bg }], children: [
908
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native5.View, { style: styles3.frame, children: [
910
909
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_chrome4.PressAwayOverlay, { active: controlsHost.openId !== null, onPress: controlsHost.close }),
911
910
  p2ZoneStyle && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_chrome4.PressAwayOverlay, { active: controlsHost.openId !== null || p2Host.openId !== null, onPress: p2Host.close, style: p2ZoneStyle }),
912
- !showMetaInSharedBand && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_chrome4.CornerActionButtons, { onBack: import_core7.safeBack, onSettings: () => setSettingsOpen(true), fg, insets }),
911
+ !showMetaInSharedBand && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_chrome4.CornerActionButtons, { onBack: import_core8.safeBack, onSettings: () => setSettingsOpen(true), fg, insets }),
913
912
  p2IsHuman ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
914
913
  import_split_screen.DualZoneLayout,
915
914
  {
916
915
  panelLayout,
917
916
  panelFadeStyle,
918
- p1: p1Panel,
917
+ p1: seatOne,
919
918
  p2: p2Panel,
920
919
  shared: sharedControls,
921
- p1Elevated: controlsHost.openId?.startsWith("p1-") ?? false,
920
+ p1Elevated: p1PickerOpen,
922
921
  p2Elevated: p2Host.openId !== null,
923
922
  sharedElevated: optionsPopoverOpen
924
923
  }
925
- ) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native4.View, { style: styles3.stackedZone, children: [
924
+ ) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native5.View, { style: styles3.stackedZone, children: [
926
925
  sharedControls,
927
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native_reanimated2.default.View, { style: [styles3.playersRow, anyPlayerPickerOpen && styles3.playersRowOpen, playersRowFade], children: [
928
- p1Panel,
929
- p2Panel
930
- ] }),
931
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lobby2.ReadyButton, { color: p1Drawn, ready: ready[1], onToggleReady: () => setReady((r) => ({ ...r, 1: !r[1] })), style: anyPlayerPickerOpen && styles3.readyButtonHidden })
926
+ seatOne
932
927
  ] })
933
928
  ] }),
934
929
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SettingsDialog, { visible: settingsOpen, onDismiss: () => setSettingsOpen(false), onShowHowToPlay: () => openFor(game.id) })
@@ -938,10 +933,16 @@ function halfOf(color) {
938
933
  const rgb = (0, import_auto_paper4.getRgb)(color);
939
934
  return rgb ? `rgba(${rgb.r},${rgb.g},${rgb.b},0.5)` : void 0;
940
935
  }
941
- var styles3 = import_react_native4.StyleSheet.create({
936
+ var styles3 = import_react_native5.StyleSheet.create({
942
937
  container: {
943
938
  flex: 1
944
939
  },
940
+ // Player 1's controls, under their row of buttons.
941
+ controlRow: {
942
+ alignItems: "center",
943
+ flexDirection: "row",
944
+ gap: 12
945
+ },
945
946
  // A real full-bleed box to size the absolutely-positioned children against.
946
947
  frame: {
947
948
  alignItems: "center",
@@ -968,21 +969,37 @@ var styles3 = import_react_native4.StyleSheet.create({
968
969
  pill: {
969
970
  borderRadius: 999
970
971
  },
971
- // The one-human row: seat 1's panel and seat 2's button read as one evenly spaced row of buttons,
972
- // at PlayerSetupPanel's own pickerRow gap (12).
972
+ // Seat 1's button and the opponent button read as one evenly spaced row of buttons, at
973
+ // PlayerSetupPanel's own pickerRow gap (12).
973
974
  playersRow: {
974
975
  flexDirection: "row",
975
976
  gap: 12
976
977
  },
977
- playersRowOpen: {
978
+ // Lifts whichever piece of player 1's has a menu open above its later siblings (each View is its
979
+ // own stacking context on the web).
980
+ raised: {
978
981
  zIndex: 100
979
982
  },
980
- // See anyPlayerPickerOpen: opacity 0 (not display:'none') keeps the button's layout space, and
983
+ // See p1PickerOpen: opacity 0 (not display:'none') keeps the button's layout space, and
981
984
  // pointerEvents:'none' keeps the sliver an open menu doesn't cover from being a hidden tap target.
982
985
  readyButtonHidden: {
983
986
  opacity: 0,
984
987
  pointerEvents: "none"
985
988
  },
989
+ // Player 1's column: their row of buttons and controls, then Ready, at PlayerSetupPanel's own
990
+ // rhythm in their half of 2 Player.
991
+ seatOne: {
992
+ alignItems: "center",
993
+ gap: 12
994
+ },
995
+ // With one person, Ready sits as far under the buttons as the match options sit above them.
996
+ seatOneAlone: {
997
+ gap: 28
998
+ },
999
+ seatOneButtons: {
1000
+ alignItems: "center",
1001
+ gap: 12
1002
+ },
986
1003
  stackedZone: {
987
1004
  alignItems: "center",
988
1005
  gap: 28
@@ -1015,7 +1032,7 @@ function useResumePoint(gameId) {
1015
1032
  if (!gameId) return;
1016
1033
  setLastActiveGame(gameId);
1017
1034
  return () => {
1018
- if (import_react_native5.AppState.currentState === "active") clearLastActiveGame();
1035
+ if (import_react_native6.AppState.currentState === "active") clearLastActiveGame();
1019
1036
  };
1020
1037
  }, [gameId, setLastActiveGame, clearLastActiveGame]);
1021
1038
  }
@@ -1027,7 +1044,7 @@ var import_chrome5 = require("@tastic/hud/chrome");
1027
1044
  var import_profile7 = require("@tastic/profile");
1028
1045
  var import_expo_router6 = require("expo-router");
1029
1046
  var import_react16 = require("react");
1030
- var import_react_native7 = require("react-native");
1047
+ var import_react_native8 = require("react-native");
1031
1048
  var import_react_native_paper = require("react-native-paper");
1032
1049
 
1033
1050
  // src/hooks/useFrozenWhileHidden.ts
@@ -1047,8 +1064,8 @@ function useFrozenWhileHidden(value, hidden) {
1047
1064
  }
1048
1065
 
1049
1066
  // src/screens/homeLayout.ts
1050
- var import_core10 = require("@tastic/core");
1051
- var import_react_native6 = require("react-native");
1067
+ var import_core11 = require("@tastic/core");
1068
+ var import_react_native7 = require("react-native");
1052
1069
  var import_react_native_safe_area_context3 = require("react-native-safe-area-context");
1053
1070
  var PREVIEW_ASPECT = 360 / 200;
1054
1071
  var ROW_PREVIEW_WIDTH = 92;
@@ -1077,9 +1094,9 @@ function computeHomeLayout(windowWidth, windowHeight, insets) {
1077
1094
  const inner = Math.max(0, usable - HOME_WIDE_EDGE_PADDING - HOME_WIDE_RIGHT_GUTTER);
1078
1095
  const content = Math.min(inner, HOME_WIDE_MAX_CONTENT_WIDTH);
1079
1096
  const slack = (inner - content) / 2;
1080
- const leftPaneWidth = Math.min((0, import_core10.clamp)(Math.round(HOME_LEFT_PANE_FRACTION * content), HOME_LEFT_PANE_MIN, HOME_LEFT_PANE_MAX), Math.max(0, content - HOME_WIDE_PANE_GAP - HOME_MIN_TILE_WIDTH));
1097
+ const leftPaneWidth = Math.min((0, import_core11.clamp)(Math.round(HOME_LEFT_PANE_FRACTION * content), HOME_LEFT_PANE_MIN, HOME_LEFT_PANE_MAX), Math.max(0, content - HOME_WIDE_PANE_GAP - HOME_MIN_TILE_WIDTH));
1081
1098
  const rightPaneWidth = Math.max(0, content - HOME_WIDE_PANE_GAP - leftPaneWidth);
1082
- const columns = (0, import_core10.clamp)(Math.floor((rightPaneWidth + HOME_GRID_GAP) / (HOME_MIN_TILE_WIDTH + HOME_GRID_GAP)), 1, HOME_MAX_COLUMNS);
1099
+ const columns = (0, import_core11.clamp)(Math.floor((rightPaneWidth + HOME_GRID_GAP) / (HOME_MIN_TILE_WIDTH + HOME_GRID_GAP)), 1, HOME_MAX_COLUMNS);
1083
1100
  const tileWidth = Math.max(0, Math.floor((rightPaneWidth - (columns - 1) * HOME_GRID_GAP) / columns));
1084
1101
  const previewWidth = Math.max(0, tileWidth - 2 * HOME_TILE_PADDING);
1085
1102
  const heroFit = {
@@ -1102,7 +1119,7 @@ function computeHomeLayout(windowWidth, windowHeight, insets) {
1102
1119
  };
1103
1120
  }
1104
1121
  function useHomeLayout() {
1105
- const { width, height } = (0, import_react_native6.useWindowDimensions)();
1122
+ const { width, height } = (0, import_react_native7.useWindowDimensions)();
1106
1123
  const insets = (0, import_react_native_safe_area_context3.useSafeAreaInsets)();
1107
1124
  return computeHomeLayout(width, height, insets);
1108
1125
  }
@@ -1132,9 +1149,9 @@ function HomeMenu() {
1132
1149
  const [settingsOpen, setSettingsOpen] = (0, import_react16.useState)(false);
1133
1150
  const [rulesGame, setRulesGame] = (0, import_react16.useState)(null);
1134
1151
  const isFocused = (0, import_expo_router6.useIsFocused)();
1135
- const [appState, setAppState] = (0, import_react16.useState)(import_react_native7.AppState.currentState);
1152
+ const [appState, setAppState] = (0, import_react16.useState)(import_react_native8.AppState.currentState);
1136
1153
  (0, import_react16.useEffect)(() => {
1137
- const subscription = import_react_native7.AppState.addEventListener("change", setAppState);
1154
+ const subscription = import_react_native8.AppState.addEventListener("change", setAppState);
1138
1155
  return () => subscription.remove();
1139
1156
  }, []);
1140
1157
  const heroActive = isFocused && appState === "active";
@@ -1176,24 +1193,24 @@ function HomeMenu() {
1176
1193
  const picker = /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_profile7.ProfilePicker, { idPrefix: SEAT_ONE_PICKER, host: pickerHost, profiles, selectedId: lastSelected[1], color: p1Drawn, dark, guestLabel: pack.guestLabel ?? "P1", nullLabel: pack.nullLabel, onSelect: (profile) => selectProfile(1, profile?.id ?? null), onManage: () => import_expo_router6.router.push(PROFILES_HREF), colorSection, alignOverride: pickerAlign, triggerStyle });
1177
1194
  const pressAway = /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_chrome5.PressAwayOverlay, { active: pickerHost.openId !== null, onPress: pickerHost.close, style: styles4.pressAway });
1178
1195
  const RulesSheet = rulesGame?.Rules;
1179
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native7.View, { style: [styles4.container, { backgroundColor: bg }], children: [
1180
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native7.View, { style: styles4.frame, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(RulesHost, { enabled: pack.games.some((game) => game.Rules), children: [
1196
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native8.View, { style: [styles4.container, { backgroundColor: bg }], children: [
1197
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native8.View, { style: styles4.frame, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(RulesHost, { enabled: pack.games.some((game) => game.Rules), children: [
1181
1198
  isWide ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
1182
1199
  pressAway,
1183
1200
  /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_auto_paper5.FadingScrollView, { ...tab && gridClamp, style: [styles4.wideRightPane, { left: layout.paddingLeft + layout.leftPaneWidth + HOME_WIDE_PANE_GAP }], contentContainerStyle: { paddingTop: 16 + insets.top, paddingBottom: 16 + insets.bottom, paddingRight: layout.paddingRight }, children: [
1184
- tabBar && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native7.View, { style: [styles4.wideTabs, { width: layout.rightPaneWidth }], children: tabBar }),
1185
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native7.View, { style: [styles4.grid, { width: layout.rightPaneWidth }], children: pack.games.map((game) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GameTile, { game, hidden: isHidden(tab, game), width: layout.tileWidth, previewWidth: layout.previewWidth, previewRevealed: previewsRevealed, dark, p1Color, p2Color, onPress: () => openGame(game), onRules: hasRules(game) ? () => openRules(game) : void 0 }, game.id)) })
1201
+ tabBar && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native8.View, { style: [styles4.wideTabs, { width: layout.rightPaneWidth }], children: tabBar }),
1202
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native8.View, { style: [styles4.grid, { width: layout.rightPaneWidth }], children: pack.games.map((game) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GameTile, { game, hidden: isHidden(tab, game), width: layout.tileWidth, previewWidth: layout.previewWidth, previewRevealed: previewsRevealed, dark, p1Color, p2Color, onPress: () => openGame(game), onRules: hasRules(game) ? () => openRules(game) : void 0 }, game.id)) })
1186
1203
  ] }),
1187
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native7.View, { style: [styles4.wideLeftPane, { left: layout.paddingLeft, width: layout.leftPaneWidth, paddingTop: HOME_TOP_BAND + insets.top, paddingBottom: 16 + insets.bottom }, pickerHost.openId !== null && styles4.pickerOpen], children: [
1204
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native8.View, { style: [styles4.wideLeftPane, { left: layout.paddingLeft, width: layout.leftPaneWidth, paddingTop: HOME_TOP_BAND + insets.top, paddingBottom: 16 + insets.bottom }, pickerHost.openId !== null && styles4.pickerOpen], children: [
1188
1205
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(pack.HeroTitle, { words: pack.wordmark, color: fg, p1Color, p2Color, active: heroActive, fit: layout.heroFit }),
1189
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native7.View, { style: styles4.widePicker, children: picker })
1206
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native8.View, { style: styles4.widePicker, children: picker })
1190
1207
  ] })
1191
1208
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_auto_paper5.FadingScrollView, { ...tab && columnClamp, style: styles4.scroll, contentContainerStyle: [styles4.content, { paddingTop: 56 + insets.top, paddingBottom: 24 + insets.bottom, paddingLeft: 16 + insets.left, paddingRight: 16 + insets.right }], children: [
1192
1209
  pressAway,
1193
1210
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(pack.HeroTitle, { words: pack.wordmark, color: fg, p1Color, p2Color, active: heroActive }),
1194
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native7.View, { style: [styles4.picker, pickerHost.openId !== null && styles4.pickerOpen], children: picker }),
1195
- tabBar && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native7.View, { style: styles4.listWidth, children: tabBar }),
1196
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native7.View, { style: styles4.list, children: pack.games.map((game) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GameRow, { game, hidden: isHidden(tab, game), dark, p1Color, p2Color, previewRevealed: previewsRevealed, onPress: () => openGame(game), onRules: hasRules(game) ? () => openRules(game) : void 0 }, game.id)) })
1211
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native8.View, { style: [styles4.picker, pickerHost.openId !== null && styles4.pickerOpen], children: picker }),
1212
+ tabBar && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native8.View, { style: styles4.listWidth, children: tabBar }),
1213
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native8.View, { style: styles4.list, children: pack.games.map((game) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GameRow, { game, hidden: isHidden(tab, game), dark, p1Color, p2Color, previewRevealed: previewsRevealed, onPress: () => openGame(game), onRules: hasRules(game) ? () => openRules(game) : void 0 }, game.id)) })
1197
1214
  ] }),
1198
1215
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_feedback_press3.IconButton, { icon: "trophy", iconColor: fg, size: 24, style: [styles4.corner, { top: 8 + insets.top, left: 8 + insets.left }], onPress: () => import_expo_router6.router.push(STATS_HREF), accessibilityLabel: "Stats & Achievements" }),
1199
1216
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_feedback_press3.IconButton, { icon: "cog", iconColor: fg, size: 24, style: [styles4.corner, { top: 8 + insets.top, right: 8 + insets.right }], onPress: () => setSettingsOpen(true), accessibilityLabel: "Settings" }),
@@ -1244,10 +1261,10 @@ function GameRow({ game, hidden, dark, p1Color, p2Color, previewRevealed, onPres
1244
1261
  return (
1245
1262
  // The rules button sits beside the row's own button, laid over its end, not inside it: a button
1246
1263
  // inside a button is invalid on the web.
1247
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native7.View, { style: hidden ? styles4.hidden : void 0, children: [
1264
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native8.View, { style: hidden ? styles4.hidden : void 0, children: [
1248
1265
  /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_feedback_press3.Pressable, { onPress, style: [styles4.row, { backgroundColor: well.bg }], accessibilityRole: "button", accessibilityLabel: game.title, children: [
1249
- game.Preview ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(PreviewBox, { game, width: ROW_PREVIEW_WIDTH, revealed: previewRevealed, dark, p1Color, p2Color }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native7.View, { style: [styles4.rowIcon, { backgroundColor: dark ? ICON_DARK_BG : ICON_LIGHT_BG }], children: GameIcon && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GameIcon, { size: ROW_ICON_SIZE, p1Color, p2Color, dark }) }),
1250
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native7.View, { style: styles4.rowText, children: [
1266
+ game.Preview ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(PreviewBox, { game, width: ROW_PREVIEW_WIDTH, revealed: previewRevealed, dark, p1Color, p2Color }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native8.View, { style: [styles4.rowIcon, { backgroundColor: dark ? ICON_DARK_BG : ICON_LIGHT_BG }], children: GameIcon && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GameIcon, { size: ROW_ICON_SIZE, p1Color, p2Color, dark }) }),
1267
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native8.View, { style: styles4.rowText, children: [
1251
1268
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper.Text, { variant: "titleLarge", style: [styles4.rowTitle, { color: well.fg }], numberOfLines: 1, children: game.title }),
1252
1269
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper.Text, { variant: "bodyMedium", style: { color: well.fgMuted }, numberOfLines: 2, children: game.blurb })
1253
1270
  ] })
@@ -1258,7 +1275,7 @@ function GameRow({ game, hidden, dark, p1Color, p2Color, previewRevealed, onPres
1258
1275
  }
1259
1276
  function GameTile({ game, hidden, width, previewWidth, dark, p1Color, p2Color, previewRevealed, onPress, onRules }) {
1260
1277
  const well = (0, import_chrome5.useWellColors)();
1261
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native7.View, { style: [{ width }, hidden && styles4.hidden], children: [
1278
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native8.View, { style: [{ width }, hidden && styles4.hidden], children: [
1262
1279
  /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_feedback_press3.Pressable, { onPress, style: [styles4.tile, { backgroundColor: well.bg }], accessibilityRole: "button", accessibilityLabel: game.title, children: [
1263
1280
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(PreviewBox, { game, width: previewWidth, revealed: previewRevealed, dark, p1Color, p2Color }),
1264
1281
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper.Text, { variant: "titleMedium", style: [styles4.rowTitle, { color: well.fg }], numberOfLines: 1, children: game.title }),
@@ -1271,13 +1288,13 @@ function PreviewBox({ game, width, revealed, dark, p1Color, p2Color }) {
1271
1288
  const height = Math.round(width / PREVIEW_ASPECT);
1272
1289
  const { Preview, icon: GameIcon } = game;
1273
1290
  const iconSize = Math.min(height, ROW_ICON_SIZE);
1274
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native7.View, { style: [styles4.preview, { width, height, backgroundColor: dark ? ICON_DARK_BG : ICON_LIGHT_BG }], children: Preview ? revealed && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Preview, { width, height, p1Color, p2Color, dark }) : GameIcon && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native7.View, { style: [styles4.previewIcon, { width: iconSize, height: iconSize, borderRadius: iconSize * 0.22 }], children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GameIcon, { size: iconSize, p1Color, p2Color, dark }) }) });
1291
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native8.View, { style: [styles4.preview, { width, height, backgroundColor: dark ? ICON_DARK_BG : ICON_LIGHT_BG }], children: Preview ? revealed && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Preview, { width, height, p1Color, p2Color, dark }) : GameIcon && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native8.View, { style: [styles4.previewIcon, { width: iconSize, height: iconSize, borderRadius: iconSize * 0.22 }], children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GameIcon, { size: iconSize, p1Color, p2Color, dark }) }) });
1275
1292
  }
1276
1293
  var ROW_ICON_SIZE = 56;
1277
1294
  var ICON_LIGHT_BG = "#FAFAFA";
1278
1295
  var ICON_DARK_BG = "#050505";
1279
1296
  var RULES_BUTTON_SPACE = 44;
1280
- var styles4 = import_react_native7.StyleSheet.create({
1297
+ var styles4 = import_react_native8.StyleSheet.create({
1281
1298
  container: {
1282
1299
  flex: 1
1283
1300
  },
@@ -1433,7 +1450,7 @@ function useRosterAlign(open, contentWidth, contentHeight) {
1433
1450
  function ProfilesRoute() {
1434
1451
  const { profiles, createProfile, updateProfile, deleteProfile } = useProfiles();
1435
1452
  const { Backdrop, foreground, newProfileColor, usePickerAlign } = usePack();
1436
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_chrome6.RotatedFrame, { rotatedSafeArea: true, portalHost: true, maxContentWidth: ROSTER_MAX_WIDTH, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_profile8.ProfilesScreen, { profiles, defaultColor: newProfileColor ?? import_auto_paper6.defaultColors[0].value, onCreate: (patch) => createProfile(patch), onSave: (id, patch) => updateProfile(id, patch), onDelete: deleteProfile, onBack: import_core7.safeBack, fg: foreground, bg: Backdrop ? "transparent" : void 0, useColorPickerAlign: usePickerAlign ? useRosterAlign : void 0 }) });
1453
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_chrome6.RotatedFrame, { rotatedSafeArea: true, portalHost: true, maxContentWidth: ROSTER_MAX_WIDTH, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_profile8.ProfilesScreen, { profiles, defaultColor: newProfileColor ?? import_auto_paper6.defaultColors[0].value, onCreate: (patch) => createProfile(patch), onSave: (id, patch) => updateProfile(id, patch), onDelete: deleteProfile, onBack: import_core8.safeBack, fg: foreground, bg: Backdrop ? "transparent" : void 0, useColorPickerAlign: usePickerAlign ? useRosterAlign : void 0 }) });
1437
1454
  }
1438
1455
 
1439
1456
  // src/screens/StatsScreen.tsx
@@ -1441,7 +1458,7 @@ var import_auto_paper7 = require("@rific/auto-paper");
1441
1458
  var import_chrome7 = require("@tastic/hud/chrome");
1442
1459
  var import_profile9 = require("@tastic/profile");
1443
1460
  var import_react17 = require("react");
1444
- var import_react_native8 = require("react-native");
1461
+ var import_react_native9 = require("react-native");
1445
1462
  var import_react_native_paper2 = require("react-native-paper");
1446
1463
  var import_react_native_safe_area_context4 = require("react-native-safe-area-context");
1447
1464
  var import_react_redux10 = require("react-redux");
@@ -1464,7 +1481,7 @@ function StatsScreen() {
1464
1481
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(StatsPressAway, { host: pickerHost, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_chrome7.RotatedFrame, { rotatedSafeArea: true, portalHost: true, maxContentWidth: STATS_MAX_WIDTH, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(StatsContent, { pickerHost }) }) });
1465
1482
  }
1466
1483
  function StatsPressAway({ host, children }) {
1467
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native8.View, { testID: "stats-press-away", style: styles5.screen, onStartShouldSetResponder: () => host.openId !== null, onResponderRelease: host.close, children });
1484
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native9.View, { testID: "stats-press-away", style: styles5.screen, onStartShouldSetResponder: () => host.openId !== null, onResponderRelease: host.close, children });
1468
1485
  }
1469
1486
  var useNoPackReset = () => null;
1470
1487
  var noPickerAlign2 = () => void 0;
@@ -1498,10 +1515,10 @@ function StatsContent({ pickerHost }) {
1498
1515
  const surface = pack.Backdrop ? { backgroundColor: colors.surface } : void 0;
1499
1516
  const triggerStyle = pack.Backdrop ? [styles5.pill, surface] : void 0;
1500
1517
  const PackTabContent = packTab?.Content;
1501
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_chrome7.BaseStatsScreen, { title, onBack: import_core7.safeBack, insets, onReset: reset.onReset, resetLabel: reset.label, resetConfirmTitle: reset.title, resetConfirmBody: reset.body, fg: pack.foreground, bg: pack.Backdrop ? "transparent" : void 0, rotation: 0, children: [
1518
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_chrome7.BaseStatsScreen, { title, onBack: import_core8.safeBack, insets, onReset: reset.onReset, resetLabel: reset.label, resetConfirmTitle: reset.title, resetConfirmBody: reset.body, fg: pack.foreground, bg: pack.Backdrop ? "transparent" : void 0, rotation: 0, children: [
1502
1519
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_chrome7.PressAwayOverlay, { active: pickerHost.openId !== null, onPress: pickerHost.close, style: styles5.pressAway }),
1503
1520
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_paper2.SegmentedButtons, { value: tab, onValueChange: setTab, buttons: tabButtons, style: [styles5.tabs, surface] }),
1504
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native8.View, { style: [styles5.filters, pickerHost.openId !== null && styles5.filtersOpen], onStartShouldSetResponder: () => pickerHost.openId !== null, children: [
1521
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native9.View, { style: [styles5.filters, pickerHost.openId !== null && styles5.filtersOpen], onStartShouldSetResponder: () => pickerHost.openId !== null, children: [
1505
1522
  profiles.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_profile9.OptionPicker, { id: PLAYER_PICKER_ID, host: pickerHost, options: playerOptions, value: profileId ?? EVERYONE, onChange: (value) => setSelectedProfileId(value === EVERYONE ? null : value), color: colors.primary, dark, icon: profileId ? "account" : "account-group", alignOverride: playerAlign, triggerStyle }),
1506
1523
  showGamePicker && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_profile9.OptionPicker, { id: GAME_PICKER_ID, host: pickerHost, options: gameOptions, value: gameId, onChange: setGameId, color: colors.primary, dark, icon: "view-grid-outline", alignOverride: gameAlign, triggerStyle })
1507
1524
  ] }),
@@ -1522,12 +1539,12 @@ function GameStats({ game, tab, profileId, showTitle, titleColor }) {
1522
1539
  const { StatsSections, AchievementList } = game;
1523
1540
  const Sections = tab === STATS ? StatsSections : AchievementList;
1524
1541
  if (!Sections) return null;
1525
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native8.View, { style: styles5.game, children: [
1542
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native9.View, { style: styles5.game, children: [
1526
1543
  showTitle && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_paper2.Text, { variant: "titleLarge", style: [styles5.gameTitle, { color: titleColor }], children: game.title }),
1527
1544
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Sections, { profileId })
1528
1545
  ] });
1529
1546
  }
1530
- var styles5 = import_react_native8.StyleSheet.create({
1547
+ var styles5 = import_react_native9.StyleSheet.create({
1531
1548
  filters: {
1532
1549
  flexDirection: "row",
1533
1550
  flexWrap: "wrap",
@@ -1561,11 +1578,11 @@ var styles5 = import_react_native8.StyleSheet.create({
1561
1578
  });
1562
1579
 
1563
1580
  // src/components/imageGameIcon.tsx
1564
- var import_react_native9 = require("react-native");
1581
+ var import_react_native10 = require("react-native");
1565
1582
  var import_jsx_runtime17 = require("react/jsx-runtime");
1566
1583
  function imageGameIcon(images) {
1567
1584
  return function ImageGameIcon({ size, dark }) {
1568
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native9.Image, { source: dark ? images.dark : images.light, style: { height: size, width: size }, accessibilityIgnoresInvertColors: true });
1585
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native10.Image, { source: dark ? images.dark : images.light, style: { height: size, width: size }, accessibilityIgnoresInvertColors: true });
1569
1586
  };
1570
1587
  }
1571
1588
 
@@ -1573,20 +1590,19 @@ function imageGameIcon(images) {
1573
1590
  var import_auto_paper8 = require("@rific/auto-paper");
1574
1591
  var import_feedback_press4 = require("@rific/feedback-press");
1575
1592
  var import_react18 = require("react");
1576
- var import_react_native10 = require("react-native");
1593
+ var import_react_native11 = require("react-native");
1577
1594
  var import_react_native_reanimated3 = __toESM(require("react-native-reanimated"));
1578
1595
  var import_react_native_safe_area_context5 = require("react-native-safe-area-context");
1579
1596
  var import_jsx_runtime18 = require("react/jsx-runtime");
1580
- var MATCH_CHIPS_DESKTOP_MIN_WIDTH = 768;
1581
1597
  var MATCH_CHIPS_FADE_MS = 150;
1582
1598
  var CHIP_SIZE = 48;
1583
1599
  var EDGE_PAD = 8;
1584
1600
  function MatchChips({ playing, faceToFace, centerLineY, onBack, onSettings }) {
1585
- const { width, height } = (0, import_react_native10.useWindowDimensions)();
1601
+ const { height } = (0, import_react_native11.useWindowDimensions)();
1586
1602
  const insets = (0, import_react_native_safe_area_context5.useSafeAreaInsets)();
1587
1603
  const { dark } = (0, import_auto_paper8.useAutoPaperTheme)();
1588
- const desktopWeb = import_react_native10.Platform.OS === "web" && width >= MATCH_CHIPS_DESKTOP_MIN_WIDTH;
1589
- const visible = desktopWeb || !playing;
1604
+ const atComputer = useAtComputer();
1605
+ const visible = atComputer || !playing;
1590
1606
  const opacity = (0, import_react_native_reanimated3.useSharedValue)(visible ? 1 : 0);
1591
1607
  (0, import_react18.useEffect)(() => {
1592
1608
  opacity.value = (0, import_react_native_reanimated3.withTiming)(visible ? 1 : 0, { duration: MATCH_CHIPS_FADE_MS });
@@ -1594,7 +1610,7 @@ function MatchChips({ playing, faceToFace, centerLineY, onBack, onSettings }) {
1594
1610
  const fadeStyle = (0, import_react_native_reanimated3.useAnimatedStyle)(() => ({ opacity: opacity.value }));
1595
1611
  const iconColor = dark ? "#FFFFFF" : "#000000";
1596
1612
  const containerColor = dark ? "rgba(0,0,0,0.55)" : "rgba(255,255,255,0.55)";
1597
- const between = faceToFace && !desktopWeb;
1613
+ const between = faceToFace && !atComputer;
1598
1614
  const top = between ? (centerLineY ?? height / 2) - CHIP_SIZE / 2 : EDGE_PAD + insets.top;
1599
1615
  const backSlot = { top, left: EDGE_PAD + insets.left };
1600
1616
  const settingsSlot = { top, right: EDGE_PAD + insets.right };
@@ -1604,7 +1620,7 @@ function MatchChips({ playing, faceToFace, centerLineY, onBack, onSettings }) {
1604
1620
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native_reanimated3.default.View, { pointerEvents, style: [styles6.slot, settingsSlot, fadeStyle], testID: "match-chips-settings", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_feedback_press4.IconButton, { icon: "cog", iconColor, containerColor, style: styles6.chip, size: 24, onPress: onSettings, accessibilityLabel: "Settings" }) })
1605
1621
  ] });
1606
1622
  }
1607
- var styles6 = import_react_native10.StyleSheet.create({
1623
+ var styles6 = import_react_native11.StyleSheet.create({
1608
1624
  chip: { margin: 0 },
1609
1625
  slot: { position: "absolute" }
1610
1626
  });
@@ -1625,7 +1641,7 @@ var import_redux_persist = require("redux-persist");
1625
1641
 
1626
1642
  // src/redux/shellState.ts
1627
1643
  var import_auto_paper9 = require("@rific/auto-paper");
1628
- var import_core11 = require("@rific/core");
1644
+ var import_core12 = require("@rific/core");
1629
1645
  var import_feedback_press5 = require("@rific/feedback-press");
1630
1646
  var import_profile10 = require("@tastic/profile");
1631
1647
  var initialSoundSettings = { ...import_feedback_press5.defaultSoundSettings, enabled: !__DEV__ };
@@ -1646,7 +1662,7 @@ function createShellReducers({ seatColors, appearance = "system", seatTheme }) {
1646
1662
  sound: appSoundReducer,
1647
1663
  // @rific/core's Keep Awake setting (see Providers.tsx's KeepAwakeBridge), under the namespace its
1648
1664
  // REHYDRATE handling expects.
1649
- keepAwake: import_core11.keepAwakeReducer,
1665
+ keepAwake: import_core12.keepAwakeReducer,
1650
1666
  // The shared App Group roster (see @tastic/profile's resolveInitialProfiles/
1651
1667
  // useSharedProfilesSync, wired in hooks/useProfiles.tsx) is layered on top of this as a separate
1652
1668
  // sync target - this slice persisting normally via redux-persist is what gives it local-fallback
@@ -1865,7 +1881,6 @@ function createPlayerSettings({ id, defaults, distinctControls }) {
1865
1881
  GUEST_PLAYER_ID,
1866
1882
  HomeScreen,
1867
1883
  LoadoutRoute,
1868
- MATCH_CHIPS_DESKTOP_MIN_WIDTH,
1869
1884
  MATCH_CHIPS_FADE_MS,
1870
1885
  MatchChips,
1871
1886
  PREVIEW_ASPECT,
@@ -1903,6 +1918,7 @@ function createPlayerSettings({ id, defaults, distinctControls }) {
1903
1918
  seatTwoFor,
1904
1919
  useActiveRound,
1905
1920
  useAppSettings,
1921
+ useAtComputer,
1906
1922
  useGameGuide,
1907
1923
  useOpponent,
1908
1924
  usePack,