@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.js CHANGED
@@ -32,6 +32,7 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  DEFAULT_APP_SETTINGS: () => DEFAULT_APP_SETTINGS,
34
34
  DEFAULT_OPPONENT: () => DEFAULT_OPPONENT,
35
+ GUEST_PLAYER_ID: () => GUEST_PLAYER_ID,
35
36
  HomeScreen: () => HomeScreen,
36
37
  LoadoutRoute: () => LoadoutRoute,
37
38
  PREVIEW_ASPECT: () => PREVIEW_ASPECT,
@@ -99,7 +100,8 @@ var import_auto_paper2 = require("@rific/auto-paper");
99
100
  var import_feedback_press2 = require("@rific/feedback-press");
100
101
  var import_toaster2 = require("@rific/toaster");
101
102
  var import_core6 = require("@tastic/core");
102
- var import_hud2 = require("@tastic/hud");
103
+ var import_chrome2 = require("@tastic/hud/chrome");
104
+ var import_profile3 = require("@tastic/profile");
103
105
  var Haptics = __toESM(require("expo-haptics"));
104
106
  var import_expo_router = require("expo-router");
105
107
  var import_expo_sensors = require("expo-sensors");
@@ -148,24 +150,35 @@ function seatTwoFor(saved, choices) {
148
150
  function humanSeatsFor(mode) {
149
151
  return mode === "twoPlayer" ? [1, 2] : [1];
150
152
  }
153
+ var GUEST_PLAYER_ID = "guest";
151
154
 
152
155
  // src/components/GuideHost.tsx
153
156
  var import_jsx_runtime3 = require("react/jsx-runtime");
157
+ var PACK_GUIDE = "__pack__";
158
+ var NO_INPUT = {};
154
159
  var GameGuideContext = (0, import_react2.createContext)(null);
155
160
  function GuideHost({ children }) {
156
161
  const pack = usePack();
157
- const [gameId, setGameId] = (0, import_react2.useState)(pack.games[0].id);
158
- const game = pack.games.find((g) => g.id === gameId) ?? pack.games[0];
162
+ const [subject, setSubject] = (0, import_react2.useState)(() => pack.guide ? PACK_GUIDE : pack.games.find((g) => g.guide)?.id ?? null);
163
+ const packGuide = subject === PACK_GUIDE ? pack.guide : void 0;
164
+ const game = subject === PACK_GUIDE ? void 0 : pack.games.find((g) => g.id === subject);
165
+ const gameGuide = game?.guide;
159
166
  const rotation = (0, import_core.useRotation)();
160
167
  const touch = (0, import_core.useIsTouchPrimaryDevice)();
161
168
  const seatTwo = (0, import_react_redux.useSelector)((state) => state.opponent.seatTwo);
162
- const input = (0, import_react_redux.useSelector)((state) => game.guide.selectInput(state), import_react_redux.shallowEqual);
163
- const seenVersion = (0, import_react_redux.useSelector)((state) => game.guide.selectVersionSeen(state));
169
+ const input = (0, import_react_redux.useSelector)((state) => gameGuide ? gameGuide.selectInput(state) : NO_INPUT, import_react_redux.shallowEqual);
170
+ const seenVersion = (0, import_react_redux.useSelector)((state) => (packGuide ?? gameGuide)?.selectVersionSeen(state) ?? 0);
164
171
  const dispatch = (0, import_react_redux.useDispatch)();
165
- const mode = roundModeFor(seatTwoFor(seatTwo, game.seatTwo));
166
- const steps = (0, import_react2.useMemo)(() => game.guide.getSteps(input, { touch, mode }), [game, input, touch, mode]);
167
- const onSeen = (0, import_react2.useCallback)((version) => dispatch(game.guide.markSeen(version)), [dispatch, game]);
168
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_guide.GuideProvider, { steps, currentVersion: game.guide.version, seenVersion, onSeen, rotation, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GameGuideBridge, { onPick: setGameId, children }) });
172
+ const mode = roundModeFor(seatTwoFor(seatTwo, game?.seatTwo));
173
+ const steps = (0, import_react2.useMemo)(() => packGuide ? packGuide.getSteps({ touch }) : gameGuide ? gameGuide.getSteps(input, { touch, mode }) : [], [packGuide, gameGuide, input, touch, mode]);
174
+ const onSeen = (0, import_react2.useCallback)(
175
+ (version) => {
176
+ const guide = packGuide ?? gameGuide;
177
+ if (guide) dispatch(guide.markSeen(version));
178
+ },
179
+ [dispatch, packGuide, gameGuide]
180
+ );
181
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_guide.GuideProvider, { steps, currentVersion: (packGuide ?? gameGuide)?.version ?? 0, seenVersion, onSeen, rotation, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GameGuideBridge, { onPick: setSubject, children }) });
169
182
  }
170
183
  function GameGuideBridge({ onPick, children }) {
171
184
  const pack = usePack();
@@ -173,6 +186,7 @@ function GameGuideBridge({ onPick, children }) {
173
186
  const { isOpen, open } = (0, import_guide.useGuide)();
174
187
  const pendingNext = (0, import_react2.useRef)(null);
175
188
  const wasOpen = (0, import_react2.useRef)(false);
189
+ const packAutoShown = (0, import_react2.useRef)(false);
176
190
  (0, import_react2.useEffect)(() => {
177
191
  if (wasOpen.current && !isOpen && pendingNext.current) {
178
192
  const next = pendingNext.current;
@@ -183,15 +197,16 @@ function GameGuideBridge({ onPick, children }) {
183
197
  }, [isOpen]);
184
198
  const openFor = (0, import_react2.useCallback)(
185
199
  (gameId) => {
200
+ if (!pack.games.find((g) => g.id === gameId)?.guide) return;
186
201
  onPick(gameId);
187
202
  open();
188
203
  },
189
- [onPick, open]
204
+ [pack, onPick, open]
190
205
  );
191
206
  const openFirstTime = (0, import_react2.useCallback)(
192
207
  (gameId, next) => {
193
- const game = pack.games.find((g) => g.id === gameId);
194
- if (!game || game.guide.selectVersionSeen(store.getState()) >= game.guide.version) {
208
+ const guide = pack.games.find((g) => g.id === gameId)?.guide;
209
+ if (!guide || guide.selectVersionSeen(store.getState()) >= guide.version) {
195
210
  next();
196
211
  return;
197
212
  }
@@ -200,7 +215,18 @@ function GameGuideBridge({ onPick, children }) {
200
215
  },
201
216
  [pack, store, openFor]
202
217
  );
203
- const value = (0, import_react2.useMemo)(() => ({ openFirstTime, openFor }), [openFirstTime, openFor]);
218
+ const openPack = (0, import_react2.useCallback)(() => {
219
+ if (!pack.guide) return;
220
+ onPick(PACK_GUIDE);
221
+ open();
222
+ }, [pack, onPick, open]);
223
+ const autoShowPack = (0, import_react2.useCallback)(() => {
224
+ const guide = pack.guide;
225
+ if (!guide || packAutoShown.current || guide.selectVersionSeen(store.getState()) >= guide.version) return;
226
+ packAutoShown.current = true;
227
+ openPack();
228
+ }, [pack, store, openPack]);
229
+ const value = (0, import_react2.useMemo)(() => ({ openFirstTime, openFor, openPack, autoShowPack }), [openFirstTime, openFor, openPack, autoShowPack]);
204
230
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GameGuideContext.Provider, { value, children });
205
231
  }
206
232
  function useGameGuide() {
@@ -220,15 +246,20 @@ var RNPaper = __toESM(require("react-native-paper"));
220
246
  var import_react_redux2 = require("react-redux");
221
247
  var import_react4 = require("redux-persist/integration/react");
222
248
  var import_jsx_runtime4 = require("react/jsx-runtime");
249
+ function useShellFeedbackSounds() {
250
+ const selection = (0, import_audio.useAudioPool)(require("./select-MLBNNA4N.wav"));
251
+ const notification = (0, import_audio.useAudioPool)(require("./notification-CGGFPYK7.wav"));
252
+ return { selection, notification };
253
+ }
223
254
  var FeedbackBridge = ({ children }) => {
224
255
  const haptic = (0, import_react_redux2.useSelector)((state) => state.haptic);
225
256
  const sound = (0, import_react_redux2.useSelector)((state) => state.sound);
226
257
  const dispatch = (0, import_react_redux2.useDispatch)();
227
258
  const onChange = (0, import_react3.useCallback)((next) => dispatch(import_feedback_press.hapticActions.initialize(next)), [dispatch]);
228
259
  const onSoundChange = (0, import_react3.useCallback)((next) => dispatch(import_feedback_press.soundActions.initialize(next)), [dispatch]);
229
- const playSelection = (0, import_audio.useAudioPool)(require("./select-MLBNNA4N.wav"));
230
- const playNotification = (0, import_audio.useAudioPool)(require("./notification-CGGFPYK7.wav"));
231
- const bridgeProps = (0, import_feedback_press.useFeedbackBridgeProps)({ initialValue: haptic, onChange, soundInitialValue: sound, onSoundChange, sound: { selection: playSelection, notification: playNotification } });
260
+ const { useFeedbackSounds = useShellFeedbackSounds } = usePack();
261
+ const sounds = useFeedbackSounds();
262
+ const bridgeProps = (0, import_feedback_press.useFeedbackBridgeProps)({ initialValue: haptic, onChange, soundInitialValue: sound, onSoundChange, sound: sounds });
232
263
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.FeedbackPressProvider, { ...bridgeProps, paper: RNPaper, children });
233
264
  };
234
265
  var EdgeGuardBridge = ({ children }) => {
@@ -271,12 +302,12 @@ function Theme({ children }) {
271
302
  // src/components/UpdateDialog.tsx
272
303
  var import_toaster = require("@rific/toaster");
273
304
  var import_core3 = require("@tastic/core");
274
- var import_hud = require("@tastic/hud");
305
+ var import_chrome = require("@tastic/hud/chrome");
275
306
  var import_jsx_runtime6 = require("react/jsx-runtime");
276
307
  function UpdateDialog() {
277
308
  const onError = (0, import_toaster.useUpdateErrorToast)();
278
309
  const rotation = (0, import_core3.useRotation)();
279
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_hud.UpdateDialog, { onError, rotation });
310
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_chrome.UpdateDialog, { onError, rotation });
280
311
  }
281
312
 
282
313
  // src/hooks/useAppSettings.ts
@@ -446,18 +477,39 @@ function useProfiles() {
446
477
  // src/RootLayout.tsx
447
478
  var import_jsx_runtime9 = require("react/jsx-runtime");
448
479
  var PLAY_OPTIONS = { statusBarHidden: true };
480
+ var KEEP_STATUS_BAR_OPTIONS = {};
481
+ var TRANSPARENT_CONTENT = { backgroundColor: "transparent" };
482
+ var noScreenOptions = () => ({});
449
483
  function AppStack() {
484
+ const pack = usePack();
485
+ const { Backdrop } = pack;
450
486
  const { dark } = (0, import_auto_paper2.useAutoPaperTheme)();
451
- const rootBackground = (0, import_core6.useThemedRootBackground)(dark, false);
487
+ const rootBackground = (0, import_core6.useThemedRootBackground)(Backdrop ? true : dark, false);
452
488
  const statusBar = (0, import_core6.useRotationAwareStatusBar)();
453
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_expo_router.Stack, { screenOptions: { ...rootBackground, ...statusBar }, children: [
489
+ const packOptions = (pack.useScreenOptions ?? noScreenOptions)();
490
+ const backdropOptions = Backdrop ? { contentStyle: TRANSPARENT_CONTENT, animation: import_react_native2.Platform.select({ android: "fade" }) } : void 0;
491
+ const playOptions = (0, import_react9.useCallback)(
492
+ ({ route }) => {
493
+ const id = route.params?.game;
494
+ return pack.games.some((g) => g.id === id && g.keepStatusBar) ? KEEP_STATUS_BAR_OPTIONS : PLAY_OPTIONS;
495
+ },
496
+ [pack.games]
497
+ );
498
+ const navigationTheme = (0, import_react9.useMemo)(() => Backdrop ? { ...import_expo_router.DarkTheme, colors: { ...import_expo_router.DarkTheme.colors, background: "transparent" } } : null, [Backdrop]);
499
+ const stack = /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_expo_router.Stack, { screenOptions: { ...rootBackground, ...statusBar, ...backdropOptions, ...packOptions }, children: [
454
500
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_expo_router.Stack.Screen, { name: "index" }),
455
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_expo_router.Stack.Screen, { name: "play/[game]", options: PLAY_OPTIONS })
501
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_expo_router.Stack.Screen, { name: "play/[game]", options: playOptions })
502
+ ] });
503
+ if (!Backdrop || !navigationTheme) return stack;
504
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_react_native2.View, { style: styles.root, children: [
505
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Backdrop, {}),
506
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_expo_router.ThemeProvider, { value: navigationTheme, children: stack })
456
507
  ] });
457
508
  }
