@tastic/shell 0.5.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,6 +35,8 @@ __export(index_exports, {
35
35
  GUEST_PLAYER_ID: () => GUEST_PLAYER_ID,
36
36
  HomeScreen: () => HomeScreen,
37
37
  LoadoutRoute: () => LoadoutRoute,
38
+ MATCH_CHIPS_FADE_MS: () => MATCH_CHIPS_FADE_MS,
39
+ MatchChips: () => MatchChips,
38
40
  PREVIEW_ASPECT: () => PREVIEW_ASPECT,
39
41
  PackProvider: () => PackProvider,
40
42
  PlayRoute: () => PlayRoute,
@@ -64,12 +66,13 @@ __export(index_exports, {
64
66
  profileSelectionActions: () => profileSelectionActions,
65
67
  profileSelectionReducer: () => profileSelectionSlice_default,
66
68
  roundModeFor: () => roundModeFor,
67
- safeBack: () => import_core7.safeBack,
69
+ safeBack: () => import_core8.safeBack,
68
70
  seatColorsActions: () => seatColorsActions,
69
71
  seatPlayerKey: () => seatPlayerKey,
70
72
  seatTwoFor: () => seatTwoFor,
71
73
  useActiveRound: () => useActiveRound,
72
74
  useAppSettings: () => useAppSettings,
75
+ useAtComputer: () => useAtComputer,
73
76
  useGameGuide: () => useGameGuide,
74
77
  useOpponent: () => useOpponent,
75
78
  usePack: () => usePack,
@@ -522,7 +525,7 @@ var styles = import_react_native2.StyleSheet.create({
522
525
  // src/screens/GameRoute.tsx
523
526
  var import_expo_router5 = require("expo-router");
524
527
  var import_react14 = require("react");
525
- var import_react_native5 = require("react-native");
528
+ var import_react_native6 = require("react-native");
526
529
 
527
530
  // src/routes.ts
528
531
  var playHref = (game) => ({ pathname: "/play/[game]", params: { game } });
@@ -532,91 +535,66 @@ var PROFILES_HREF = "/profiles";
532
535
 
533
536
  // src/screens/LoadoutScreen.tsx
534
537
  var import_auto_paper4 = require("@rific/auto-paper");
535
- var import_core9 = require("@tastic/core");
538
+ var import_core10 = require("@tastic/core");
536
539
  var import_chrome4 = require("@tastic/hud/chrome");
537
540
  var import_lobby2 = require("@tastic/hud/lobby");
538
541
  var import_split_screen = require("@tastic/split-screen");
539
542
  var import_expo_router4 = require("expo-router");
540
543
  var import_react13 = require("react");
541
- var import_react_native4 = require("react-native");
544
+ var import_react_native5 = require("react-native");
542
545
  var import_react_native_reanimated2 = __toESM(require("react-native-reanimated"));
543
546
  var import_react_native_safe_area_context2 = require("react-native-safe-area-context");
544
547
  var import_react_redux9 = require("react-redux");
545
548
 
546
- // src/components/SeatTwoPanel.tsx
549
+ // src/components/OpponentPicker.tsx
547
550
  var import_lobby = require("@tastic/hud/lobby");
548
551
  var import_profile4 = require("@tastic/profile");
549
552
  var import_react9 = require("react");
550
553
  var import_react_native3 = require("react-native");
551
554
  var import_jsx_runtime10 = require("react/jsx-runtime");
552
555
  var CPU_PREFIX = "cpu:";
553
- var PROFILE_PREFIX = "profile:";
554
556
  var NOBODY = "nobody";
555
- var GUEST = "guest";
556
- var GUEST_ICON = "account-outline";
557
+ var PERSON = "person";
558
+ var PERSON_ICON = "account-outline";
557
559
  var NOBODY_ICON = "account-off-outline";
558
560
  var CPU_ICON = "robot";
559
- function getSeatTwoPopoverId(idPrefix) {
560
- return `${idPrefix}-seat`;
561
+ function getOpponentPopoverId(idPrefix) {
562
+ return `${idPrefix}-opponent`;
561
563
  }
562
- 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 }) {
563
- const isPerson = seatTwo === "person";
564
+ function OpponentPicker({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDifficulty, color, onColorChange, swatches, takenColor, allowSwapTaken, dark, previewValue, triggerStyle, usePickerAlign }) {
564
565
  const drawn = previewValue ? previewValue(color) : color;
565
- const selectedProfile = isPerson ? profiles.find((p) => p.id === selectedProfileId) ?? null : null;
566
566
  const cpuOptions = cpuDifficulty?.options;
567
567
  const options = (0, import_react9.useMemo)(() => {
568
568
  const list = [];
569
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 }]);
570
- if (choices.includes("nobody")) list.push({ value: NOBODY, label: "Nobody", icon: NOBODY_ICON });
571
- if (choices.includes("person")) {
572
- list.push({ value: GUEST, label: "Player 2", icon: GUEST_ICON });
573
- 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 })));
574
- }
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 });
575
572
  return list;
