@tastic/shell 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -26,12 +26,13 @@ import { useAutoPaperTheme } from "@rific/auto-paper";
26
26
  import { useHapticSettings } from "@rific/feedback-press";
27
27
  import { Toaster, ToastProvider } from "@rific/toaster";
28
28
  import { OrientationProvider, useRotationAwareStatusBar, useThemedRootBackground } from "@tastic/core";
29
- import { RotatedFrame } from "@tastic/hud";
29
+ import { RotatedFrame } from "@tastic/hud/chrome";
30
+ import { ProfileColorProvider } from "@tastic/profile";
30
31
  import * as Haptics from "expo-haptics";
31
- import { Stack } from "expo-router";
32
+ import { DarkTheme, Stack, ThemeProvider as NavigationThemeProvider } from "expo-router";
32
33
  import { DeviceMotion } from "expo-sensors";
33
- import { useCallback as useCallback6 } from "react";
34
- import { StyleSheet } from "react-native";
34
+ import { useCallback as useCallback6, useMemo as useMemo3 } from "react";
35
+ import { Platform as Platform2, StyleSheet, View } from "react-native";
35
36
  import { GestureHandlerRootView } from "react-native-gesture-handler";
36
37
  import * as RNPaper2 from "react-native-paper";
37
38
  import { SafeAreaProvider } from "react-native-safe-area-context";