458
509
  function OrientationBridge({ children }) {
459
510
  const { settings, setSettings } = useAppSettings();
460
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_core6.OrientationProvider, { deviceMotion: import_expo_sensors.DeviceMotion, freezeWhileLocked: true, lockInitialValue: { locked: settings.lockOrientation, rotation: settings.lockedRotation }, onLockChange: (next) => setSettings({ lockOrientation: next.locked, lockedRotation: next.rotation }), children });
511
+ const { portraitWhileKeyboard } = usePack();
512
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_core6.OrientationProvider, { deviceMotion: import_expo_sensors.DeviceMotion, freezeWhileLocked: true, lockInitialValue: { locked: settings.lockOrientation, rotation: settings.lockedRotation }, onLockChange: (next) => setSettings({ lockOrientation: next.locked, lockedRotation: next.rotation }), portraitWhileKeyboard, children });
461
513
  }
462
514
  function GatedApp({ children }) {
463
515
  const { loaded } = useProfiles();
@@ -465,24 +517,38 @@ function GatedApp({ children }) {
465
517
  }
466
518
  function HapticGatedToastProvider({ children }) {
467
519
  const { settings } = (0, import_feedback_press2.useHapticSettings)();
468
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_toaster2.ToastProvider, { haptics: settings.vibrate ? Haptics : void 0, paper: RNPaper2, children });
520
+ const { Toaster: PackToasterComponent } = usePack();
521
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_toaster2.ToastProvider, { haptics: settings.vibrate ? Haptics : void 0, paper: PackToasterComponent ? void 0 : RNPaper2, children });
469
522
  }
470
523
  function RotatingToaster() {
471
524
  const renderFrame = (0, import_react9.useCallback)(
472
- (toasts) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_hud2.RotatedFrame, { rotatedSafeArea: true, passThrough: true, children: toasts }),
525
+ (toasts) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_chrome2.RotatedFrame, { rotatedSafeArea: true, passThrough: true, children: toasts }),
473
526
  []
474
527
  );
475
528
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_toaster2.Toaster, { historyButton: null, clearButton: null, limit: 3, renderFrame });
476
529
  }
530
+ function PackToaster({ Toaster: PackToasterComponent }) {
531
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native2.View, { style: import_react_native2.StyleSheet.absoluteFill, pointerEvents: "box-none", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_chrome2.RotatedFrame, { rotatedSafeArea: true, passThrough: true, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PackToasterComponent, {}) }) });
532
+ }
477
533
  function GameProviders({ games, children }) {
478
- return games.reduceRight((inner, game) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(game.Provider, { children: inner }, game.id), children);
534
+ return games.reduceRight((inner, game) => game.Provider ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(game.Provider, { children: inner }, game.id) : inner, children);
535
+ }
536
+ function PreviewColors({ previewColor, children }) {
537
+ const { dark } = (0, import_auto_paper2.useAutoPaperTheme)();
538
+ const preview = (0, import_react9.useCallback)((hex) => previewColor(hex, dark), [previewColor, dark]);
539
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_profile3.ProfileColorProvider, { preview, children });
540
+ }
541
+ function PackRoot({ pack, children }) {
542
+ const { Provider, previewColor } = pack;
543
+ const inner = Provider ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Provider, { children }) : children;
544
+ return previewColor ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PreviewColors, { previewColor, children: inner }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_jsx_runtime9.Fragment, { children: inner });
479
545
  }
480
546
  function RootLayout({ pack, store, persistor }) {
481
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_gesture_handler.GestureHandlerRootView, { style: styles.root, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_safe_area_context.SafeAreaProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PackProvider, { pack, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Fonts, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Providers, { store, persistor, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Theme, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ProfilesProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(LastActiveGameProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(GameProviders, { games: pack.games, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(GatedApp, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(OrientationBridge, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(HapticGatedToastProvider, { children: [
547
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_gesture_handler.GestureHandlerRootView, { style: styles.root, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_safe_area_context.SafeAreaProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PackProvider, { pack, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Fonts, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Providers, { store, persistor, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Theme, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ProfilesProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(LastActiveGameProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(GameProviders, { games: pack.games, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PackRoot, { pack, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(GatedApp, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(OrientationBridge, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(HapticGatedToastProvider, { children: [
482
548
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(GuideHost, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(AppStack, {}) }),
483
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(RotatingToaster, {}),
549
+ pack.Toaster ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PackToaster, { Toaster: pack.Toaster }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(RotatingToaster, {}),
484
550
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(UpdateDialog, {})
485
- ] }) }) }) }) }) }) }) }) }) }) }) });
551
+ ] }) }) }) }) }) }) }) }) }) }) }) }) });
486
552
  }
487
553
  var styles = import_react_native2.StyleSheet.create({
488
554
  root: { flex: 1 }
@@ -491,11 +557,19 @@ var styles = import_react_native2.StyleSheet.create({
491
557
  // src/screens/GameRoute.tsx
492
558
  var import_expo_router5 = require("expo-router");
493
559
  var import_react14 = require("react");
560
+ var import_react_native5 = require("react-native");
561
+
562
+ // src/routes.ts
563
+ var playHref = (game) => ({ pathname: "/play/[game]", params: { game } });
564
+ var loadoutHref = (game) => ({ pathname: "/loadout/[game]", params: { game } });
565
+ var STATS_HREF = "/stats";
566
+ var PROFILES_HREF = "/profiles";
494
567
 
495
568
  // src/screens/LoadoutScreen.tsx
496
569
  var import_auto_paper4 = require("@rific/auto-paper");
497
570
  var import_core10 = require("@tastic/core");
498
- var import_hud5 = require("@tastic/hud");
571
+ var import_chrome4 = require("@tastic/hud/chrome");
572
+ var import_lobby2 = require("@tastic/hud/lobby");
499
573
  var import_split_screen = require("@tastic/split-screen");
500
574
  var import_expo_router4 = require("expo-router");
501
575
  var import_react13 = require("react");
@@ -505,8 +579,8 @@ var import_react_native_safe_area_context2 = require("react-native-safe-area-con
505
579
  var import_react_redux9 = require("react-redux");
506
580
 
507
581
  // src/components/SeatTwoPanel.tsx
508
- var import_hud3 = require("@tastic/hud");
509
- var import_profile3 = require("@tastic/profile");
582
+ var import_lobby = require("@tastic/hud/lobby");
583
+ var import_profile4 = require("@tastic/profile");
510
584
  var import_react10 = require("react");
511
585
  var import_react_native3 = require("react-native");
512
586
  var import_jsx_runtime10 = require("react/jsx-runtime");
@@ -549,14 +623,14 @@ function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDi
549
623
  };
550
624
  const colorSection = seatTwo === "nobody" ? void 0 : { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, caption: selectedProfile ? colorCaption : void 0 };
551
625
  const pickerId = getSeatTwoPopoverId(idPrefix);
552
- const size = (0, import_profile3.getOptionPickerContentSize)(options.length, colorSection);
553
- const align = (0, import_hud3.useZoneClampedAlign)(host.openId === pickerId, size.width, size.height);
626
+ const size = (0, import_profile4.getOptionPickerContentSize)(options.length, colorSection);
627
+ const align = (0, import_lobby.useZoneClampedAlign)(host.openId === pickerId, size.width, size.height);
554
628
  const ownPopoverOpen = host.openId?.startsWith(`${idPrefix}-`) ?? false;
555
629
  const pickerRowOpen = secondPickerId !== void 0 && host.openId === secondPickerId;
556
630
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native3.View, { style: [styles2.panel, ownPopoverOpen && styles2.panelOpen], children: [
557
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_profile3.OptionPicker, { id: pickerId, host, options, value, onChange: handleChange, color, dark, icon, colorSection, alignOverride: align }),
631
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_profile4.OptionPicker, { id: pickerId, host, options, value, onChange: handleChange, color, dark, icon, colorSection, alignOverride: align }),
558
632
  isPerson && secondPicker && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native3.View, { style: [styles2.pickerRow, pickerRowOpen && styles2.pickerRowOpen], children: secondPicker }),
559
- isPerson && showReadyButton && onToggleReady && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_hud3.ReadyButton, { color, ready: ready ?? false, onToggleReady })
633
+ isPerson && showReadyButton && onToggleReady && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lobby.ReadyButton, { color, ready: ready ?? false, onToggleReady })
560
634
  ] });
561
635
  }
