@tastic/shell 0.1.1 → 0.2.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.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 }
@@ -419,23 +484,30 @@ var styles = StyleSheet.create({
419
484
  import { Redirect, useLocalSearchParams } from "expo-router";
420
485
  import { useEffect as useEffect5 } from "react";
421
486
 
487
+ // src/routes.ts
488
+ var playHref = (game) => ({ pathname: "/play/[game]", params: { game } });
489
+ var loadoutHref = (game) => ({ pathname: "/loadout/[game]", params: { game } });
490
+ var STATS_HREF = "/stats";
491
+ var PROFILES_HREF = "/profiles";
492
+
422
493
  // src/screens/LoadoutScreen.tsx
423
494
  import { defaultColors, useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
424
495
  import { FakeLandscapeView, getViewRotation, rotateInsets, useOrientationState } from "@tastic/core";
425
- import { CornerActionButtons, PlayerSetupPanel, PressAwayOverlay, ReadyButton as ReadyButton2, SharedActionBand, usePopoverHost } from "@tastic/hud";
496
+ import { CornerActionButtons, PressAwayOverlay, usePopoverHost } from "@tastic/hud/chrome";
497
+ import { PlayerSetupPanel, ReadyButton as ReadyButton2, SharedActionBand } from "@tastic/hud/lobby";
426
498
  import { DualZoneLayout, needsSharedNeutralZone, useDualZoneLayout } from "@tastic/split-screen";
427
499
  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";
500
+ import { useCallback as useCallback9, useEffect as useEffect4, useMemo as useMemo5, useRef as useRef4, useState as useState4 } from "react";
501
+ import { Platform as Platform3, StyleSheet as StyleSheet3, View as View3 } from "react-native";
430
502
  import Animated from "react-native-reanimated";
431
503
  import { useSafeAreaInsets } from "react-native-safe-area-context";
432
504
  import { useDispatch as useDispatch9 } from "react-redux";
433
505
 
434
506
  // src/components/SeatTwoPanel.tsx
435
- import { ReadyButton, useZoneClampedAlign } from "@tastic/hud";
507
+ import { ReadyButton, useZoneClampedAlign } from "@tastic/hud/lobby";
436
508
  import { getOptionPickerContentSize, OptionPicker } from "@tastic/profile";
437
- import { useMemo as useMemo3 } from "react";
438
- import { StyleSheet as StyleSheet2, View } from "react-native";
509
+ import { useMemo as useMemo4 } from "react";
510
+ import { StyleSheet as StyleSheet2, View as View2 } from "react-native";
439
511
  import { jsx as jsx10, jsxs as jsxs3 } from "react/jsx-runtime";
440
512
  var CPU_PREFIX = "cpu:";
441
513
  var PROFILE_PREFIX = "profile:";
@@ -451,7 +523,7 @@ function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDi
451
523
  const isPerson = seatTwo === "person";
452
524
  const selectedProfile = isPerson ? profiles.find((p) => p.id === selectedProfileId) ?? null : null;
453
525
  const cpuOptions = cpuDifficulty?.options;
454
- const options = useMemo3(() => {
526
+ const options = useMemo4(() => {
455
527
  const list = [];
456
528
  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
529
  if (choices.includes("nobody")) list.push({ value: NOBODY, label: "Nobody", icon: NOBODY_ICON });
@@ -480,9 +552,9 @@ function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDi
480
552
  const align = useZoneClampedAlign(host.openId === pickerId, size.width, size.height);
481
553
  const ownPopoverOpen = host.openId?.startsWith(`${idPrefix}-`) ?? false;
482
554
  const pickerRowOpen = secondPickerId !== void 0 && host.openId === secondPickerId;
483
- return /* @__PURE__ */ jsxs3(View, { style: [styles2.panel, ownPopoverOpen && styles2.panelOpen], children: [
555
+ return /* @__PURE__ */ jsxs3(View2, { style: [styles2.panel, ownPopoverOpen && styles2.panelOpen], children: [
484
556
  /* @__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 }),
557
+ isPerson && secondPicker && /* @__PURE__ */ jsx10(View2, { style: [styles2.pickerRow, pickerRowOpen && styles2.pickerRowOpen], children: secondPicker }),
486
558
  isPerson && showReadyButton && onToggleReady && /* @__PURE__ */ jsx10(ReadyButton, { color, ready: ready ?? false, onToggleReady })
487
559
  ] });
488
560
  }
@@ -509,13 +581,14 @@ var styles2 = StyleSheet2.create({
509
581
 
510
582
  // src/components/SettingsDialog.tsx
511
583
  import { useUpdateErrorToast as useUpdateErrorToast2 } from "@rific/toaster";
512
- import { BaseSettingsDialog } from "@tastic/hud";
584
+ import { BaseSettingsDialog } from "@tastic/hud/chrome";
513
585
  import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
514
586
  function SettingsDialog({ visible, onDismiss, rotation = 0, onShowHowToPlay, children }) {
515
587
  const onUpdateError = useUpdateErrorToast2();
516
588
  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: [
589
+ const { otaVersion, SettingsSections, edgeGuard = true } = usePack();
590
+ const edgeGuardProps = edgeGuard ? { deferBottomEdgeGestures: settings.deferBottomEdgeGestures, onDeferBottomEdgeGestures: (value) => setSettings({ deferBottomEdgeGestures: value }) } : {};
591
+ return /* @__PURE__ */ jsxs4(BaseSettingsDialog, { visible, onDismiss, rotation, version: otaVersion, lockOrientation: settings.lockOrientation, onLockOrientationChange: (value) => setSettings({ lockOrientation: value }), ...edgeGuardProps, onUpdateError, onShowHowToPlay, children: [
519
592
  children,
520
593
  SettingsSections && /* @__PURE__ */ jsx11(SettingsSections, {})
521
594
  ] });
@@ -560,13 +633,17 @@ function createSeatColorsReducer(seatColors) {
560
633
  var seatColorsActions = createSeatColorsSlice("pack", defaultSeatColorsState({ 1: "", 2: "" })).actions;
561
634
 
562
635
  // src/hooks/useSeatColors.ts
636
+ function sameTheme(current, next) {
637
+ const a = current.color;
638
+ const b = next.color;
639
+ 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();
640
+ return sameColor && (next.harmony === void 0 || next.harmony === current.harmony);
641
+ }
563
642
  function useSeatColors(p2IsHuman) {
564
- const { colors: themeColors } = useAutoPaperTheme2();
565
- const { set: setThemeColor } = useThemeSettings();
566
- const p1Color = themeColors.primary;
567
- const p2Color = themeColors.secondary;
643
+ const { dark, colors: themeColors } = useAutoPaperTheme2();
644
+ const { settings: themeSettings, set: setThemeColor } = useThemeSettings();
568
645
  const { profiles, lastSelected } = useProfiles();
569
- const { seatColors } = usePack();
646
+ const { seatColors, seatTheme } = usePack();
570
647
  const seatDefaultColors = [seatColors[1], seatColors[2]];
571
648
  const dispatch = useDispatch8();
572
649
  const guestColors = useSelector7((state) => state.seatColors.lastGuestColor);
@@ -574,11 +651,21 @@ function useSeatColors(p2IsHuman) {
574
651
  const profileColors = useSelector7((state) => state.seatColors.profileColors);
575
652
  const p1Profile = profiles.find((p) => p.id === lastSelected[1]) ?? null;
576
653
  const p2Profile = p2IsHuman ? profiles.find((p) => p.id === lastSelected[2]) ?? null : null;
654
+ const rememberedColors = (p1Prof, p2Prof, picks, guest, cpu) => {
655
+ const p1 = p1Prof ? profileSeatColor(p1Prof, picks) : guest[1];
656
+ const p2 = avoidSeatColorClash(p1, p2Prof ? profileSeatColor(p2Prof, picks) : p2IsHuman ? guest[2] : cpu, seatDefaultColors);
657
+ return { p1, p2 };
658
+ };
659
+ const remembered = rememberedColors(p1Profile, p2Profile, profileColors, guestColors, cpuColor);
660
+ const p1Color = seatTheme ? remembered.p1 : themeColors.primary;
661
+ const p2Color = seatTheme ? remembered.p2 : themeColors.secondary;
662
+ const themeFor = useCallback8((p1, p2) => seatTheme ? seatTheme({ 1: p1, 2: p2 }, dark) : { color: { primary: p1, secondary: p2, tertiary: getThirdColor(p1, p2) } }, [seatTheme, dark]);
577
663
  const p1ProfileRef = useRef3(p1Profile);
578
664
  const p2ProfileRef = useRef3(p2Profile);
579
665
  const guestColorsRef = useRef3(guestColors);
580
666
  const cpuColorRef = useRef3(cpuColor);
581
667
  const profileColorsRef = useRef3(profileColors);
668
+ const themeForRef = useRef3(themeFor);
582
669
  useEffect3(() => {
583
670
  p1ProfileRef.current = p1Profile;
584
671
  }, [p1Profile]);
@@ -595,21 +682,17 @@ function useSeatColors(p2IsHuman) {
595
682
  profileColorsRef.current = profileColors;
596
683
  }, [profileColors]);
597
684
  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]);
685
+ themeForRef.current = themeFor;
686
+ }, [themeFor]);
687
+ useEffect3(() => {
688
+ const next = themeFor(remembered.p1, remembered.p2);
689
+ if (sameTheme(themeSettings, next)) return;
690
+ setThemeColor(next);
691
+ }, [lastSelected[1], lastSelected[2], p2IsHuman, guestColors, cpuColor, profileColors, profiles.length, dark]);
603
692
  useFocusEffect(
604
693
  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) } });
694
+ const next = rememberedColors(p1ProfileRef.current, p2ProfileRef.current, profileColorsRef.current, guestColorsRef.current, cpuColorRef.current);
695
+ setThemeColor(themeForRef.current(next.p1, next.p2));
613
696
  }, [lastSelected[1], lastSelected[2], p2IsHuman, setThemeColor])
614
697
  );
615
698
  const persistSeatColor = useCallback8(
@@ -631,21 +714,21 @@ function useSeatColors(p2IsHuman) {
631
714
  (hex) => {
632
715
  const swapped = hex.toLowerCase() === p2Color.toLowerCase();
633
716
  const nextP2 = swapped ? p1Color : p2Color;
634
- setThemeColor({ color: { primary: hex, secondary: nextP2, tertiary: getThirdColor(hex, nextP2) } });
717
+ setThemeColor(themeFor(hex, nextP2));
635
718
  persistSeatColor(1, hex);
636
719
  if (swapped) persistSeatColor(2, nextP2);
637
720
  },
638
- [setThemeColor, p1Color, p2Color, persistSeatColor]
721
+ [setThemeColor, themeFor, p1Color, p2Color, persistSeatColor]
639
722
  );
640
723
  const handleP2ColorChange = useCallback8(
641
724
  (hex) => {
642
725
  const swapped = hex.toLowerCase() === p1Color.toLowerCase();
643
726
  const nextP1 = swapped ? p2Color : p1Color;
644
- setThemeColor({ color: { primary: nextP1, secondary: hex, tertiary: getThirdColor(nextP1, hex) } });
727
+ setThemeColor(themeFor(nextP1, hex));
645
728
  persistSeatColor(2, hex);
646
729
  if (swapped) persistSeatColor(1, nextP1);
647
730
  },
648
- [setThemeColor, p1Color, p2Color, persistSeatColor]
731
+ [setThemeColor, themeFor, p1Color, p2Color, persistSeatColor]
649
732
  );
650
733
  return { p1Color, p2Color, handleP1ColorChange, handleP2ColorChange };
651
734
  }
@@ -671,16 +754,17 @@ function LoadoutScreen({ game }) {
671
754
  const { seatTwo: savedSeatTwo, setSeatTwo } = useOpponent();
672
755
  const seatTwo = seatTwoFor(savedSeatTwo, game.seatTwo);
673
756
  const mode = roundModeFor(seatTwo);
674
- const humanSeats = useMemo4(() => humanSeatsFor(mode), [mode]);
757
+ const humanSeats = useMemo5(() => humanSeatsFor(mode), [mode]);
675
758
  const p2IsHuman = mode === "twoPlayer";
676
759
  const p2Exists = mode !== "solo";
677
760
  const loadout = game.useLoadout(mode);
678
761
  const { openFor } = useGameGuide();
679
762
  const { profiles, lastSelected, selectProfile } = useProfiles();
680
763
  const { orientationMode, p1OnRight, upsideDown, resolved: p1OnRightResolved } = useOrientationState();
681
- const fixedSeats = p2IsHuman && (!!game.twoPlayerSeats?.faceToFaceOnWeb || Platform2.OS !== "web");
764
+ const fixedSeats = p2IsHuman && (!!game.twoPlayerSeats?.faceToFaceOnWeb || Platform3.OS !== "web");
682
765
  const fixedUpsideDown = !!game.twoPlayerSeats?.followUpsideDown && orientationMode === "faceToFace" && upsideDown;
683
766
  const { panelLayout, panelFadeStyle } = useDualZoneLayout(fixedSeats ? "faceToFace" : orientationMode, fixedSeats ? true : p1OnRight, fixedSeats || p1OnRightResolved, fixedSeats ? fixedUpsideDown : upsideDown, PANEL_SWAP_FADE_MS);
767
+ const playersRowFade = panelFadeStyle;
684
768
  const [settingsOpen, setSettingsOpen] = useState4(false);
685
769
  const rotation = getViewRotation(panelLayout.orientationMode, panelLayout.p1OnRight, panelLayout.upsideDown);
686
770
  const insets = rotateInsets(useSafeAreaInsets(), rotation);
@@ -709,7 +793,7 @@ function LoadoutScreen({ game }) {
709
793
  useEffect4(() => {
710
794
  if (!humanSeats.every((s) => ready[s])) return;
711
795
  dispatch(liveplayActions.setActiveRound({ game: game.id, mode, settings: roundSettingsRef.current }));
712
- router2.push({ pathname: "/play/[game]", params: { game: game.id } });
796
+ router2.push(playHref(game.id));
713
797
  }, [ready, humanSeats, game.id, mode, dispatch]);
714
798
  const bg = dark ? "#000000" : "#FFFFFF";
715
799
  const fg = dark ? "#FFFFFF" : "#000000";
@@ -737,10 +821,10 @@ function LoadoutScreen({ game }) {
737
821
  const sharedControls = showMetaInSharedBand ? /* @__PURE__ */ jsx12(SharedActionBand, { onBack: safeBack, onSettings: () => setSettingsOpen(true), onRandomize: loadout.randomize, onReset: loadout.reset, fg, popoverOpen: optionsPopoverOpen, children: optionsRow }) : optionsRow;
738
822
  const controlLook = { accentColor: themeColors.tertiary, mutedColor: fgMuted, dark, rotation };
739
823
  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") });
824
+ 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
825
  const p2Control = p2IsHuman ? loadout.seatControl(2, p2Host, { ...controlLook, seatColor: p2Color }) : void 0;
742
826
  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: [
827
+ return /* @__PURE__ */ jsxs5(View3, { style: [styles3.container, { backgroundColor: bg }], children: [
744
828
  /* @__PURE__ */ jsxs5(FakeLandscapeView, { orientationMode: panelLayout.orientationMode, p1OnRight: panelLayout.p1OnRight, upsideDown: panelLayout.upsideDown, style: styles3.rotatable, children: [
745
829
  /* @__PURE__ */ jsx12(PressAwayOverlay, { active: controlsHost.openId !== null, onPress: controlsHost.close }),
746
830
  p2ZoneStyle && /* @__PURE__ */ jsx12(PressAwayOverlay, { active: controlsHost.openId !== null || p2Host.openId !== null, onPress: p2Host.close, style: p2ZoneStyle }),
@@ -757,9 +841,9 @@ function LoadoutScreen({ game }) {
757
841
  p2Elevated: p2Host.openId !== null,
758
842
  sharedElevated: optionsPopoverOpen
759
843
  }
760
- ) : /* @__PURE__ */ jsxs5(View2, { style: styles3.stackedZone, children: [
844
+ ) : /* @__PURE__ */ jsxs5(View3, { style: styles3.stackedZone, children: [
761
845
  sharedControls,
762
- /* @__PURE__ */ jsxs5(Animated.View, { style: [styles3.playersRow, anyPlayerPickerOpen && styles3.playersRowOpen, panelFadeStyle], children: [
846
+ /* @__PURE__ */ jsxs5(Animated.View, { style: [styles3.playersRow, anyPlayerPickerOpen && styles3.playersRowOpen, playersRowFade], children: [
763
847
  !isPortrait && panelLayout.p1OnRight ? p2Panel : p1Panel,
764
848
  !isPortrait && panelLayout.p1OnRight ? p1Panel : p2Panel
765
849
  ] }),
@@ -829,7 +913,7 @@ function LoadoutRoute() {
829
913
  const { game: id } = useLocalSearchParams();
830
914
  const game = usePackGame(id);
831
915
  if (!game) return /* @__PURE__ */ jsx13(Redirect, { href: "/" });
832
- if (!hasLoadout(game)) return /* @__PURE__ */ jsx13(Redirect, { href: { pathname: "/play/[game]", params: { game: game.id } } });
916
+ if (!hasLoadout(game)) return /* @__PURE__ */ jsx13(Redirect, { href: playHref(game.id) });
833
917
  return /* @__PURE__ */ jsx13(LoadoutScreen, { game }, game.id);
834
918
  }
835
919
  function hasLoadout(game) {
@@ -853,15 +937,15 @@ function useResumePoint(gameId) {
853
937
  }
854
938
 
855
939
  // src/screens/HomeScreen.tsx
856
- import { defaultColors as defaultColors2, useAutoPaperTheme as useAutoPaperTheme4 } from "@rific/auto-paper";
940
+ import { defaultColors as defaultColors2, FadingScrollView, useAutoPaperTheme as useAutoPaperTheme4 } from "@rific/auto-paper";
857
941
  import { IconButton, Pressable } from "@rific/feedback-press";
858
942
  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";
943
+ import { PressAwayOverlay as PressAwayOverlay2, usePopoverHost as usePopoverHost2, useWellColors, WELL_SHADOW } from "@tastic/hud/chrome";
944
+ import { getProfilePickerContentSize, ProfilePicker } from "@tastic/profile";
861
945
  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";
946
+ import { useCallback as useCallback10, useEffect as useEffect6, useMemo as useMemo6, useState as useState6 } from "react";
947
+ import { AppState, StyleSheet as StyleSheet4, View as View4 } from "react-native";
948
+ import { Icon, Portal, Text } from "react-native-paper";
865
949
 
866
950
  // src/hooks/useFrozenWhileHidden.ts
867
951
  import { useState as useState5 } from "react";
@@ -941,15 +1025,19 @@ function useHomeLayout() {
941
1025
  }
942
1026
 
943
1027
  // src/screens/HomeScreen.tsx
944
- import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs6 } from "react/jsx-runtime";
1028
+ import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs6 } from "react/jsx-runtime";
945
1029
  function HomeScreen() {
946
1030
  const pack = usePack();
947
1031
  const { consumeResumeGame } = useLastActiveGame();
1032
+ const { autoShowPack } = useGameGuide();
948
1033
  const [resumeGame] = useState6(() => {
949
1034
  const id = consumeResumeGame();
950
1035
  return pack.games.find((g) => g.id === id && g.resumable) ?? null;
951
1036
  });
952
- if (resumeGame) return /* @__PURE__ */ jsx14(Redirect2, { href: { pathname: "/play/[game]", params: { game: resumeGame.id } } });
1037
+ useEffect6(() => {
1038
+ if (!resumeGame) autoShowPack();
1039
+ }, [resumeGame, autoShowPack]);
1040
+ if (resumeGame) return /* @__PURE__ */ jsx14(Redirect2, { href: playHref(resumeGame.id) });
953
1041
  return /* @__PURE__ */ jsx14(HomeMenu, {});
954
1042
  }
955
1043
  function HomeMenu() {
@@ -957,9 +1045,10 @@ function HomeMenu() {
957
1045
  const rotation = useRotation4();
958
1046
  const layout = useHomeLayout();
959
1047
  const { insets } = layout;
960
- const { dark } = useAutoPaperTheme4();
961
- const { openFirstTime, openFor } = useGameGuide();
1048
+ const { dark, colors } = useAutoPaperTheme4();
1049
+ const { openFirstTime, openFor, openPack } = useGameGuide();
962
1050
  const [settingsOpen, setSettingsOpen] = useState6(false);
1051
+ const [rulesGame, setRulesGame] = useState6(null);
963
1052
  const isFocused = useIsFocused();
964
1053
  const [appState, setAppState] = useState6(AppState.currentState);
965
1054
  useEffect6(() => {
@@ -983,65 +1072,85 @@ function HomeMenu() {
983
1072
  const closePicker = pickerHost.close;
984
1073
  useEffect6(() => {
985
1074
  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: [
1075
+ }, [isWide, rotation, layout.windowWidth, layout.windowHeight, closePicker]);
1076
+ const bg = pack.Backdrop ? "transparent" : dark ? "#000000" : "#FFFFFF";
1077
+ const fg = pack.foreground ?? (dark ? "#FFFFFF" : "#000000");
1078
+ const { previewColor } = pack;
1079
+ const p1Drawn = previewColor ? previewColor(p1Color, dark) : p1Color;
1080
+ const triggerStyle = useMemo6(() => pack.Backdrop ? [styles4.pill, { backgroundColor: colors.surface }] : void 0, [pack.Backdrop, colors.surface]);
1081
+ const openGame = useCallback10((game) => openFirstTime(game.id, () => router3.push(game.useLoadout ? loadoutHref(game.id) : playHref(game.id))), [openFirstTime]);
1082
+ const openRules = useCallback10((game) => game.Rules ? setRulesGame(game) : openFor(game.id), [openFor]);
1083
+ const colorSection = { value: p1Color, onChange: handleP1ColorChange, swatches: defaultColors2, caption: selectedProfile ? pack.colorCaption : void 0, previewValue: previewColor ? (hex) => previewColor(hex, dark) : void 0 };
1084
+ const usePickerAlign = pack.usePickerAlign ?? noPickerAlign;
1085
+ const pickerSize = getProfilePickerContentSize(profiles.length, true, colorSection);
1086
+ const pickerAlign = usePickerAlign(pickerHost.openId === `${SEAT_ONE_PICKER}-profile`, pickerSize.width, pickerSize.height);
1087
+ 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 });
1088
+ const pressAway = /* @__PURE__ */ jsx14(PressAwayOverlay2, { active: pickerHost.openId !== null, onPress: pickerHost.close, style: styles4.pressAway });
1089
+ const RulesSheet = rulesGame?.Rules;
1090
+ return /* @__PURE__ */ jsxs6(View4, { style: [styles4.container, { backgroundColor: bg }], children: [
1091
+ /* @__PURE__ */ jsx14(FakeLandscapeView2, { style: styles4.rotatable, children: /* @__PURE__ */ jsxs6(RulesHost, { enabled: pack.games.some((game) => game.Rules), children: [
1092
+ isWide ? /* @__PURE__ */ jsxs6(Fragment4, { children: [
1093
+ pressAway,
1094
+ /* @__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)) }) }),
1095
+ /* @__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
1096
  /* @__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 })
1097
+ /* @__PURE__ */ jsx14(View4, { style: styles4.widePicker, children: picker })
999
1098
  ] })
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: [
1099
+ ] }) : /* @__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: [
1100
+ pressAway,
1001
1101
  /* @__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)) })
1102
+ /* @__PURE__ */ jsx14(View4, { style: [styles4.picker, pickerHost.openId !== null && styles4.pickerOpen], children: picker }),
1103
+ /* @__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
1104
  ] }),
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 })
1105
+ /* @__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" }),
1106
+ /* @__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" }),
1107
+ RulesSheet && /* @__PURE__ */ jsx14(RulesSheet, { visible: true, onDismiss: () => setRulesGame(null) })
1108
+ ] }) }),
1109
+ /* @__PURE__ */ jsx14(SettingsDialog, { visible: settingsOpen, onDismiss: () => setSettingsOpen(false), rotation, onShowHowToPlay: pack.guide ? openPack : void 0 })
1009
1110
  ] });
1010
1111
  }
1112
+ var SEAT_ONE_PICKER = "home-p1";
1113
+ var noPickerAlign = () => void 0;
1114
+ function hasRules(game) {
1115
+ return !!(game.Rules || game.guide);
1116
+ }
1117
+ function RulesHost({ enabled, children }) {
1118
+ return enabled ? /* @__PURE__ */ jsx14(Portal.Host, { children }) : /* @__PURE__ */ jsx14(Fragment4, { children });
1119
+ }
1011
1120
  function GameRow({ game, dark, p1Color, p2Color, previewRevealed, onPress, onRules }) {
1012
1121
  const well = useWellColors();
1013
1122
  const GameIcon = game.icon;
1014
1123
  return (
1015
1124
  // The rules button sits beside the row's own button, laid over its end, not inside it: a button
1016
1125
  // inside a button is invalid on the web.
1017
- /* @__PURE__ */ jsxs6(View3, { children: [
1126
+ /* @__PURE__ */ jsxs6(View4, { children: [
1018
1127
  /* @__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: [
1128
+ 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 }) }),
1129
+ /* @__PURE__ */ jsxs6(View4, { style: styles4.rowText, children: [
1021
1130
  /* @__PURE__ */ jsx14(Text, { variant: "titleLarge", style: [styles4.rowTitle, { color: well.fg }], numberOfLines: 1, children: game.title }),
1022
1131
  /* @__PURE__ */ jsx14(Text, { variant: "bodyMedium", style: { color: well.fgMuted }, numberOfLines: 2, children: game.blurb })
1023
1132
  ] })
1024
1133
  ] }),
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 }) })
1134
+ 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
1135
  ] })
1027
1136
  );
1028
1137
  }
1029
1138
  function GameTile({ game, width, previewWidth, dark, p1Color, p2Color, previewRevealed, onPress, onRules }) {
1030
1139
  const well = useWellColors();
1031
- return /* @__PURE__ */ jsxs6(View3, { style: { width }, children: [
1140
+ return /* @__PURE__ */ jsxs6(View4, { style: { width }, children: [
1032
1141
  /* @__PURE__ */ jsxs6(Pressable, { onPress, style: [styles4.tile, { backgroundColor: well.bg }], accessibilityRole: "button", accessibilityLabel: game.title, children: [
1033
1142
  /* @__PURE__ */ jsx14(PreviewBox, { game, width: previewWidth, revealed: previewRevealed, dark, p1Color, p2Color }),
1034
1143
  /* @__PURE__ */ jsx14(Text, { variant: "titleMedium", style: [styles4.rowTitle, { color: well.fg }], numberOfLines: 1, children: game.title }),
1035
1144
  /* @__PURE__ */ jsx14(Text, { variant: "bodySmall", style: { color: well.fgMuted }, numberOfLines: 3, children: game.blurb })
1036
1145
  ] }),
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" }) })
1146
+ 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
1147
  ] });
1039
1148
  }
1040
1149
  function PreviewBox({ game, width, revealed, dark, p1Color, p2Color }) {
1041
1150
  const height = Math.round(width / PREVIEW_ASPECT);
1042
1151
  const { Preview, icon: GameIcon } = game;
1043
1152
  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 }) }) });
1153
+ 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
1154
  }
1046
1155
  var ROW_ICON_SIZE = 56;
1047
1156
  var ICON_LIGHT_BG = "#FAFAFA";
@@ -1076,6 +1185,15 @@ var styles4 = StyleSheet4.create({
1076
1185
  pickerOpen: {
1077
1186
  zIndex: 100
1078
1187
  },
1188
+ // Seat 1's button on the theme's surface (over a backdrop), the same pill as the tiles' wells.
1189
+ pill: {
1190
+ borderRadius: 999
1191
+ },
1192
+ // Over the rows (and, wide, the grid), whose own stacking is unset, so a press on them reaches it on
1193
+ // the web too, and under the raised button and left pane, so the menu's rows stay pressable.
1194
+ pressAway: {
1195
+ zIndex: 5
1196
+ },
1079
1197
  // The preview's own rounding, clipping whatever it draws to the box.
1080
1198
  preview: {
1081
1199
  alignItems: "center",
@@ -1166,66 +1284,109 @@ var styles4 = StyleSheet4.create({
1166
1284
 
1167
1285
  // src/screens/ProfilesRoute.tsx
1168
1286
  import { defaultColors as defaultColors3 } from "@rific/auto-paper";
1287
+ import { RotatedFrame as RotatedFrame2 } from "@tastic/hud/chrome";
1169
1288
  import { ProfilesScreen } from "@tastic/profile";
1170
1289
  import { jsx as jsx15 } from "react/jsx-runtime";
1290
+ var ROSTER_MAX_WIDTH = 640;
1291
+ function useRosterAlign(open, contentWidth, contentHeight) {
1292
+ const { usePickerAlign } = usePack();
1293
+ return usePickerAlign(open, contentWidth, contentHeight, { maxContentWidth: ROSTER_MAX_WIDTH });
1294
+ }
1171
1295
  function ProfilesRoute() {
1172
1296
  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 });
1297
+ const { Backdrop, foreground, newProfileColor, usePickerAlign } = usePack();
1298
+ 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
1299
  }
1175
1300
 
1176
1301
  // src/screens/StatsScreen.tsx
1177
1302
  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";
1303
+ import { BaseStatsScreen, PressAwayOverlay as PressAwayOverlay3, RotatedFrame as RotatedFrame3, usePopoverHost as usePopoverHost3 } from "@tastic/hud/chrome";
1304
+ import { getOptionPickerContentSize as getOptionPickerContentSize2, OptionPicker as OptionPicker2 } from "@tastic/profile";
1305
+ import { useMemo as useMemo7, useState as useState7 } from "react";
1306
+ import { StyleSheet as StyleSheet5, View as View5 } from "react-native";
1183
1307
  import { SegmentedButtons, Text as Text2 } from "react-native-paper";
1184
1308
  import { useSafeAreaInsets as useSafeAreaInsets3 } from "react-native-safe-area-context";
1185
1309
  import { useDispatch as useDispatch10 } from "react-redux";
1186
1310
  import { jsx as jsx16, jsxs as jsxs7 } from "react/jsx-runtime";
1311
+ var STATS = "stats";
1312
+ var ACHIEVEMENTS = "achievements";
1187
1313
  var TABS = [
1188
- { value: "stats", label: "Stats", icon: "chart-box-outline" },
1189
- { value: "achievements", label: "Achievements", icon: "trophy-outline" }
1314
+ { value: STATS, label: "Stats", icon: "chart-box-outline" },
1315
+ { value: ACHIEVEMENTS, label: "Achievements", icon: "trophy-outline" }
1190
1316
  ];
1317
+ var LABEL_WIDE_TAB = { flexBasis: "auto", flexGrow: 1, flexShrink: 1 };
1318
+ var NO_PACK_TABS = [];
1191
1319
  var EVERYONE = "__everyone__";
1192
1320
  var ALL_GAMES = "__all__";
1193
1321
  var PLAYER_PICKER_ID = "stats-player";
1194
1322
  var GAME_PICKER_ID = "stats-game";
1323
+ var STATS_MAX_WIDTH = 640;
1195
1324
  function StatsScreen() {
1325
+ const pickerHost = usePopoverHost3();
1326
+ return /* @__PURE__ */ jsx16(StatsPressAway, { host: pickerHost, children: /* @__PURE__ */ jsx16(RotatedFrame3, { rotatedSafeArea: true, portalHost: true, maxContentWidth: STATS_MAX_WIDTH, children: /* @__PURE__ */ jsx16(StatsContent, { pickerHost }) }) });
1327
+ }
1328
+ function StatsPressAway({ host, children }) {
1329
+ return /* @__PURE__ */ jsx16(View5, { testID: "stats-press-away", style: styles5.screen, onStartShouldSetResponder: () => host.openId !== null, onResponderRelease: host.close, children });
1330
+ }
1331
+ var useNoPackReset = () => null;
1332
+ var noPickerAlign2 = () => void 0;
1333
+ function StatsContent({ pickerHost }) {
1196
1334
  const pack = usePack();
1197
1335
  const { dark, colors } = useAutoPaperTheme5();
1198
- const rotation = useRotation5();
1199
- const insets = rotateInsets3(useSafeAreaInsets3(), rotation);
1200
- const { profiles } = useProfiles();
1336
+ const insets = useSafeAreaInsets3();
1337
+ const { profiles, lastSelected } = useProfiles();
1201
1338
  const dispatch = useDispatch10();
1202
- const pickerHost = usePopoverHost3();
1203
- const [tab, setTab] = useState7("stats");
1339
+ const fg = pack.foreground ?? colors.onBackground;
1340
+ const [tab, setTab] = useState7(STATS);
1204
1341
  const [gameId, setGameId] = useState7(ALL_GAMES);
1205
- const [selectedProfileId, setSelectedProfileId] = useState7(null);
1206
- const profileId = selectedProfileId && profiles.some((p) => p.id === selectedProfileId) ? selectedProfileId : null;
1342
+ const [selectedProfileId, setSelectedProfileId] = useState7(() => pack.guestStats ? lastSelected[1] ?? GUEST_PLAYER_ID : null);
1343
+ const profileId = selectedProfileId && profiles.length > 0 && (pack.guestStats && selectedProfileId === GUEST_PLAYER_ID || profiles.some((p) => p.id === selectedProfileId)) ? selectedProfileId : null;
1207
1344
  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: [
1345
+ const packTabs = pack.statsTabs ?? NO_PACK_TABS;
1346
+ const packTab = packTabs.find((t) => t.value === tab);
1347
+ const showGamePicker = !(tab === ACHIEVEMENTS && pack.AchievementList);
1348
+ 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]);
1349
+ 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]);
1350
+ 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]);
1351
+ const title = tab === STATS ? "Stats" : tab === ACHIEVEMENTS ? "Achievements" : packTab?.label ?? "";
1352
+ const usePackReset = pack.resetStats?.useReset ?? useNoPackReset;
1353
+ const packReset = usePackReset();
1354
+ const reset = resetFor(pack, packReset, profileId, profiles, games, () => games.forEach((g) => g.resetStats && dispatch(g.resetStats())));
1355
+ const usePickerAlign = pack.usePickerAlign ?? noPickerAlign2;
1356
+ const playerSize = getOptionPickerContentSize2(playerOptions.length);
1357
+ const gameSize = getOptionPickerContentSize2(gameOptions.length);
1358
+ const playerAlign = usePickerAlign(pickerHost.openId === PLAYER_PICKER_ID, playerSize.width, playerSize.height);
1359
+ const gameAlign = usePickerAlign(pickerHost.openId === GAME_PICKER_ID, gameSize.width, gameSize.height);
1360
+ const surface = pack.Backdrop ? { backgroundColor: colors.surface } : void 0;
1361
+ const triggerStyle = pack.Backdrop ? [styles5.pill, surface] : void 0;
1362
+ const PackTabContent = packTab?.Content;
1363
+ 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
1364
  /* @__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" })
1365
+ /* @__PURE__ */ jsx16(SegmentedButtons, { value: tab, onValueChange: setTab, buttons: tabButtons, style: [styles5.tabs, surface] }),
1366
+ /* @__PURE__ */ jsxs7(View5, { style: [styles5.filters, pickerHost.openId !== null && styles5.filtersOpen], onStartShouldSetResponder: () => pickerHost.openId !== null, children: [
1367
+ 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 }),
1368
+ 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
1369
  ] }),
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
- ] }) });
1370
+ tab === STATS && pack.StatsSections && /* @__PURE__ */ jsx16(pack.StatsSections, { profileId, gameId: gameId === ALL_GAMES ? null : gameId }),
1371
+ PackTabContent && /* @__PURE__ */ jsx16(PackTabContent, { profileId, gameId: gameId === ALL_GAMES ? null : gameId }),
1372
+ 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))
1373
+ ] });
1222
1374
  }
1223
- function GameStats({ game, tab, profileId, showTitle }) {
1224
- const { colors } = useAutoPaperTheme5();
1375
+ function resetFor(pack, packReset, profileId, profiles, games, resetGames) {
1376
+ if (pack.resetStats && packReset) {
1377
+ const name = profileId === GUEST_PLAYER_ID ? "Guest" : profileId ? profiles.find((p) => p.id === profileId)?.name ?? null : null;
1378
+ return { onReset: () => packReset(profileId), ...pack.resetStats.labels(name) };
1379
+ }
1380
+ const scope = games.length === 1 ? games[0].title : "every game";
1381
+ 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.` };
1382
+ }
1383
+ function GameStats({ game, tab, profileId, showTitle, titleColor }) {
1225
1384
  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 })
1385
+ const Sections = tab === STATS ? StatsSections : AchievementList;
1386
+ if (!Sections) return null;
1387
+ return /* @__PURE__ */ jsxs7(View5, { style: styles5.game, children: [
1388
+ showTitle && /* @__PURE__ */ jsx16(Text2, { variant: "titleLarge", style: [styles5.gameTitle, { color: titleColor }], children: game.title }),
1389
+ /* @__PURE__ */ jsx16(Sections, { profileId })
1229
1390
  ] });
1230
1391
  }
1231
1392
  var styles5 = StyleSheet5.create({
@@ -1245,14 +1406,17 @@ var styles5 = StyleSheet5.create({
1245
1406
  fontWeight: "bold",
1246
1407
  marginTop: 8
1247
1408
  },
1409
+ // A picker's button on the theme's surface (over a backdrop), the same pill as Home's.
1410
+ pill: {
1411
+ borderRadius: 999
1412
+ },
1248
1413
  pressAway: {
1249
1414
  zIndex: 50
1250
1415
  },
1251
- // A real full-bleed box for FakeLandscapeView to size its turned content against.
1252
- rotatable: {
1416
+ screen: {
1253
1417
  flex: 1
1254
1418
  },
1255
- // SegmentedButtons' own rounding.
1419
+ // SegmentedButtons' own rounding, so a surface behind it is the same pill.
1256
1420
  tabs: {
1257
1421
  borderRadius: 20
1258
1422
  }
@@ -1288,7 +1452,7 @@ import { defaultSoundSettings, hapticReducer, soundReducer } from "@rific/feedba
1288
1452
  import { profilesReducer } from "@tastic/profile";
1289
1453
  var initialSoundSettings = { ...defaultSoundSettings, enabled: !__DEV__ };
1290
1454
  var appSoundReducer = (state = initialSoundSettings, action) => soundReducer(state, action);
1291
- function createShellReducers({ seatColors, appearance = "system" }) {
1455
+ function createShellReducers({ seatColors, appearance = "system", seatTheme }) {
1292
1456
  return {
1293
1457
  // Explicit primary/secondary triad (rather than a single seed expanded via harmony) so the pack's
1294
1458
  // identity is pinned exactly instead of drifting with whatever hue math a harmony offset would
@@ -1297,7 +1461,9 @@ function createShellReducers({ seatColors, appearance = "system" }) {
1297
1461
  // derived, not picked, via getThirdColor: the hue maximally distant from both (see
1298
1462
  // @rific/auto-paper's README "Explicit triad" section). The appearance is the pack's ('system',
1299
1463
  // 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]) } }),
1464
+ // A pack with its own seatTheme starts from what seat 1's default color makes of it instead (dark
1465
+ // only when the pack's appearance is; hooks/useSeatColors.ts re-applies it with the live mode).
1466
+ theme: createThemeReducer(seatTheme ? { appearance, ...seatTheme(seatColors, appearance === "dark") } : { appearance, color: { primary: seatColors[1], secondary: seatColors[2], tertiary: getThirdColor2(seatColors[1], seatColors[2]) } }),
1301
1467
  haptic: hapticReducer,
1302
1468
  sound: appSoundReducer,
1303
1469
  // @rific/core's Keep Awake setting (see Providers.tsx's KeepAwakeBridge), under the namespace its
@@ -1325,7 +1491,7 @@ var errorMiddleware = () => (next) => (action) => {
1325
1491
  if (!hasError(action)) return next(action);
1326
1492
  return action;
1327
1493
  };
1328
- function createPackStore(pack, { keyPrefix, migrate }) {
1494
+ function createPackStore(pack, { keyPrefix, migrate, blacklist = [], serializableIgnoredPaths = [], middleware = [] }) {
1329
1495
  const gameReducers = Object.assign({}, ...pack.games.map((game) => game.reducers));
1330
1496
  const resumeReducers = pack.games.some((game) => game.resumable) ? { lastActiveGame: lastActiveGameSlice_default } : {};
1331
1497
  const rootReducer = combineReducers({ ...createShellReducers(pack), ...resumeReducers, ...pack.reducers, ...gameReducers });
@@ -1343,7 +1509,7 @@ function createPackStore(pack, { keyPrefix, migrate }) {
1343
1509
  // failing to ever resolve at all isn't a failure mode worth a 5s failsafe for.
1344
1510
  timeout: 0,
1345
1511
  // liveplay is per-round state that must never survive a relaunch (see liveplaySlice.ts).
1346
- blacklist: ["liveplay"],
1512
+ blacklist: ["liveplay", ...blacklist],
1347
1513
  migrate
1348
1514
  },
1349
1515
  rootReducer
@@ -1353,10 +1519,11 @@ function createPackStore(pack, { keyPrefix, migrate }) {
1353
1519
  const defaultMiddleware = getDefaultMiddleware({
1354
1520
  immutableCheck: false,
1355
1521
  serializableCheck: {
1356
- ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
1522
+ ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
1523
+ ignoredPaths: [...serializableIgnoredPaths]
1357
1524
  }
1358
1525
  });
1359
- return defaultMiddleware.concat(errorMiddleware);
1526
+ return defaultMiddleware.concat(errorMiddleware, ...middleware);
1360
1527
  },
1361
1528
  reducer: persistedReducer
1362
1529
  });
@@ -1407,7 +1574,7 @@ function forgetProfileAchievements(dropStats) {
1407
1574
 
1408
1575
  // src/gameStats.tsx
1409
1576
  import { createAchievementsSlice, mapUnlocksBySeat, useAchievements } from "@tastic/achievements";
1410
- import { createContext as createContext5, useCallback as useCallback11, useContext as useContext5, useMemo as useMemo6 } from "react";
1577
+ import { createContext as createContext5, useCallback as useCallback11, useContext as useContext5, useMemo as useMemo8 } from "react";
1411
1578
  import { jsx as jsx18 } from "react/jsx-runtime";
1412
1579
  var SEATS = [1, 2];
1413
1580
  function createGameStats({ key, defaultStats, isValidStats, catalog, getProfileStatsView }) {
@@ -1440,7 +1607,7 @@ function createGameStats({ key, defaultStats, isValidStats, catalog, getProfileS
1440
1607
  },
1441
1608
  [recordOutcome]
1442
1609
  );
1443
- const value = useMemo6(() => ({ stats, unlockedAchievements, record, resetAll }), [stats, unlockedAchievements, record, resetAll]);
1610
+ const value = useMemo8(() => ({ stats, unlockedAchievements, record, resetAll }), [stats, unlockedAchievements, record, resetAll]);
1444
1611
  return /* @__PURE__ */ jsx18(Context.Provider, { value, children });
1445
1612
  }
1446
1613
  function useStats() {
@@ -1452,7 +1619,7 @@ function createGameStats({ key, defaultStats, isValidStats, catalog, getProfileS
1452
1619
  }
1453
1620
 
1454
1621
  // src/playerSettings.ts
1455
- import { useCallback as useCallback12, useMemo as useMemo7 } from "react";
1622
+ import { useCallback as useCallback12, useMemo as useMemo9 } from "react";
1456
1623
  import { useDispatch as useDispatch11, useSelector as useSelector9 } from "react-redux";
1457
1624
  var SEATS2 = [1, 2];
1458
1625
  function seatPlayerKey(selection, seat) {
@@ -1496,7 +1663,7 @@ function createPlayerSettings({ id, defaults, distinctControls }) {
1496
1663
  const saved = useSelector9((state) => state[settingsKey]);
1497
1664
  const controls = useSelector9((state) => state[controlsKey]);
1498
1665
  const dispatch = useDispatch11();
1499
- const settings = useMemo7(() => compose(saved, controls, selection, seatTwoIsPerson), [saved, controls, selection, seatTwoIsPerson]);
1666
+ const settings = useMemo9(() => compose(saved, controls, selection, seatTwoIsPerson), [saved, controls, selection, seatTwoIsPerson]);
1500
1667
  const setSettings = useCallback12(
1501
1668
  (next) => {
1502
1669
  const { controlScheme, ...changes } = next;
@@ -1516,6 +1683,7 @@ function createPlayerSettings({ id, defaults, distinctControls }) {
1516
1683
  export {
1517
1684
  DEFAULT_APP_SETTINGS,
1518
1685
  DEFAULT_OPPONENT,
1686
+ GUEST_PLAYER_ID,
1519
1687
  HomeScreen,
1520
1688
  LoadoutRoute,
1521
1689
  PREVIEW_ASPECT,