@@ -75,24 +76,35 @@ function seatTwoFor(saved, choices) {
75
76
  function humanSeatsFor(mode) {
76
77
  return mode === "twoPlayer" ? [1, 2] : [1];
77
78
  }
79
+ var GUEST_PLAYER_ID = "guest";
78
80
 
79
81
  // src/components/GuideHost.tsx
80
82
  import { jsx as jsx3 } from "react/jsx-runtime";
83
+ var PACK_GUIDE = "__pack__";
84
+ var NO_INPUT = {};
81
85
  var GameGuideContext = createContext2(null);
82
86
  function GuideHost({ children }) {
83
87
  const pack = usePack();
84
- const [gameId, setGameId] = useState(pack.games[0].id);
85
- const game = pack.games.find((g) => g.id === gameId) ?? pack.games[0];
88
+ const [subject, setSubject] = useState(() => pack.guide ? PACK_GUIDE : pack.games.find((g) => g.guide)?.id ?? null);
89
+ const packGuide = subject === PACK_GUIDE ? pack.guide : void 0;
90
+ const game = subject === PACK_GUIDE ? void 0 : pack.games.find((g) => g.id === subject);
91
+ const gameGuide = game?.guide;
86
92
  const rotation = useRotation();
87
93
  const touch = useIsTouchPrimaryDevice();
88
94
  const seatTwo = useSelector((state) => state.opponent.seatTwo);
89
- const input = useSelector((state) => game.guide.selectInput(state), shallowEqual);
90
- const seenVersion = useSelector((state) => game.guide.selectVersionSeen(state));
95
+ const input = useSelector((state) => gameGuide ? gameGuide.selectInput(state) : NO_INPUT, shallowEqual);
96
+ const seenVersion = useSelector((state) => (packGuide ?? gameGuide)?.selectVersionSeen(state) ?? 0);
91
97
  const dispatch = useDispatch();
92
- const mode = roundModeFor(seatTwoFor(seatTwo, game.seatTwo));
93
- const steps = useMemo(() => game.guide.getSteps(input, { touch, mode }), [game, input, touch, mode]);
94
- const onSeen = useCallback((version) => dispatch(game.guide.markSeen(version)), [dispatch, game]);
95
- return /* @__PURE__ */ jsx3(GuideProvider, { steps, currentVersion: game.guide.version, seenVersion, onSeen, rotation, children: /* @__PURE__ */ jsx3(GameGuideBridge, { onPick: setGameId, children }) });
98
+ const mode = roundModeFor(seatTwoFor(seatTwo, game?.seatTwo));
99
+ const steps = useMemo(() => packGuide ? packGuide.getSteps({ touch }) : gameGuide ? gameGuide.getSteps(input, { touch, mode }) : [], [packGuide, gameGuide, input, touch, mode]);
100
+ const onSeen = useCallback(
101
+ (version) => {
102
+ const guide = packGuide ?? gameGuide;
103
+ if (guide) dispatch(guide.markSeen(version));
104
+ },
105
+ [dispatch, packGuide, gameGuide]
106
+ );
107
+ return /* @__PURE__ */ jsx3(GuideProvider, { steps, currentVersion: (packGuide ?? gameGuide)?.version ?? 0, seenVersion, onSeen, rotation, children: /* @__PURE__ */ jsx3(GameGuideBridge, { onPick: setSubject, children }) });
96
108
  }
97
109
  function GameGuideBridge({ onPick, children }) {
98
110
  const pack = usePack();
@@ -100,6 +112,7 @@ function GameGuideBridge({ onPick, children }) {
100
112
  const { isOpen, open } = useGuide();
101
113
  const pendingNext = useRef(null);
102
114
  const wasOpen = useRef(false);
115
+ const packAutoShown = useRef(false);
103
116
  useEffect(() => {
104
117
  if (wasOpen.current && !isOpen && pendingNext.current) {
105
118
  const next = pendingNext.current;
@@ -110,15 +123,16 @@ function GameGuideBridge({ onPick, children }) {
110
123
  }, [isOpen]);
111
124
  const openFor = useCallback(
112
125
  (gameId) => {
126
+ if (!pack.games.find((g) => g.id === gameId)?.guide) return;
113
127
  onPick(gameId);
114
128
  open();
115
129
  },
116
- [onPick, open]
130
+ [pack, onPick, open]
117
131
  );
118
132
  const openFirstTime = useCallback(
119
133
  (gameId, next) => {
120
- const game = pack.games.find((g) => g.id === gameId);
121
- if (!game || game.guide.selectVersionSeen(store.getState()) >= game.guide.version) {
134
+ const guide = pack.games.find((g) => g.id === gameId)?.guide;
135
+ if (!guide || guide.selectVersionSeen(store.getState()) >= guide.version) {
122
136
  next();
123
137
  return;
124
138
  }
@@ -127,7 +141,18 @@ function GameGuideBridge({ onPick, children }) {
127
141
  },
128
142
  [pack, store, openFor]
129
143
  );
130
- const value = useMemo(() => ({ openFirstTime, openFor }), [openFirstTime, openFor]);
144
+ const openPack = useCallback(() => {
145
+ if (!pack.guide) return;
146
+ onPick(PACK_GUIDE);
147
+ open();
148
+ }, [pack, onPick, open]);
149
+ const autoShowPack = useCallback(() => {
150
+ const guide = pack.guide;
151
+ if (!guide || packAutoShown.current || guide.selectVersionSeen(store.getState()) >= guide.version) return;
152
+ packAutoShown.current = true;
153
+ openPack();
154
+ }, [pack, store, openPack]);
155
+ const value = useMemo(() => ({ openFirstTime, openFor, openPack, autoShowPack }), [openFirstTime, openFor, openPack, autoShowPack]);
131
156
  return /* @__PURE__ */ jsx3(GameGuideContext.Provider, { value, children });
132
157
  }
133
158
  function useGameGuide() {
@@ -147,15 +172,20 @@ import * as RNPaper from "react-native-paper";
147
172
  import { Provider as ReduxProvider, useDispatch as useDispatch2, useSelector as useSelector2 } from "react-redux";
148
173
  import { PersistGate } from "redux-persist/integration/react";
149
174
  import { Fragment as Fragment2, jsx as jsx4 } from "react/jsx-runtime";
175
+ function useShellFeedbackSounds() {
176
+ const selection = useAudioPool(__require("./select-MLBNNA4N.wav"));
177
+ const notification = useAudioPool(__require("./notification-CGGFPYK7.wav"));
178
+ return { selection, notification };
179
+ }
150
180
  var FeedbackBridge = ({ children }) => {
151
181
  const haptic = useSelector2((state) => state.haptic);
152
182
  const sound = useSelector2((state) => state.sound);
153
183
  const dispatch = useDispatch2();
154
184
  const onChange = useCallback2((next) => dispatch(hapticActions.initialize(next)), [dispatch]);
155
185
  const onSoundChange = useCallback2((next) => dispatch(soundActions.initialize(next)), [dispatch]);
156
- const playSelection = useAudioPool(__require("./select-MLBNNA4N.wav"));
157
- const playNotification = useAudioPool(__require("./notification-CGGFPYK7.wav"));
158
- const bridgeProps = useFeedbackBridgeProps({ initialValue: haptic, onChange, soundInitialValue: sound, onSoundChange, sound: { selection: playSelection, notification: playNotification } });
186
+ const { useFeedbackSounds = useShellFeedbackSounds } = usePack();
187
+ const sounds = useFeedbackSounds();
188
+ const bridgeProps = useFeedbackBridgeProps({ initialValue: haptic, onChange, soundInitialValue: sound, onSoundChange, sound: sounds });
159
189
  return /* @__PURE__ */ jsx4(FeedbackPressProvider, { ...bridgeProps, paper: RNPaper, children });
160
190
  };
161
191
  var EdgeGuardBridge = ({ children }) => {
@@ -198,7 +228,7 @@ function Theme({ children }) {
198
228
  // src/components/UpdateDialog.tsx
199
229
  import { useUpdateErrorToast } from "@rific/toaster";
200
230
  import { useRotation as useRotation2 } from "@tastic/core";
201
- import { UpdateDialog as SharedUpdateDialog } from "@tastic/hud";
231
+ import { UpdateDialog as SharedUpdateDialog } from "@tastic/hud/chrome";
202
232
  import { jsx as jsx6 } from "react/jsx-runtime";
203
233
  function UpdateDialog() {
204
234
  const onError = useUpdateErrorToast();
@@ -371,20 +401,41 @@ function useProfiles() {
371
401
  }
372
402
 
373
403
  // src/RootLayout.tsx
374
- import { jsx as jsx9, jsxs as jsxs2 } from "react/jsx-runtime";
404
+ import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs2 } from "react/jsx-runtime";
375
405
  var PLAY_OPTIONS = { statusBarHidden: true };
406
+ var KEEP_STATUS_BAR_OPTIONS = {};
407
+ var TRANSPARENT_CONTENT = { backgroundColor: "transparent" };
408
+ var noScreenOptions = () => ({});
376
409
  function AppStack() {
410
+ const pack = usePack();
411
+ const { Backdrop } = pack;
377
412
  const { dark } = useAutoPaperTheme();
378
- const rootBackground = useThemedRootBackground(dark, false);
413
+ const rootBackground = useThemedRootBackground(Backdrop ? true : dark, false);
379
414
  const statusBar = useRotationAwareStatusBar();
380
- return /* @__PURE__ */ jsxs2(Stack, { screenOptions: { ...rootBackground, ...statusBar }, children: [
415
+ const packOptions = (pack.useScreenOptions ?? noScreenOptions)();
416
+ const backdropOptions = Backdrop ? { contentStyle: TRANSPARENT_CONTENT, animation: Platform2.select({ android: "fade" }) } : void 0;
417
+ const playOptions = useCallback6(
418
+ ({ route }) => {
419
+ const id = route.params?.game;
420
+ return pack.games.some((g) => g.id === id && g.keepStatusBar) ? KEEP_STATUS_BAR_OPTIONS : PLAY_OPTIONS;
421
+ },
422
+ [pack.games]
423
+ );
424
+ const navigationTheme = useMemo3(() => Backdrop ? { ...DarkTheme, colors: { ...DarkTheme.colors, background: "transparent" } } : null, [Backdrop]);
425
+ const stack = /* @__PURE__ */ jsxs2(Stack, { screenOptions: { ...rootBackground, ...statusBar, ...backdropOptions, ...packOptions }, children: [
381
426
  /* @__PURE__ */ jsx9(Stack.Screen, { name: "index" }),
382
- /* @__PURE__ */ jsx9(Stack.Screen, { name: "play/[game]", options: PLAY_OPTIONS })
427
+ /* @__PURE__ */ jsx9(Stack.Screen, { name: "play/[game]", options: playOptions })
428
+ ] });
429
+ if (!Backdrop || !navigationTheme) return stack;
430
+ return /* @__PURE__ */ jsxs2(View, { style: styles.root, children: [
431
+ /* @__PURE__ */ jsx9(Backdrop, {}),
432
+ /* @__PURE__ */ jsx9(NavigationThemeProvider, { value: navigationTheme, children: stack })
383
433
  ] });
384
434
  }
385
435
  function OrientationBridge({ children }) {
386
436
  const { settings, setSettings } = useAppSettings();
387
- return /* @__PURE__ */ jsx9(OrientationProvider, { deviceMotion: DeviceMotion, freezeWhileLocked: true, lockInitialValue: { locked: settings.lockOrientation, rotation: settings.lockedRotation }, onLockChange: (next) => setSettings({ lockOrientation: next.locked, lockedRotation: next.rotation }), children });
437
+ const { portraitWhileKeyboard } = usePack();
438
+ return /* @__PURE__ */ jsx9(OrientationProvider, { deviceMotion: DeviceMotion, freezeWhileLocked: true, lockInitialValue: { locked: settings.lockOrientation, rotation: settings.lockedRotation }, onLockChange: (next) => setSettings({ lockOrientation: next.locked, lockedRotation: next.rotation }), portraitWhileKeyboard, children });
388
439
  }
389
440
  function GatedApp({ children }) {
390
441
  const { loaded } = useProfiles();
@@ -392,7 +443,8 @@ function GatedApp({ children }) {
392
443
  }
393
444
  function HapticGatedToastProvider({ children }) {
394
445
  const { settings } = useHapticSettings();
395
- return /* @__PURE__ */ jsx9(ToastProvider, { haptics: settings.vibrate ? Haptics : void 0, paper: RNPaper2, children });
446
+ const { Toaster: PackToasterComponent } = usePack();
447
+ return /* @__PURE__ */ jsx9(ToastProvider, { haptics: settings.vibrate ? Haptics : void 0, paper: PackToasterComponent ? void 0 : RNPaper2, children });
396
448
  }
397
449
  function RotatingToaster() {
398
450
  const renderFrame = useCallback6(
@@ -401,15 +453,28 @@ function RotatingToaster() {
401
453
  );
402
454
  return /* @__PURE__ */ jsx9(Toaster, { historyButton: null, clearButton: null, limit: 3, renderFrame });
403
455
  }
456
+ function PackToaster({ Toaster: PackToasterComponent }) {
457
+ return /* @__PURE__ */ jsx9(View, { style: StyleSheet.absoluteFill, pointerEvents: "box-none", children: /* @__PURE__ */ jsx9(RotatedFrame, { rotatedSafeArea: true, passThrough: true, children: /* @__PURE__ */ jsx9(PackToasterComponent, {}) }) });
458
+ }
404
459
  function GameProviders({ games, children }) {
405
- return games.reduceRight((inner, game) => /* @__PURE__ */ jsx9(game.Provider, { children: inner }, game.id), children);
460
+ return games.reduceRight((inner, game) => game.Provider ? /* @__PURE__ */ jsx9(game.Provider, { children: inner }, game.id) : inner, children);
461
+ }
462
+ function PreviewColors({ previewColor, children }) {
463
+ const { dark } = useAutoPaperTheme();
464
+ const preview = useCallback6((hex) => previewColor(hex, dark), [previewColor, dark]);
465
+ return /* @__PURE__ */ jsx9(ProfileColorProvider, { preview, children });
466
+ }
467
+ function PackRoot({ pack, children }) {
468
+ const { Provider, previewColor } = pack;
469
+ const inner = Provider ? /* @__PURE__ */ jsx9(Provider, { children }) : children;
470
+ return previewColor ? /* @__PURE__ */ jsx9(PreviewColors, { previewColor, children: inner }) : /* @__PURE__ */ jsx9(Fragment3, { children: inner });
406
471
  }
407
472
  function RootLayout({ pack, store, persistor }) {
408
- return /* @__PURE__ */ jsx9(GestureHandlerRootView, { style: styles.root, children: /* @__PURE__ */ jsx9(SafeAreaProvider, { children: /* @__PURE__ */ jsx9(PackProvider, { pack, children: /* @__PURE__ */ jsx9(Fonts, { children: /* @__PURE__ */ jsx9(Providers, { store, persistor, children: /* @__PURE__ */ jsx9(Theme, { children: /* @__PURE__ */ jsx9(ProfilesProvider, { children: /* @__PURE__ */ jsx9(LastActiveGameProvider, { children: /* @__PURE__ */ jsx9(GameProviders, { games: pack.games, children: /* @__PURE__ */ jsx9(GatedApp, { children: /* @__PURE__ */ jsx9(OrientationBridge, { children: /* @__PURE__ */ jsxs2(HapticGatedToastProvider, { children: [
473
+ return /* @__PURE__ */ jsx9(GestureHandlerRootView, { style: styles.root, children: /* @__PURE__ */ jsx9(SafeAreaProvider, { children: /* @__PURE__ */ jsx9(PackProvider, { pack, children: /* @__PURE__ */ jsx9(Fonts, { children: /* @__PURE__ */ jsx9(Providers, { store, persistor, children: /* @__PURE__ */ jsx9(Theme, { children: /* @__PURE__ */ jsx9(ProfilesProvider, { children: /* @__PURE__ */ jsx9(LastActiveGameProvider, { children: /* @__PURE__ */ jsx9(GameProviders, { games: pack.games, children: /* @__PURE__ */ jsx9(PackRoot, { pack, children: /* @__PURE__ */ jsx9(GatedApp, { children: /* @__PURE__ */ jsx9(OrientationBridge, { children: /* @__PURE__ */ jsxs2(HapticGatedToastProvider, { children: [
409
474
  /* @__PURE__ */ jsx9(GuideHost, { children: /* @__PURE__ */ jsx9(AppStack, {}) }),
410
- /* @__PURE__ */ jsx9(RotatingToaster, {}),
475
+ pack.Toaster ? /* @__PURE__ */ jsx9(PackToaster, { Toaster: pack.Toaster }) : /* @__PURE__ */ jsx9(RotatingToaster, {}),
411
476
  /* @__PURE__ */ jsx9(UpdateDialog, {})
412
- ] }) }) }) }) }) }) }) }) }) }) }) });
477
+ ] }) }) }) }) }) }) }) }) }) }) }) }) });
413
478
  }
414
479
  var styles = StyleSheet.create({
415
480
  root: { flex: 1 }
@@ -418,24 +483,32 @@ var styles = StyleSheet.create({
418
483
  // src/screens/GameRoute.tsx
419
484
  import { Redirect, useLocalSearchParams } from "expo-router";
420
485
  import { useEffect as useEffect5 } from "react";
486
+ import { AppState } from "react-native";
487
+
488
+ // src/routes.ts
489
+ var playHref = (game) => ({ pathname: "/play/[game]", params: { game } });
490
+ var loadoutHref = (game) => ({ pathname: "/loadout/[game]", params: { game } });
491
+ var STATS_HREF = "/stats";
492
+ var PROFILES_HREF = "/profiles";
421
493
 
422
494
  // src/screens/LoadoutScreen.tsx
423
495
  import { defaultColors, useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
424
496
  import { FakeLandscapeView, getViewRotation, rotateInsets, useOrientationState } from "@tastic/core";
425
- import { CornerActionButtons, PlayerSetupPanel, PressAwayOverlay, ReadyButton as ReadyButton2, SharedActionBand, usePopoverHost } from "@tastic/hud";
497
+ import { CornerActionButtons, PressAwayOverlay, usePopoverHost } from "@tastic/hud/chrome";
498
+ import { PlayerSetupPanel, ReadyButton as ReadyButton2, SharedActionBand } from "@tastic/hud/lobby";
426
499
  import { DualZoneLayout, needsSharedNeutralZone, useDualZoneLayout } from "@tastic/split-screen";
427
500
  import { router as router2, useFocusEffect as useFocusEffect2 } from "expo-router";
428
- import { useCallback as useCallback9, useEffect as useEffect4, useMemo as useMemo4, useRef as useRef4, useState as useState4 } from "react";
429
- import { Platform as Platform2, StyleSheet as StyleSheet3, View as View2 } from "react-native";
501
+ import { useCallback as useCallback9, useEffect as useEffect4, useMemo as useMemo5, useRef as useRef4, useState as useState4 } from "react";
502
+ import { Platform as Platform3, StyleSheet as StyleSheet3, View as View3 } from "react-native";
430
503
  import Animated from "react-native-reanimated";
431
504
  import { useSafeAreaInsets } from "react-native-safe-area-context";
432
505
  import { useDispatch as useDispatch9 } from "react-redux";
433
506
 
434
507
  // src/components/SeatTwoPanel.tsx
435
- import { ReadyButton, useZoneClampedAlign } from "@tastic/hud";
508
+ import { ReadyButton, useZoneClampedAlign } from "@tastic/hud/lobby";
436
509
  import { getOptionPickerContentSize, OptionPicker } from "@tastic/profile";
437
- import { useMemo as useMemo3 } from "react";
438
- import { StyleSheet as StyleSheet2, View } from "react-native";
510
+ import { useMemo as useMemo4 } from "react";
511
+ import { StyleSheet as StyleSheet2, View as View2 } from "react-native";
439
512
  import { jsx as jsx10, jsxs as jsxs3 } from "react/jsx-runtime";
440
513
  var CPU_PREFIX = "cpu:";
441
514
  var PROFILE_PREFIX = "profile:";
@@ -451,7 +524,7 @@ function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDi
451
524
  const isPerson = seatTwo === "person";
452
525
  const selectedProfile = isPerson ? profiles.find((p) => p.id === selectedProfileId) ?? null : null;
453
526
  const cpuOptions = cpuDifficulty?.options;
454
- const options = useMemo3(() => {
527
+ const options = useMemo4(() => {
455
528
  const list = [];
456
529
  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 }]);
457
530
  if (choices.includes("nobody")) list.push({ value: NOBODY, label: "Nobody", icon: NOBODY_ICON });
@@ -480,9 +553,9 @@ function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDi
480
553
  const align = useZoneClampedAlign(host.openId === pickerId, size.width, size.height);
481
554
  const ownPopoverOpen = host.openId?.startsWith(`${idPrefix}-`) ?? false;
482
555
  const pickerRowOpen = secondPickerId !== void 0 && host.openId === secondPickerId;
483
- return /* @__PURE__ */ jsxs3(View, { style: [styles2.panel, ownPopoverOpen && styles2.panelOpen], children: [
556
+ return /* @__PURE__ */ jsxs3(View2, { style: [styles2.panel, ownPopoverOpen && styles2.panelOpen], children: [
484
557
  /* @__PURE__ */ jsx10(OptionPicker, { id: pickerId, host, options, value, onChange: handleChange, color, dark, icon, colorSection, alignOverride: align }),
485
- isPerson && secondPicker && /* @__PURE__ */ jsx10(View, { style: [styles2.pickerRow, pickerRowOpen && styles2.pickerRowOpen], children: secondPicker }),
558
+ isPerson && secondPicker && /* @__PURE__ */ jsx10(View2, { style: [styles2.pickerRow, pickerRowOpen && styles2.pickerRowOpen], children: secondPicker }),
486
559
  isPerson && showReadyButton && onToggleReady && /* @__PURE__ */ jsx10(ReadyButton, { color, ready: ready ?? false, onToggleReady })
487
560
  ] });
488
561
  }
@@ -509,13 +582,14 @@ var styles2 = StyleSheet2.create({
509
582
 
510
583
  // src/components/SettingsDialog.tsx
511
584
  import { useUpdateErrorToast as useUpdateErrorToast2 } from "@rific/toaster";
512
- import { BaseSettingsDialog } from "@tastic/hud";
585
+ import { BaseSettingsDialog } from "@tastic/hud/chrome";
513
586
  import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
514
587
  function SettingsDialog({ visible, onDismiss, rotation = 0, onShowHowToPlay, children }) {
515
588
  const onUpdateError = useUpdateErrorToast2();
516
589
  const { settings, setSettings } = useAppSettings();
517
- const { otaVersion, SettingsSections } = usePack();
518
- return /* @__PURE__ */ jsxs4(BaseSettingsDialog, { visible, onDismiss, rotation, version: otaVersion, lockOrientation: settings.lockOrientation, onLockOrientationChange: (value) => setSettings({ lockOrientation: value }), deferBottomEdgeGestures: settings.deferBottomEdgeGestures, onDeferBottomEdgeGestures: (value) => setSettings({ deferBottomEdgeGestures: value }), onUpdateError, onShowHowToPlay, children: [
590
+ const { otaVersion, SettingsSections, edgeGuard = true } = usePack();
591
+ const edgeGuardProps = edgeGuard ? { deferBottomEdgeGestures: settings.deferBottomEdgeGestures, onDeferBottomEdgeGestures: (value) => setSettings({ deferBottomEdgeGestures: value }) } : {};
592
+ return /* @__PURE__ */ jsxs4(BaseSettingsDialog, { visible, onDismiss, rotation, version: otaVersion, lockOrientation: settings.lockOrientation, onLockOrientationChange: (value) => setSettings({ lockOrientation: value }), ...edgeGuardProps, onUpdateError, onShowHowToPlay, children: [
519
593
  children,
520
594
  SettingsSections && /* @__PURE__ */ jsx11(SettingsSections, {})
521
595
  ] });
@@ -560,13 +634,17 @@ function createSeatColorsReducer(seatColors) {
560
634
  var seatColorsActions = createSeatColorsSlice("pack", defaultSeatColorsState({ 1: "", 2: "" })).actions;
561
635
 
562
636
  // src/hooks/useSeatColors.ts
637
+ function sameTheme(current, next) {
638
+ const a = current.color;
639
+ const b = next.color;
640
+ const sameColor = typeof a === "string" || typeof b === "string" ? typeof a === typeof b && String(a).toLowerCase() === String(b).toLowerCase() : !!a && !!b && a.primary.toLowerCase() === b.primary.toLowerCase() && a.secondary.toLowerCase() === b.secondary.toLowerCase();
641
+ return sameColor && (next.harmony === void 0 || next.harmony === current.harmony);
642
+ }
563
643
  function useSeatColors(p2IsHuman) {
564
- const { colors: themeColors } = useAutoPaperTheme2();
565
- const { set: setThemeColor } = useThemeSettings();
566
- const p1Color = themeColors.primary;
567
- const p2Color = themeColors.secondary;
644
+ const { dark, colors: themeColors } = useAutoPaperTheme2();
645
+ const { settings: themeSettings, set: setThemeColor } = useThemeSettings();
568
646
  const { profiles, lastSelected } = useProfiles();
569
- const { seatColors } = usePack();
647
+ const { seatColors, seatTheme } = usePack();
570
648
  const seatDefaultColors = [seatColors[1], seatColors[2]];
571
649
  const dispatch = useDispatch8();
572
650
  const guestColors = useSelector7((state) => state.seatColors.lastGuestColor);
@@ -574,11 +652,21 @@ function useSeatColors(p2IsHuman) {
574
652
  const profileColors = useSelector7((state) => state.seatColors.profileColors);
575
653
  const p1Profile = profiles.find((p) => p.id === lastSelected[1]) ?? null;
576
654
  const p2Profile = p2IsHuman ? profiles.find((p) => p.id === lastSelected[2]) ?? null : null;
655
+ const rememberedColors = (p1Prof, p2Prof, picks, guest, cpu) => {
656
+ const p1 = p1Prof ? profileSeatColor(p1Prof, picks) : guest[1];
657
+ const p2 = avoidSeatColorClash(p1, p2Prof ? profileSeatColor(p2Prof, picks) : p2IsHuman ? guest[2] : cpu, seatDefaultColors);
658
+ return { p1, p2 };
659
+ };
660
+ const remembered = rememberedColors(p1Profile, p2Profile, profileColors, guestColors, cpuColor);
661
+ const p1Color = seatTheme ? remembered.p1 : themeColors.primary;
662
+ const p2Color = seatTheme ? remembered.p2 : themeColors.secondary;
663
+ const themeFor = useCallback8((p1, p2) => seatTheme ? seatTheme({ 1: p1, 2: p2 }, dark) : { color: { primary: p1, secondary: p2, tertiary: getThirdColor(p1, p2) } }, [seatTheme, dark]);
577
664
  const p1ProfileRef = useRef3(p1Profile);
578
665
  const p2ProfileRef = useRef3(p2Profile);
579
666
  const guestColorsRef = useRef3(guestColors);
580
667
  const cpuColorRef = useRef3(cpuColor);
581
668
  const profileColorsRef = useRef3(profileColors);
669
+ const themeForRef = useRef3(themeFor);
582
670
  useEffect3(() => {
583
671
  p1ProfileRef.current = p1Profile;
584
672
  }, [p1Profile]);
@@ -595,21 +683,17 @@ function useSeatColors(p2IsHuman) {
595
683
  profileColorsRef.current = profileColors;
596
684
  }, [profileColors]);
597
685
  useEffect3(() => {
598
- const nextP1 = p1Profile ? profileSeatColor(p1Profile, profileColors) : guestColors[1];
599
- const nextP2 = avoidSeatColorClash(nextP1, p2Profile ? profileSeatColor(p2Profile, profileColors) : p2IsHuman ? guestColors[2] : cpuColor, seatDefaultColors);
600
- if (nextP1.toLowerCase() === p1Color.toLowerCase() && nextP2.toLowerCase() === p2Color.toLowerCase()) return;
601
- setThemeColor({ color: { primary: nextP1, secondary: nextP2, tertiary: getThirdColor(nextP1, nextP2) } });
602
- }, [lastSelected[1], lastSelected[2], p2IsHuman, guestColors, cpuColor, profileColors, profiles.length]);
686
+ themeForRef.current = themeFor;
687
+ }, [themeFor]);
688
+ useEffect3(() => {
689
+ const next = themeFor(remembered.p1, remembered.p2);
690
+ if (sameTheme(themeSettings, next)) return;
691
+ setThemeColor(next);
692
+ }, [lastSelected[1], lastSelected[2], p2IsHuman, guestColors, cpuColor, profileColors, profiles.length, dark]);
603
693
  useFocusEffect(
604
694
  useCallback8(() => {
605
- const guest = guestColorsRef.current;
606
- const cpu = cpuColorRef.current;
607
- const p1Prof = p1ProfileRef.current;
608
- const p2Prof = p2ProfileRef.current;
609
- const picks = profileColorsRef.current;
610
- const nextP1 = p1Prof ? profileSeatColor(p1Prof, picks) : guest[1];
611
- const nextP2 = avoidSeatColorClash(nextP1, p2Prof ? profileSeatColor(p2Prof, picks) : p2IsHuman ? guest[2] : cpu, seatDefaultColors);
612
- setThemeColor({ color: { primary: nextP1, secondary: nextP2, tertiary: getThirdColor(nextP1, nextP2) } });
695
+ const next = rememberedColors(p1ProfileRef.current, p2ProfileRef.current, profileColorsRef.current, guestColorsRef.current, cpuColorRef.current);
696
+ setThemeColor(themeForRef.current(next.p1, next.p2));
613
697
  }, [lastSelected[1], lastSelected[2], p2IsHuman, setThemeColor])
614
698
  );
615
699
  const persistSeatColor = useCallback8(
@@ -631,21 +715,21 @@ function useSeatColors(p2IsHuman) {
631
715
  (hex) => {
632
716
  const swapped = hex.toLowerCase() === p2Color.toLowerCase();
633
717
  const nextP2 = swapped ? p1Color : p2Color;
634
- setThemeColor({ color: { primary: hex, secondary: nextP2, tertiary: getThirdColor(hex, nextP2) } });
718
+ setThemeColor(themeFor(hex, nextP2));
635
719
  persistSeatColor(1, hex);
636
720
  if (swapped) persistSeatColor(2, nextP2);
637
721
  },
638
- [setThemeColor, p1Color, p2Color, persistSeatColor]
722
+ [setThemeColor, themeFor, p1Color, p2Color, persistSeatColor]
639
723
  );
640
724
  const handleP2ColorChange = useCallback8(
641
725
  (hex) => {
642
726
  const swapped = hex.toLowerCase() === p1Color.toLowerCase();
643
727
  const nextP1 = swapped ? p2Color : p1Color;
644
- setThemeColor({ color: { primary: nextP1, secondary: hex, tertiary: getThirdColor(nextP1, hex) } });
728
+ setThemeColor(themeFor(nextP1, hex));
645
729
  persistSeatColor(2, hex);
646
730
  if (swapped) persistSeatColor(1, nextP1);
647
731
  },
648
- [setThemeColor, p1Color, p2Color, persistSeatColor]
732
+ [setThemeColor, themeFor, p1Color, p2Color, persistSeatColor]
649
733
  );
650
734
  return { p1Color, p2Color, handleP1ColorChange, handleP2ColorChange };
651
735
  }
@@ -671,16 +755,17 @@ function LoadoutScreen({ game }) {
671
755
  const { seatTwo: savedSeatTwo, setSeatTwo } = useOpponent();
672
756
  const seatTwo = seatTwoFor(savedSeatTwo, game.seatTwo);
673
757
  const mode = roundModeFor(seatTwo);
674
- const humanSeats = useMemo4(() => humanSeatsFor(mode), [mode]);
758
+ const humanSeats = useMemo5(() => humanSeatsFor(mode), [mode]);
675
759
  const p2IsHuman = mode === "twoPlayer";
676
760
  const p2Exists = mode !== "solo";
677
761
  const loadout = game.useLoadout(mode);
678
762
  const { openFor } = useGameGuide();
679
763
  const { profiles, lastSelected, selectProfile } = useProfiles();
680
764
  const { orientationMode, p1OnRight, upsideDown, resolved: p1OnRightResolved } = useOrientationState();
681
- const fixedSeats = p2IsHuman && (!!game.twoPlayerSeats?.faceToFaceOnWeb || Platform2.OS !== "web");
765
+ const fixedSeats = p2IsHuman && (!!game.twoPlayerSeats?.faceToFaceOnWeb || Platform3.OS !== "web");
682
766
  const fixedUpsideDown = !!game.twoPlayerSeats?.followUpsideDown && orientationMode === "faceToFace" && upsideDown;
683
767
  const { panelLayout, panelFadeStyle } = useDualZoneLayout(fixedSeats ? "faceToFace" : orientationMode, fixedSeats ? true : p1OnRight, fixedSeats || p1OnRightResolved, fixedSeats ? fixedUpsideDown : upsideDown, PANEL_SWAP_FADE_MS);
768
+ const playersRowFade = panelFadeStyle;
684
769
  const [settingsOpen, setSettingsOpen] = useState4(false);
685
770
  const rotation = getViewRotation(panelLayout.orientationMode, panelLayout.p1OnRight, panelLayout.upsideDown);
686
771
  const insets = rotateInsets(useSafeAreaInsets(), rotation);
@@ -709,7 +794,7 @@ function LoadoutScreen({ game }) {
709
794
  useEffect4(() => {
710
795
  if (!humanSeats.every((s) => ready[s])) return;
711
796
  dispatch(liveplayActions.setActiveRound({ game: game.id, mode, settings: roundSettingsRef.current }));
712
- router2.push({ pathname: "/play/[game]", params: { game: game.id } });
797
+ router2.push(playHref(game.id));
713
798
  }, [ready, humanSeats, game.id, mode, dispatch]);
714
799
  const bg = dark ? "#000000" : "#FFFFFF";
715
800
  const fg = dark ? "#FFFFFF" : "#000000";
@@ -737,10 +822,10 @@ function LoadoutScreen({ game }) {
737
822
  const sharedControls = showMetaInSharedBand ? /* @__PURE__ */ jsx12(SharedActionBand, { onBack: safeBack, onSettings: () => setSettingsOpen(true), onRandomize: loadout.randomize, onReset: loadout.reset, fg, popoverOpen: optionsPopoverOpen, children: optionsRow }) : optionsRow;
738
823
  const controlLook = { accentColor: themeColors.tertiary, mutedColor: fgMuted, dark, rotation };
739
824
  const p1Control = loadout.seatControl(1, controlsHost, { ...controlLook, seatColor: p1Color });
740
- const p1Panel = /* @__PURE__ */ jsx12(PlayerSetupPanel, { idPrefix: "p1", host: controlsHost, color: p1Color, onColorChange: handleP1ColorChange, swatches: 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: "P1", onProfileSelect: handleP1ProfileSelect, onManageProfiles: () => router2.push("/profiles") });
825
+ const p1Panel = /* @__PURE__ */ jsx12(PlayerSetupPanel, { idPrefix: "p1", host: controlsHost, color: p1Color, onColorChange: handleP1ColorChange, swatches: 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: "P1", onProfileSelect: handleP1ProfileSelect, onManageProfiles: () => router2.push(PROFILES_HREF) });
741
826
  const p2Control = p2IsHuman ? loadout.seatControl(2, p2Host, { ...controlLook, seatColor: p2Color }) : void 0;
742
827
  const p2Panel = game.seatTwo && /* @__PURE__ */ jsx12(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: 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 });
743
- return /* @__PURE__ */ jsxs5(View2, { style: [styles3.container, { backgroundColor: bg }], children: [
828
+ return /* @__PURE__ */ jsxs5(View3, { style: [styles3.container, { backgroundColor: bg }], children: [
744
829
  /* @__PURE__ */ jsxs5(FakeLandscapeView, { orientationMode: panelLayout.orientationMode, p1OnRight: panelLayout.p1OnRight, upsideDown: panelLayout.upsideDown, style: styles3.rotatable, children: [
745
830
  /* @__PURE__ */ jsx12(PressAwayOverlay, { active: controlsHost.openId !== null, onPress: controlsHost.close }),
746
831
  p2ZoneStyle && /* @__PURE__ */ jsx12(PressAwayOverlay, { active: controlsHost.openId !== null || p2Host.openId !== null, onPress: p2Host.close, style: p2ZoneStyle }),
@@ -757,9 +842,9 @@ function LoadoutScreen({ game }) {
757
842
  p2Elevated: p2Host.openId !== null,
758
843
  sharedElevated: optionsPopoverOpen
759
844
  }
760
- ) : /* @__PURE__ */ jsxs5(View2, { style: styles3.stackedZone, children: [
845
+ ) : /* @__PURE__ */ jsxs5(View3, { style: styles3.stackedZone, children: [
761
846
  sharedControls,
762
- /* @__PURE__ */ jsxs5(Animated.View, { style: [styles3.playersRow, anyPlayerPickerOpen && styles3.playersRowOpen, panelFadeStyle], children: [
847
+ /* @__PURE__ */ jsxs5(Animated.View, { style: [styles3.playersRow, anyPlayerPickerOpen && styles3.playersRowOpen, playersRowFade], children: [
763
848
  !isPortrait && panelLayout.p1OnRight ? p2Panel : p1Panel,
764
849
  !isPortrait && panelLayout.p1OnRight ? p1Panel : p2Panel
765
850
  ] }),
@@ -829,7 +914,7 @@ function LoadoutRoute() {
829
914
  const { game: id } = useLocalSearchParams();
830
915
  const game = usePackGame(id);
831
916
  if (!game) return /* @__PURE__ */ jsx13(Redirect, { href: "/" });
832
- if (!hasLoadout(game)) return /* @__PURE__ */ jsx13(Redirect, { href: { pathname: "/play/[game]", params: { game: game.id } } });
917
+ if (!hasLoadout(game)) return /* @__PURE__ */ jsx13(Redirect, { href: playHref(game.id) });
833
918
  return /* @__PURE__ */ jsx13(LoadoutScreen, { game }, game.id);
834
919
  }
835
920
  function hasLoadout(game) {
@@ -848,20 +933,22 @@ function useResumePoint(gameId) {
848
933
  useEffect5(() => {
849
934
  if (!gameId) return;
850
935
  setLastActiveGame(gameId);
851
- return clearLastActiveGame;
936
+ return () => {
937
+ if (AppState.currentState === "active") clearLastActiveGame();
938
+ };
852
939
  }, [gameId, setLastActiveGame, clearLastActiveGame]);
853
940
  }
854
941
 
855
942
  // src/screens/HomeScreen.tsx
856
- import { defaultColors as defaultColors2, useAutoPaperTheme as useAutoPaperTheme4 } from "@rific/auto-paper";
943
+ import { defaultColors as defaultColors2, FadingScrollView, useAutoPaperTheme as useAutoPaperTheme4 } from "@rific/auto-paper";
857
944
  import { IconButton, Pressable } from "@rific/feedback-press";
858
945
  import { FakeLandscapeView as FakeLandscapeView2, useRotation as useRotation4 } from "@tastic/core";
859
- import { PressAwayOverlay as PressAwayOverlay2, usePopoverHost as usePopoverHost2, useWellColors, WELL_SHADOW } from "@tastic/hud";
860
- import { ProfilePicker } from "@tastic/profile";
946
+ import { PressAwayOverlay as PressAwayOverlay2, usePopoverHost as usePopoverHost2, useWellColors, WELL_SHADOW } from "@tastic/hud/chrome";
947
+ import { getProfilePickerContentSize, ProfilePicker } from "@tastic/profile";
861
948
  import { Redirect as Redirect2, router as router3, useIsFocused } from "expo-router";
862
- import { useCallback as useCallback10, useEffect as useEffect6, useState as useState6 } from "react";
863
- import { AppState, ScrollView, StyleSheet as StyleSheet4, View as View3 } from "react-native";
864
- import { Icon, Text } from "react-native-paper";
949
+ import { useCallback as useCallback10, useEffect as useEffect6, useMemo as useMemo6, useState as useState6 } from "react";
950
+ import { AppState as AppState2, StyleSheet as StyleSheet4, View as View4 } from "react-native";
951
+ import { Icon, Portal, Text } from "react-native-paper";
865
952
 
866
953
  // src/hooks/useFrozenWhileHidden.ts
867
954
  import { useState as useState5 } from "react";
@@ -941,15 +1028,19 @@ function useHomeLayout() {
941
1028
  }
942
1029
 
943
1030
  // src/screens/HomeScreen.tsx
944
- import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs6 } from "react/jsx-runtime";
1031
+ import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs6 } from "react/jsx-runtime";
945
1032
  function HomeScreen() {
946
1033
  const pack = usePack();
947
1034
  const { consumeResumeGame } = useLastActiveGame();
1035
+ const { autoShowPack } = useGameGuide();
948
1036
  const [resumeGame] = useState6(() => {
949
1037
  const id = consumeResumeGame();
950
1038
  return pack.games.find((g) => g.id === id && g.resumable) ?? null;
951
1039
  });
952
- if (resumeGame) return /* @__PURE__ */ jsx14(Redirect2, { href: { pathname: "/play/[game]", params: { game: resumeGame.id } } });
1040
+ useEffect6(() => {
1041
+ if (!resumeGame) autoShowPack();
1042
+ }, [resumeGame, autoShowPack]);
1043
+ if (resumeGame) return /* @__PURE__ */ jsx14(Redirect2, { href: playHref(resumeGame.id) });
953
1044
  return /* @__PURE__ */ jsx14(HomeMenu, {});
954
1045
  }
955
1046
  function HomeMenu() {
@@ -957,13 +1048,14 @@ function HomeMenu() {
957
1048
  const rotation = useRotation4();
958
1049
  const layout = useHomeLayout();
959
1050
  const { insets } = layout;
960
- const { dark } = useAutoPaperTheme4();
961
- const { openFirstTime, openFor } = useGameGuide();
1051
+ const { dark, colors } = useAutoPaperTheme4();
1052
+ const { openFirstTime, openFor, openPack } = useGameGuide();
962
1053
  const [settingsOpen, setSettingsOpen] = useState6(false);
1054
+ const [rulesGame, setRulesGame] = useState6(null);
963
1055
  const isFocused = useIsFocused();
964
- const [appState, setAppState] = useState6(AppState.currentState);
1056
+ const [appState, setAppState] = useState6(AppState2.currentState);
965
1057
  useEffect6(() => {
966
- const subscription = AppState.addEventListener("change", setAppState);
1058
+ const subscription = AppState2.addEventListener("change", setAppState);
967
1059
  return () => subscription.remove();
968
1060
  }, []);
969
1061
  const heroActive = isFocused && appState === "active";
@@ -983,65 +1075,85 @@ function HomeMenu() {
983
1075
  const closePicker = pickerHost.close;
984
1076
  useEffect6(() => {
985
1077
  closePicker();
986
- }, [isWide, closePicker]);
987
- const bg = dark ? "#000000" : "#FFFFFF";
988
- const fg = dark ? "#FFFFFF" : "#000000";
989
- const openGame = useCallback10((game) => openFirstTime(game.id, () => router3.push(game.useLoadout ? { pathname: "/loadout/[game]", params: { game: game.id } } : { pathname: "/play/[game]", params: { game: game.id } })), [openFirstTime]);
990
- const picker = /* @__PURE__ */ jsx14(ProfilePicker, { idPrefix: "home-p1", host: pickerHost, profiles, selectedId: lastSelected[1], color: p1Color, dark, guestLabel: "P1", onSelect: (profile) => selectProfile(1, profile?.id ?? null), onManage: () => router3.push("/profiles"), colorSection: { value: p1Color, onChange: handleP1ColorChange, swatches: defaultColors2, caption: selectedProfile ? pack.colorCaption : void 0 } });
991
- return /* @__PURE__ */ jsxs6(View3, { style: [styles4.container, { backgroundColor: bg }], children: [
992
- /* @__PURE__ */ jsxs6(FakeLandscapeView2, { style: styles4.rotatable, children: [
993
- /* @__PURE__ */ jsx14(PressAwayOverlay2, { active: pickerHost.openId !== null, onPress: pickerHost.close }),
994
- isWide ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
995
- /* @__PURE__ */ jsx14(ScrollView, { 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: /* @__PURE__ */ jsx14(View3, { style: [styles4.grid, { width: layout.rightPaneWidth }], children: pack.games.map((game) => /* @__PURE__ */ jsx14(GameTile, { game, width: layout.tileWidth, previewWidth: layout.previewWidth, previewRevealed: previewsRevealed, dark, p1Color, p2Color, onPress: () => openGame(game), onRules: () => openFor(game.id) }, game.id)) }) }),
996
- /* @__PURE__ */ jsxs6(View3, { 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: [
1078
+ }, [isWide, rotation, layout.windowWidth, layout.windowHeight, closePicker]);
1079
+ const bg = pack.Backdrop ? "transparent" : dark ? "#000000" : "#FFFFFF";
1080
+ const fg = pack.foreground ?? (dark ? "#FFFFFF" : "#000000");
1081
+ const { previewColor } = pack;
1082
+ const p1Drawn = previewColor ? previewColor(p1Color, dark) : p1Color;
1083
+ const triggerStyle = useMemo6(() => pack.Backdrop ? [styles4.pill, { backgroundColor: colors.surface }] : void 0, [pack.Backdrop, colors.surface]);
1084
+ const openGame = useCallback10((game) => openFirstTime(game.id, () => router3.push(game.useLoadout ? loadoutHref(game.id) : playHref(game.id))), [openFirstTime]);
1085
+ const openRules = useCallback10((game) => game.Rules ? setRulesGame(game) : openFor(game.id), [openFor]);
1086
+ const colorSection = { value: p1Color, onChange: handleP1ColorChange, swatches: defaultColors2, caption: selectedProfile ? pack.colorCaption : void 0, previewValue: previewColor ? (hex) => previewColor(hex, dark) : void 0 };
1087
+ const usePickerAlign = pack.usePickerAlign ?? noPickerAlign;
1088
+ const pickerSize = getProfilePickerContentSize(profiles.length, true, colorSection);
1089
+ const pickerAlign = usePickerAlign(pickerHost.openId === `${SEAT_ONE_PICKER}-profile`, pickerSize.width, pickerSize.height);
1090
+ const picker = /* @__PURE__ */ jsx14(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: () => router3.push(PROFILES_HREF), colorSection, alignOverride: pickerAlign, triggerStyle });
1091
+ const pressAway = /* @__PURE__ */ jsx14(PressAwayOverlay2, { active: pickerHost.openId !== null, onPress: pickerHost.close, style: styles4.pressAway });
1092
+ const RulesSheet = rulesGame?.Rules;
1093
+ return /* @__PURE__ */ jsxs6(View4, { style: [styles4.container, { backgroundColor: bg }], children: [
1094
+ /* @__PURE__ */ jsx14(FakeLandscapeView2, { style: styles4.rotatable, children: /* @__PURE__ */ jsxs6(RulesHost, { enabled: pack.games.some((game) => game.Rules), children: [
1095
+ isWide ? /* @__PURE__ */ jsxs6(Fragment4, { children: [
1096
+ pressAway,
1097
+ /* @__PURE__ */ jsx14(FadingScrollView, { 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: /* @__PURE__ */ jsx14(View4, { style: [styles4.grid, { width: layout.rightPaneWidth }], children: pack.games.map((game) => /* @__PURE__ */ jsx14(GameTile, { game, width: layout.tileWidth, previewWidth: layout.previewWidth, previewRevealed: previewsRevealed, dark, p1Color, p2Color, onPress: () => openGame(game), onRules: hasRules(game) ? () => openRules(game) : void 0 }, game.id)) }) }),
1098
+ /* @__PURE__ */ jsxs6(View4, { 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: [
997
1099
  /* @__PURE__ */ jsx14(pack.HeroTitle, { words: pack.wordmark, color: fg, p1Color, p2Color, active: heroActive, fit: layout.heroFit }),
998
- /* @__PURE__ */ jsx14(View3, { style: styles4.widePicker, children: picker })
1100
+ /* @__PURE__ */ jsx14(View4, { style: styles4.widePicker, children: picker })
999
1101
  ] })
1000
- ] }) : /* @__PURE__ */ jsxs6(ScrollView, { style: styles4.scroll, contentContainerStyle: [styles4.content, { paddingTop: 56 + insets.top, paddingBottom: 24 + insets.bottom, paddingLeft: 16 + insets.left, paddingRight: 16 + insets.right }], children: [
1102
+ ] }) : /* @__PURE__ */ jsxs6(FadingScrollView, { style: styles4.scroll, contentContainerStyle: [styles4.content, { paddingTop: 56 + insets.top, paddingBottom: 24 + insets.bottom, paddingLeft: 16 + insets.left, paddingRight: 16 + insets.right }], children: [
1103
+ pressAway,
1001
1104
  /* @__PURE__ */ jsx14(pack.HeroTitle, { words: pack.wordmark, color: fg, p1Color, p2Color, active: heroActive }),
1002
- /* @__PURE__ */ jsx14(View3, { style: [styles4.picker, pickerHost.openId !== null && styles4.pickerOpen], children: picker }),
1003
- /* @__PURE__ */ jsx14(View3, { style: styles4.list, children: pack.games.map((game) => /* @__PURE__ */ jsx14(GameRow, { game, dark, p1Color, p2Color, previewRevealed: previewsRevealed, onPress: () => openGame(game), onRules: () => openFor(game.id) }, game.id)) })
1105
+ /* @__PURE__ */ jsx14(View4, { style: [styles4.picker, pickerHost.openId !== null && styles4.pickerOpen], children: picker }),
1106
+ /* @__PURE__ */ jsx14(View4, { style: styles4.list, children: pack.games.map((game) => /* @__PURE__ */ jsx14(GameRow, { game, dark, p1Color, p2Color, previewRevealed: previewsRevealed, onPress: () => openGame(game), onRules: hasRules(game) ? () => openRules(game) : void 0 }, game.id)) })
1004
1107
  ] }),
1005
- /* @__PURE__ */ jsx14(IconButton, { icon: "trophy", iconColor: fg, size: 24, style: [styles4.corner, { top: 8 + insets.top, left: 8 + insets.left }], onPress: () => router3.push("/stats"), accessibilityLabel: "Stats & Achievements" }),
1006
- /* @__PURE__ */ jsx14(IconButton, { icon: "cog", iconColor: fg, size: 24, style: [styles4.corner, { top: 8 + insets.top, right: 8 + insets.right }], onPress: () => setSettingsOpen(true), accessibilityLabel: "Settings" })
1007
- ] }),
1008
- /* @__PURE__ */ jsx14(SettingsDialog, { visible: settingsOpen, onDismiss: () => setSettingsOpen(false), rotation })
1108
+ /* @__PURE__ */ jsx14(IconButton, { icon: "trophy", iconColor: fg, size: 24, style: [styles4.corner, { top: 8 + insets.top, left: 8 + insets.left }], onPress: () => router3.push(STATS_HREF), accessibilityLabel: "Stats & Achievements" }),
1109
+ /* @__PURE__ */ jsx14(IconButton, { icon: "cog", iconColor: fg, size: 24, style: [styles4.corner, { top: 8 + insets.top, right: 8 + insets.right }], onPress: () => setSettingsOpen(true), accessibilityLabel: "Settings" }),
1110
+ RulesSheet && /* @__PURE__ */ jsx14(RulesSheet, { visible: true, onDismiss: () => setRulesGame(null) })
1111
+ ] }) }),
1112
+ /* @__PURE__ */ jsx14(SettingsDialog, { visible: settingsOpen, onDismiss: () => setSettingsOpen(false), rotation, onShowHowToPlay: pack.guide ? openPack : void 0 })
1009
1113
  ] });
1010
1114
  }
1115
+ var SEAT_ONE_PICKER = "home-p1";
1116
+ var noPickerAlign = () => void 0;
1117
+ function hasRules(game) {
1118
+ return !!(game.Rules || game.guide);
1119
+ }
1120
+ function RulesHost({ enabled, children }) {
1121
+ return enabled ? /* @__PURE__ */ jsx14(Portal.Host, { children }) : /* @__PURE__ */ jsx14(Fragment4, { children });
1122
+ }
1011
1123
  function GameRow({ game, dark, p1Color, p2Color, previewRevealed, onPress, onRules }) {
1012
1124
  const well = useWellColors();
1013
1125
  const GameIcon = game.icon;
1014
1126
  return (
1015
1127
  // The rules button sits beside the row's own button, laid over its end, not inside it: a button
1016
1128
  // inside a button is invalid on the web.
1017
- /* @__PURE__ */ jsxs6(View3, { children: [
1129
+ /* @__PURE__ */ jsxs6(View4, { children: [
1018
1130
  /* @__PURE__ */ jsxs6(Pressable, { onPress, style: [styles4.row, { backgroundColor: well.bg }], accessibilityRole: "button", accessibilityLabel: game.title, children: [
1019
- game.Preview ? /* @__PURE__ */ jsx14(PreviewBox, { game, width: ROW_PREVIEW_WIDTH, revealed: previewRevealed, dark, p1Color, p2Color }) : /* @__PURE__ */ jsx14(View3, { style: [styles4.rowIcon, { backgroundColor: dark ? ICON_DARK_BG : ICON_LIGHT_BG }], children: /* @__PURE__ */ jsx14(GameIcon, { size: ROW_ICON_SIZE, p1Color, p2Color, dark }) }),
1020
- /* @__PURE__ */ jsxs6(View3, { style: styles4.rowText, children: [
1131
+ game.Preview ? /* @__PURE__ */ jsx14(PreviewBox, { game, width: ROW_PREVIEW_WIDTH, revealed: previewRevealed, dark, p1Color, p2Color }) : /* @__PURE__ */ jsx14(View4, { style: [styles4.rowIcon, { backgroundColor: dark ? ICON_DARK_BG : ICON_LIGHT_BG }], children: GameIcon && /* @__PURE__ */ jsx14(GameIcon, { size: ROW_ICON_SIZE, p1Color, p2Color, dark }) }),
1132
+ /* @__PURE__ */ jsxs6(View4, { style: styles4.rowText, children: [
1021
1133
  /* @__PURE__ */ jsx14(Text, { variant: "titleLarge", style: [styles4.rowTitle, { color: well.fg }], numberOfLines: 1, children: game.title }),
1022
1134
  /* @__PURE__ */ jsx14(Text, { variant: "bodyMedium", style: { color: well.fgMuted }, numberOfLines: 2, children: game.blurb })
1023
1135
  ] })
1024
1136
  ] }),
1025
- /* @__PURE__ */ jsx14(Pressable, { onPress: onRules, style: styles4.rulesButton, hitSlop: 12, accessibilityRole: "button", accessibilityLabel: `${game.title} rules`, children: /* @__PURE__ */ jsx14(Icon, { source: "information-outline", size: 22, color: well.fgMuted }) })
1137
+ onRules && /* @__PURE__ */ jsx14(Pressable, { onPress: onRules, style: styles4.rulesButton, hitSlop: 12, accessibilityRole: "button", accessibilityLabel: `${game.title} rules`, children: /* @__PURE__ */ jsx14(Icon, { source: "information-outline", size: 22, color: well.fgMuted }) })
1026
1138
  ] })
1027
1139
  );
1028
1140
  }
1029
1141
  function GameTile({ game, width, previewWidth, dark, p1Color, p2Color, previewRevealed, onPress, onRules }) {
1030
1142
  const well = useWellColors();
1031
- return /* @__PURE__ */ jsxs6(View3, { style: { width }, children: [
1143
+ return /* @__PURE__ */ jsxs6(View4, { style: { width }, children: [
1032
1144
  /* @__PURE__ */ jsxs6(Pressable, { onPress, style: [styles4.tile, { backgroundColor: well.bg }], accessibilityRole: "button", accessibilityLabel: game.title, children: [
1033
1145
  /* @__PURE__ */ jsx14(PreviewBox, { game, width: previewWidth, revealed: previewRevealed, dark, p1Color, p2Color }),
1034
1146
  /* @__PURE__ */ jsx14(Text, { variant: "titleMedium", style: [styles4.rowTitle, { color: well.fg }], numberOfLines: 1, children: game.title }),
1035
1147
  /* @__PURE__ */ jsx14(Text, { variant: "bodySmall", style: { color: well.fgMuted }, numberOfLines: 3, children: game.blurb })
1036
1148
  ] }),
1037
- /* @__PURE__ */ jsx14(Pressable, { onPress: onRules, style: styles4.tileRulesButton, hitSlop: 12, accessibilityRole: "button", accessibilityLabel: `${game.title} rules`, children: /* @__PURE__ */ jsx14(Icon, { source: "information-outline", size: 20, color: "#FFFFFF" }) })
1149
+ onRules && /* @__PURE__ */ jsx14(Pressable, { onPress: onRules, style: styles4.tileRulesButton, hitSlop: 12, accessibilityRole: "button", accessibilityLabel: `${game.title} rules`, children: /* @__PURE__ */ jsx14(Icon, { source: "information-outline", size: 20, color: "#FFFFFF" }) })
1038
1150
  ] });
1039
1151
  }
1040
1152
  function PreviewBox({ game, width, revealed, dark, p1Color, p2Color }) {
1041
1153
  const height = Math.round(width / PREVIEW_ASPECT);
1042
1154
  const { Preview, icon: GameIcon } = game;
1043
1155
  const iconSize = Math.min(height, ROW_ICON_SIZE);
1044
- return /* @__PURE__ */ jsx14(View3, { style: [styles4.preview, { width, height, backgroundColor: dark ? ICON_DARK_BG : ICON_LIGHT_BG }], children: Preview ? revealed && /* @__PURE__ */ jsx14(Preview, { width, height, p1Color, p2Color, dark }) : /* @__PURE__ */ jsx14(View3, { style: [styles4.previewIcon, { width: iconSize, height: iconSize, borderRadius: iconSize * 0.22 }], children: /* @__PURE__ */ jsx14(GameIcon, { size: iconSize, p1Color, p2Color, dark }) }) });
1156
+ return /* @__PURE__ */ jsx14(View4, { style: [styles4.preview, { width, height, backgroundColor: dark ? ICON_DARK_BG : ICON_LIGHT_BG }], children: Preview ? revealed && /* @__PURE__ */ jsx14(Preview, { width, height, p1Color, p2Color, dark }) : GameIcon && /* @__PURE__ */ jsx14(View4, { style: [styles4.previewIcon, { width: iconSize, height: iconSize, borderRadius: iconSize * 0.22 }], children: /* @__PURE__ */ jsx14(GameIcon, { size: iconSize, p1Color, p2Color, dark }) }) });
1045
1157
  }
1046
1158
  var ROW_ICON_SIZE = 56;
1047
1159
  var ICON_LIGHT_BG = "#FAFAFA";
@@ -1076,6 +1188,15 @@ var styles4 = StyleSheet4.create({
1076
1188
  pickerOpen: {
1077
1189
  zIndex: 100
1078
1190
  },
1191
+ // Seat 1's button on the theme's surface (over a backdrop), the same pill as the tiles' wells.
1192
+ pill: {
1193
+ borderRadius: 999
1194
+ },
1195
+ // Over the rows (and, wide, the grid), whose own stacking is unset, so a press on them reaches it on
1196
+ // the web too, and under the raised button and left pane, so the menu's rows stay pressable.
1197
+ pressAway: {
1198
+ zIndex: 5
1199
+ },
1079
1200
  // The preview's own rounding, clipping whatever it draws to the box.
1080
1201
  preview: {
1081
1202
  alignItems: "center",
@@ -1166,66 +1287,109 @@ var styles4 = StyleSheet4.create({
1166
1287
 
1167
1288
  // src/screens/ProfilesRoute.tsx
1168
1289
  import { defaultColors as defaultColors3 } from "@rific/auto-paper";
1290
+ import { RotatedFrame as RotatedFrame2 } from "@tastic/hud/chrome";
1169
1291
  import { ProfilesScreen } from "@tastic/profile";
1170
1292
  import { jsx as jsx15 } from "react/jsx-runtime";
1293
+ var ROSTER_MAX_WIDTH = 640;
1294
+ function useRosterAlign(open, contentWidth, contentHeight) {
1295
+ const { usePickerAlign } = usePack();
1296
+ return usePickerAlign(open, contentWidth, contentHeight, { maxContentWidth: ROSTER_MAX_WIDTH });
1297
+ }
1171
1298
  function ProfilesRoute() {
1172
1299
  const { profiles, createProfile, updateProfile, deleteProfile } = useProfiles();
1173
- return /* @__PURE__ */ jsx15(ProfilesScreen, { profiles, defaultColor: defaultColors3[0].value, onCreate: (patch) => createProfile(patch), onSave: (id, patch) => updateProfile(id, patch), onDelete: deleteProfile, onBack: safeBack });
1300
+ const { Backdrop, foreground, newProfileColor, usePickerAlign } = usePack();
1301
+ return /* @__PURE__ */ jsx15(RotatedFrame2, { rotatedSafeArea: true, portalHost: true, maxContentWidth: ROSTER_MAX_WIDTH, children: /* @__PURE__ */ jsx15(ProfilesScreen, { profiles, defaultColor: newProfileColor ?? defaultColors3[0].value, onCreate: (patch) => createProfile(patch), onSave: (id, patch) => updateProfile(id, patch), onDelete: deleteProfile, onBack: safeBack, fg: foreground, bg: Backdrop ? "transparent" : void 0, useColorPickerAlign: usePickerAlign ? useRosterAlign : void 0 }) });
1174
1302
  }
1175
1303
 
1176
1304
  // src/screens/StatsScreen.tsx
1177
1305
  import { useAutoPaperTheme as useAutoPaperTheme5 } from "@rific/auto-paper";
1178
- import { FakeLandscapeView as FakeLandscapeView3, rotateInsets as rotateInsets3, useRotation as useRotation5 } from "@tastic/core";
1179
- import { BaseStatsScreen, PressAwayOverlay as PressAwayOverlay3, usePopoverHost as usePopoverHost3 } from "@tastic/hud";
1180
- import { OptionPicker as OptionPicker2 } from "@tastic/profile";
1181
- import { useMemo as useMemo5, useState as useState7 } from "react";
1182
- import { StyleSheet as StyleSheet5, View as View4 } from "react-native";
1306
+ import { BaseStatsScreen, PressAwayOverlay as PressAwayOverlay3, RotatedFrame as RotatedFrame3, usePopoverHost as usePopoverHost3 } from "@tastic/hud/chrome";
1307
+ import { getOptionPickerContentSize as getOptionPickerContentSize2, OptionPicker as OptionPicker2 } from "@tastic/profile";
1308
+ import { useMemo as useMemo7, useState as useState7 } from "react";
1309
+ import { StyleSheet as StyleSheet5, View as View5 } from "react-native";
1183
1310
  import { SegmentedButtons, Text as Text2 } from "react-native-paper";
1184
1311
  import { useSafeAreaInsets as useSafeAreaInsets3 } from "react-native-safe-area-context";
1185
1312
  import { useDispatch as useDispatch10 } from "react-redux";
1186
1313
  import { jsx as jsx16, jsxs as jsxs7 } from "react/jsx-runtime";
1314
+ var STATS = "stats";
1315
+ var ACHIEVEMENTS = "achievements";
1187
1316
  var TABS = [
1188
- { value: "stats", label: "Stats", icon: "chart-box-outline" },
1189
- { value: "achievements", label: "Achievements", icon: "trophy-outline" }
1317
+ { value: STATS, label: "Stats", icon: "chart-box-outline" },
1318
+ { value: ACHIEVEMENTS, label: "Achievements", icon: "trophy-outline" }
1190
1319
  ];
1320
+ var LABEL_WIDE_TAB = { flexBasis: "auto", flexGrow: 1, flexShrink: 1 };
1321
+ var NO_PACK_TABS = [];
1191
1322
  var EVERYONE = "__everyone__";
1192
1323
  var ALL_GAMES = "__all__";
1193
1324
  var PLAYER_PICKER_ID = "stats-player";
1194
1325
  var GAME_PICKER_ID = "stats-game";
1326
+ var STATS_MAX_WIDTH = 640;
1195
1327
  function StatsScreen() {
1328
+ const pickerHost = usePopoverHost3();
1329
+ return /* @__PURE__ */ jsx16(StatsPressAway, { host: pickerHost, children: /* @__PURE__ */ jsx16(RotatedFrame3, { rotatedSafeArea: true, portalHost: true, maxContentWidth: STATS_MAX_WIDTH, children: /* @__PURE__ */ jsx16(StatsContent, { pickerHost }) }) });
1330
+ }
1331
+ function StatsPressAway({ host, children }) {
1332
+ return /* @__PURE__ */ jsx16(View5, { testID: "stats-press-away", style: styles5.screen, onStartShouldSetResponder: () => host.openId !== null, onResponderRelease: host.close, children });
1333
+ }
1334
+ var useNoPackReset = () => null;
1335
+ var noPickerAlign2 = () => void 0;
1336
+ function StatsContent({ pickerHost }) {
1196
1337
  const pack = usePack();
1197
1338
  const { dark, colors } = useAutoPaperTheme5();
1198
- const rotation = useRotation5();
1199
- const insets = rotateInsets3(useSafeAreaInsets3(), rotation);
1200
- const { profiles } = useProfiles();
1339
+ const insets = useSafeAreaInsets3();
1340
+ const { profiles, lastSelected } = useProfiles();
1201
1341
  const dispatch = useDispatch10();
1202
- const pickerHost = usePopoverHost3();
1203
- const [tab, setTab] = useState7("stats");
1342
+ const fg = pack.foreground ?? colors.onBackground;
1343
+ const [tab, setTab] = useState7(STATS);
1204
1344
  const [gameId, setGameId] = useState7(ALL_GAMES);
1205
- const [selectedProfileId, setSelectedProfileId] = useState7(null);
1206
- const profileId = selectedProfileId && profiles.some((p) => p.id === selectedProfileId) ? selectedProfileId : null;
1345
+ const [selectedProfileId, setSelectedProfileId] = useState7(() => pack.guestStats ? lastSelected[1] ?? GUEST_PLAYER_ID : null);
1346
+ const profileId = selectedProfileId && profiles.length > 0 && (pack.guestStats && selectedProfileId === GUEST_PLAYER_ID || profiles.some((p) => p.id === selectedProfileId)) ? selectedProfileId : null;
1207
1347
  const games = gameId === ALL_GAMES ? pack.games : pack.games.filter((g) => g.id === gameId);
1208
- const playerOptions = useMemo5(() => [{ value: EVERYONE, label: "Everyone", icon: "account-group" }, ...profiles.map((p) => ({ value: p.id, label: p.name, icon: "account", color: p.color }))], [profiles]);
1209
- const gameOptions = useMemo5(() => [{ value: ALL_GAMES, label: "All Games", icon: "view-grid-outline" }, ...pack.games.map((g) => ({ value: g.id, label: g.title }))], [pack.games]);
1210
- const resetScope = games.length === 1 ? games[0].title : "every game";
1211
- const onReset = () => games.forEach((g) => dispatch(g.resetStats()));
1212
- return /* @__PURE__ */ jsx16(FakeLandscapeView3, { style: styles5.rotatable, children: /* @__PURE__ */ jsxs7(BaseStatsScreen, { title: tab === "stats" ? "Stats" : "Achievements", onBack: safeBack, insets, onReset, resetLabel: games.length === 1 ? `Reset ${resetScope} Stats` : "Reset All Stats", resetConfirmTitle: "Reset Stats?", resetConfirmBody: `Erases every player's stats and achievements in ${resetScope}. This cannot be undone.`, rotation, children: [
1348
+ const packTabs = pack.statsTabs ?? NO_PACK_TABS;
1349
+ const packTab = packTabs.find((t) => t.value === tab);
1350
+ const showGamePicker = !(tab === ACHIEVEMENTS && pack.AchievementList);
1351
+ const playerOptions = useMemo7(() => [{ value: EVERYONE, label: "Everyone", icon: "account-group" }, ...pack.guestStats ? [{ value: GUEST_PLAYER_ID, label: "Guest", icon: "account-outline" }] : [], ...profiles.map((p) => ({ value: p.id, label: p.name, icon: "account", color: pack.previewColor ? pack.previewColor(p.color, dark) : p.color }))], [profiles, pack, dark]);
1352
+ const gameOptions = useMemo7(() => [{ value: ALL_GAMES, label: "All Games", icon: "view-grid-outline" }, ...pack.games.map((g) => ({ value: g.id, label: g.title }))], [pack.games]);
1353
+ const tabButtons = useMemo7(() => packTabs.length > 0 ? [{ value: STATS, label: "Stats" }, ...packTabs.map(({ value, label }) => ({ value, label })), { value: ACHIEVEMENTS, label: "Achievements" }].map((button) => ({ ...button, style: LABEL_WIDE_TAB })) : TABS, [packTabs]);
1354
+ const title = tab === STATS ? "Stats" : tab === ACHIEVEMENTS ? "Achievements" : packTab?.label ?? "";
1355
+ const usePackReset = pack.resetStats?.useReset ?? useNoPackReset;
1356
+ const packReset = usePackReset();
1357
+ const reset = resetFor(pack, packReset, profileId, profiles, games, () => games.forEach((g) => g.resetStats && dispatch(g.resetStats())));
1358
+ const usePickerAlign = pack.usePickerAlign ?? noPickerAlign2;
1359
+ const playerSize = getOptionPickerContentSize2(playerOptions.length);
1360
+ const gameSize = getOptionPickerContentSize2(gameOptions.length);
1361
+ const playerAlign = usePickerAlign(pickerHost.openId === PLAYER_PICKER_ID, playerSize.width, playerSize.height);
1362
+ const gameAlign = usePickerAlign(pickerHost.openId === GAME_PICKER_ID, gameSize.width, gameSize.height);
1363
+ const surface = pack.Backdrop ? { backgroundColor: colors.surface } : void 0;
1364
+ const triggerStyle = pack.Backdrop ? [styles5.pill, surface] : void 0;
1365
+ const PackTabContent = packTab?.Content;
1366
+ return /* @__PURE__ */ jsxs7(BaseStatsScreen, { title, onBack: 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: [
1213
1367
  /* @__PURE__ */ jsx16(PressAwayOverlay3, { active: pickerHost.openId !== null, onPress: pickerHost.close, style: styles5.pressAway }),
1214
- /* @__PURE__ */ jsx16(SegmentedButtons, { value: tab, onValueChange: (value) => setTab(value), buttons: TABS, style: styles5.tabs }),
1215
- /* @__PURE__ */ jsxs7(View4, { style: [styles5.filters, pickerHost.openId !== null && styles5.filtersOpen], children: [
1216
- profiles.length > 0 && /* @__PURE__ */ jsx16(OptionPicker2, { 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" }),
1217
- /* @__PURE__ */ jsx16(OptionPicker2, { id: GAME_PICKER_ID, host: pickerHost, options: gameOptions, value: gameId, onChange: setGameId, color: colors.primary, dark, icon: "view-grid-outline" })
1368
+ /* @__PURE__ */ jsx16(SegmentedButtons, { value: tab, onValueChange: setTab, buttons: tabButtons, style: [styles5.tabs, surface] }),
1369
+ /* @__PURE__ */ jsxs7(View5, { style: [styles5.filters, pickerHost.openId !== null && styles5.filtersOpen], onStartShouldSetResponder: () => pickerHost.openId !== null, children: [
1370
+ profiles.length > 0 && /* @__PURE__ */ jsx16(OptionPicker2, { 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 }),
1371
+ showGamePicker && /* @__PURE__ */ jsx16(OptionPicker2, { id: GAME_PICKER_ID, host: pickerHost, options: gameOptions, value: gameId, onChange: setGameId, color: colors.primary, dark, icon: "view-grid-outline", alignOverride: gameAlign, triggerStyle })
1218
1372
  ] }),
1219
- tab === "stats" && pack.StatsSections && /* @__PURE__ */ jsx16(pack.StatsSections, { profileId, gameId: gameId === ALL_GAMES ? null : gameId }),
1220
- games.map((game) => /* @__PURE__ */ jsx16(GameStats, { game, tab, profileId, showTitle: games.length > 1 }, game.id))
1221
- ] }) });
1373
+ tab === STATS && pack.StatsSections && /* @__PURE__ */ jsx16(pack.StatsSections, { profileId, gameId: gameId === ALL_GAMES ? null : gameId }),
1374
+ PackTabContent && /* @__PURE__ */ jsx16(PackTabContent, { profileId, gameId: gameId === ALL_GAMES ? null : gameId }),
1375
+ tab === ACHIEVEMENTS && pack.AchievementList ? /* @__PURE__ */ jsx16(pack.AchievementList, { profileId }) : (tab === STATS || tab === ACHIEVEMENTS) && games.map((game) => /* @__PURE__ */ jsx16(GameStats, { game, tab, profileId, showTitle: games.length > 1 && pack.statsGameTitles !== false, titleColor: fg }, game.id))
1376
+ ] });
1222
1377
  }
1223
- function GameStats({ game, tab, profileId, showTitle }) {
1224
- const { colors } = useAutoPaperTheme5();
1378
+ function resetFor(pack, packReset, profileId, profiles, games, resetGames) {
1379
+ if (pack.resetStats && packReset) {
1380
+ const name = profileId === GUEST_PLAYER_ID ? "Guest" : profileId ? profiles.find((p) => p.id === profileId)?.name ?? null : null;
1381
+ return { onReset: () => packReset(profileId), ...pack.resetStats.labels(name) };
1382
+ }
1383
+ const scope = games.length === 1 ? games[0].title : "every game";
1384
+ return { onReset: resetGames, label: games.length === 1 ? `Reset ${scope} Stats` : "Reset All Stats", title: "Reset Stats?", body: `Erases every player's stats and achievements in ${scope}. This cannot be undone.` };
1385
+ }
1386
+ function GameStats({ game, tab, profileId, showTitle, titleColor }) {
1225
1387
  const { StatsSections, AchievementList } = game;
1226
- return /* @__PURE__ */ jsxs7(View4, { style: styles5.game, children: [
1227
- showTitle && /* @__PURE__ */ jsx16(Text2, { variant: "titleLarge", style: [styles5.gameTitle, { color: colors.onBackground }], children: game.title }),
1228
- tab === "stats" ? /* @__PURE__ */ jsx16(StatsSections, { profileId }) : /* @__PURE__ */ jsx16(AchievementList, { profileId })
1388
+ const Sections = tab === STATS ? StatsSections : AchievementList;
1389
+ if (!Sections) return null;
1390
+ return /* @__PURE__ */ jsxs7(View5, { style: styles5.game, children: [
1391
+ showTitle && /* @__PURE__ */ jsx16(Text2, { variant: "titleLarge", style: [styles5.gameTitle, { color: titleColor }], children: game.title }),
1392
+ /* @__PURE__ */ jsx16(Sections, { profileId })
1229
1393
  ] });
1230
1394
  }
1231
1395
  var styles5 = StyleSheet5.create({
@@ -1245,14 +1409,17 @@ var styles5 = StyleSheet5.create({
1245
1409
  fontWeight: "bold",
1246
1410
  marginTop: 8
1247
1411
  },
1412
+ // A picker's button on the theme's surface (over a backdrop), the same pill as Home's.
1413
+ pill: {
1414
+ borderRadius: 999
1415
+ },
1248
1416
  pressAway: {
1249
1417
  zIndex: 50
1250
1418
  },
1251
- // A real full-bleed box for FakeLandscapeView to size its turned content against.
1252
- rotatable: {
1419
+ screen: {
1253
1420
  flex: 1
1254
1421
  },
1255
- // SegmentedButtons' own rounding.
1422
+ // SegmentedButtons' own rounding, so a surface behind it is the same pill.
1256
1423
  tabs: {
1257
1424
  borderRadius: 20
1258
1425
  }
@@ -1288,7 +1455,7 @@ import { defaultSoundSettings, hapticReducer, soundReducer } from "@rific/feedba
1288
1455
  import { profilesReducer } from "@tastic/profile";
1289
1456
  var initialSoundSettings = { ...defaultSoundSettings, enabled: !__DEV__ };
1290
1457
  var appSoundReducer = (state = initialSoundSettings, action) => soundReducer(state, action);
1291
- function createShellReducers({ seatColors, appearance = "system" }) {
1458
+ function createShellReducers({ seatColors, appearance = "system", seatTheme }) {
1292
1459
  return {
1293
1460
  // Explicit primary/secondary triad (rather than a single seed expanded via harmony) so the pack's
1294
1461
  // identity is pinned exactly instead of drifting with whatever hue math a harmony offset would
@@ -1297,7 +1464,9 @@ function createShellReducers({ seatColors, appearance = "system" }) {
1297
1464
  // derived, not picked, via getThirdColor: the hue maximally distant from both (see
1298
1465
  // @rific/auto-paper's README "Explicit triad" section). The appearance is the pack's ('system',
1299
1466
  // createThemeReducer's own default, when it sets none).
1300
- theme: createThemeReducer({ appearance, color: { primary: seatColors[1], secondary: seatColors[2], tertiary: getThirdColor2(seatColors[1], seatColors[2]) } }),
1467
+ // A pack with its own seatTheme starts from what seat 1's default color makes of it instead (dark
1468
+ // only when the pack's appearance is; hooks/useSeatColors.ts re-applies it with the live mode).
1469
+ theme: createThemeReducer(seatTheme ? { appearance, ...seatTheme(seatColors, appearance === "dark") } : { appearance, color: { primary: seatColors[1], secondary: seatColors[2], tertiary: getThirdColor2(seatColors[1], seatColors[2]) } }),
1301
1470
  haptic: hapticReducer,
1302
1471
  sound: appSoundReducer,
1303
1472
  // @rific/core's Keep Awake setting (see Providers.tsx's KeepAwakeBridge), under the namespace its
@@ -1325,7 +1494,7 @@ var errorMiddleware = () => (next) => (action) => {
1325
1494
  if (!hasError(action)) return next(action);
1326
1495
  return action;
1327
1496
  };
1328
- function createPackStore(pack, { keyPrefix, migrate }) {
1497
+ function createPackStore(pack, { keyPrefix, migrate, blacklist = [], serializableIgnoredPaths = [], middleware = [] }) {
1329
1498
  const gameReducers = Object.assign({}, ...pack.games.map((game) => game.reducers));
1330
1499
  const resumeReducers = pack.games.some((game) => game.resumable) ? { lastActiveGame: lastActiveGameSlice_default } : {};
1331
1500
  const rootReducer = combineReducers({ ...createShellReducers(pack), ...resumeReducers, ...pack.reducers, ...gameReducers });
@@ -1343,7 +1512,7 @@ function createPackStore(pack, { keyPrefix, migrate }) {
1343
1512
  // failing to ever resolve at all isn't a failure mode worth a 5s failsafe for.
1344
1513
  timeout: 0,
1345
1514
  // liveplay is per-round state that must never survive a relaunch (see liveplaySlice.ts).
1346
- blacklist: ["liveplay"],
1515
+ blacklist: ["liveplay", ...blacklist],
1347
1516
  migrate
1348
1517
  },
1349
1518
  rootReducer
@@ -1353,10 +1522,11 @@ function createPackStore(pack, { keyPrefix, migrate }) {
1353
1522
  const defaultMiddleware = getDefaultMiddleware({
1354
1523
  immutableCheck: false,
1355
1524
  serializableCheck: {
1356
- ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
1525
+ ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
1526
+ ignoredPaths: [...serializableIgnoredPaths]
1357
1527
  }
1358
1528
  });
1359
- return defaultMiddleware.concat(errorMiddleware);
1529
+ return defaultMiddleware.concat(errorMiddleware, ...middleware);
1360
1530
  },
1361
1531
  reducer: persistedReducer
1362
1532
  });
@@ -1407,7 +1577,7 @@ function forgetProfileAchievements(dropStats) {
1407
1577
 
1408
1578
  // src/gameStats.tsx
1409
1579
  import { createAchievementsSlice, mapUnlocksBySeat, useAchievements } from "@tastic/achievements";
1410
- import { createContext as createContext5, useCallback as useCallback11, useContext as useContext5, useMemo as useMemo6 } from "react";
1580
+ import { createContext as createContext5, useCallback as useCallback11, useContext as useContext5, useMemo as useMemo8 } from "react";
1411
1581
  import { jsx as jsx18 } from "react/jsx-runtime";
1412
1582
  var SEATS = [1, 2];
1413
1583
  function createGameStats({ key, defaultStats, isValidStats, catalog, getProfileStatsView }) {
@@ -1440,7 +1610,7 @@ function createGameStats({ key, defaultStats, isValidStats, catalog, getProfileS
1440
1610
  },
1441
1611
  [recordOutcome]
1442
1612
  );
1443
- const value = useMemo6(() => ({ stats, unlockedAchievements, record, resetAll }), [stats, unlockedAchievements, record, resetAll]);
1613
+ const value = useMemo8(() => ({ stats, unlockedAchievements, record, resetAll }), [stats, unlockedAchievements, record, resetAll]);
1444
1614
  return /* @__PURE__ */ jsx18(Context.Provider, { value, children });
1445
1615
  }
1446
1616
  function useStats() {
@@ -1452,7 +1622,7 @@ function createGameStats({ key, defaultStats, isValidStats, catalog, getProfileS
1452
1622
  }
1453
1623
 
1454
1624
  // src/playerSettings.ts
1455
- import { useCallback as useCallback12, useMemo as useMemo7 } from "react";
1625
+ import { useCallback as useCallback12, useMemo as useMemo9 } from "react";
1456
1626
  import { useDispatch as useDispatch11, useSelector as useSelector9 } from "react-redux";
1457
1627
  var SEATS2 = [1, 2];
1458
1628
  function seatPlayerKey(selection, seat) {
@@ -1496,7 +1666,7 @@ function createPlayerSettings({ id, defaults, distinctControls }) {
1496
1666
  const saved = useSelector9((state) => state[settingsKey]);
1497
1667
  const controls = useSelector9((state) => state[controlsKey]);
1498
1668
  const dispatch = useDispatch11();
1499
- const settings = useMemo7(() => compose(saved, controls, selection, seatTwoIsPerson), [saved, controls, selection, seatTwoIsPerson]);
1669
+ const settings = useMemo9(() => compose(saved, controls, selection, seatTwoIsPerson), [saved, controls, selection, seatTwoIsPerson]);
1500
1670
  const setSettings = useCallback12(
1501
1671
  (next) => {
1502
1672
  const { controlScheme, ...changes } = next;
@@ -1516,6 +1686,7 @@ function createPlayerSettings({ id, defaults, distinctControls }) {
1516
1686
  export {
1517
1687
  DEFAULT_APP_SETTINGS,
1518
1688
  DEFAULT_OPPONENT,
1689
+ GUEST_PLAYER_ID,
1519
1690
  HomeScreen,
1520
1691
  LoadoutRoute,
1521
1692
  PREVIEW_ASPECT,