562
636
  var styles2 = import_react_native3.StyleSheet.create({
@@ -582,13 +656,14 @@ var styles2 = import_react_native3.StyleSheet.create({
582
656
 
583
657
  // src/components/SettingsDialog.tsx
584
658
  var import_toaster3 = require("@rific/toaster");
585
- var import_hud4 = require("@tastic/hud");
659
+ var import_chrome3 = require("@tastic/hud/chrome");
586
660
  var import_jsx_runtime11 = require("react/jsx-runtime");
587
661
  function SettingsDialog({ visible, onDismiss, rotation = 0, onShowHowToPlay, children }) {
588
662
  const onUpdateError = (0, import_toaster3.useUpdateErrorToast)();
589
663
  const { settings, setSettings } = useAppSettings();
590
- const { otaVersion, SettingsSections } = usePack();
591
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_hud4.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: [
664
+ const { otaVersion, SettingsSections, edgeGuard = true } = usePack();
665
+ const edgeGuardProps = edgeGuard ? { deferBottomEdgeGestures: settings.deferBottomEdgeGestures, onDeferBottomEdgeGestures: (value) => setSettings({ deferBottomEdgeGestures: value }) } : {};
666
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_chrome3.BaseSettingsDialog, { visible, onDismiss, rotation, version: otaVersion, lockOrientation: settings.lockOrientation, onLockOrientationChange: (value) => setSettings({ lockOrientation: value }), ...edgeGuardProps, onUpdateError, onShowHowToPlay, children: [
592
667
  children,
593
668
  SettingsSections && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SettingsSections, {})
594
669
  ] });
@@ -617,29 +692,33 @@ function useOpponent() {
617
692
 
618
693
  // src/hooks/useSeatColors.ts
619
694
  var import_auto_paper3 = require("@rific/auto-paper");
620
- var import_profile5 = require("@tastic/profile");
695
+ var import_profile6 = require("@tastic/profile");
621
696
  var import_expo_router2 = require("expo-router");
622
697
  var import_react12 = require("react");
623
698
  var import_react_redux8 = require("react-redux");
624
699
 
625
700
  // src/redux/seatColorsSlice.ts
626
- var import_profile4 = require("@tastic/profile");
701
+ var import_profile5 = require("@tastic/profile");
627
702
  function defaultSeatColorsState(seatColors) {
628
703
  return { lastGuestColor: { 1: seatColors[1], 2: seatColors[2] }, lastCpuColor: seatColors[2], profileColors: {} };
629
704
  }
630
705
  function createSeatColorsReducer(seatColors) {
631
- return (0, import_profile4.createSeatColorsSlice)("pack", defaultSeatColorsState(seatColors)).reducer;
706
+ return (0, import_profile5.createSeatColorsSlice)("pack", defaultSeatColorsState(seatColors)).reducer;
632
707
  }
633
- var seatColorsActions = (0, import_profile4.createSeatColorsSlice)("pack", defaultSeatColorsState({ 1: "", 2: "" })).actions;
708
+ var seatColorsActions = (0, import_profile5.createSeatColorsSlice)("pack", defaultSeatColorsState({ 1: "", 2: "" })).actions;
634
709
 
635
710
  // src/hooks/useSeatColors.ts
711
+ function sameTheme(current, next) {
712
+ const a = current.color;
713
+ const b = next.color;
714
+ 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();
715
+ return sameColor && (next.harmony === void 0 || next.harmony === current.harmony);
716
+ }
636
717
  function useSeatColors(p2IsHuman) {
637
- const { colors: themeColors } = (0, import_auto_paper3.useAutoPaperTheme)();
638
- const { set: setThemeColor } = (0, import_auto_paper3.useThemeSettings)();
639
- const p1Color = themeColors.primary;
640
- const p2Color = themeColors.secondary;
718
+ const { dark, colors: themeColors } = (0, import_auto_paper3.useAutoPaperTheme)();
719
+ const { settings: themeSettings, set: setThemeColor } = (0, import_auto_paper3.useThemeSettings)();
641
720
  const { profiles, lastSelected } = useProfiles();
642
- const { seatColors } = usePack();
721
+ const { seatColors, seatTheme } = usePack();
643
722
  const seatDefaultColors = [seatColors[1], seatColors[2]];
644
723
  const dispatch = (0, import_react_redux8.useDispatch)();
645
724
  const guestColors = (0, import_react_redux8.useSelector)((state) => state.seatColors.lastGuestColor);
@@ -647,11 +726,21 @@ function useSeatColors(p2IsHuman) {
647
726
  const profileColors = (0, import_react_redux8.useSelector)((state) => state.seatColors.profileColors);
648
727
  const p1Profile = profiles.find((p) => p.id === lastSelected[1]) ?? null;
649
728
  const p2Profile = p2IsHuman ? profiles.find((p) => p.id === lastSelected[2]) ?? null : null;
729
+ const rememberedColors = (p1Prof, p2Prof, picks, guest, cpu) => {
730
+ const p1 = p1Prof ? (0, import_profile6.profileSeatColor)(p1Prof, picks) : guest[1];
731
+ const p2 = (0, import_profile6.avoidSeatColorClash)(p1, p2Prof ? (0, import_profile6.profileSeatColor)(p2Prof, picks) : p2IsHuman ? guest[2] : cpu, seatDefaultColors);
732
+ return { p1, p2 };
733
+ };
734
+ const remembered = rememberedColors(p1Profile, p2Profile, profileColors, guestColors, cpuColor);
735
+ const p1Color = seatTheme ? remembered.p1 : themeColors.primary;
736
+ const p2Color = seatTheme ? remembered.p2 : themeColors.secondary;
737
+ const themeFor = (0, import_react12.useCallback)((p1, p2) => seatTheme ? seatTheme({ 1: p1, 2: p2 }, dark) : { color: { primary: p1, secondary: p2, tertiary: (0, import_auto_paper3.getThirdColor)(p1, p2) } }, [seatTheme, dark]);
650
738
  const p1ProfileRef = (0, import_react12.useRef)(p1Profile);
651
739
  const p2ProfileRef = (0, import_react12.useRef)(p2Profile);
652
740
  const guestColorsRef = (0, import_react12.useRef)(guestColors);
653
741
  const cpuColorRef = (0, import_react12.useRef)(cpuColor);
654
742
  const profileColorsRef = (0, import_react12.useRef)(profileColors);
743
+ const themeForRef = (0, import_react12.useRef)(themeFor);
655
744
  (0, import_react12.useEffect)(() => {
656
745
  p1ProfileRef.current = p1Profile;
657
746
  }, [p1Profile]);
@@ -668,21 +757,17 @@ function useSeatColors(p2IsHuman) {
668
757
  profileColorsRef.current = profileColors;
669
758
  }, [profileColors]);
670
759
  (0, import_react12.useEffect)(() => {
671
- const nextP1 = p1Profile ? (0, import_profile5.profileSeatColor)(p1Profile, profileColors) : guestColors[1];
672
- const nextP2 = (0, import_profile5.avoidSeatColorClash)(nextP1, p2Profile ? (0, import_profile5.profileSeatColor)(p2Profile, profileColors) : p2IsHuman ? guestColors[2] : cpuColor, seatDefaultColors);
673
- if (nextP1.toLowerCase() === p1Color.toLowerCase() && nextP2.toLowerCase() === p2Color.toLowerCase()) return;
674
- setThemeColor({ color: { primary: nextP1, secondary: nextP2, tertiary: (0, import_auto_paper3.getThirdColor)(nextP1, nextP2) } });
675
- }, [lastSelected[1], lastSelected[2], p2IsHuman, guestColors, cpuColor, profileColors, profiles.length]);
760
+ themeForRef.current = themeFor;
761
+ }, [themeFor]);
762
+ (0, import_react12.useEffect)(() => {
763
+ const next = themeFor(remembered.p1, remembered.p2);
764
+ if (sameTheme(themeSettings, next)) return;
765
+ setThemeColor(next);
766
+ }, [lastSelected[1], lastSelected[2], p2IsHuman, guestColors, cpuColor, profileColors, profiles.length, dark]);
676
767
  (0, import_expo_router2.useFocusEffect)(
677
768
  (0, import_react12.useCallback)(() => {
678
- const guest = guestColorsRef.current;
679
- const cpu = cpuColorRef.current;
680
- const p1Prof = p1ProfileRef.current;
681
- const p2Prof = p2ProfileRef.current;
682
- const picks = profileColorsRef.current;
683
- const nextP1 = p1Prof ? (0, import_profile5.profileSeatColor)(p1Prof, picks) : guest[1];
684
- const nextP2 = (0, import_profile5.avoidSeatColorClash)(nextP1, p2Prof ? (0, import_profile5.profileSeatColor)(p2Prof, picks) : p2IsHuman ? guest[2] : cpu, seatDefaultColors);
685
- setThemeColor({ color: { primary: nextP1, secondary: nextP2, tertiary: (0, import_auto_paper3.getThirdColor)(nextP1, nextP2) } });
769
+ const next = rememberedColors(p1ProfileRef.current, p2ProfileRef.current, profileColorsRef.current, guestColorsRef.current, cpuColorRef.current);
770
+ setThemeColor(themeForRef.current(next.p1, next.p2));
686
771
  }, [lastSelected[1], lastSelected[2], p2IsHuman, setThemeColor])
687
772
  );
688
773
  const persistSeatColor = (0, import_react12.useCallback)(
@@ -704,21 +789,21 @@ function useSeatColors(p2IsHuman) {
704
789
  (hex) => {
705
790
  const swapped = hex.toLowerCase() === p2Color.toLowerCase();
706
791
  const nextP2 = swapped ? p1Color : p2Color;
707
- setThemeColor({ color: { primary: hex, secondary: nextP2, tertiary: (0, import_auto_paper3.getThirdColor)(hex, nextP2) } });
792
+ setThemeColor(themeFor(hex, nextP2));
708
793
  persistSeatColor(1, hex);
709
794
  if (swapped) persistSeatColor(2, nextP2);
710
795
  },
711
- [setThemeColor, p1Color, p2Color, persistSeatColor]
796
+ [setThemeColor, themeFor, p1Color, p2Color, persistSeatColor]
712
797
  );
713
798
  const handleP2ColorChange = (0, import_react12.useCallback)(
714
799
  (hex) => {
715
800
  const swapped = hex.toLowerCase() === p1Color.toLowerCase();
716
801
  const nextP1 = swapped ? p2Color : p1Color;
717
- setThemeColor({ color: { primary: nextP1, secondary: hex, tertiary: (0, import_auto_paper3.getThirdColor)(nextP1, hex) } });
802
+ setThemeColor(themeFor(nextP1, hex));
718
803
  persistSeatColor(2, hex);
719
804
  if (swapped) persistSeatColor(1, nextP1);
720
805
  },
721
- [setThemeColor, p1Color, p2Color, persistSeatColor]
806
+ [setThemeColor, themeFor, p1Color, p2Color, persistSeatColor]
722
807
  );
723
808
  return { p1Color, p2Color, handleP1ColorChange, handleP2ColorChange };
724
809
  }
@@ -754,6 +839,7 @@ function LoadoutScreen({ game }) {
754
839
  const fixedSeats = p2IsHuman && (!!game.twoPlayerSeats?.faceToFaceOnWeb || import_react_native4.Platform.OS !== "web");
755
840
  const fixedUpsideDown = !!game.twoPlayerSeats?.followUpsideDown && orientationMode === "faceToFace" && upsideDown;
756
841
  const { panelLayout, panelFadeStyle } = (0, import_split_screen.useDualZoneLayout)(fixedSeats ? "faceToFace" : orientationMode, fixedSeats ? true : p1OnRight, fixedSeats || p1OnRightResolved, fixedSeats ? fixedUpsideDown : upsideDown, PANEL_SWAP_FADE_MS);
842
+ const playersRowFade = panelFadeStyle;
757
843
  const [settingsOpen, setSettingsOpen] = (0, import_react13.useState)(false);
758
844
  const rotation = (0, import_core10.getViewRotation)(panelLayout.orientationMode, panelLayout.p1OnRight, panelLayout.upsideDown);
759
845
  const insets = (0, import_core10.rotateInsets)((0, import_react_native_safe_area_context2.useSafeAreaInsets)(), rotation);
@@ -782,7 +868,7 @@ function LoadoutScreen({ game }) {
782
868
  (0, import_react13.useEffect)(() => {
783
869
  if (!humanSeats.every((s) => ready[s])) return;
784
870
  dispatch(liveplayActions.setActiveRound({ game: game.id, mode, settings: roundSettingsRef.current }));
785
- import_expo_router4.router.push({ pathname: "/play/[game]", params: { game: game.id } });
871
+ import_expo_router4.router.push(playHref(game.id));
786
872
  }, [ready, humanSeats, game.id, mode, dispatch]);
787
873
  const bg = dark ? "#000000" : "#FFFFFF";
788
874
  const fg = dark ? "#FFFFFF" : "#000000";
@@ -791,8 +877,8 @@ function LoadoutScreen({ game }) {
791
877
  const showMetaInSharedBand = (0, import_split_screen.needsSharedNeutralZone)(panelLayout.orientationMode, humanSeats.length);
792
878
  const p2ZoneStyle = p2IsHuman ? isPortrait ? styles3.p2ZoneTop : panelLayout.p1OnRight ? styles3.p2ZoneLeft : styles3.p2ZoneRight : null;
793
879
  const showReadyButton = p2IsHuman;
794
- const controlsHost = (0, import_hud5.usePopoverHost)();
795
- const p2PanelHost = (0, import_hud5.usePopoverHost)();
880
+ const controlsHost = (0, import_chrome4.usePopoverHost)();
881
+ const p2PanelHost = (0, import_chrome4.usePopoverHost)();
796
882
  const p2Host = p2IsHuman ? p2PanelHost : controlsHost;
797
883
  const anyPlayerPickerOpen = !!(controlsHost.openId?.startsWith("p1-") || controlsHost.openId?.startsWith("p2-"));
798
884
  const optionsRow = loadout.options({
@@ -807,17 +893,17 @@ function LoadoutScreen({ game }) {
807
893
  onReset: showMetaInSharedBand ? void 0 : loadout.reset
808
894
  });
809
895
  const optionsPopoverOpen = controlsHost.openId !== null && loadout.optionIds.includes(controlsHost.openId);
810
- const sharedControls = showMetaInSharedBand ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_hud5.SharedActionBand, { onBack: import_core8.safeBack, onSettings: () => setSettingsOpen(true), onRandomize: loadout.randomize, onReset: loadout.reset, fg, popoverOpen: optionsPopoverOpen, children: optionsRow }) : optionsRow;
896
+ const sharedControls = showMetaInSharedBand ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lobby2.SharedActionBand, { onBack: import_core8.safeBack, onSettings: () => setSettingsOpen(true), onRandomize: loadout.randomize, onReset: loadout.reset, fg, popoverOpen: optionsPopoverOpen, children: optionsRow }) : optionsRow;
811
897
  const controlLook = { accentColor: themeColors.tertiary, mutedColor: fgMuted, dark, rotation };
812
898
  const p1Control = loadout.seatControl(1, controlsHost, { ...controlLook, seatColor: p1Color });
813
- const p1Panel = /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_hud5.PlayerSetupPanel, { idPrefix: "p1", host: controlsHost, color: p1Color, onColorChange: handleP1ColorChange, swatches: import_auto_paper4.defaultColors, takenColor: p2Exists ? p2Color : void 0, allowSwapTaken: mode === "vsCpu", colorCaption: pack.colorCaption, isHuman: true, ready: ready[1], onToggleReady: () => setReady((r) => ({ ...r, 1: !r[1] })), dark, showReadyButton, secondPicker: p1Control, secondPickerId: p1Control ? "p1-control" : void 0, profiles, selectedProfileId: lastSelected[1], takenProfileId: p2IsHuman ? lastSelected[2] : null, guestLabel: "P1", onProfileSelect: handleP1ProfileSelect, onManageProfiles: () => import_expo_router4.router.push("/profiles") });
899
+ const p1Panel = /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lobby2.PlayerSetupPanel, { idPrefix: "p1", host: controlsHost, color: p1Color, onColorChange: handleP1ColorChange, swatches: import_auto_paper4.defaultColors, takenColor: p2Exists ? p2Color : void 0, allowSwapTaken: mode === "vsCpu", colorCaption: pack.colorCaption, isHuman: true, ready: ready[1], onToggleReady: () => setReady((r) => ({ ...r, 1: !r[1] })), dark, showReadyButton, secondPicker: p1Control, secondPickerId: p1Control ? "p1-control" : void 0, profiles, selectedProfileId: lastSelected[1], takenProfileId: p2IsHuman ? lastSelected[2] : null, guestLabel: "P1", onProfileSelect: handleP1ProfileSelect, onManageProfiles: () => import_expo_router4.router.push(PROFILES_HREF) });
814
900
  const p2Control = p2IsHuman ? loadout.seatControl(2, p2Host, { ...controlLook, seatColor: p2Color }) : void 0;
815
901
  const p2Panel = game.seatTwo && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SeatTwoPanel, { idPrefix: "p2", host: p2Host, choices: game.seatTwo, seatTwo, onSeatTwoChange: changeSeatTwo, cpuDifficulty: loadout.cpuDifficulty, profiles, selectedProfileId: lastSelected[2], onProfileSelect: handleP2ProfileSelect, color: p2Color, onColorChange: handleP2ColorChange, swatches: import_auto_paper4.defaultColors, takenColor: p1Color, allowSwapTaken: mode === "vsCpu", colorCaption: pack.colorCaption, dark, secondPicker: p2Control, secondPickerId: p2Control ? "p2-control" : void 0, ready: ready[2], onToggleReady: () => setReady((r) => ({ ...r, 2: !r[2] })), showReadyButton });
816
902
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native4.View, { style: [styles3.container, { backgroundColor: bg }], children: [
817
903
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_core10.FakeLandscapeView, { orientationMode: panelLayout.orientationMode, p1OnRight: panelLayout.p1OnRight, upsideDown: panelLayout.upsideDown, style: styles3.rotatable, children: [
818
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_hud5.PressAwayOverlay, { active: controlsHost.openId !== null, onPress: controlsHost.close }),
819
- p2ZoneStyle && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_hud5.PressAwayOverlay, { active: controlsHost.openId !== null || p2Host.openId !== null, onPress: p2Host.close, style: p2ZoneStyle }),
820
- !showMetaInSharedBand && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_hud5.CornerActionButtons, { onBack: import_core8.safeBack, onSettings: () => setSettingsOpen(true), fg, insets }),
904
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_chrome4.PressAwayOverlay, { active: controlsHost.openId !== null, onPress: controlsHost.close }),
905
+ p2ZoneStyle && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_chrome4.PressAwayOverlay, { active: controlsHost.openId !== null || p2Host.openId !== null, onPress: p2Host.close, style: p2ZoneStyle }),
906
+ !showMetaInSharedBand && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_chrome4.CornerActionButtons, { onBack: import_core8.safeBack, onSettings: () => setSettingsOpen(true), fg, insets }),
821
907
  p2IsHuman ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
822
908
  import_split_screen.DualZoneLayout,
823
909
  {
@@ -832,11 +918,11 @@ function LoadoutScreen({ game }) {
832
918
  }
833
919
  ) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native4.View, { style: styles3.stackedZone, children: [
834
920
  sharedControls,
835
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native_reanimated2.default.View, { style: [styles3.playersRow, anyPlayerPickerOpen && styles3.playersRowOpen, panelFadeStyle], children: [
921
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native_reanimated2.default.View, { style: [styles3.playersRow, anyPlayerPickerOpen && styles3.playersRowOpen, playersRowFade], children: [
836
922
  !isPortrait && panelLayout.p1OnRight ? p2Panel : p1Panel,
837
923
  !isPortrait && panelLayout.p1OnRight ? p1Panel : p2Panel
838
924
  ] }),
839
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_hud5.ReadyButton, { color: p1Color, ready: ready[1], onToggleReady: () => setReady((r) => ({ ...r, 1: !r[1] })), style: anyPlayerPickerOpen && styles3.readyButtonHidden })
925
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lobby2.ReadyButton, { color: p1Color, ready: ready[1], onToggleReady: () => setReady((r) => ({ ...r, 1: !r[1] })), style: anyPlayerPickerOpen && styles3.readyButtonHidden })
840
926
  ] })