576
- }, [choices, cpuOptions, profiles, previewValue]);
577
- const value = seatTwo === "cpu" ? `${CPU_PREFIX}${cpuDifficulty?.value ?? ""}` : seatTwo === "nobody" ? NOBODY : selectedProfile ? `${PROFILE_PREFIX}${selectedProfile.id}` : GUEST;
578
- 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;
579
576
  const handleChange = (next) => {
580
577
  if (next.startsWith(CPU_PREFIX)) {
581
578
  cpuDifficulty?.onChange(next.slice(CPU_PREFIX.length));
582
579
  onSeatTwoChange("cpu");
583
- } else if (next === NOBODY) {
584
- onSeatTwoChange("nobody");
585
580
  } else {
586
- onProfileSelect(next.startsWith(PROFILE_PREFIX) ? next.slice(PROFILE_PREFIX.length) : null);
587
- onSeatTwoChange("person");
581
+ onSeatTwoChange(next === NOBODY ? "nobody" : "person");
588
582
  }
589
583
  };
590
- const colorSection = seatTwo === "nobody" ? void 0 : { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, caption: selectedProfile ? colorCaption : void 0, previewValue };
591
- const pickerId = getSeatTwoPopoverId(idPrefix);
592
- 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);
593
587
  const useAlign = usePickerAlign ?? import_lobby.useZoneClampedAlign;
594
588
  const align = useAlign(host.openId === pickerId, size.width, size.height);
595
- const ownPopoverOpen = host.openId?.startsWith(`${idPrefix}-`) ?? false;
596
- const pickerRowOpen = secondPickerId !== void 0 && host.openId === secondPickerId;
597
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native3.View, { style: [styles2.panel, ownPopoverOpen && styles2.panelOpen], children: [
598
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_profile4.OptionPicker, { id: pickerId, host, options, value, onChange: handleChange, color: drawn, dark, icon, colorSection, alignOverride: align, triggerStyle }),
599
- isPerson && secondPicker && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native3.View, { style: [styles2.pickerRow, pickerRowOpen && styles2.pickerRowOpen], children: secondPicker }),
600
- isPerson && showReadyButton && onToggleReady && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lobby.ReadyButton, { color: drawn, ready: ready ?? false, onToggleReady })
601
- ] });
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 }) });
602
590
  }
603
591
  var styles2 = import_react_native3.StyleSheet.create({
604
- panel: {
605
- alignItems: "center",
606
- gap: 12
607
- },
608
- // A popover escaping this panel needs the panel itself elevated to paint above a later sibling.
609
- panelOpen: {
610
- zIndex: 100
611
- },
612
- pickerRow: {
613
- alignItems: "center",
614
- flexDirection: "row",
615
- 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"
616
595
  },
617
- // Lifts the controls picker's popover above the Ready button below it (each View is its own
618
- // stacking context on the web).
619
- pickerRowOpen: {
596
+ // A popover escaping this button needs the button itself elevated to paint above a later sibling.
597
+ buttonOpen: {
620
598
  zIndex: 100
621
599
  }
622
600
  });
@@ -668,14 +646,22 @@ function SettingsDialog({ visible, onDismiss, onShowHowToPlay, children }) {
668
646
  ] });
669
647
  }
670
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
+
671
657
  // src/hooks/useOpponent.ts
672
658
  var import_react11 = require("react");
673
659
  var import_react_redux7 = require("react-redux");
674
660
 
675
661
  // src/redux/opponentSlice.ts
676
- var import_core6 = require("@rific/core");
662
+ var import_core7 = require("@rific/core");
677
663
  var DEFAULT_OPPONENT = { seatTwo: "cpu" };
678
- var { actions: actions4, reducer: reducer4 } = (0, import_core6.createSettingsSlice)("opponent", {
664
+ var { actions: actions4, reducer: reducer4 } = (0, import_core7.createSettingsSlice)("opponent", {
679
665
  initialState: DEFAULT_OPPONENT
680
666
  });
681
667
  var opponentActions = actions4;
@@ -808,13 +794,13 @@ function useSeatColors(p2IsHuman) {
808
794
  }
809
795
 
810
796
  // src/navigation.ts
811
- var import_core7 = require("@rific/core");
797
+ var import_core8 = require("@rific/core");
812
798
  var import_expo_router3 = require("expo-router");
813
- (0, import_core7.configureNavigation)({ router: import_expo_router3.router });
799
+ (0, import_core8.configureNavigation)({ router: import_expo_router3.router });
814
800
 
815
801
  // src/redux/liveplaySlice.ts
816
- var import_core8 = require("@rific/core");
817
- 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", {
818
804
  initialState: { activelyPlaying: false, activeRound: null }
819
805
  });
820
806
  var liveplayActions = actions5;
@@ -823,6 +809,8 @@ var liveplaySlice_default = reducer5;
823
809
  // src/screens/LoadoutScreen.tsx
824
810
  var import_jsx_runtime12 = require("react/jsx-runtime");
825
811
  var PANEL_SWAP_FADE_MS = 180;
