@tastic/shell 0.6.0 → 0.8.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
  });
@@ -660,25 +635,35 @@ function useAppSettings() {
660
635
 
661
636
  // src/components/SettingsDialog.tsx
662
637
  var import_jsx_runtime11 = require("react/jsx-runtime");
638
+ var noSettingsRules = () => null;
663
639
  function SettingsDialog({ visible, onDismiss, onShowHowToPlay, children }) {
664
640
  const onUpdateError = (0, import_toaster3.useUpdateErrorToast)();
665
641
  const { settings, setSettings } = useAppSettings();
666
- const { otaVersion, SettingsSections, edgeGuard = true } = usePack();
642
+ const { otaVersion, SettingsSections, edgeGuard = true, useSettingsRules = noSettingsRules } = usePack();
643
+ const rules = useSettingsRules();
667
644
  const edgeGuardProps = edgeGuard ? { deferBottomEdgeGestures: settings.deferBottomEdgeGestures, onDeferBottomEdgeGestures: (value) => setSettings({ deferBottomEdgeGestures: value }) } : {};
668
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_chrome3.BaseSettingsDialog, { visible, onDismiss, version: otaVersion, ...edgeGuardProps, onUpdateError, onShowHowToPlay, children: [
645
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_chrome3.BaseSettingsDialog, { visible, onDismiss, version: otaVersion, ...edgeGuardProps, onUpdateError, onShowHowToPlay, leading: rules, children: [
669
646
  children,
670
647
  SettingsSections && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SettingsSections, {})
671
648
  ] });
672
649
  }
673
650
 
651
+ // src/hooks/useAtComputer.ts
652
+ var import_core6 = require("@tastic/core");
653
+ var import_react_native4 = require("react-native");
654
+ function useAtComputer() {
655
+ const touchPrimary = (0, import_core6.useIsTouchPrimaryDevice)();
656
+ return import_react_native4.Platform.OS === "web" && !touchPrimary;
657
+ }
658
+
674
659
  // src/hooks/useOpponent.ts
675
660
  var import_react11 = require("react");
676
661
  var import_react_redux7 = require("react-redux");
677
662
 
678
663
  // src/redux/opponentSlice.ts
679
- var import_core6 = require("@rific/core");
664
+ var import_core7 = require("@rific/core");
680
665
  var DEFAULT_OPPONENT = { seatTwo: "cpu" };
681
- var { actions: actions4, reducer: reducer4 } = (0, import_core6.createSettingsSlice)("opponent", {
666
+ var { actions: actions4, reducer: reducer4 } = (0, import_core7.createSettingsSlice)("opponent", {
682
667
  initialState: DEFAULT_OPPONENT
683
668
  });
684
669
  var opponentActions = actions4;
@@ -811,13 +796,13 @@ function useSeatColors(p2IsHuman) {
811
796
  }
812
797
 
813
798
  // src/navigation.ts
814
- var import_core7 = require("@rific/core");
799
+ var import_core8 = require("@rific/core");
815
800
  var import_expo_router3 = require("expo-router");
816
- (0, import_core7.configureNavigation)({ router: import_expo_router3.router });
801
+ (0, import_core8.configureNavigation)({ router: import_expo_router3.router });
817
802
 
818
803
  // src/redux/liveplaySlice.ts
819
- var import_core8 = require("@rific/core");
820
- var { actions: actions5, reducer: reducer5 } = (0, import_core8.createSettingsSlice)("liveplay", {
804
+ var import_core9 = require("@rific/core");
805
+ var { actions: actions5, reducer: reducer5 } = (0, import_core9.createSettingsSlice)("liveplay", {
821
806
  initialState: { activelyPlaying: false, activeRound: null }
822
807
  });
823
808
  var liveplayActions = actions5;
@@ -826,6 +811,8 @@ var liveplaySlice_default = reducer5;
826
811
  // src/screens/LoadoutScreen.tsx
827
812
  var import_jsx_runtime12 = require("react/jsx-runtime");
828
813
  var PANEL_SWAP_FADE_MS = 180;
814
+ var P1_CONTROL_ID = "p1-control";
815
+ var P2_CONTROL_ID = "p2-control";
829
816
  function LoadoutScreen({ game }) {
830
817
  const pack = usePack();
831
818
  const { seatTwo: savedSeatTwo, setSeatTwo } = useOpponent();
@@ -837,8 +824,10 @@ function LoadoutScreen({ game }) {
837
824
  const loadout = game.useLoadout(mode);
838
825
  const { openFor } = useGameGuide();
839
826
  const { profiles, lastSelected, selectProfile } = useProfiles();
840
- const { orientationMode } = (0, import_core9.useOrientationState)();
841
- const panelMode = p2IsHuman && !game.sideBySideWhenWide ? "faceToFace" : orientationMode;
827
+ const { orientationMode } = (0, import_core10.useOrientationState)();
828
+ const atComputer = useAtComputer();
829
+ const twoPlayerMode = game.sideBySideAtComputer && atComputer ? "sideBySide" : game.sideBySideWhenWide ? orientationMode : "faceToFace";
830
+ const panelMode = p2IsHuman ? twoPlayerMode : orientationMode;
842
831
  const { panelLayout, panelFadeStyle } = (0, import_split_screen.useDualZoneLayout)(panelMode, false, true, false, PANEL_SWAP_FADE_MS);
843
832
  const playersRowFade = panelFadeStyle;
844
833
  const [settingsOpen, setSettingsOpen] = (0, import_react13.useState)(false);
@@ -847,7 +836,7 @@ function LoadoutScreen({ game }) {
847
836
  const { p1Color, p2Color, handleP1ColorChange, handleP2ColorChange } = useSeatColors(p2IsHuman);
848
837
  const dispatch = (0, import_react_redux9.useDispatch)();
849
838
  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]);
839
+ const handleP2ProfileSelect = (0, import_react13.useCallback)((profile) => selectProfile(2, profile?.id ?? null), [selectProfile]);
851
840
  const [ready, setReady] = (0, import_react13.useState)({ 1: false, 2: false });
852
841
  (0, import_expo_router4.useFocusEffect)(
853
842
  (0, import_react13.useCallback)(() => {
@@ -878,16 +867,16 @@ function LoadoutScreen({ game }) {
878
867
  const p1Drawn = seatPreview ? seatPreview(p1Color) : p1Color;
879
868
  const p2Drawn = seatPreview ? seatPreview(p2Color) : p2Color;
880
869
  const triggerStyle = (0, import_react13.useMemo)(() => pack.Backdrop ? [styles3.pill, { backgroundColor: themeColors.surface }] : void 0, [pack.Backdrop, themeColors.surface]);
870
+ const readySurface = pack.Backdrop ? themeColors.surface : void 0;
881
871
  const isPortrait = panelLayout.orientationMode === "faceToFace";
882
872
  const showMetaInSharedBand = (0, import_split_screen.needsSharedNeutralZone)(panelLayout.orientationMode, humanSeats.length);
883
873
  const p2ZoneStyle = p2IsHuman ? isPortrait ? styles3.p2ZoneTop : styles3.p2ZoneRight : null;
884
- const showReadyButton = p2IsHuman;
885
874
  const seatPickerAlign = p2IsHuman ? void 0 : pack.usePickerAlign;
886
875
  const layoutKey = p2IsHuman ? "zones" : "row";
887
876
  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-"));
877
+ const p2Host = (0, import_chrome4.usePopoverHost)();
878
+ const p1PickerOpen = controlsHost.openId?.startsWith("p1-") ?? false;
879
+ const p1ControlOpen = controlsHost.openId === P1_CONTROL_ID;
891
880
  const optionsRow = loadout.options({
892
881
  host: controlsHost,
893
882
  accentColor: themeColors.tertiary,
@@ -899,36 +888,44 @@ function LoadoutScreen({ game }) {
899
888
  onReset: showMetaInSharedBand ? void 0 : loadout.reset
900
889
  });
901
890
  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;
891
+ 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
892
  const controlLook = { accentColor: themeColors.tertiary, mutedColor: fgMuted, dark };
904
893
  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}`);
894
+ 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}`);
895
+ 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}`);
896
+ const toggleP1Ready = () => setReady((r) => ({ ...r, 1: !r[1] }));
897
+ const seatOne = /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native5.View, { style: [styles3.seatOne, !p2IsHuman && styles3.seatOneAlone, p1PickerOpen && styles3.raised], children: [
898
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native5.View, { style: [styles3.seatOneButtons, p1PickerOpen && styles3.raised], children: [
899
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native_reanimated2.default.View, { style: [styles3.playersRow, p1PickerOpen && styles3.raised, !p2IsHuman && playersRowFade], children: [
900
+ p1Panel,
901
+ opponentPicker
902
+ ] }),
903
+ p1Control && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native5.View, { style: [styles3.controlRow, p1ControlOpen && styles3.raised], children: p1Control })
904
+ ] }),
905
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lobby2.ReadyButton, { color: p1Drawn, ready: ready[1], onToggleReady: toggleP1Ready, surfaceColor: readySurface, dark, style: !p2IsHuman && p1PickerOpen && styles3.readyButtonHidden })
906
+ ] });
906
907
  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: [
908
+ 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");
909
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native5.View, { style: [styles3.container, { backgroundColor: bg }], children: [
910
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native5.View, { style: styles3.frame, children: [
910
911
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_chrome4.PressAwayOverlay, { active: controlsHost.openId !== null, onPress: controlsHost.close }),
911
912
  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 }),
913
+ !showMetaInSharedBand && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_chrome4.CornerActionButtons, { onBack: import_core8.safeBack, onSettings: () => setSettingsOpen(true), fg, insets }),
913
914
  p2IsHuman ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
914
915
  import_split_screen.DualZoneLayout,
915
916
  {
916
917
  panelLayout,
917
918
  panelFadeStyle,
918
- p1: p1Panel,
919
+ p1: seatOne,
919
920
  p2: p2Panel,
920
921
  shared: sharedControls,
921
- p1Elevated: controlsHost.openId?.startsWith("p1-") ?? false,
922
+ p1Elevated: p1PickerOpen,
922
923
  p2Elevated: p2Host.openId !== null,
923
924
  sharedElevated: optionsPopoverOpen
924
925
  }
925
- ) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native4.View, { style: styles3.stackedZone, children: [
926
+ ) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native5.View, { style: styles3.stackedZone, children: [
926
927
  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 })
928
+ seatOne
932
929
  ] })
933
930
  ] }),
934
931
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SettingsDialog, { visible: settingsOpen, onDismiss: () => setSettingsOpen(false), onShowHowToPlay: () => openFor(game.id) })
@@ -938,10 +935,16 @@ function halfOf(color) {
938
935
  const rgb = (0, import_auto_paper4.getRgb)(color);
939
936
  return rgb ? `rgba(${rgb.r},${rgb.g},${rgb.b},0.5)` : void 0;
940
937
  }
941
- var styles3 = import_react_native4.StyleSheet.create({
938
+ var styles3 = import_react_native5.StyleSheet.create({
942
939
  container: {
943
940
  flex: 1
944
941
  },
942
+ // Player 1's controls, under their row of buttons.
943
+ controlRow: {
944
+ alignItems: "center",
945
+ flexDirection: "row",
946
+ gap: 12
947
+ },
945
948
  // A real full-bleed box to size the absolutely-positioned children against.
946
949
  frame: {
947
950
  alignItems: "center",
@@ -968,21 +971,37 @@ var styles3 = import_react_native4.StyleSheet.create({
968
971
  pill: {
969
972
  borderRadius: 999
970
973
  },
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).
974
+ // Seat 1's button and the opponent button read as one evenly spaced row of buttons, at
975
+ // PlayerSetupPanel's own pickerRow gap (12).
973
976
  playersRow: {
974
977
  flexDirection: "row",
975
978
  gap: 12
976
979
  },
977
- playersRowOpen: {
980
+ // Lifts whichever piece of player 1's has a menu open above its later siblings (each View is its
981
+ // own stacking context on the web).
982
+ raised: {
978
983
  zIndex: 100
979
984
  },
980
- // See anyPlayerPickerOpen: opacity 0 (not display:'none') keeps the button's layout space, and
985
+ // See p1PickerOpen: opacity 0 (not display:'none') keeps the button's layout space, and
981
986
  // pointerEvents:'none' keeps the sliver an open menu doesn't cover from being a hidden tap target.
982
987
  readyButtonHidden: {
983
988
  opacity: 0,
984
989
  pointerEvents: "none"
985
990
  },
991
+ // Player 1's column: their row of buttons and controls, then Ready, at PlayerSetupPanel's own
992
+ // rhythm in their half of 2 Player.
993
+ seatOne: {
994
+ alignItems: "center",
995
+ gap: 12
996
+ },
997
+ // With one person, Ready sits as far under the buttons as the match options sit above them.
998
+ seatOneAlone: {
999
+ gap: 28
1000
+ },
1001
+ seatOneButtons: {
1002
+ alignItems: "center",
1003
+ gap: 12
1004
+ },
986
1005
  stackedZone: {
987
1006
  alignItems: "center",
988
1007
  gap: 28
@@ -1015,7 +1034,7 @@ function useResumePoint(gameId) {
1015
1034
  if (!gameId) return;
1016
1035
  setLastActiveGame(gameId);
1017
1036
  return () => {
1018
- if (import_react_native5.AppState.currentState === "active") clearLastActiveGame();
1037
+ if (import_react_native6.AppState.currentState === "active") clearLastActiveGame();
1019
1038
  };
1020
1039
  }, [gameId, setLastActiveGame, clearLastActiveGame]);
1021
1040
  }
@@ -1027,7 +1046,7 @@ var import_chrome5 = require("@tastic/hud/chrome");
1027
1046
  var import_profile7 = require("@tastic/profile");
1028
1047
  var import_expo_router6 = require("expo-router");
1029
1048
  var import_react16 = require("react");
1030
- var import_react_native7 = require("react-native");
1049
+ var import_react_native8 = require("react-native");
1031
1050
  var import_react_native_paper = require("react-native-paper");
1032
1051
 
1033
1052
  // src/hooks/useFrozenWhileHidden.ts
@@ -1047,8 +1066,8 @@ function useFrozenWhileHidden(value, hidden) {
1047
1066
  }
1048
1067
 
1049
1068
  // src/screens/homeLayout.ts
1050
- var import_core10 = require("@tastic/core");
1051
- var import_react_native6 = require("react-native");
1069
+ var import_core11 = require("@tastic/core");
1070
+ var import_react_native7 = require("react-native");
1052
1071
  var import_react_native_safe_area_context3 = require("react-native-safe-area-context");
1053
1072
  var PREVIEW_ASPECT = 360 / 200;
1054
1073
  var ROW_PREVIEW_WIDTH = 92;
@@ -1077,9 +1096,9 @@ function computeHomeLayout(windowWidth, windowHeight, insets) {
1077
1096
  const inner = Math.max(0, usable - HOME_WIDE_EDGE_PADDING - HOME_WIDE_RIGHT_GUTTER);
1078
1097
  const content = Math.min(inner, HOME_WIDE_MAX_CONTENT_WIDTH);
1079
1098
  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));
1099
+ 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
1100
  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);
1101
+ const columns = (0, import_core11.clamp)(Math.floor((rightPaneWidth + HOME_GRID_GAP) / (HOME_MIN_TILE_WIDTH + HOME_GRID_GAP)), 1, HOME_MAX_COLUMNS);
1083
1102
  const tileWidth = Math.max(0, Math.floor((rightPaneWidth - (columns - 1) * HOME_GRID_GAP) / columns));
1084
1103
  const previewWidth = Math.max(0, tileWidth - 2 * HOME_TILE_PADDING);
1085
1104
  const heroFit = {
@@ -1102,7 +1121,7 @@ function computeHomeLayout(windowWidth, windowHeight, insets) {
1102
1121
  };
1103
1122
  }
1104
1123
  function useHomeLayout() {
1105
- const { width, height } = (0, import_react_native6.useWindowDimensions)();
1124
+ const { width, height } = (0, import_react_native7.useWindowDimensions)();
1106
1125
  const insets = (0, import_react_native_safe_area_context3.useSafeAreaInsets)();
1107
1126
  return computeHomeLayout(width, height, insets);
1108
1127
  }
@@ -1132,9 +1151,9 @@ function HomeMenu() {
1132
1151
  const [settingsOpen, setSettingsOpen] = (0, import_react16.useState)(false);
1133
1152
  const [rulesGame, setRulesGame] = (0, import_react16.useState)(null);
1134
1153
  const isFocused = (0, import_expo_router6.useIsFocused)();
1135
- const [appState, setAppState] = (0, import_react16.useState)(import_react_native7.AppState.currentState);
1154
+ const [appState, setAppState] = (0, import_react16.useState)(import_react_native8.AppState.currentState);
1136
1155
  (0, import_react16.useEffect)(() => {
1137
- const subscription = import_react_native7.AppState.addEventListener("change", setAppState);
1156
+ const subscription = import_react_native8.AppState.addEventListener("change", setAppState);
1138
1157
  return () => subscription.remove();
1139
1158
  }, []);
1140
1159
  const heroActive = isFocused && appState === "active";
@@ -1176,24 +1195,24 @@ function HomeMenu() {
1176
1195
  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
1196
  const pressAway = /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_chrome5.PressAwayOverlay, { active: pickerHost.openId !== null, onPress: pickerHost.close, style: styles4.pressAway });
1178
1197
  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: [
1198
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native8.View, { style: [styles4.container, { backgroundColor: bg }], children: [
1199
+ /* @__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
1200
  isWide ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
1182
1201
  pressAway,
1183
1202
  /* @__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)) })
1203
+ tabBar && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native8.View, { style: [styles4.wideTabs, { width: layout.rightPaneWidth }], children: tabBar }),
1204
+ /* @__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
1205
  ] }),
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: [
1206
+ /* @__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
1207
  /* @__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 })
1208
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native8.View, { style: styles4.widePicker, children: picker })
1190
1209
  ] })
1191
1210
  ] }) : /* @__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
1211
  pressAway,
1193
1212
  /* @__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)) })
1213
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native8.View, { style: [styles4.picker, pickerHost.openId !== null && styles4.pickerOpen], children: picker }),
1214
+ tabBar && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native8.View, { style: styles4.listWidth, children: tabBar }),
1215
+ /* @__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
1216
  ] }),
1198
1217
  /* @__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
1218
  /* @__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 +1263,10 @@ function GameRow({ game, hidden, dark, p1Color, p2Color, previewRevealed, onPres
1244
1263
  return (
1245
1264
  // The rules button sits beside the row's own button, laid over its end, not inside it: a button
1246
1265
  // 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: [
1266
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native8.View, { style: hidden ? styles4.hidden : void 0, children: [
1248
1267
  /* @__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: [
1268
+ 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 }) }),
1269
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native8.View, { style: styles4.rowText, children: [
1251
1270
  /* @__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
1271
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper.Text, { variant: "bodyMedium", style: { color: well.fgMuted }, numberOfLines: 2, children: game.blurb })
1253
1272
  ] })
@@ -1258,7 +1277,7 @@ function GameRow({ game, hidden, dark, p1Color, p2Color, previewRevealed, onPres
1258
1277
  }
1259
1278
  function GameTile({ game, hidden, width, previewWidth, dark, p1Color, p2Color, previewRevealed, onPress, onRules }) {
1260
1279
  const well = (0, import_chrome5.useWellColors)();
1261
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native7.View, { style: [{ width }, hidden && styles4.hidden], children: [
1280
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native8.View, { style: [{ width }, hidden && styles4.hidden], children: [
1262
1281
  /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_feedback_press3.Pressable, { onPress, style: [styles4.tile, { backgroundColor: well.bg }], accessibilityRole: "button", accessibilityLabel: game.title, children: [
1263
1282
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(PreviewBox, { game, width: previewWidth, revealed: previewRevealed, dark, p1Color, p2Color }),
1264
1283
  /* @__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 +1290,13 @@ function PreviewBox({ game, width, revealed, dark, p1Color, p2Color }) {
1271
1290
  const height = Math.round(width / PREVIEW_ASPECT);
1272
1291
  const { Preview, icon: GameIcon } = game;
1273
1292
  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 }) }) });
1293
+ 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
1294
  }
1276
1295
  var ROW_ICON_SIZE = 56;
1277
1296
  var ICON_LIGHT_BG = "#FAFAFA";
1278
1297
  var ICON_DARK_BG = "#050505";
1279
1298
  var RULES_BUTTON_SPACE = 44;
1280
- var styles4 = import_react_native7.StyleSheet.create({
1299
+ var styles4 = import_react_native8.StyleSheet.create({
1281
1300
  container: {
1282
1301
  flex: 1
1283
1302
  },
@@ -1433,7 +1452,7 @@ function useRosterAlign(open, contentWidth, contentHeight) {
1433
1452
  function ProfilesRoute() {
1434
1453
  const { profiles, createProfile, updateProfile, deleteProfile } = useProfiles();
1435
1454
  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 }) });
1455
+ 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
1456
  }
1438
1457
 
1439
1458
  // src/screens/StatsScreen.tsx
@@ -1441,7 +1460,7 @@ var import_auto_paper7 = require("@rific/auto-paper");
1441
1460
  var import_chrome7 = require("@tastic/hud/chrome");
1442
1461
  var import_profile9 = require("@tastic/profile");
1443
1462
  var import_react17 = require("react");
1444
- var import_react_native8 = require("react-native");
1463
+ var import_react_native9 = require("react-native");
1445
1464
  var import_react_native_paper2 = require("react-native-paper");
1446
1465
  var import_react_native_safe_area_context4 = require("react-native-safe-area-context");
1447
1466
  var import_react_redux10 = require("react-redux");
@@ -1464,7 +1483,7 @@ function StatsScreen() {
1464
1483
  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
1484
  }
1466
1485
  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 });
1486
+ 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
1487
  }
1469
1488
  var useNoPackReset = () => null;
1470
1489
  var noPickerAlign2 = () => void 0;
@@ -1498,10 +1517,10 @@ function StatsContent({ pickerHost }) {
1498
1517
  const surface = pack.Backdrop ? { backgroundColor: colors.surface } : void 0;
1499
1518
  const triggerStyle = pack.Backdrop ? [styles5.pill, surface] : void 0;
1500
1519
  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: [
1520
+ 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
1521
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_chrome7.PressAwayOverlay, { active: pickerHost.openId !== null, onPress: pickerHost.close, style: styles5.pressAway }),
1503
1522
  /* @__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: [
1523
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native9.View, { style: [styles5.filters, pickerHost.openId !== null && styles5.filtersOpen], onStartShouldSetResponder: () => pickerHost.openId !== null, children: [
1505
1524
  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
1525
  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
1526
  ] }),
@@ -1522,12 +1541,12 @@ function GameStats({ game, tab, profileId, showTitle, titleColor }) {
1522
1541
  const { StatsSections, AchievementList } = game;
1523
1542
  const Sections = tab === STATS ? StatsSections : AchievementList;
1524
1543
  if (!Sections) return null;
1525
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native8.View, { style: styles5.game, children: [
1544
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native9.View, { style: styles5.game, children: [
1526
1545
  showTitle && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_paper2.Text, { variant: "titleLarge", style: [styles5.gameTitle, { color: titleColor }], children: game.title }),
1527
1546
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Sections, { profileId })
1528
1547
  ] });
1529
1548
  }
1530
- var styles5 = import_react_native8.StyleSheet.create({
1549
+ var styles5 = import_react_native9.StyleSheet.create({
1531
1550
  filters: {
1532
1551
  flexDirection: "row",
1533
1552
  flexWrap: "wrap",
@@ -1561,11 +1580,11 @@ var styles5 = import_react_native8.StyleSheet.create({
1561
1580
  });
1562
1581
 
1563
1582
  // src/components/imageGameIcon.tsx
1564
- var import_react_native9 = require("react-native");
1583
+ var import_react_native10 = require("react-native");
1565
1584
  var import_jsx_runtime17 = require("react/jsx-runtime");
1566
1585
  function imageGameIcon(images) {
1567
1586
  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 });
1587
+ 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
1588
  };
1570
1589
  }
1571
1590
 
@@ -1573,20 +1592,19 @@ function imageGameIcon(images) {
1573
1592
  var import_auto_paper8 = require("@rific/auto-paper");
1574
1593
  var import_feedback_press4 = require("@rific/feedback-press");
1575
1594
  var import_react18 = require("react");
1576
- var import_react_native10 = require("react-native");
1595
+ var import_react_native11 = require("react-native");
1577
1596
  var import_react_native_reanimated3 = __toESM(require("react-native-reanimated"));
1578
1597
  var import_react_native_safe_area_context5 = require("react-native-safe-area-context");
1579
1598
  var import_jsx_runtime18 = require("react/jsx-runtime");
1580
- var MATCH_CHIPS_DESKTOP_MIN_WIDTH = 768;
1581
1599
  var MATCH_CHIPS_FADE_MS = 150;
1582
1600
  var CHIP_SIZE = 48;
1583
1601
  var EDGE_PAD = 8;
1584
1602
  function MatchChips({ playing, faceToFace, centerLineY, onBack, onSettings }) {
1585
- const { width, height } = (0, import_react_native10.useWindowDimensions)();
1603
+ const { height } = (0, import_react_native11.useWindowDimensions)();
1586
1604
  const insets = (0, import_react_native_safe_area_context5.useSafeAreaInsets)();
1587
1605
  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;
1606
+ const atComputer = useAtComputer();
1607
+ const visible = atComputer || !playing;
1590
1608
  const opacity = (0, import_react_native_reanimated3.useSharedValue)(visible ? 1 : 0);
1591
1609
  (0, import_react18.useEffect)(() => {
1592
1610
  opacity.value = (0, import_react_native_reanimated3.withTiming)(visible ? 1 : 0, { duration: MATCH_CHIPS_FADE_MS });
@@ -1594,7 +1612,7 @@ function MatchChips({ playing, faceToFace, centerLineY, onBack, onSettings }) {
1594
1612
  const fadeStyle = (0, import_react_native_reanimated3.useAnimatedStyle)(() => ({ opacity: opacity.value }));
1595
1613
  const iconColor = dark ? "#FFFFFF" : "#000000";
1596
1614
  const containerColor = dark ? "rgba(0,0,0,0.55)" : "rgba(255,255,255,0.55)";
1597
- const between = faceToFace && !desktopWeb;
1615
+ const between = faceToFace && !atComputer;
1598
1616
  const top = between ? (centerLineY ?? height / 2) - CHIP_SIZE / 2 : EDGE_PAD + insets.top;
1599
1617
  const backSlot = { top, left: EDGE_PAD + insets.left };
1600
1618
  const settingsSlot = { top, right: EDGE_PAD + insets.right };
@@ -1604,7 +1622,7 @@ function MatchChips({ playing, faceToFace, centerLineY, onBack, onSettings }) {
1604
1622
  /* @__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
1623
  ] });
1606
1624
  }
1607
- var styles6 = import_react_native10.StyleSheet.create({
1625
+ var styles6 = import_react_native11.StyleSheet.create({
1608
1626
  chip: { margin: 0 },
1609
1627
  slot: { position: "absolute" }
1610
1628
  });
@@ -1625,7 +1643,7 @@ var import_redux_persist = require("redux-persist");
1625
1643
 
1626
1644
  // src/redux/shellState.ts
1627
1645
  var import_auto_paper9 = require("@rific/auto-paper");
1628
- var import_core11 = require("@rific/core");
1646
+ var import_core12 = require("@rific/core");
1629
1647
  var import_feedback_press5 = require("@rific/feedback-press");
1630
1648
  var import_profile10 = require("@tastic/profile");
1631
1649
  var initialSoundSettings = { ...import_feedback_press5.defaultSoundSettings, enabled: !__DEV__ };
@@ -1646,7 +1664,7 @@ function createShellReducers({ seatColors, appearance = "system", seatTheme }) {
1646
1664
  sound: appSoundReducer,
1647
1665
  // @rific/core's Keep Awake setting (see Providers.tsx's KeepAwakeBridge), under the namespace its
1648
1666
  // REHYDRATE handling expects.
1649
- keepAwake: import_core11.keepAwakeReducer,
1667
+ keepAwake: import_core12.keepAwakeReducer,
1650
1668
  // The shared App Group roster (see @tastic/profile's resolveInitialProfiles/
1651
1669
  // useSharedProfilesSync, wired in hooks/useProfiles.tsx) is layered on top of this as a separate
1652
1670
  // sync target - this slice persisting normally via redux-persist is what gives it local-fallback
@@ -1865,7 +1883,6 @@ function createPlayerSettings({ id, defaults, distinctControls }) {
1865
1883
  GUEST_PLAYER_ID,
1866
1884
  HomeScreen,
1867
1885
  LoadoutRoute,
1868
- MATCH_CHIPS_DESKTOP_MIN_WIDTH,
1869
1886
  MATCH_CHIPS_FADE_MS,
1870
1887
  MatchChips,
1871
1888
  PREVIEW_ASPECT,
@@ -1903,6 +1920,7 @@ function createPlayerSettings({ id, defaults, distinctControls }) {
1903
1920
  seatTwoFor,
1904
1921
  useActiveRound,
1905
1922
  useAppSettings,
1923
+ useAtComputer,
1906
1924
  useGameGuide,
1907
1925
  useOpponent,
1908
1926
  usePack,