841
927
  ] }),
842
928
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SettingsDialog, { visible: settingsOpen, onDismiss: () => setSettingsOpen(false), rotation, onShowHowToPlay: () => openFor(game.id) })
@@ -902,7 +988,7 @@ function LoadoutRoute() {
902
988
  const { game: id } = (0, import_expo_router5.useLocalSearchParams)();
903
989
  const game = usePackGame(id);
904
990
  if (!game) return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_expo_router5.Redirect, { href: "/" });
905
- if (!hasLoadout(game)) return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_expo_router5.Redirect, { href: { pathname: "/play/[game]", params: { game: game.id } } });
991
+ if (!hasLoadout(game)) return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_expo_router5.Redirect, { href: playHref(game.id) });
906
992
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(LoadoutScreen, { game }, game.id);
907
993
  }
908
994
  function hasLoadout(game) {
@@ -921,7 +1007,9 @@ function useResumePoint(gameId) {
921
1007
  (0, import_react14.useEffect)(() => {
922
1008
  if (!gameId) return;
923
1009
  setLastActiveGame(gameId);
924
- return clearLastActiveGame;
1010
+ return () => {
1011
+ if (import_react_native5.AppState.currentState === "active") clearLastActiveGame();
1012
+ };
925
1013
  }, [gameId, setLastActiveGame, clearLastActiveGame]);
926
1014
  }
927
1015
 
@@ -929,11 +1017,11 @@ function useResumePoint(gameId) {
929
1017
  var import_auto_paper5 = require("@rific/auto-paper");
930
1018
  var import_feedback_press3 = require("@rific/feedback-press");
931
1019
  var import_core12 = require("@tastic/core");
932
- var import_hud6 = require("@tastic/hud");
933
- var import_profile6 = require("@tastic/profile");
1020
+ var import_chrome5 = require("@tastic/hud/chrome");
1021
+ var import_profile7 = require("@tastic/profile");
934
1022
  var import_expo_router6 = require("expo-router");
935
1023
  var import_react16 = require("react");
936
- var import_react_native5 = require("react-native");
1024
+ var import_react_native6 = require("react-native");
937
1025
  var import_react_native_paper = require("react-native-paper");
938
1026
 
939
1027
  // src/hooks/useFrozenWhileHidden.ts
@@ -1018,11 +1106,15 @@ var import_jsx_runtime14 = require("react/jsx-runtime");
1018
1106
  function HomeScreen() {
1019
1107
  const pack = usePack();
1020
1108
  const { consumeResumeGame } = useLastActiveGame();
1109
+ const { autoShowPack } = useGameGuide();
1021
1110
  const [resumeGame] = (0, import_react16.useState)(() => {
1022
1111
  const id = consumeResumeGame();
1023
1112
  return pack.games.find((g) => g.id === id && g.resumable) ?? null;
1024
1113
  });
1025
- if (resumeGame) return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_expo_router6.Redirect, { href: { pathname: "/play/[game]", params: { game: resumeGame.id } } });
1114
+ (0, import_react16.useEffect)(() => {
1115
+ if (!resumeGame) autoShowPack();
1116
+ }, [resumeGame, autoShowPack]);
1117
+ if (resumeGame) return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_expo_router6.Redirect, { href: playHref(resumeGame.id) });
1026
1118
  return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(HomeMenu, {});
1027
1119
  }