812
+ var P1_CONTROL_ID = "p1-control";
813
+ var P2_CONTROL_ID = "p2-control";
826
814
  function LoadoutScreen({ game }) {
827
815
  const pack = usePack();
828
816
  const { seatTwo: savedSeatTwo, setSeatTwo } = useOpponent();
@@ -834,8 +822,10 @@ function LoadoutScreen({ game }) {
834
822
  const loadout = game.useLoadout(mode);
835
823
  const { openFor } = useGameGuide();
836
824
  const { profiles, lastSelected, selectProfile } = useProfiles();
837
- const { orientationMode } = (0, import_core9.useOrientationState)();
838
- 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;
839
829
  const { panelLayout, panelFadeStyle } = (0, import_split_screen.useDualZoneLayout)(panelMode, false, true, false, PANEL_SWAP_FADE_MS);
840
830
  const playersRowFade = panelFadeStyle;
841
831
  const [settingsOpen, setSettingsOpen] = (0, import_react13.useState)(false);
@@ -844,7 +834,7 @@ function LoadoutScreen({ game }) {
844
834
  const { p1Color, p2Color, handleP1ColorChange, handleP2ColorChange } = useSeatColors(p2IsHuman);
845
835
  const dispatch = (0, import_react_redux9.useDispatch)();
846
836
  const handleP1ProfileSelect = (0, import_react13.useCallback)((profile) => selectProfile(1, profile?.id ?? null), [selectProfile]);
847
- 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]);
848
838
  const [ready, setReady] = (0, import_react13.useState)({ 1: false, 2: false });
849
839
  (0, import_expo_router4.useFocusEffect)(
850
840
  (0, import_react13.useCallback)(() => {
@@ -875,16 +865,16 @@ function LoadoutScreen({ game }) {
875
865
  const p1Drawn = seatPreview ? seatPreview(p1Color) : p1Color;
876
866
  const p2Drawn = seatPreview ? seatPreview(p2Color) : p2Color;
877
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;
878
869
  const isPortrait = panelLayout.orientationMode === "faceToFace";
879
870
  const showMetaInSharedBand = (0, import_split_screen.needsSharedNeutralZone)(panelLayout.orientationMode, humanSeats.length);
880
871
  const p2ZoneStyle = p2IsHuman ? isPortrait ? styles3.p2ZoneTop : styles3.p2ZoneRight : null;
881
- const showReadyButton = p2IsHuman;
882
872
  const seatPickerAlign = p2IsHuman ? void 0 : pack.usePickerAlign;
883
873
  const layoutKey = p2IsHuman ? "zones" : "row";
884
874
  const controlsHost = (0, import_chrome4.usePopoverHost)();
885
- const p2PanelHost = (0, import_chrome4.usePopoverHost)();
886
- const p2Host = p2IsHuman ? p2PanelHost : controlsHost;
887
- 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;
888
878
  const optionsRow = loadout.options({
889
879
  host: controlsHost,
890
880
  accentColor: themeColors.tertiary,
@@ -896,36 +886,44 @@ function LoadoutScreen({ game }) {
896
886
  onReset: showMetaInSharedBand ? void 0 : loadout.reset
897
887
  });
898
888
  const optionsPopoverOpen = controlsHost.openId !== null && loadout.optionIds.includes(controlsHost.openId);
899
- 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;
900
890
  const controlLook = { accentColor: themeColors.tertiary, mutedColor: fgMuted, dark };
901
891
  const p1Control = loadout.seatControl(1, controlsHost, { ...controlLook, seatColor: p1Drawn });
902
- 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
+ ] });
903
905
  const p2Control = p2IsHuman ? loadout.seatControl(2, p2Host, { ...controlLook, seatColor: p2Drawn }) : void 0;
904
- 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}`);
905
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native4.View, { style: [styles3.container, { backgroundColor: bg }], children: [
906
- /* @__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: [
907
909
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_chrome4.PressAwayOverlay, { active: controlsHost.openId !== null, onPress: controlsHost.close }),
908
910
  p2ZoneStyle && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_chrome4.PressAwayOverlay, { active: controlsHost.openId !== null || p2Host.openId !== null, onPress: p2Host.close, style: p2ZoneStyle }),
909
- !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 }),
910
912
  p2IsHuman ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
911
913
  import_split_screen.DualZoneLayout,
912
914
  {
913
915
  panelLayout,
914
916
  panelFadeStyle,
915
- p1: p1Panel,
917
+ p1: seatOne,
916
918
  p2: p2Panel,
917
919
  shared: sharedControls,
918
- p1Elevated: controlsHost.openId?.startsWith("p1-") ?? false,
920
+ p1Elevated: p1PickerOpen,
919
921
  p2Elevated: p2Host.openId !== null,
920
922
  sharedElevated: optionsPopoverOpen
921
923
  }
922
- ) : /* @__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: [
923
925
  sharedControls,
924
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native_reanimated2.default.View, { style: [styles3.playersRow, anyPlayerPickerOpen && styles3.playersRowOpen, playersRowFade], children: [
925
- p1Panel,
926
- p2Panel
927
- ] }),
928
- /* @__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
929
927
  ] })
930
928
  ] }),
931
929
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SettingsDialog, { visible: settingsOpen, onDismiss: () => setSettingsOpen(false), onShowHowToPlay: () => openFor(game.id) })
@@ -935,10 +933,16 @@ function halfOf(color) {
935
933
  const rgb = (0, import_auto_paper4.getRgb)(color);
936
934
  return rgb ? `rgba(${rgb.r},${rgb.g},${rgb.b},0.5)` : void 0;
937
935
  }
938
- var styles3 = import_react_native4.StyleSheet.create({
936
+ var styles3 = import_react_native5.StyleSheet.create({
939
937
  container: {
940
938
  flex: 1
941
939
  },
940
+ // Player 1's controls, under their row of buttons.
941
+ controlRow: {
942
+ alignItems: "center",
943
+ flexDirection: "row",
944
+ gap: 12
945
+ },
942
946
  // A real full-bleed box to size the absolutely-positioned children against.
943
947
  frame: {
944
948
  alignItems: "center",
@@ -965,21 +969,37 @@ var styles3 = import_react_native4.StyleSheet.create({
965
969
  pill: {
966
970
  borderRadius: 999
967
971
  },
968
- // The one-human row: seat 1's panel and seat 2's button read as one evenly spaced row of buttons,
969
- // 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).
970
974
  playersRow: {
971
975
  flexDirection: "row",
972
976
  gap: 12
973
977
  },
974
- 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: {
975
981
  zIndex: 100
976
982
  },
977
- // 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
978
984
  // pointerEvents:'none' keeps the sliver an open menu doesn't cover from being a hidden tap target.
979
985
  readyButtonHidden: {
980
986
  opacity: 0,
981
987
  pointerEvents: "none"
982
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
+ },
983
1003
  stackedZone: {
984
1004
  alignItems: "center",
985
1005
  gap: 28
@@ -1012,7 +1032,7 @@ function useResumePoint(gameId) {
1012
1032
  if (!gameId) return;
1013
1033
  setLastActiveGame(gameId);
1014
1034
  return () => {
1015
- if (import_react_native5.AppState.currentState === "active") clearLastActiveGame();
1035
+ if (import_react_native6.AppState.currentState === "active") clearLastActiveGame();
1016
1036
  };
1017
1037
  }, [gameId, setLastActiveGame, clearLastActiveGame]);
1018
1038
  }
@@ -1024,7 +1044,7 @@ var import_chrome5 = require("@tastic/hud/chrome");
1024
1044
  var import_profile7 = require("@tastic/profile");
1025
1045
  var import_expo_router6 = require("expo-router");
1026
1046
  var import_react16 = require("react");
1027
- var import_react_native7 = require("react-native");
1047
+ var import_react_native8 = require("react-native");
1028
1048
  var import_react_native_paper = require("react-native-paper");
1029
1049
 
1030
1050
  // src/hooks/useFrozenWhileHidden.ts
@@ -1044,8 +1064,8 @@ function useFrozenWhileHidden(value, hidden) {
1044
1064
  }
1045
1065
 
1046
1066
  // src/screens/homeLayout.ts
1047
- var import_core10 = require("@tastic/core");
1048
- var import_react_native6 = require("react-native");
1067
+ var import_core11 = require("@tastic/core");
1068
+ var import_react_native7 = require("react-native");
1049
1069
  var import_react_native_safe_area_context3 = require("react-native-safe-area-context");
1050
1070
  var PREVIEW_ASPECT = 360 / 200;
1051
1071
  var ROW_PREVIEW_WIDTH = 92;
@@ -1074,9 +1094,9 @@ function computeHomeLayout(windowWidth, windowHeight, insets) {
1074
1094
  const inner = Math.max(0, usable - HOME_WIDE_EDGE_PADDING - HOME_WIDE_RIGHT_GUTTER);
1075
1095
  const content = Math.min(inner, HOME_WIDE_MAX_CONTENT_WIDTH);
1076
1096
  const slack = (inner - content) / 2;
1077
- 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));
1078
1098
  const rightPaneWidth = Math.max(0, content - HOME_WIDE_PANE_GAP - leftPaneWidth);
1079
- 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);
1080
1100
  const tileWidth = Math.max(0, Math.floor((rightPaneWidth - (columns - 1) * HOME_GRID_GAP) / columns));
1081
1101
  const previewWidth = Math.max(0, tileWidth - 2 * HOME_TILE_PADDING);
1082
1102
  const heroFit = {
@@ -1099,7 +1119,7 @@ function computeHomeLayout(windowWidth, windowHeight, insets) {
1099
1119
  };
1100
1120
  }
1101
1121
  function useHomeLayout() {
1102
- const { width, height } = (0, import_react_native6.useWindowDimensions)();
1122
+ const { width, height } = (0, import_react_native7.useWindowDimensions)();
1103
1123
  const insets = (0, import_react_native_safe_area_context3.useSafeAreaInsets)();
1104
1124
  return computeHomeLayout(width, height, insets);
1105
1125
  }
@@ -1129,9 +1149,9 @@ function HomeMenu() {
1129
1149
  const [settingsOpen, setSettingsOpen] = (0, import_react16.useState)(false);
1130
1150
  const [rulesGame, setRulesGame] = (0, import_react16.useState)(null);
1131
1151
  const isFocused = (0, import_expo_router6.useIsFocused)();
1132
- 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);
1133
1153
  (0, import_react16.useEffect)(() => {
1134
- const subscription = import_react_native7.AppState.addEventListener("change", setAppState);
1154
+ const subscription = import_react_native8.AppState.addEventListener("change", setAppState);
1135
1155
  return () => subscription.remove();
1136
1156
  }, []);
1137
1157
  const heroActive = isFocused && appState === "active";
@@ -1173,24 +1193,24 @@ function HomeMenu() {
1173
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 });
1174
1194
  const pressAway = /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_chrome5.PressAwayOverlay, { active: pickerHost.openId !== null, onPress: pickerHost.close, style: styles4.pressAway });
1175
1195
  const RulesSheet = rulesGame?.Rules;
1176
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native7.View, { style: [styles4.container, { backgroundColor: bg }], children: [
1177
- /* @__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: [
1178
1198
  isWide ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
1179
1199
  pressAway,
1180
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: [
1181
- tabBar && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native7.View, { style: [styles4.wideTabs, { width: layout.rightPaneWidth }], children: tabBar }),
1182
- /* @__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)) })
1183
1203
  ] }),
1184
- /* @__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: [
1185
1205
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(pack.HeroTitle, { words: pack.wordmark, color: fg, p1Color, p2Color, active: heroActive, fit: layout.heroFit }),
1186
- /* @__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 })
1187
1207
  ] })
1188
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: [
1189
1209
  pressAway,
1190
1210
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(pack.HeroTitle, { words: pack.wordmark, color: fg, p1Color, p2Color, active: heroActive }),
1191
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native7.View, { style: [styles4.picker, pickerHost.openId !== null && styles4.pickerOpen], children: picker }),
1192
- tabBar && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native7.View, { style: styles4.listWidth, children: tabBar }),
1193
- /* @__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)) })
1194
1214
  ] }),
1195
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" }),
1196
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" }),
@@ -1241,10 +1261,10 @@ function GameRow({ game, hidden, dark, p1Color, p2Color, previewRevealed, onPres
1241
1261
  return (
1242
1262
  // The rules button sits beside the row's own button, laid over its end, not inside it: a button
1243
1263
  // inside a button is invalid on the web.
1244
- /* @__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: [
1245
1265
  /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_feedback_press3.Pressable, { onPress, style: [styles4.row, { backgroundColor: well.bg }], accessibilityRole: "button", accessibilityLabel: game.title, children: [
1246
- 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 }) }),
1247
- /* @__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: [
1248
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 }),
1249
1269
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper.Text, { variant: "bodyMedium", style: { color: well.fgMuted }, numberOfLines: 2, children: game.blurb })
1250
1270
  ] })
@@ -1255,7 +1275,7 @@ function GameRow({ game, hidden, dark, p1Color, p2Color, previewRevealed, onPres
1255
1275
  }
1256
1276
  function GameTile({ game, hidden, width, previewWidth, dark, p1Color, p2Color, previewRevealed, onPress, onRules }) {
1257
1277
  const well = (0, import_chrome5.useWellColors)();
1258
- 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: [
1259
1279
  /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_feedback_press3.Pressable, { onPress, style: [styles4.tile, { backgroundColor: well.bg }], accessibilityRole: "button", accessibilityLabel: game.title, children: [
1260
1280
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(PreviewBox, { game, width: previewWidth, revealed: previewRevealed, dark, p1Color, p2Color }),
1261
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 }),
@@ -1268,13 +1288,13 @@ function PreviewBox({ game, width, revealed, dark, p1Color, p2Color }) {
1268
1288
  const height = Math.round(width / PREVIEW_ASPECT);
1269
1289
  const { Preview, icon: GameIcon } = game;
1270
1290
  const iconSize = Math.min(height, ROW_ICON_SIZE);
1271
- 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 }) }) });
1272
1292
  }
1273
1293
  var ROW_ICON_SIZE = 56;
1274
1294
  var ICON_LIGHT_BG = "#FAFAFA";
1275
1295
  var ICON_DARK_BG = "#050505";
1276
1296
  var RULES_BUTTON_SPACE = 44;
1277
- var styles4 = import_react_native7.StyleSheet.create({
1297
+ var styles4 = import_react_native8.StyleSheet.create({
1278
1298
  container: {
1279
1299
  flex: 1
1280
1300
  },
@@ -1430,7 +1450,7 @@ function useRosterAlign(open, contentWidth, contentHeight) {
1430
1450
  function ProfilesRoute() {
1431
1451
  const { profiles, createProfile, updateProfile, deleteProfile } = useProfiles();
1432
1452
  const { Backdrop, foreground, newProfileColor, usePickerAlign } = usePack();
1433
- 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 }) });
1434
1454
  }
1435
1455
 
1436
1456
  // src/screens/StatsScreen.tsx
@@ -1438,7 +1458,7 @@ var import_auto_paper7 = require("@rific/auto-paper");
1438
1458
  var import_chrome7 = require("@tastic/hud/chrome");
1439
1459
  var import_profile9 = require("@tastic/profile");
1440
1460
  var import_react17 = require("react");
1441
- var import_react_native8 = require("react-native");
1461
+ var import_react_native9 = require("react-native");
1442
1462
  var import_react_native_paper2 = require("react-native-paper");
1443
1463
  var import_react_native_safe_area_context4 = require("react-native-safe-area-context");
1444
1464
  var import_react_redux10 = require("react-redux");
@@ -1461,7 +1481,7 @@ function StatsScreen() {
1461
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 }) }) });
1462
1482
  }
1463
1483
  function StatsPressAway({ host, children }) {
1464
- 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 });
1465
1485
  }
1466
1486
  var useNoPackReset = () => null;
1467
1487
  var noPickerAlign2 = () => void 0;
@@ -1495,10 +1515,10 @@ function StatsContent({ pickerHost }) {
1495
1515
  const surface = pack.Backdrop ? { backgroundColor: colors.surface } : void 0;
1496
1516
  const triggerStyle = pack.Backdrop ? [styles5.pill, surface] : void 0;
1497
1517
  const PackTabContent = packTab?.Content;
1498
- 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: [
1499
1519
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_chrome7.PressAwayOverlay, { active: pickerHost.openId !== null, onPress: pickerHost.close, style: styles5.pressAway }),
1500
1520
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_paper2.SegmentedButtons, { value: tab, onValueChange: setTab, buttons: tabButtons, style: [styles5.tabs, surface] }),
1501
- /* @__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: [
1502
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 }),
1503
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 })
1504
1524
  ] }),
@@ -1519,12 +1539,12 @@ function GameStats({ game, tab, profileId, showTitle, titleColor }) {
1519
1539
  const { StatsSections, AchievementList } = game;
1520
1540
  const Sections = tab === STATS ? StatsSections : AchievementList;
1521
1541
  if (!Sections) return null;
1522
- 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: [
1523
1543
  showTitle && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_paper2.Text, { variant: "titleLarge", style: [styles5.gameTitle, { color: titleColor }], children: game.title }),
1524
1544
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Sections, { profileId })
1525
1545
  ] });
1526
1546
  }
1527
- var styles5 = import_react_native8.StyleSheet.create({
1547
+ var styles5 = import_react_native9.StyleSheet.create({
1528
1548
  filters: {
1529
1549
  flexDirection: "row",
1530
1550
  flexWrap: "wrap",
@@ -1558,14 +1578,53 @@ var styles5 = import_react_native8.StyleSheet.create({
1558
1578
  });
1559
1579
 
1560
1580
  // src/components/imageGameIcon.tsx
1561
- var import_react_native9 = require("react-native");
1581
+ var import_react_native10 = require("react-native");
1562
1582
  var import_jsx_runtime17 = require("react/jsx-runtime");
1563
1583
  function imageGameIcon(images) {
1564
1584
  return function ImageGameIcon({ size, dark }) {
1565
- 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 });
1566
1586
  };
1567
1587
  }
1568
1588
 
1589
+ // src/components/MatchChips.tsx
1590
+ var import_auto_paper8 = require("@rific/auto-paper");
1591
+ var import_feedback_press4 = require("@rific/feedback-press");
1592
+ var import_react18 = require("react");
1593
+ var import_react_native11 = require("react-native");
1594
+ var import_react_native_reanimated3 = __toESM(require("react-native-reanimated"));
1595
+ var import_react_native_safe_area_context5 = require("react-native-safe-area-context");
1596
+ var import_jsx_runtime18 = require("react/jsx-runtime");
1597
+ var MATCH_CHIPS_FADE_MS = 150;
1598
+ var CHIP_SIZE = 48;
1599
+ var EDGE_PAD = 8;
1600
+ function MatchChips({ playing, faceToFace, centerLineY, onBack, onSettings }) {
1601
+ const { height } = (0, import_react_native11.useWindowDimensions)();
1602
+ const insets = (0, import_react_native_safe_area_context5.useSafeAreaInsets)();
1603
+ const { dark } = (0, import_auto_paper8.useAutoPaperTheme)();
1604
+ const atComputer = useAtComputer();
1605
+ const visible = atComputer || !playing;
1606
+ const opacity = (0, import_react_native_reanimated3.useSharedValue)(visible ? 1 : 0);
1607
+ (0, import_react18.useEffect)(() => {
1608
+ opacity.value = (0, import_react_native_reanimated3.withTiming)(visible ? 1 : 0, { duration: MATCH_CHIPS_FADE_MS });
1609
+ }, [visible]);
1610
+ const fadeStyle = (0, import_react_native_reanimated3.useAnimatedStyle)(() => ({ opacity: opacity.value }));
1611
+ const iconColor = dark ? "#FFFFFF" : "#000000";
1612
+ const containerColor = dark ? "rgba(0,0,0,0.55)" : "rgba(255,255,255,0.55)";
1613
+ const between = faceToFace && !atComputer;
1614
+ const top = between ? (centerLineY ?? height / 2) - CHIP_SIZE / 2 : EDGE_PAD + insets.top;
1615
+ const backSlot = { top, left: EDGE_PAD + insets.left };
1616
+ const settingsSlot = { top, right: EDGE_PAD + insets.right };
1617
+ const pointerEvents = visible ? "box-none" : "none";
1618
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
1619
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native_reanimated3.default.View, { pointerEvents, style: [styles6.slot, backSlot, fadeStyle], testID: "match-chips-back", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_feedback_press4.IconButton, { icon: "arrow-left", iconColor, containerColor, style: styles6.chip, size: 24, onPress: onBack, accessibilityLabel: "Back" }) }),
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" }) })
1621
+ ] });
1622
+ }
1623
+ var styles6 = import_react_native11.StyleSheet.create({
1624
+ chip: { margin: 0 },
1625
+ slot: { position: "absolute" }
1626
+ });
1627
+
1569
1628
  // src/hooks/useActiveRound.ts
1570
1629
  var import_react_redux11 = require("react-redux");
1571
1630
  function useActiveRound(game, defaults) {
@@ -1581,12 +1640,12 @@ var import_toolkit = require("@reduxjs/toolkit");
1581
1640
  var import_redux_persist = require("redux-persist");
1582
1641
 
1583
1642
  // src/redux/shellState.ts
1584
- var import_auto_paper8 = require("@rific/auto-paper");
1585
- var import_core11 = require("@rific/core");
1586
- var import_feedback_press4 = require("@rific/feedback-press");
1643
+ var import_auto_paper9 = require("@rific/auto-paper");
1644
+ var import_core12 = require("@rific/core");
1645
+ var import_feedback_press5 = require("@rific/feedback-press");
1587
1646
  var import_profile10 = require("@tastic/profile");
1588
- var initialSoundSettings = { ...import_feedback_press4.defaultSoundSettings, enabled: !__DEV__ };
1589
- var appSoundReducer = (state = initialSoundSettings, action) => (0, import_feedback_press4.soundReducer)(state, action);
1647
+ var initialSoundSettings = { ...import_feedback_press5.defaultSoundSettings, enabled: !__DEV__ };
1648
+ var appSoundReducer = (state = initialSoundSettings, action) => (0, import_feedback_press5.soundReducer)(state, action);
1590
1649
  function createShellReducers({ seatColors, appearance = "system", seatTheme }) {
1591
1650
  return {
1592
1651
  // Explicit primary/secondary triad (rather than a single seed expanded via harmony) so the pack's
@@ -1598,12 +1657,12 @@ function createShellReducers({ seatColors, appearance = "system", seatTheme }) {
1598
1657
  // createThemeReducer's own default, when it sets none).
1599
1658
  // A pack with its own seatTheme starts from what seat 1's default color makes of it instead (dark
1600
1659
  // only when the pack's appearance is; hooks/useSeatColors.ts re-applies it with the live mode).
1601
- theme: (0, import_auto_paper8.createThemeReducer)(seatTheme ? { appearance, ...seatTheme(seatColors, appearance === "dark") } : { appearance, color: { primary: seatColors[1], secondary: seatColors[2], tertiary: (0, import_auto_paper8.getThirdColor)(seatColors[1], seatColors[2]) } }),
1602
- haptic: import_feedback_press4.hapticReducer,
1660
+ theme: (0, import_auto_paper9.createThemeReducer)(seatTheme ? { appearance, ...seatTheme(seatColors, appearance === "dark") } : { appearance, color: { primary: seatColors[1], secondary: seatColors[2], tertiary: (0, import_auto_paper9.getThirdColor)(seatColors[1], seatColors[2]) } }),
1661
+ haptic: import_feedback_press5.hapticReducer,
1603
1662
  sound: appSoundReducer,
1604
1663
  // @rific/core's Keep Awake setting (see Providers.tsx's KeepAwakeBridge), under the namespace its
1605
1664
  // REHYDRATE handling expects.
1606
- keepAwake: import_core11.keepAwakeReducer,
1665
+ keepAwake: import_core12.keepAwakeReducer,
1607
1666
  // The shared App Group roster (see @tastic/profile's resolveInitialProfiles/
1608
1667
  // useSharedProfilesSync, wired in hooks/useProfiles.tsx) is layered on top of this as a separate
1609
1668
  // sync target - this slice persisting normally via redux-persist is what gives it local-fallback
@@ -1709,8 +1768,8 @@ function forgetProfileAchievements(dropStats) {
1709
1768
 
1710
1769
  // src/gameStats.tsx
1711
1770
  var import_achievements2 = require("@tastic/achievements");
1712
- var import_react18 = require("react");
1713
- var import_jsx_runtime18 = require("react/jsx-runtime");
1771
+ var import_react19 = require("react");
1772
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1714
1773
  var SEATS = [1, 2];
1715
1774
  function createGameStats({ key, defaultStats, isValidStats, catalog, getProfileStatsView }) {
1716
1775
  const { actions: actions6, reducer: sliceReducer } = (0, import_achievements2.createAchievementsSlice)({
@@ -1727,7 +1786,7 @@ function createGameStats({ key, defaultStats, isValidStats, catalog, getProfileS
1727
1786
  };
1728
1787
  const reducer6 = onProfileRemoved(sliceReducer, forgetProfileAchievements(dropProfileStats));
1729
1788
  const profileViews = (stats) => Object.fromEntries(Object.entries(stats.profiles).map(([profileId, bucket]) => [profileId, getProfileStatsView(bucket)]));
1730
- const Context = (0, import_react18.createContext)(null);
1789
+ const Context = (0, import_react19.createContext)(null);
1731
1790
  function Provider({ children }) {
1732
1791
  const { stats, unlockedAchievements, recordOutcome, resetAll } = (0, import_achievements2.useAchievements)({
1733
1792
  catalog,
@@ -1735,18 +1794,18 @@ function createGameStats({ key, defaultStats, isValidStats, catalog, getProfileS
1735
1794
  actions: actions6,
1736
1795
  profileViews
1737
1796
  });
1738
- const record = (0, import_react18.useCallback)(
1797
+ const record = (0, import_react19.useCallback)(
1739
1798
  (update, profileIds) => {
1740
1799
  const result = recordOutcome(update);
1741
1800
  return { device: result.device, profiles: (0, import_achievements2.mapUnlocksBySeat)(SEATS, profileIds, result.profiles) };
1742
1801
  },
1743
1802
  [recordOutcome]
1744
1803
  );
1745
- const value = (0, import_react18.useMemo)(() => ({ stats, unlockedAchievements, record, resetAll }), [stats, unlockedAchievements, record, resetAll]);
1746
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Context.Provider, { value, children });
1804
+ const value = (0, import_react19.useMemo)(() => ({ stats, unlockedAchievements, record, resetAll }), [stats, unlockedAchievements, record, resetAll]);
1805
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Context.Provider, { value, children });
1747
1806
  }
1748
1807
  function useStats() {
1749
- const ctx = (0, import_react18.useContext)(Context);
1808
+ const ctx = (0, import_react19.useContext)(Context);
1750
1809
  if (!ctx) throw new Error(`The ${key} stats are read within their game's Provider`);
1751
1810
  return ctx;
1752
1811
  }
@@ -1754,7 +1813,7 @@ function createGameStats({ key, defaultStats, isValidStats, catalog, getProfileS
1754
1813
  }
1755
1814
 
1756
1815
  // src/playerSettings.ts
1757
- var import_react19 = require("react");
1816
+ var import_react20 = require("react");
1758
1817
  var import_react_redux12 = require("react-redux");
1759
1818
  var SEATS2 = [1, 2];
1760
1819
  function seatPlayerKey(selection, seat) {
@@ -1798,8 +1857,8 @@ function createPlayerSettings({ id, defaults, distinctControls }) {
1798
1857
  const saved = (0, import_react_redux12.useSelector)((state) => state[settingsKey]);
1799
1858
  const controls = (0, import_react_redux12.useSelector)((state) => state[controlsKey]);
1800
1859
  const dispatch = (0, import_react_redux12.useDispatch)();
1801
- const settings = (0, import_react19.useMemo)(() => compose(saved, controls, selection, seatTwoIsPerson), [saved, controls, selection, seatTwoIsPerson]);
1802
- const setSettings = (0, import_react19.useCallback)(
1860
+ const settings = (0, import_react20.useMemo)(() => compose(saved, controls, selection, seatTwoIsPerson), [saved, controls, selection, seatTwoIsPerson]);
1861
+ const setSettings = (0, import_react20.useCallback)(
1803
1862
  (next) => {
1804
1863
  const { controlScheme, ...changes } = next;
1805
1864
  if (Object.keys(changes).length > 0) dispatch(patch(seatPlayerKey(selection, 1), changes));
@@ -1822,6 +1881,8 @@ function createPlayerSettings({ id, defaults, distinctControls }) {
1822
1881
  GUEST_PLAYER_ID,
1823
1882
  HomeScreen,
1824
1883
  LoadoutRoute,
1884
+ MATCH_CHIPS_FADE_MS,
1885
+ MatchChips,
1825
1886
  PREVIEW_ASPECT,
1826
1887
  PackProvider,
1827
1888
  PlayRoute,
@@ -1857,6 +1918,7 @@ function createPlayerSettings({ id, defaults, distinctControls }) {
1857
1918
  seatTwoFor,
1858
1919
  useActiveRound,
1859
1920
  useAppSettings,
1921
+ useAtComputer,
1860
1922
  useGameGuide,
1861
1923
  useOpponent,
1862
1924
  usePack,