1028
1120
  function HomeMenu() {
@@ -1030,20 +1122,21 @@ function HomeMenu() {
1030
1122
  const rotation = (0, import_core12.useRotation)();
1031
1123
  const layout = useHomeLayout();
1032
1124
  const { insets } = layout;
1033
- const { dark } = (0, import_auto_paper5.useAutoPaperTheme)();
1034
- const { openFirstTime, openFor } = useGameGuide();
1125
+ const { dark, colors } = (0, import_auto_paper5.useAutoPaperTheme)();
1126
+ const { openFirstTime, openFor, openPack } = useGameGuide();
1035
1127
  const [settingsOpen, setSettingsOpen] = (0, import_react16.useState)(false);
1128
+ const [rulesGame, setRulesGame] = (0, import_react16.useState)(null);
1036
1129
  const isFocused = (0, import_expo_router6.useIsFocused)();
1037
- const [appState, setAppState] = (0, import_react16.useState)(import_react_native5.AppState.currentState);
1130
+ const [appState, setAppState] = (0, import_react16.useState)(import_react_native6.AppState.currentState);
1038
1131
  (0, import_react16.useEffect)(() => {
1039
- const subscription = import_react_native5.AppState.addEventListener("change", setAppState);
1132
+ const subscription = import_react_native6.AppState.addEventListener("change", setAppState);
1040
1133
  return () => subscription.remove();
1041
1134
  }, []);
1042
1135
  const heroActive = isFocused && appState === "active";
1043
1136
  const { profiles, lastSelected, selectProfile } = useProfiles();
1044
1137
  const { seatTwo } = useOpponent();
1045
1138
  const { p1Color, p2Color, handleP1ColorChange } = useSeatColors(seatTwo === "person");
1046
- const pickerHost = (0, import_hud6.usePopoverHost)();
1139
+ const pickerHost = (0, import_chrome5.usePopoverHost)();
1047
1140
  const selectedProfile = profiles.find((p) => p.id === lastSelected[1]) ?? null;
1048
1141
  const tiled = pack.games.some((game) => game.Preview);
1049
1142
  const isWide = useFrozenWhileHidden(tiled && layout.isWide, !isFocused);
@@ -1056,71 +1149,91 @@ function HomeMenu() {
1056
1149
  const closePicker = pickerHost.close;
1057
1150
  (0, import_react16.useEffect)(() => {
1058
1151
  closePicker();
1059
- }, [isWide, closePicker]);
1060
- const bg = dark ? "#000000" : "#FFFFFF";
1061
- const fg = dark ? "#FFFFFF" : "#000000";
1062
- const openGame = (0, import_react16.useCallback)((game) => openFirstTime(game.id, () => import_expo_router6.router.push(game.useLoadout ? { pathname: "/loadout/[game]", params: { game: game.id } } : { pathname: "/play/[game]", params: { game: game.id } })), [openFirstTime]);
1063
- const picker = /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_profile6.ProfilePicker, { idPrefix: "home-p1", host: pickerHost, profiles, selectedId: lastSelected[1], color: p1Color, dark, guestLabel: "P1", onSelect: (profile) => selectProfile(1, profile?.id ?? null), onManage: () => import_expo_router6.router.push("/profiles"), colorSection: { value: p1Color, onChange: handleP1ColorChange, swatches: import_auto_paper5.defaultColors, caption: selectedProfile ? pack.colorCaption : void 0 } });
1064
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native5.View, { style: [styles4.container, { backgroundColor: bg }], children: [
1065
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_core12.FakeLandscapeView, { style: styles4.rotatable, children: [
1066
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_hud6.PressAwayOverlay, { active: pickerHost.openId !== null, onPress: pickerHost.close }),
1152
+ }, [isWide, rotation, layout.windowWidth, layout.windowHeight, closePicker]);
1153
+ const bg = pack.Backdrop ? "transparent" : dark ? "#000000" : "#FFFFFF";
1154
+ const fg = pack.foreground ?? (dark ? "#FFFFFF" : "#000000");
1155
+ const { previewColor } = pack;
1156
+ const p1Drawn = previewColor ? previewColor(p1Color, dark) : p1Color;
1157
+ const triggerStyle = (0, import_react16.useMemo)(() => pack.Backdrop ? [styles4.pill, { backgroundColor: colors.surface }] : void 0, [pack.Backdrop, colors.surface]);
1158
+ const openGame = (0, import_react16.useCallback)((game) => openFirstTime(game.id, () => import_expo_router6.router.push(game.useLoadout ? loadoutHref(game.id) : playHref(game.id))), [openFirstTime]);
1159
+ const openRules = (0, import_react16.useCallback)((game) => game.Rules ? setRulesGame(game) : openFor(game.id), [openFor]);
1160
+ const colorSection = { value: p1Color, onChange: handleP1ColorChange, swatches: import_auto_paper5.defaultColors, caption: selectedProfile ? pack.colorCaption : void 0, previewValue: previewColor ? (hex) => previewColor(hex, dark) : void 0 };
1161
+ const usePickerAlign = pack.usePickerAlign ?? noPickerAlign;
1162
+ const pickerSize = (0, import_profile7.getProfilePickerContentSize)(profiles.length, true, colorSection);
1163
+ const pickerAlign = usePickerAlign(pickerHost.openId === `${SEAT_ONE_PICKER}-profile`, pickerSize.width, pickerSize.height);
1164
+ const picker = /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_profile7.ProfilePicker, { idPrefix: SEAT_ONE_PICKER, host: pickerHost, profiles, selectedId: lastSelected[1], color: p1Drawn, dark, guestLabel: pack.guestLabel ?? "P1", nullLabel: pack.nullLabel, onSelect: (profile) => selectProfile(1, profile?.id ?? null), onManage: () => import_expo_router6.router.push(PROFILES_HREF), colorSection, alignOverride: pickerAlign, triggerStyle });
1165
+ const pressAway = /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_chrome5.PressAwayOverlay, { active: pickerHost.openId !== null, onPress: pickerHost.close, style: styles4.pressAway });
1166
+ const RulesSheet = rulesGame?.Rules;
1167
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native6.View, { style: [styles4.container, { backgroundColor: bg }], children: [
1168
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_core12.FakeLandscapeView, { style: styles4.rotatable, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(RulesHost, { enabled: pack.games.some((game) => game.Rules), children: [
1067
1169
  isWide ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
1068
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native5.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__ */ (0, import_jsx_runtime14.jsx)(import_react_native5.View, { style: [styles4.grid, { width: layout.rightPaneWidth }], children: pack.games.map((game) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GameTile, { game, width: layout.tileWidth, previewWidth: layout.previewWidth, previewRevealed: previewsRevealed, dark, p1Color, p2Color, onPress: () => openGame(game), onRules: () => openFor(game.id) }, game.id)) }) }),
1069
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native5.View, { style: [styles4.wideLeftPane, { left: layout.paddingLeft, width: layout.leftPaneWidth, paddingTop: HOME_TOP_BAND + insets.top, paddingBottom: 16 + insets.bottom }, pickerHost.openId !== null && styles4.pickerOpen], children: [
1170
+ pressAway,
1171
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_auto_paper5.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__ */ (0, import_jsx_runtime14.jsx)(import_react_native6.View, { style: [styles4.grid, { width: layout.rightPaneWidth }], children: pack.games.map((game) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(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)) }) }),
1172
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native6.View, { style: [styles4.wideLeftPane, { left: layout.paddingLeft, width: layout.leftPaneWidth, paddingTop: HOME_TOP_BAND + insets.top, paddingBottom: 16 + insets.bottom }, pickerHost.openId !== null && styles4.pickerOpen], children: [
1070
1173
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(pack.HeroTitle, { words: pack.wordmark, color: fg, p1Color, p2Color, active: heroActive, fit: layout.heroFit }),
1071
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native5.View, { style: styles4.widePicker, children: picker })
1174
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native6.View, { style: styles4.widePicker, children: picker })
1072
1175
  ] })
1073
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native5.ScrollView, { style: styles4.scroll, contentContainerStyle: [styles4.content, { paddingTop: 56 + insets.top, paddingBottom: 24 + insets.bottom, paddingLeft: 16 + insets.left, paddingRight: 16 + insets.right }], children: [
1176
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_auto_paper5.FadingScrollView, { style: styles4.scroll, contentContainerStyle: [styles4.content, { paddingTop: 56 + insets.top, paddingBottom: 24 + insets.bottom, paddingLeft: 16 + insets.left, paddingRight: 16 + insets.right }], children: [
1177
+ pressAway,
1074
1178
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(pack.HeroTitle, { words: pack.wordmark, color: fg, p1Color, p2Color, active: heroActive }),
1075
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native5.View, { style: [styles4.picker, pickerHost.openId !== null && styles4.pickerOpen], children: picker }),
1076
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native5.View, { style: styles4.list, children: pack.games.map((game) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GameRow, { game, dark, p1Color, p2Color, previewRevealed: previewsRevealed, onPress: () => openGame(game), onRules: () => openFor(game.id) }, game.id)) })
1179
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native6.View, { style: [styles4.picker, pickerHost.openId !== null && styles4.pickerOpen], children: picker }),
1180
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native6.View, { style: styles4.list, children: pack.games.map((game) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GameRow, { game, dark, p1Color, p2Color, previewRevealed: previewsRevealed, onPress: () => openGame(game), onRules: hasRules(game) ? () => openRules(game) : void 0 }, game.id)) })
1077
1181
  ] }),
1078
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_feedback_press3.IconButton, { icon: "trophy", iconColor: fg, size: 24, style: [styles4.corner, { top: 8 + insets.top, left: 8 + insets.left }], onPress: () => import_expo_router6.router.push("/stats"), accessibilityLabel: "Stats & Achievements" }),
1079
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_feedback_press3.IconButton, { icon: "cog", iconColor: fg, size: 24, style: [styles4.corner, { top: 8 + insets.top, right: 8 + insets.right }], onPress: () => setSettingsOpen(true), accessibilityLabel: "Settings" })
1080
- ] }),
1081
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SettingsDialog, { visible: settingsOpen, onDismiss: () => setSettingsOpen(false), rotation })
1182
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_feedback_press3.IconButton, { icon: "trophy", iconColor: fg, size: 24, style: [styles4.corner, { top: 8 + insets.top, left: 8 + insets.left }], onPress: () => import_expo_router6.router.push(STATS_HREF), accessibilityLabel: "Stats & Achievements" }),
1183
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_feedback_press3.IconButton, { icon: "cog", iconColor: fg, size: 24, style: [styles4.corner, { top: 8 + insets.top, right: 8 + insets.right }], onPress: () => setSettingsOpen(true), accessibilityLabel: "Settings" }),
1184
+ RulesSheet && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(RulesSheet, { visible: true, onDismiss: () => setRulesGame(null) })
1185
+ ] }) }),
1186
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SettingsDialog, { visible: settingsOpen, onDismiss: () => setSettingsOpen(false), rotation, onShowHowToPlay: pack.guide ? openPack : void 0 })
1082
1187
  ] });
1083
1188
  }
1189
+ var SEAT_ONE_PICKER = "home-p1";
1190
+ var noPickerAlign = () => void 0;
1191
+ function hasRules(game) {
1192
+ return !!(game.Rules || game.guide);
1193
+ }
1194
+ function RulesHost({ enabled, children }) {
1195
+ return enabled ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper.Portal.Host, { children }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_jsx_runtime14.Fragment, { children });
1196
+ }
1084
1197
  function GameRow({ game, dark, p1Color, p2Color, previewRevealed, onPress, onRules }) {
1085
- const well = (0, import_hud6.useWellColors)();
1198
+ const well = (0, import_chrome5.useWellColors)();
1086
1199
  const GameIcon = game.icon;
1087
1200
  return (
1088
1201
  // The rules button sits beside the row's own button, laid over its end, not inside it: a button
1089
1202
  // inside a button is invalid on the web.
1090
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native5.View, { children: [
1203
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native6.View, { children: [
1091
1204
  /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_feedback_press3.Pressable, { onPress, style: [styles4.row, { backgroundColor: well.bg }], accessibilityRole: "button", accessibilityLabel: game.title, children: [
1092
- game.Preview ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(PreviewBox, { game, width: ROW_PREVIEW_WIDTH, revealed: previewRevealed, dark, p1Color, p2Color }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native5.View, { style: [styles4.rowIcon, { backgroundColor: dark ? ICON_DARK_BG : ICON_LIGHT_BG }], children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GameIcon, { size: ROW_ICON_SIZE, p1Color, p2Color, dark }) }),
1093
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native5.View, { style: styles4.rowText, children: [
1205
+ game.Preview ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(PreviewBox, { game, width: ROW_PREVIEW_WIDTH, revealed: previewRevealed, dark, p1Color, p2Color }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native6.View, { style: [styles4.rowIcon, { backgroundColor: dark ? ICON_DARK_BG : ICON_LIGHT_BG }], children: GameIcon && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GameIcon, { size: ROW_ICON_SIZE, p1Color, p2Color, dark }) }),
1206
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native6.View, { style: styles4.rowText, children: [
1094
1207
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper.Text, { variant: "titleLarge", style: [styles4.rowTitle, { color: well.fg }], numberOfLines: 1, children: game.title }),
1095
1208
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper.Text, { variant: "bodyMedium", style: { color: well.fgMuted }, numberOfLines: 2, children: game.blurb })
1096
1209
  ] })
1097
1210
  ] }),
1098
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_feedback_press3.Pressable, { onPress: onRules, style: styles4.rulesButton, hitSlop: 12, accessibilityRole: "button", accessibilityLabel: `${game.title} rules`, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper.Icon, { source: "information-outline", size: 22, color: well.fgMuted }) })
1211
+ onRules && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_feedback_press3.Pressable, { onPress: onRules, style: styles4.rulesButton, hitSlop: 12, accessibilityRole: "button", accessibilityLabel: `${game.title} rules`, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper.Icon, { source: "information-outline", size: 22, color: well.fgMuted }) })
1099
1212
  ] })
1100
1213
  );
1101
1214
  }
1102
1215
  function GameTile({ game, width, previewWidth, dark, p1Color, p2Color, previewRevealed, onPress, onRules }) {
1103
- const well = (0, import_hud6.useWellColors)();
1104
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native5.View, { style: { width }, children: [
1216
+ const well = (0, import_chrome5.useWellColors)();
1217
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native6.View, { style: { width }, children: [
1105
1218
  /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_feedback_press3.Pressable, { onPress, style: [styles4.tile, { backgroundColor: well.bg }], accessibilityRole: "button", accessibilityLabel: game.title, children: [
1106
1219
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(PreviewBox, { game, width: previewWidth, revealed: previewRevealed, dark, p1Color, p2Color }),
1107
1220
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper.Text, { variant: "titleMedium", style: [styles4.rowTitle, { color: well.fg }], numberOfLines: 1, children: game.title }),
1108
1221
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper.Text, { variant: "bodySmall", style: { color: well.fgMuted }, numberOfLines: 3, children: game.blurb })
1109
1222
  ] }),
1110
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_feedback_press3.Pressable, { onPress: onRules, style: styles4.tileRulesButton, hitSlop: 12, accessibilityRole: "button", accessibilityLabel: `${game.title} rules`, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper.Icon, { source: "information-outline", size: 20, color: "#FFFFFF" }) })
1223
+ onRules && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_feedback_press3.Pressable, { onPress: onRules, style: styles4.tileRulesButton, hitSlop: 12, accessibilityRole: "button", accessibilityLabel: `${game.title} rules`, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper.Icon, { source: "information-outline", size: 20, color: "#FFFFFF" }) })
1111
1224
  ] });
1112
1225
  }
1113
1226
  function PreviewBox({ game, width, revealed, dark, p1Color, p2Color }) {
1114
1227
  const height = Math.round(width / PREVIEW_ASPECT);
1115
1228
  const { Preview, icon: GameIcon } = game;
1116
1229
  const iconSize = Math.min(height, ROW_ICON_SIZE);
1117
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native5.View, { style: [styles4.preview, { width, height, backgroundColor: dark ? ICON_DARK_BG : ICON_LIGHT_BG }], children: Preview ? revealed && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Preview, { width, height, p1Color, p2Color, dark }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native5.View, { style: [styles4.previewIcon, { width: iconSize, height: iconSize, borderRadius: iconSize * 0.22 }], children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GameIcon, { size: iconSize, p1Color, p2Color, dark }) }) });
1230
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native6.View, { style: [styles4.preview, { width, height, backgroundColor: dark ? ICON_DARK_BG : ICON_LIGHT_BG }], children: Preview ? revealed && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Preview, { width, height, p1Color, p2Color, dark }) : GameIcon && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native6.View, { style: [styles4.previewIcon, { width: iconSize, height: iconSize, borderRadius: iconSize * 0.22 }], children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GameIcon, { size: iconSize, p1Color, p2Color, dark }) }) });
1118
1231
  }
1119
1232
  var ROW_ICON_SIZE = 56;
1120
1233
  var ICON_LIGHT_BG = "#FAFAFA";
1121
1234
  var ICON_DARK_BG = "#050505";
1122
1235
  var RULES_BUTTON_SPACE = 44;
1123
- var styles4 = import_react_native5.StyleSheet.create({
1236
+ var styles4 = import_react_native6.StyleSheet.create({
1124
1237
  container: {
1125
1238
  flex: 1
1126
1239
  },
@@ -1149,6 +1262,15 @@ var styles4 = import_react_native5.StyleSheet.create({
1149
1262
  pickerOpen: {
1150
1263
  zIndex: 100
1151
1264
  },
1265
+ // Seat 1's button on the theme's surface (over a backdrop), the same pill as the tiles' wells.
1266
+ pill: {
1267
+ borderRadius: 999
1268
+ },
1269
+ // Over the rows (and, wide, the grid), whose own stacking is unset, so a press on them reaches it on
1270
+ // the web too, and under the raised button and left pane, so the menu's rows stay pressable.
1271
+ pressAway: {
1272
+ zIndex: 5
1273
+ },
1152
1274
  // The preview's own rounding, clipping whatever it draws to the box.
1153
1275
  preview: {
1154
1276
  alignItems: "center",
@@ -1167,7 +1289,7 @@ var styles4 = import_react_native5.StyleSheet.create({
1167
1289
  row: {
1168
1290
  alignItems: "center",
1169
1291
  borderRadius: 12,
1170
- boxShadow: import_hud6.WELL_SHADOW,
1292
+ boxShadow: import_chrome5.WELL_SHADOW,
1171
1293
  flexDirection: "row",
1172
1294
  gap: 14,
1173
1295
  padding: 14,
@@ -1199,7 +1321,7 @@ var styles4 = import_react_native5.StyleSheet.create({
1199
1321
  // The same well as a row, turned on its side.
1200
1322
  tile: {
1201
1323
  borderRadius: 12,
1202
- boxShadow: import_hud6.WELL_SHADOW,
1324
+ boxShadow: import_chrome5.WELL_SHADOW,
1203
1325
  gap: 6,
1204
1326
  padding: HOME_TILE_PADDING
1205
1327
  },
@@ -1239,69 +1361,112 @@ var styles4 = import_react_native5.StyleSheet.create({
1239
1361
 
1240
1362
  // src/screens/ProfilesRoute.tsx
1241
1363
  var import_auto_paper6 = require("@rific/auto-paper");
1242
- var import_profile7 = require("@tastic/profile");
1364
+ var import_chrome6 = require("@tastic/hud/chrome");
1365
+ var import_profile8 = require("@tastic/profile");
1243
1366
  var import_jsx_runtime15 = require("react/jsx-runtime");
1367
+ var ROSTER_MAX_WIDTH = 640;
1368
+ function useRosterAlign(open, contentWidth, contentHeight) {
1369
+ const { usePickerAlign } = usePack();
1370
+ return usePickerAlign(open, contentWidth, contentHeight, { maxContentWidth: ROSTER_MAX_WIDTH });
1371
+ }
1244
1372
  function ProfilesRoute() {
1245
1373
  const { profiles, createProfile, updateProfile, deleteProfile } = useProfiles();
1246
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_profile7.ProfilesScreen, { profiles, defaultColor: import_auto_paper6.defaultColors[0].value, onCreate: (patch) => createProfile(patch), onSave: (id, patch) => updateProfile(id, patch), onDelete: deleteProfile, onBack: import_core8.safeBack });
1374
+ const { Backdrop, foreground, newProfileColor, usePickerAlign } = usePack();
1375
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_chrome6.RotatedFrame, { rotatedSafeArea: true, portalHost: true, maxContentWidth: ROSTER_MAX_WIDTH, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_profile8.ProfilesScreen, { profiles, defaultColor: newProfileColor ?? import_auto_paper6.defaultColors[0].value, onCreate: (patch) => createProfile(patch), onSave: (id, patch) => updateProfile(id, patch), onDelete: deleteProfile, onBack: import_core8.safeBack, fg: foreground, bg: Backdrop ? "transparent" : void 0, useColorPickerAlign: usePickerAlign ? useRosterAlign : void 0 }) });
1247
1376
  }
1248
1377
 
1249
1378
  // src/screens/StatsScreen.tsx
1250
1379
  var import_auto_paper7 = require("@rific/auto-paper");
1251
- var import_core13 = require("@tastic/core");
1252
- var import_hud7 = require("@tastic/hud");
1253
- var import_profile8 = require("@tastic/profile");
1380
+ var import_chrome7 = require("@tastic/hud/chrome");
1381
+ var import_profile9 = require("@tastic/profile");
1254
1382
  var import_react17 = require("react");
1255
- var import_react_native6 = require("react-native");
1383
+ var import_react_native7 = require("react-native");
1256
1384
  var import_react_native_paper2 = require("react-native-paper");
1257
1385
  var import_react_native_safe_area_context4 = require("react-native-safe-area-context");
1258
1386
  var import_react_redux10 = require("react-redux");
1259
1387
  var import_jsx_runtime16 = require("react/jsx-runtime");
1388
+ var STATS = "stats";
1389
+ var ACHIEVEMENTS = "achievements";
1260
1390
  var TABS = [
1261
- { value: "stats", label: "Stats", icon: "chart-box-outline" },
1262
- { value: "achievements", label: "Achievements", icon: "trophy-outline" }
1391
+ { value: STATS, label: "Stats", icon: "chart-box-outline" },
1392
+ { value: ACHIEVEMENTS, label: "Achievements", icon: "trophy-outline" }
1263
1393
  ];
1394
+ var LABEL_WIDE_TAB = { flexBasis: "auto", flexGrow: 1, flexShrink: 1 };
1395
+ var NO_PACK_TABS = [];
1264
1396
  var EVERYONE = "__everyone__";
1265
1397
  var ALL_GAMES = "__all__";
1266
1398
  var PLAYER_PICKER_ID = "stats-player";
1267
1399
  var GAME_PICKER_ID = "stats-game";
1400
+ var STATS_MAX_WIDTH = 640;
1268
1401
  function StatsScreen() {
1402
+ const pickerHost = (0, import_chrome7.usePopoverHost)();
1403
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(StatsPressAway, { host: pickerHost, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_chrome7.RotatedFrame, { rotatedSafeArea: true, portalHost: true, maxContentWidth: STATS_MAX_WIDTH, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(StatsContent, { pickerHost }) }) });
1404
+ }
1405
+ function StatsPressAway({ host, children }) {
1406
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native7.View, { testID: "stats-press-away", style: styles5.screen, onStartShouldSetResponder: () => host.openId !== null, onResponderRelease: host.close, children });
1407
+ }
1408
+ var useNoPackReset = () => null;
1409
+ var noPickerAlign2 = () => void 0;
1410
+ function StatsContent({ pickerHost }) {
1269
1411
  const pack = usePack();
1270
1412
  const { dark, colors } = (0, import_auto_paper7.useAutoPaperTheme)();
1271
- const rotation = (0, import_core13.useRotation)();
1272
- const insets = (0, import_core13.rotateInsets)((0, import_react_native_safe_area_context4.useSafeAreaInsets)(), rotation);
1273
- const { profiles } = useProfiles();
1413
+ const insets = (0, import_react_native_safe_area_context4.useSafeAreaInsets)();
1414
+ const { profiles, lastSelected } = useProfiles();
1274
1415
  const dispatch = (0, import_react_redux10.useDispatch)();
1275
- const pickerHost = (0, import_hud7.usePopoverHost)();
1276
- const [tab, setTab] = (0, import_react17.useState)("stats");
1416
+ const fg = pack.foreground ?? colors.onBackground;
1417
+ const [tab, setTab] = (0, import_react17.useState)(STATS);
1277
1418
  const [gameId, setGameId] = (0, import_react17.useState)(ALL_GAMES);
1278
- const [selectedProfileId, setSelectedProfileId] = (0, import_react17.useState)(null);
1279
- const profileId = selectedProfileId && profiles.some((p) => p.id === selectedProfileId) ? selectedProfileId : null;
1419
+ const [selectedProfileId, setSelectedProfileId] = (0, import_react17.useState)(() => pack.guestStats ? lastSelected[1] ?? GUEST_PLAYER_ID : null);
1420
+ const profileId = selectedProfileId && profiles.length > 0 && (pack.guestStats && selectedProfileId === GUEST_PLAYER_ID || profiles.some((p) => p.id === selectedProfileId)) ? selectedProfileId : null;
1280
1421
  const games = gameId === ALL_GAMES ? pack.games : pack.games.filter((g) => g.id === gameId);
1281
- const playerOptions = (0, import_react17.useMemo)(() => [{ value: EVERYONE, label: "Everyone", icon: "account-group" }, ...profiles.map((p) => ({ value: p.id, label: p.name, icon: "account", color: p.color }))], [profiles]);
1422
+ const packTabs = pack.statsTabs ?? NO_PACK_TABS;
1423
+ const packTab = packTabs.find((t) => t.value === tab);
1424
+ const showGamePicker = !(tab === ACHIEVEMENTS && pack.AchievementList);
1425
+ const playerOptions = (0, import_react17.useMemo)(() => [{ 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]);
1282
1426
  const gameOptions = (0, import_react17.useMemo)(() => [{ value: ALL_GAMES, label: "All Games", icon: "view-grid-outline" }, ...pack.games.map((g) => ({ value: g.id, label: g.title }))], [pack.games]);
1283
- const resetScope = games.length === 1 ? games[0].title : "every game";
1284
- const onReset = () => games.forEach((g) => dispatch(g.resetStats()));
1285
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_core13.FakeLandscapeView, { style: styles5.rotatable, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_hud7.BaseStatsScreen, { title: tab === "stats" ? "Stats" : "Achievements", onBack: import_core8.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: [
1286
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_hud7.PressAwayOverlay, { active: pickerHost.openId !== null, onPress: pickerHost.close, style: styles5.pressAway }),
1287
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_paper2.SegmentedButtons, { value: tab, onValueChange: (value) => setTab(value), buttons: TABS, style: styles5.tabs }),
1288
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native6.View, { style: [styles5.filters, pickerHost.openId !== null && styles5.filtersOpen], children: [
1289
- profiles.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_profile8.OptionPicker, { id: PLAYER_PICKER_ID, host: pickerHost, options: playerOptions, value: profileId ?? EVERYONE, onChange: (value) => setSelectedProfileId(value === EVERYONE ? null : value), color: colors.primary, dark, icon: profileId ? "account" : "account-group" }),
1290
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_profile8.OptionPicker, { id: GAME_PICKER_ID, host: pickerHost, options: gameOptions, value: gameId, onChange: setGameId, color: colors.primary, dark, icon: "view-grid-outline" })
1427
+ const tabButtons = (0, import_react17.useMemo)(() => 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]);
1428
+ const title = tab === STATS ? "Stats" : tab === ACHIEVEMENTS ? "Achievements" : packTab?.label ?? "";
1429
+ const usePackReset = pack.resetStats?.useReset ?? useNoPackReset;
1430
+ const packReset = usePackReset();
1431
+ const reset = resetFor(pack, packReset, profileId, profiles, games, () => games.forEach((g) => g.resetStats && dispatch(g.resetStats())));
1432
+ const usePickerAlign = pack.usePickerAlign ?? noPickerAlign2;
1433
+ const playerSize = (0, import_profile9.getOptionPickerContentSize)(playerOptions.length);
1434
+ const gameSize = (0, import_profile9.getOptionPickerContentSize)(gameOptions.length);
1435
+ const playerAlign = usePickerAlign(pickerHost.openId === PLAYER_PICKER_ID, playerSize.width, playerSize.height);
1436
+ const gameAlign = usePickerAlign(pickerHost.openId === GAME_PICKER_ID, gameSize.width, gameSize.height);
1437
+ const surface = pack.Backdrop ? { backgroundColor: colors.surface } : void 0;
1438
+ const triggerStyle = pack.Backdrop ? [styles5.pill, surface] : void 0;
1439
+ const PackTabContent = packTab?.Content;
1440
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_chrome7.BaseStatsScreen, { title, onBack: import_core8.safeBack, insets, onReset: reset.onReset, resetLabel: reset.label, resetConfirmTitle: reset.title, resetConfirmBody: reset.body, fg: pack.foreground, bg: pack.Backdrop ? "transparent" : void 0, rotation: 0, children: [
1441
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_chrome7.PressAwayOverlay, { active: pickerHost.openId !== null, onPress: pickerHost.close, style: styles5.pressAway }),
1442
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_paper2.SegmentedButtons, { value: tab, onValueChange: setTab, buttons: tabButtons, style: [styles5.tabs, surface] }),
1443
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native7.View, { style: [styles5.filters, pickerHost.openId !== null && styles5.filtersOpen], onStartShouldSetResponder: () => pickerHost.openId !== null, children: [
1444
+ profiles.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_profile9.OptionPicker, { id: PLAYER_PICKER_ID, host: pickerHost, options: playerOptions, value: profileId ?? EVERYONE, onChange: (value) => setSelectedProfileId(value === EVERYONE ? null : value), color: colors.primary, dark, icon: profileId ? "account" : "account-group", alignOverride: playerAlign, triggerStyle }),
1445
+ showGamePicker && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_profile9.OptionPicker, { id: GAME_PICKER_ID, host: pickerHost, options: gameOptions, value: gameId, onChange: setGameId, color: colors.primary, dark, icon: "view-grid-outline", alignOverride: gameAlign, triggerStyle })
1291
1446
  ] }),
1292
- tab === "stats" && pack.StatsSections && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(pack.StatsSections, { profileId, gameId: gameId === ALL_GAMES ? null : gameId }),
1293
- games.map((game) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(GameStats, { game, tab, profileId, showTitle: games.length > 1 }, game.id))
1294
- ] }) });
1447
+ tab === STATS && pack.StatsSections && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(pack.StatsSections, { profileId, gameId: gameId === ALL_GAMES ? null : gameId }),
1448
+ PackTabContent && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(PackTabContent, { profileId, gameId: gameId === ALL_GAMES ? null : gameId }),
1449
+ tab === ACHIEVEMENTS && pack.AchievementList ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(pack.AchievementList, { profileId }) : (tab === STATS || tab === ACHIEVEMENTS) && games.map((game) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(GameStats, { game, tab, profileId, showTitle: games.length > 1 && pack.statsGameTitles !== false, titleColor: fg }, game.id))
1450
+ ] });
1451
+ }
1452
+ function resetFor(pack, packReset, profileId, profiles, games, resetGames) {
1453
+ if (pack.resetStats && packReset) {
1454
+ const name = profileId === GUEST_PLAYER_ID ? "Guest" : profileId ? profiles.find((p) => p.id === profileId)?.name ?? null : null;
1455
+ return { onReset: () => packReset(profileId), ...pack.resetStats.labels(name) };
1456
+ }
1457
+ const scope = games.length === 1 ? games[0].title : "every game";
1458
+ 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.` };
1295
1459
  }
1296
- function GameStats({ game, tab, profileId, showTitle }) {
1297
- const { colors } = (0, import_auto_paper7.useAutoPaperTheme)();
1460
+ function GameStats({ game, tab, profileId, showTitle, titleColor }) {
1298
1461
  const { StatsSections, AchievementList } = game;
1299
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native6.View, { style: styles5.game, children: [
1300
- showTitle && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_paper2.Text, { variant: "titleLarge", style: [styles5.gameTitle, { color: colors.onBackground }], children: game.title }),
1301
- tab === "stats" ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(StatsSections, { profileId }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(AchievementList, { profileId })
1462
+ const Sections = tab === STATS ? StatsSections : AchievementList;
1463
+ if (!Sections) return null;
1464
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native7.View, { style: styles5.game, children: [
1465
+ showTitle && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_paper2.Text, { variant: "titleLarge", style: [styles5.gameTitle, { color: titleColor }], children: game.title }),
1466
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Sections, { profileId })
1302
1467
  ] });
1303
1468
  }
1304
- var styles5 = import_react_native6.StyleSheet.create({
1469
+ var styles5 = import_react_native7.StyleSheet.create({
1305
1470
  filters: {
1306
1471
  flexDirection: "row",
1307
1472
  flexWrap: "wrap",
@@ -1318,25 +1483,28 @@ var styles5 = import_react_native6.StyleSheet.create({
1318
1483
  fontWeight: "bold",
1319
1484
  marginTop: 8
1320
1485
  },
1486
+ // A picker's button on the theme's surface (over a backdrop), the same pill as Home's.
1487
+ pill: {
1488
+ borderRadius: 999
1489
+ },
1321
1490
  pressAway: {
1322
1491
  zIndex: 50
1323
1492
  },
1324
- // A real full-bleed box for FakeLandscapeView to size its turned content against.
1325
- rotatable: {
1493
+ screen: {
1326
1494
  flex: 1
1327
1495
  },
1328
- // SegmentedButtons' own rounding.
1496
+ // SegmentedButtons' own rounding, so a surface behind it is the same pill.
1329
1497
  tabs: {
1330
1498
  borderRadius: 20
1331
1499
  }
1332
1500
  });
1333
1501
 
1334
1502
  // src/components/imageGameIcon.tsx
1335
- var import_react_native7 = require("react-native");
1503
+ var import_react_native8 = require("react-native");
1336
1504
  var import_jsx_runtime17 = require("react/jsx-runtime");
1337
1505
  function imageGameIcon(images) {
1338
1506
  return function ImageGameIcon({ size, dark }) {
1339
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native7.Image, { source: dark ? images.dark : images.light, style: { height: size, width: size }, accessibilityIgnoresInvertColors: true });
1507
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native8.Image, { source: dark ? images.dark : images.light, style: { height: size, width: size }, accessibilityIgnoresInvertColors: true });
1340
1508
  };
1341
1509
  }
1342
1510
 
@@ -1356,12 +1524,12 @@ var import_redux_persist = require("redux-persist");
1356
1524
 
1357
1525
  // src/redux/shellState.ts
1358
1526
  var import_auto_paper8 = require("@rific/auto-paper");
1359
- var import_core14 = require("@rific/core");
1527
+ var import_core13 = require("@rific/core");
1360
1528
  var import_feedback_press4 = require("@rific/feedback-press");
1361
- var import_profile9 = require("@tastic/profile");
1529
+ var import_profile10 = require("@tastic/profile");
1362
1530
  var initialSoundSettings = { ...import_feedback_press4.defaultSoundSettings, enabled: !__DEV__ };
1363
1531
  var appSoundReducer = (state = initialSoundSettings, action) => (0, import_feedback_press4.soundReducer)(state, action);
1364
- function createShellReducers({ seatColors, appearance = "system" }) {
1532
+ function createShellReducers({ seatColors, appearance = "system", seatTheme }) {
1365
1533
  return {
1366
1534
  // Explicit primary/secondary triad (rather than a single seed expanded via harmony) so the pack's
1367
1535
  // identity is pinned exactly instead of drifting with whatever hue math a harmony offset would
@@ -1370,17 +1538,19 @@ function createShellReducers({ seatColors, appearance = "system" }) {
1370
1538
  // derived, not picked, via getThirdColor: the hue maximally distant from both (see
1371
1539
  // @rific/auto-paper's README "Explicit triad" section). The appearance is the pack's ('system',
1372
1540
  // createThemeReducer's own default, when it sets none).
1373
- theme: (0, import_auto_paper8.createThemeReducer)({ appearance, color: { primary: seatColors[1], secondary: seatColors[2], tertiary: (0, import_auto_paper8.getThirdColor)(seatColors[1], seatColors[2]) } }),
1541
+ // A pack with its own seatTheme starts from what seat 1's default color makes of it instead (dark
1542
+ // only when the pack's appearance is; hooks/useSeatColors.ts re-applies it with the live mode).
1543
+ theme: (0, import_auto_paper8.createThemeReducer)(seatTheme ? { appearance, ...seatTheme(seatColors, appearance === "dark") } : { appearance, color: { primary: seatColors[1], secondary: seatColors[2], tertiary: (0, import_auto_paper8.getThirdColor)(seatColors[1], seatColors[2]) } }),
1374
1544
  haptic: import_feedback_press4.hapticReducer,
1375
1545
  sound: appSoundReducer,
1376
1546
  // @rific/core's Keep Awake setting (see Providers.tsx's KeepAwakeBridge), under the namespace its
1377
1547
  // REHYDRATE handling expects.
1378
- keepAwake: import_core14.keepAwakeReducer,
1548
+ keepAwake: import_core13.keepAwakeReducer,
1379
1549
  // The shared App Group roster (see @tastic/profile's resolveInitialProfiles/
1380
1550
  // useSharedProfilesSync, wired in hooks/useProfiles.tsx) is layered on top of this as a separate
1381
1551
  // sync target - this slice persisting normally via redux-persist is what gives it local-fallback
1382
1552
  // behavior where the shared store isn't available, with no separate AsyncStorage path.
1383
- profiles: import_profile9.profilesReducer,
1553
+ profiles: import_profile10.profilesReducer,
1384
1554
  profileSelection: profileSelectionSlice_default,
1385
1555
  // The loadout's "remember this color for next time" memory (guest, CPU, and each profile's pick
1386
1556
  // in this pack) - separate from `theme` above, which always reflects whatever's live right now.
@@ -1398,7 +1568,7 @@ var errorMiddleware = () => (next) => (action) => {
1398
1568
  if (!hasError(action)) return next(action);
1399
1569
  return action;
1400
1570
  };
1401
- function createPackStore(pack, { keyPrefix, migrate }) {
1571
+ function createPackStore(pack, { keyPrefix, migrate, blacklist = [], serializableIgnoredPaths = [], middleware = [] }) {
1402
1572
  const gameReducers = Object.assign({}, ...pack.games.map((game) => game.reducers));
1403
1573
  const resumeReducers = pack.games.some((game) => game.resumable) ? { lastActiveGame: lastActiveGameSlice_default } : {};
1404
1574
  const rootReducer = (0, import_toolkit.combineReducers)({ ...createShellReducers(pack), ...resumeReducers, ...pack.reducers, ...gameReducers });
@@ -1416,7 +1586,7 @@ function createPackStore(pack, { keyPrefix, migrate }) {
1416
1586
  // failing to ever resolve at all isn't a failure mode worth a 5s failsafe for.
1417
1587
  timeout: 0,
1418
1588
  // liveplay is per-round state that must never survive a relaunch (see liveplaySlice.ts).
1419
- blacklist: ["liveplay"],
1589
+ blacklist: ["liveplay", ...blacklist],
1420
1590
  migrate
1421
1591
  },
1422
1592
  rootReducer
@@ -1426,10 +1596,11 @@ function createPackStore(pack, { keyPrefix, migrate }) {
1426
1596
  const defaultMiddleware = getDefaultMiddleware({
1427
1597
  immutableCheck: false,
1428
1598
  serializableCheck: {
1429
- ignoredActions: [import_redux_persist.FLUSH, import_redux_persist.REHYDRATE, import_redux_persist.PAUSE, import_redux_persist.PERSIST, import_redux_persist.PURGE, import_redux_persist.REGISTER]
1599
+ ignoredActions: [import_redux_persist.FLUSH, import_redux_persist.REHYDRATE, import_redux_persist.PAUSE, import_redux_persist.PERSIST, import_redux_persist.PURGE, import_redux_persist.REGISTER],
1600
+ ignoredPaths: [...serializableIgnoredPaths]
1430
1601
  }
1431
1602
  });
1432
- return defaultMiddleware.concat(errorMiddleware);
1603
+ return defaultMiddleware.concat(errorMiddleware, ...middleware);
1433
1604
  },
1434
1605
  reducer: persistedReducer
1435
1606
  });
@@ -1463,11 +1634,11 @@ function createGameGuideSlice(mountKey) {
1463
1634
 
1464
1635
  // src/redux/onProfileRemoved.ts
1465
1636
  var import_achievements = require("@tastic/achievements");
1466
- var import_profile10 = require("@tastic/profile");
1637
+ var import_profile11 = require("@tastic/profile");
1467
1638
  function onProfileRemoved(reducer6, forget) {
1468
1639
  return (state, action) => {
1469
1640
  const next = reducer6(state, action);
1470
- return import_profile10.profilesActions.remove.match(action) ? forget(next, action.payload) : next;
1641
+ return import_profile11.profilesActions.remove.match(action) ? forget(next, action.payload) : next;
1471
1642
  };
1472
1643
  }
1473
1644
  function forgetProfileAchievements(dropStats) {
@@ -1590,6 +1761,7 @@ function createPlayerSettings({ id, defaults, distinctControls }) {
1590
1761
  0 && (module.exports = {
1591
1762
  DEFAULT_APP_SETTINGS,
1592
1763
  DEFAULT_OPPONENT,
1764
+ GUEST_PLAYER_ID,
1593
1765
  HomeScreen,
1594
1766
  LoadoutRoute,
1595
1767
  PREVIEW_ASPECT,