@tastic/shell 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -460,7 +460,7 @@ var PROFILES_HREF = "/profiles";
460
460
  import { defaultColors, getRgb, useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
461
461
  import { useOrientationState } from "@tastic/core";
462
462
  import { CornerActionButtons, PressAwayOverlay, usePopoverHost } from "@tastic/hud/chrome";
463
- import { PlayerSetupPanel, ReadyButton as ReadyButton2, SharedActionBand } from "@tastic/hud/lobby";
463
+ import { PlayerSetupPanel, ReadyButton, SharedActionBand } from "@tastic/hud/lobby";
464
464
  import { DualZoneLayout, needsSharedNeutralZone, useDualZoneLayout } from "@tastic/split-screen";
465
465
  import { router as router2, useFocusEffect as useFocusEffect2 } from "expo-router";
466
466
  import { useCallback as useCallback9, useEffect as useEffect4, useMemo as useMemo5, useRef as useRef4, useState as useState4 } from "react";
@@ -469,80 +469,55 @@ import Animated from "react-native-reanimated";
469
469
  import { useSafeAreaInsets } from "react-native-safe-area-context";
470
470
  import { useDispatch as useDispatch9 } from "react-redux";
471
471
 
472
- // src/components/SeatTwoPanel.tsx
473
- import { ReadyButton, useZoneClampedAlign } from "@tastic/hud/lobby";
472
+ // src/components/OpponentPicker.tsx
473
+ import { useZoneClampedAlign } from "@tastic/hud/lobby";
474
474
  import { getOptionPickerContentSize, OptionPicker } from "@tastic/profile";
475
475
  import { useMemo as useMemo4 } from "react";
476
476
  import { StyleSheet as StyleSheet2, View as View2 } from "react-native";
477
- import { jsx as jsx10, jsxs as jsxs3 } from "react/jsx-runtime";
477
+ import { jsx as jsx10 } from "react/jsx-runtime";
478
478
  var CPU_PREFIX = "cpu:";
479
- var PROFILE_PREFIX = "profile:";
480
479
  var NOBODY = "nobody";
481
- var GUEST = "guest";
482
- var GUEST_ICON = "account-outline";
480
+ var PERSON = "person";
481
+ var PERSON_ICON = "account-outline";
483
482
  var NOBODY_ICON = "account-off-outline";
484
483
  var CPU_ICON = "robot";
485
- function getSeatTwoPopoverId(idPrefix) {
486
- return `${idPrefix}-seat`;
484
+ function getOpponentPopoverId(idPrefix) {
485
+ return `${idPrefix}-opponent`;
487
486
  }
488
- function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDifficulty, profiles, selectedProfileId, onProfileSelect, color, onColorChange, swatches, takenColor, allowSwapTaken, colorCaption, dark, secondPicker, secondPickerId, ready, onToggleReady, showReadyButton = true, previewValue, triggerStyle, usePickerAlign }) {
489
- const isPerson = seatTwo === "person";
487
+ function OpponentPicker({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDifficulty, color, onColorChange, swatches, takenColor, allowSwapTaken, dark, previewValue, triggerStyle, usePickerAlign }) {
490
488
  const drawn = previewValue ? previewValue(color) : color;
491
- const selectedProfile = isPerson ? profiles.find((p) => p.id === selectedProfileId) ?? null : null;
492
489
  const cpuOptions = cpuDifficulty?.options;
493
490
  const options = useMemo4(() => {
494
491
  const list = [];
495
492
  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 }]);
496
- if (choices.includes("nobody")) list.push({ value: NOBODY, label: "Nobody", icon: NOBODY_ICON });
497
- if (choices.includes("person")) {
498
- list.push({ value: GUEST, label: "Player 2", icon: GUEST_ICON });
499
- list.push(...[...profiles].sort((a, b) => a.name.localeCompare(b.name)).map((p) => ({ value: `${PROFILE_PREFIX}${p.id}`, label: p.name, icon: "account", color: previewValue ? previewValue(p.color) : p.color })));
500
- }
493
+ if (choices.includes("nobody")) list.push({ value: NOBODY, label: "Solo", icon: NOBODY_ICON, dividerBefore: true });
494
+ if (choices.includes("person")) list.push({ value: PERSON, label: "Player", icon: PERSON_ICON, dividerBefore: true });
501
495
  return list;
502
- }, [choices, cpuOptions, profiles, previewValue]);
503
- const value = seatTwo === "cpu" ? `${CPU_PREFIX}${cpuDifficulty?.value ?? ""}` : seatTwo === "nobody" ? NOBODY : selectedProfile ? `${PROFILE_PREFIX}${selectedProfile.id}` : GUEST;
504
- const icon = seatTwo === "cpu" ? CPU_ICON : seatTwo === "nobody" ? NOBODY_ICON : selectedProfile ? "account" : GUEST_ICON;
496
+ }, [choices, cpuOptions]);
497
+ const value = seatTwo === "cpu" ? `${CPU_PREFIX}${cpuDifficulty?.value ?? ""}` : seatTwo === "nobody" ? NOBODY : PERSON;
498
+ const icon = seatTwo === "cpu" ? CPU_ICON : seatTwo === "nobody" ? NOBODY_ICON : PERSON_ICON;
505
499
  const handleChange = (next) => {
506
500
  if (next.startsWith(CPU_PREFIX)) {
507
501
  cpuDifficulty?.onChange(next.slice(CPU_PREFIX.length));
508
502
  onSeatTwoChange("cpu");
509
- } else if (next === NOBODY) {
510
- onSeatTwoChange("nobody");
511
503
  } else {
512
- onProfileSelect(next.startsWith(PROFILE_PREFIX) ? next.slice(PROFILE_PREFIX.length) : null);
513
- onSeatTwoChange("person");
504
+ onSeatTwoChange(next === NOBODY ? "nobody" : "person");
514
505
  }
515
506
  };
516
- const colorSection = seatTwo === "nobody" ? void 0 : { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, caption: selectedProfile ? colorCaption : void 0, previewValue };
517
- const pickerId = getSeatTwoPopoverId(idPrefix);
518
- const size = getOptionPickerContentSize(options.length, colorSection);
507
+ const colorSection = seatTwo === "cpu" ? { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, previewValue } : void 0;
508
+ const pickerId = getOpponentPopoverId(idPrefix);
509
+ const size = getOptionPickerContentSize(options, colorSection);
519
510
  const useAlign = usePickerAlign ?? useZoneClampedAlign;
520
511
  const align = useAlign(host.openId === pickerId, size.width, size.height);
521
- const ownPopoverOpen = host.openId?.startsWith(`${idPrefix}-`) ?? false;
522
- const pickerRowOpen = secondPickerId !== void 0 && host.openId === secondPickerId;
523
- return /* @__PURE__ */ jsxs3(View2, { style: [styles2.panel, ownPopoverOpen && styles2.panelOpen], children: [
524
- /* @__PURE__ */ jsx10(OptionPicker, { id: pickerId, host, options, value, onChange: handleChange, color: drawn, dark, icon, colorSection, alignOverride: align, triggerStyle }),
525
- isPerson && secondPicker && /* @__PURE__ */ jsx10(View2, { style: [styles2.pickerRow, pickerRowOpen && styles2.pickerRowOpen], children: secondPicker }),
526
- isPerson && showReadyButton && onToggleReady && /* @__PURE__ */ jsx10(ReadyButton, { color: drawn, ready: ready ?? false, onToggleReady })
527
- ] });
512
+ return /* @__PURE__ */ jsx10(View2, { style: [styles2.button, host.openId === pickerId && styles2.buttonOpen], children: /* @__PURE__ */ jsx10(OptionPicker, { id: pickerId, host, options, value, onChange: handleChange, color: drawn, dark, icon, colorSection, alignOverride: align, triggerStyle }) });
528
513
  }
529
514
  var styles2 = StyleSheet2.create({
530
- panel: {
531
- alignItems: "center",
532
- gap: 12
533
- },
534
- // A popover escaping this panel needs the panel itself elevated to paint above a later sibling.
535
- panelOpen: {
536
- zIndex: 100
537
- },
538
- pickerRow: {
539
- alignItems: "center",
540
- flexDirection: "row",
541
- gap: 12
515
+ // Its own column, so a row of seat buttons doesn't stretch the pill to the tallest of them.
516
+ button: {
517
+ alignItems: "center"
542
518
  },
543
- // Lifts the controls picker's popover above the Ready button below it (each View is its own
544
- // stacking context on the web).
545
- pickerRowOpen: {
519
+ // A popover escaping this button needs the button itself elevated to paint above a later sibling.
520
+ buttonOpen: {
546
521
  zIndex: 100
547
522
  }
548
523
  });
@@ -582,18 +557,28 @@ function useAppSettings() {
582
557
  }
583
558
 
584
559
  // src/components/SettingsDialog.tsx
585
- import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
560
+ import { jsx as jsx11, jsxs as jsxs3 } from "react/jsx-runtime";
561
+ var noSettingsRules = () => null;
586
562
  function SettingsDialog({ visible, onDismiss, onShowHowToPlay, children }) {
587
563
  const onUpdateError = useUpdateErrorToast2();
588
564
  const { settings, setSettings } = useAppSettings();
589
- const { otaVersion, SettingsSections, edgeGuard = true } = usePack();
565
+ const { otaVersion, SettingsSections, edgeGuard = true, useSettingsRules = noSettingsRules } = usePack();
566
+ const rules = useSettingsRules();
590
567
  const edgeGuardProps = edgeGuard ? { deferBottomEdgeGestures: settings.deferBottomEdgeGestures, onDeferBottomEdgeGestures: (value) => setSettings({ deferBottomEdgeGestures: value }) } : {};
591
- return /* @__PURE__ */ jsxs4(BaseSettingsDialog, { visible, onDismiss, version: otaVersion, ...edgeGuardProps, onUpdateError, onShowHowToPlay, children: [
568
+ return /* @__PURE__ */ jsxs3(BaseSettingsDialog, { visible, onDismiss, version: otaVersion, ...edgeGuardProps, onUpdateError, onShowHowToPlay, leading: rules, children: [
592
569
  children,
593
570
  SettingsSections && /* @__PURE__ */ jsx11(SettingsSections, {})
594
571
  ] });
595
572
  }
596
573
 
574
+ // src/hooks/useAtComputer.ts
575
+ import { useIsTouchPrimaryDevice as useIsTouchPrimaryDevice2 } from "@tastic/core";
576
+ import { Platform as Platform3 } from "react-native";
577
+ function useAtComputer() {
578
+ const touchPrimary = useIsTouchPrimaryDevice2();
579
+ return Platform3.OS === "web" && !touchPrimary;
580
+ }
581
+
597
582
  // src/hooks/useOpponent.ts
598
583
  import { useCallback as useCallback7 } from "react";
599
584
  import { useDispatch as useDispatch7, useSelector as useSelector6 } from "react-redux";
@@ -747,8 +732,10 @@ var liveplayActions = actions5;
747
732
  var liveplaySlice_default = reducer5;
748
733
 
749
734
  // src/screens/LoadoutScreen.tsx
750
- import { jsx as jsx12, jsxs as jsxs5 } from "react/jsx-runtime";
735
+ import { jsx as jsx12, jsxs as jsxs4 } from "react/jsx-runtime";
751
736
  var PANEL_SWAP_FADE_MS = 180;
737
+ var P1_CONTROL_ID = "p1-control";
738
+ var P2_CONTROL_ID = "p2-control";
752
739
  function LoadoutScreen({ game }) {
753
740
  const pack = usePack();
754
741
  const { seatTwo: savedSeatTwo, setSeatTwo } = useOpponent();
@@ -761,7 +748,9 @@ function LoadoutScreen({ game }) {
761
748
  const { openFor } = useGameGuide();
762
749
  const { profiles, lastSelected, selectProfile } = useProfiles();
763
750
  const { orientationMode } = useOrientationState();
764
- const panelMode = p2IsHuman && !game.sideBySideWhenWide ? "faceToFace" : orientationMode;
751
+ const atComputer = useAtComputer();
752
+ const twoPlayerMode = game.sideBySideAtComputer && atComputer ? "sideBySide" : game.sideBySideWhenWide ? orientationMode : "faceToFace";
753
+ const panelMode = p2IsHuman ? twoPlayerMode : orientationMode;
765
754
  const { panelLayout, panelFadeStyle } = useDualZoneLayout(panelMode, false, true, false, PANEL_SWAP_FADE_MS);
766
755
  const playersRowFade = panelFadeStyle;
767
756
  const [settingsOpen, setSettingsOpen] = useState4(false);
@@ -770,7 +759,7 @@ function LoadoutScreen({ game }) {
770
759
  const { p1Color, p2Color, handleP1ColorChange, handleP2ColorChange } = useSeatColors(p2IsHuman);
771
760
  const dispatch = useDispatch9();
772
761
  const handleP1ProfileSelect = useCallback9((profile) => selectProfile(1, profile?.id ?? null), [selectProfile]);
773
- const handleP2ProfileSelect = useCallback9((profileId) => selectProfile(2, profileId), [selectProfile]);
762
+ const handleP2ProfileSelect = useCallback9((profile) => selectProfile(2, profile?.id ?? null), [selectProfile]);
774
763
  const [ready, setReady] = useState4({ 1: false, 2: false });
775
764
  useFocusEffect2(
776
765
  useCallback9(() => {
@@ -801,16 +790,16 @@ function LoadoutScreen({ game }) {
801
790
  const p1Drawn = seatPreview ? seatPreview(p1Color) : p1Color;
802
791
  const p2Drawn = seatPreview ? seatPreview(p2Color) : p2Color;
803
792
  const triggerStyle = useMemo5(() => pack.Backdrop ? [styles3.pill, { backgroundColor: themeColors.surface }] : void 0, [pack.Backdrop, themeColors.surface]);
793
+ const readySurface = pack.Backdrop ? themeColors.surface : void 0;
804
794
  const isPortrait = panelLayout.orientationMode === "faceToFace";
805
795
  const showMetaInSharedBand = needsSharedNeutralZone(panelLayout.orientationMode, humanSeats.length);
806
796
  const p2ZoneStyle = p2IsHuman ? isPortrait ? styles3.p2ZoneTop : styles3.p2ZoneRight : null;
807
- const showReadyButton = p2IsHuman;
808
797
  const seatPickerAlign = p2IsHuman ? void 0 : pack.usePickerAlign;
809
798
  const layoutKey = p2IsHuman ? "zones" : "row";
810
799
  const controlsHost = usePopoverHost();
811
- const p2PanelHost = usePopoverHost();
812
- const p2Host = p2IsHuman ? p2PanelHost : controlsHost;
813
- const anyPlayerPickerOpen = !!(controlsHost.openId?.startsWith("p1-") || controlsHost.openId?.startsWith("p2-"));
800
+ const p2Host = usePopoverHost();
801
+ const p1PickerOpen = controlsHost.openId?.startsWith("p1-") ?? false;
802
+ const p1ControlOpen = controlsHost.openId === P1_CONTROL_ID;
814
803
  const optionsRow = loadout.options({
815
804
  host: controlsHost,
816
805
  accentColor: themeColors.tertiary,
@@ -825,11 +814,23 @@ function LoadoutScreen({ game }) {
825
814
  const sharedControls = showMetaInSharedBand ? /* @__PURE__ */ jsx12(SharedActionBand, { onBack: safeBack, onSettings: () => setSettingsOpen(true), onRandomize: loadout.randomize, onReset: loadout.reset, fg, popoverOpen: optionsPopoverOpen, children: optionsRow }) : optionsRow;
826
815
  const controlLook = { accentColor: themeColors.tertiary, mutedColor: fgMuted, dark };
827
816
  const p1Control = loadout.seatControl(1, controlsHost, { ...controlLook, seatColor: p1Drawn });
828
- 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: pack.guestLabel ?? "P1", nullLabel: pack.nullLabel, onProfileSelect: handleP1ProfileSelect, onManageProfiles: () => router2.push(PROFILES_HREF), previewValue: seatPreview, triggerStyle, useSeatPickerAlign: seatPickerAlign }, `p1-${layoutKey}`);
817
+ 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, dark, showReadyButton: false, profiles, selectedProfileId: lastSelected[1], takenProfileId: p2IsHuman ? lastSelected[2] : null, guestLabel: pack.guestLabel ?? "P1", nullLabel: pack.nullLabel, onProfileSelect: handleP1ProfileSelect, onManageProfiles: () => router2.push(PROFILES_HREF), previewValue: seatPreview, triggerStyle, useSeatPickerAlign: seatPickerAlign }, `p1-${layoutKey}`);
818
+ const opponentPicker = game.seatTwo && /* @__PURE__ */ jsx12(OpponentPicker, { idPrefix: "p1", host: controlsHost, choices: game.seatTwo, seatTwo, onSeatTwoChange: changeSeatTwo, cpuDifficulty: loadout.cpuDifficulty, color: p2Color, onColorChange: handleP2ColorChange, swatches: defaultColors, takenColor: p1Color, allowSwapTaken: mode === "vsCpu", dark, previewValue: seatPreview, triggerStyle, usePickerAlign: seatPickerAlign }, `opponent-${layoutKey}`);
819
+ const toggleP1Ready = () => setReady((r) => ({ ...r, 1: !r[1] }));
820
+ const seatOne = /* @__PURE__ */ jsxs4(View3, { style: [styles3.seatOne, !p2IsHuman && styles3.seatOneAlone, p1PickerOpen && styles3.raised], children: [
821
+ /* @__PURE__ */ jsxs4(View3, { style: [styles3.seatOneButtons, p1PickerOpen && styles3.raised], children: [
822
+ /* @__PURE__ */ jsxs4(Animated.View, { style: [styles3.playersRow, p1PickerOpen && styles3.raised, !p2IsHuman && playersRowFade], children: [
823
+ p1Panel,
824
+ opponentPicker
825
+ ] }),
826
+ p1Control && /* @__PURE__ */ jsx12(View3, { style: [styles3.controlRow, p1ControlOpen && styles3.raised], children: p1Control })
827
+ ] }),
828
+ /* @__PURE__ */ jsx12(ReadyButton, { color: p1Drawn, ready: ready[1], onToggleReady: toggleP1Ready, surfaceColor: readySurface, dark, style: !p2IsHuman && p1PickerOpen && styles3.readyButtonHidden })
829
+ ] });
829
830
  const p2Control = p2IsHuman ? loadout.seatControl(2, p2Host, { ...controlLook, seatColor: p2Drawn }) : void 0;
830
- 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, previewValue: seatPreview, triggerStyle, usePickerAlign: seatPickerAlign }, `p2-${layoutKey}`);
831
- return /* @__PURE__ */ jsxs5(View3, { style: [styles3.container, { backgroundColor: bg }], children: [
832
- /* @__PURE__ */ jsxs5(View3, { style: styles3.frame, children: [
831
+ const p2Panel = p2IsHuman && /* @__PURE__ */ jsx12(PlayerSetupPanel, { idPrefix: "p2", host: p2Host, color: p2Color, onColorChange: handleP2ColorChange, swatches: defaultColors, takenColor: p1Color, colorCaption: pack.colorCaption, isHuman: true, ready: ready[2], onToggleReady: () => setReady((r) => ({ ...r, 2: !r[2] })), dark, secondPicker: p2Control, secondPickerId: p2Control ? P2_CONTROL_ID : void 0, profiles, selectedProfileId: lastSelected[2], takenProfileId: lastSelected[1], guestLabel: pack.guestLabel ?? "P2", nullLabel: pack.nullLabel, onProfileSelect: handleP2ProfileSelect, previewValue: seatPreview, triggerStyle, readySurfaceColor: readySurface }, "p2-zones");
832
+ return /* @__PURE__ */ jsxs4(View3, { style: [styles3.container, { backgroundColor: bg }], children: [
833
+ /* @__PURE__ */ jsxs4(View3, { style: styles3.frame, children: [
833
834
  /* @__PURE__ */ jsx12(PressAwayOverlay, { active: controlsHost.openId !== null, onPress: controlsHost.close }),
834
835
  p2ZoneStyle && /* @__PURE__ */ jsx12(PressAwayOverlay, { active: controlsHost.openId !== null || p2Host.openId !== null, onPress: p2Host.close, style: p2ZoneStyle }),
835
836
  !showMetaInSharedBand && /* @__PURE__ */ jsx12(CornerActionButtons, { onBack: safeBack, onSettings: () => setSettingsOpen(true), fg, insets }),
@@ -838,20 +839,16 @@ function LoadoutScreen({ game }) {
838
839
  {
839
840
  panelLayout,
840
841
  panelFadeStyle,
841
- p1: p1Panel,
842
+ p1: seatOne,
842
843
  p2: p2Panel,
843
844
  shared: sharedControls,
844
- p1Elevated: controlsHost.openId?.startsWith("p1-") ?? false,
845
+ p1Elevated: p1PickerOpen,
845
846
  p2Elevated: p2Host.openId !== null,
846
847
  sharedElevated: optionsPopoverOpen
847
848
  }
848
- ) : /* @__PURE__ */ jsxs5(View3, { style: styles3.stackedZone, children: [
849
+ ) : /* @__PURE__ */ jsxs4(View3, { style: styles3.stackedZone, children: [
849
850
  sharedControls,
850
- /* @__PURE__ */ jsxs5(Animated.View, { style: [styles3.playersRow, anyPlayerPickerOpen && styles3.playersRowOpen, playersRowFade], children: [
851
- p1Panel,
852
- p2Panel
853
- ] }),
854
- /* @__PURE__ */ jsx12(ReadyButton2, { color: p1Drawn, ready: ready[1], onToggleReady: () => setReady((r) => ({ ...r, 1: !r[1] })), style: anyPlayerPickerOpen && styles3.readyButtonHidden })
851
+ seatOne
855
852
  ] })
856
853
  ] }),
857
854
  /* @__PURE__ */ jsx12(SettingsDialog, { visible: settingsOpen, onDismiss: () => setSettingsOpen(false), onShowHowToPlay: () => openFor(game.id) })
@@ -865,6 +862,12 @@ var styles3 = StyleSheet3.create({
865
862
  container: {
866
863
  flex: 1
867
864
  },
865
+ // Player 1's controls, under their row of buttons.
866
+ controlRow: {
867
+ alignItems: "center",
868
+ flexDirection: "row",
869
+ gap: 12
870
+ },
868
871
  // A real full-bleed box to size the absolutely-positioned children against.
869
872
  frame: {
870
873
  alignItems: "center",
@@ -891,21 +894,37 @@ var styles3 = StyleSheet3.create({
891
894
  pill: {
892
895
  borderRadius: 999
893
896
  },
894
- // The one-human row: seat 1's panel and seat 2's button read as one evenly spaced row of buttons,
895
- // at PlayerSetupPanel's own pickerRow gap (12).
897
+ // Seat 1's button and the opponent button read as one evenly spaced row of buttons, at
898
+ // PlayerSetupPanel's own pickerRow gap (12).
896
899
  playersRow: {
897
900
  flexDirection: "row",
898
901
  gap: 12
899
902
  },
900
- playersRowOpen: {
903
+ // Lifts whichever piece of player 1's has a menu open above its later siblings (each View is its
904
+ // own stacking context on the web).
905
+ raised: {
901
906
  zIndex: 100
902
907
  },
903
- // See anyPlayerPickerOpen: opacity 0 (not display:'none') keeps the button's layout space, and
908
+ // See p1PickerOpen: opacity 0 (not display:'none') keeps the button's layout space, and
904
909
  // pointerEvents:'none' keeps the sliver an open menu doesn't cover from being a hidden tap target.
905
910
  readyButtonHidden: {
906
911
  opacity: 0,
907
912
  pointerEvents: "none"
908
913
  },
914
+ // Player 1's column: their row of buttons and controls, then Ready, at PlayerSetupPanel's own
915
+ // rhythm in their half of 2 Player.
916
+ seatOne: {
917
+ alignItems: "center",
918
+ gap: 12
919
+ },
920
+ // With one person, Ready sits as far under the buttons as the match options sit above them.
921
+ seatOneAlone: {
922
+ gap: 28
923
+ },
924
+ seatOneButtons: {
925
+ alignItems: "center",
926
+ gap: 12
927
+ },
909
928
  stackedZone: {
910
929
  alignItems: "center",
911
930
  gap: 28
@@ -1031,7 +1050,7 @@ function useHomeLayout() {
1031
1050
  }
1032
1051
 
1033
1052
  // src/screens/HomeScreen.tsx
1034
- import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs6 } from "react/jsx-runtime";
1053
+ import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs5 } from "react/jsx-runtime";
1035
1054
  function HomeScreen() {
1036
1055
  const pack = usePack();
1037
1056
  const { consumeResumeGame } = useLastActiveGame();
@@ -1099,19 +1118,19 @@ function HomeMenu() {
1099
1118
  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 });
1100
1119
  const pressAway = /* @__PURE__ */ jsx14(PressAwayOverlay2, { active: pickerHost.openId !== null, onPress: pickerHost.close, style: styles4.pressAway });
1101
1120
  const RulesSheet = rulesGame?.Rules;
1102
- return /* @__PURE__ */ jsxs6(View4, { style: [styles4.container, { backgroundColor: bg }], children: [
1103
- /* @__PURE__ */ jsx14(View4, { style: styles4.frame, children: /* @__PURE__ */ jsxs6(RulesHost, { enabled: pack.games.some((game) => game.Rules), children: [
1104
- isWide ? /* @__PURE__ */ jsxs6(Fragment4, { children: [
1121
+ return /* @__PURE__ */ jsxs5(View4, { style: [styles4.container, { backgroundColor: bg }], children: [
1122
+ /* @__PURE__ */ jsx14(View4, { style: styles4.frame, children: /* @__PURE__ */ jsxs5(RulesHost, { enabled: pack.games.some((game) => game.Rules), children: [
1123
+ isWide ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
1105
1124
  pressAway,
1106
- /* @__PURE__ */ jsxs6(FadingScrollView, { ...tab && gridClamp, style: [styles4.wideRightPane, { left: layout.paddingLeft + layout.leftPaneWidth + HOME_WIDE_PANE_GAP }], contentContainerStyle: { paddingTop: 16 + insets.top, paddingBottom: 16 + insets.bottom, paddingRight: layout.paddingRight }, children: [
1125
+ /* @__PURE__ */ jsxs5(FadingScrollView, { ...tab && gridClamp, style: [styles4.wideRightPane, { left: layout.paddingLeft + layout.leftPaneWidth + HOME_WIDE_PANE_GAP }], contentContainerStyle: { paddingTop: 16 + insets.top, paddingBottom: 16 + insets.bottom, paddingRight: layout.paddingRight }, children: [
1107
1126
  tabBar && /* @__PURE__ */ jsx14(View4, { style: [styles4.wideTabs, { width: layout.rightPaneWidth }], children: tabBar }),
1108
1127
  /* @__PURE__ */ jsx14(View4, { style: [styles4.grid, { width: layout.rightPaneWidth }], children: pack.games.map((game) => /* @__PURE__ */ jsx14(GameTile, { game, hidden: isHidden(tab, game), width: layout.tileWidth, previewWidth: layout.previewWidth, previewRevealed: previewsRevealed, dark, p1Color, p2Color, onPress: () => openGame(game), onRules: hasRules(game) ? () => openRules(game) : void 0 }, game.id)) })
1109
1128
  ] }),
1110
- /* @__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: [
1129
+ /* @__PURE__ */ jsxs5(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: [
1111
1130
  /* @__PURE__ */ jsx14(pack.HeroTitle, { words: pack.wordmark, color: fg, p1Color, p2Color, active: heroActive, fit: layout.heroFit }),
1112
1131
  /* @__PURE__ */ jsx14(View4, { style: styles4.widePicker, children: picker })
1113
1132
  ] })
1114
- ] }) : /* @__PURE__ */ jsxs6(FadingScrollView, { ...tab && columnClamp, style: styles4.scroll, contentContainerStyle: [styles4.content, { paddingTop: 56 + insets.top, paddingBottom: 24 + insets.bottom, paddingLeft: 16 + insets.left, paddingRight: 16 + insets.right }], children: [
1133
+ ] }) : /* @__PURE__ */ jsxs5(FadingScrollView, { ...tab && columnClamp, style: styles4.scroll, contentContainerStyle: [styles4.content, { paddingTop: 56 + insets.top, paddingBottom: 24 + insets.bottom, paddingLeft: 16 + insets.left, paddingRight: 16 + insets.right }], children: [
1115
1134
  pressAway,
1116
1135
  /* @__PURE__ */ jsx14(pack.HeroTitle, { words: pack.wordmark, color: fg, p1Color, p2Color, active: heroActive }),
1117
1136
  /* @__PURE__ */ jsx14(View4, { style: [styles4.picker, pickerHost.openId !== null && styles4.pickerOpen], children: picker }),
@@ -1167,10 +1186,10 @@ function GameRow({ game, hidden, dark, p1Color, p2Color, previewRevealed, onPres
1167
1186
  return (
1168
1187
  // The rules button sits beside the row's own button, laid over its end, not inside it: a button
1169
1188
  // inside a button is invalid on the web.
1170
- /* @__PURE__ */ jsxs6(View4, { style: hidden ? styles4.hidden : void 0, children: [
1171
- /* @__PURE__ */ jsxs6(Pressable, { onPress, style: [styles4.row, { backgroundColor: well.bg }], accessibilityRole: "button", accessibilityLabel: game.title, children: [
1189
+ /* @__PURE__ */ jsxs5(View4, { style: hidden ? styles4.hidden : void 0, children: [
1190
+ /* @__PURE__ */ jsxs5(Pressable, { onPress, style: [styles4.row, { backgroundColor: well.bg }], accessibilityRole: "button", accessibilityLabel: game.title, children: [
1172
1191
  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 }) }),
1173
- /* @__PURE__ */ jsxs6(View4, { style: styles4.rowText, children: [
1192
+ /* @__PURE__ */ jsxs5(View4, { style: styles4.rowText, children: [
1174
1193
  /* @__PURE__ */ jsx14(Text, { variant: "titleLarge", style: [styles4.rowTitle, { color: well.fg }], numberOfLines: 1, children: game.title }),
1175
1194
  /* @__PURE__ */ jsx14(Text, { variant: "bodyMedium", style: { color: well.fgMuted }, numberOfLines: 2, children: game.blurb })
1176
1195
  ] })
@@ -1181,8 +1200,8 @@ function GameRow({ game, hidden, dark, p1Color, p2Color, previewRevealed, onPres
1181
1200
  }
1182
1201
  function GameTile({ game, hidden, width, previewWidth, dark, p1Color, p2Color, previewRevealed, onPress, onRules }) {
1183
1202
  const well = useWellColors();
1184
- return /* @__PURE__ */ jsxs6(View4, { style: [{ width }, hidden && styles4.hidden], children: [
1185
- /* @__PURE__ */ jsxs6(Pressable, { onPress, style: [styles4.tile, { backgroundColor: well.bg }], accessibilityRole: "button", accessibilityLabel: game.title, children: [
1203
+ return /* @__PURE__ */ jsxs5(View4, { style: [{ width }, hidden && styles4.hidden], children: [
1204
+ /* @__PURE__ */ jsxs5(Pressable, { onPress, style: [styles4.tile, { backgroundColor: well.bg }], accessibilityRole: "button", accessibilityLabel: game.title, children: [
1186
1205
  /* @__PURE__ */ jsx14(PreviewBox, { game, width: previewWidth, revealed: previewRevealed, dark, p1Color, p2Color }),
1187
1206
  /* @__PURE__ */ jsx14(Text, { variant: "titleMedium", style: [styles4.rowTitle, { color: well.fg }], numberOfLines: 1, children: game.title }),
1188
1207
  /* @__PURE__ */ jsx14(Text, { variant: "bodySmall", style: { color: well.fgMuted }, numberOfLines: 3, children: game.blurb })
@@ -1368,7 +1387,7 @@ import { StyleSheet as StyleSheet5, View as View5 } from "react-native";
1368
1387
  import { SegmentedButtons as SegmentedButtons2, Text as Text2 } from "react-native-paper";
1369
1388
  import { useSafeAreaInsets as useSafeAreaInsets3 } from "react-native-safe-area-context";
1370
1389
  import { useDispatch as useDispatch10 } from "react-redux";
1371
- import { jsx as jsx16, jsxs as jsxs7 } from "react/jsx-runtime";
1390
+ import { jsx as jsx16, jsxs as jsxs6 } from "react/jsx-runtime";
1372
1391
  var STATS = "stats";
1373
1392
  var ACHIEVEMENTS = "achievements";
1374
1393
  var TABS = [
@@ -1421,10 +1440,10 @@ function StatsContent({ pickerHost }) {
1421
1440
  const surface = pack.Backdrop ? { backgroundColor: colors.surface } : void 0;
1422
1441
  const triggerStyle = pack.Backdrop ? [styles5.pill, surface] : void 0;
1423
1442
  const PackTabContent = packTab?.Content;
1424
- 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: [
1443
+ return /* @__PURE__ */ jsxs6(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: [
1425
1444
  /* @__PURE__ */ jsx16(PressAwayOverlay3, { active: pickerHost.openId !== null, onPress: pickerHost.close, style: styles5.pressAway }),
1426
1445
  /* @__PURE__ */ jsx16(SegmentedButtons2, { value: tab, onValueChange: setTab, buttons: tabButtons, style: [styles5.tabs, surface] }),
1427
- /* @__PURE__ */ jsxs7(View5, { style: [styles5.filters, pickerHost.openId !== null && styles5.filtersOpen], onStartShouldSetResponder: () => pickerHost.openId !== null, children: [
1446
+ /* @__PURE__ */ jsxs6(View5, { style: [styles5.filters, pickerHost.openId !== null && styles5.filtersOpen], onStartShouldSetResponder: () => pickerHost.openId !== null, children: [
1428
1447
  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 }),
1429
1448
  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 })
1430
1449
  ] }),
@@ -1445,7 +1464,7 @@ function GameStats({ game, tab, profileId, showTitle, titleColor }) {
1445
1464
  const { StatsSections, AchievementList } = game;
1446
1465
  const Sections = tab === STATS ? StatsSections : AchievementList;
1447
1466
  if (!Sections) return null;
1448
- return /* @__PURE__ */ jsxs7(View5, { style: styles5.game, children: [
1467
+ return /* @__PURE__ */ jsxs6(View5, { style: styles5.game, children: [
1449
1468
  showTitle && /* @__PURE__ */ jsx16(Text2, { variant: "titleLarge", style: [styles5.gameTitle, { color: titleColor }], children: game.title }),
1450
1469
  /* @__PURE__ */ jsx16(Sections, { profileId })
1451
1470
  ] });
@@ -1496,20 +1515,19 @@ function imageGameIcon(images) {
1496
1515
  import { useAutoPaperTheme as useAutoPaperTheme6 } from "@rific/auto-paper";
1497
1516
  import { IconButton as IconButton2 } from "@rific/feedback-press";
1498
1517
  import { useEffect as useEffect7 } from "react";
1499
- import { Platform as Platform3, StyleSheet as StyleSheet6, useWindowDimensions as useWindowDimensions2 } from "react-native";
1518
+ import { StyleSheet as StyleSheet6, useWindowDimensions as useWindowDimensions2 } from "react-native";
1500
1519
  import Animated2, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
1501
1520
  import { useSafeAreaInsets as useSafeAreaInsets4 } from "react-native-safe-area-context";
1502
- import { Fragment as Fragment5, jsx as jsx18, jsxs as jsxs8 } from "react/jsx-runtime";
1503
- var MATCH_CHIPS_DESKTOP_MIN_WIDTH = 768;
1521
+ import { Fragment as Fragment5, jsx as jsx18, jsxs as jsxs7 } from "react/jsx-runtime";
1504
1522
  var MATCH_CHIPS_FADE_MS = 150;
1505
1523
  var CHIP_SIZE = 48;
1506
1524
  var EDGE_PAD = 8;
1507
1525
  function MatchChips({ playing, faceToFace, centerLineY, onBack, onSettings }) {
1508
- const { width, height } = useWindowDimensions2();
1526
+ const { height } = useWindowDimensions2();
1509
1527
  const insets = useSafeAreaInsets4();
1510
1528
  const { dark } = useAutoPaperTheme6();
1511
- const desktopWeb = Platform3.OS === "web" && width >= MATCH_CHIPS_DESKTOP_MIN_WIDTH;
1512
- const visible = desktopWeb || !playing;
1529
+ const atComputer = useAtComputer();
1530
+ const visible = atComputer || !playing;
1513
1531
  const opacity = useSharedValue(visible ? 1 : 0);
1514
1532
  useEffect7(() => {
1515
1533
  opacity.value = withTiming(visible ? 1 : 0, { duration: MATCH_CHIPS_FADE_MS });
@@ -1517,12 +1535,12 @@ function MatchChips({ playing, faceToFace, centerLineY, onBack, onSettings }) {
1517
1535
  const fadeStyle = useAnimatedStyle(() => ({ opacity: opacity.value }));
1518
1536
  const iconColor = dark ? "#FFFFFF" : "#000000";
1519
1537
  const containerColor = dark ? "rgba(0,0,0,0.55)" : "rgba(255,255,255,0.55)";
1520
- const between = faceToFace && !desktopWeb;
1538
+ const between = faceToFace && !atComputer;
1521
1539
  const top = between ? (centerLineY ?? height / 2) - CHIP_SIZE / 2 : EDGE_PAD + insets.top;
1522
1540
  const backSlot = { top, left: EDGE_PAD + insets.left };
1523
1541
  const settingsSlot = { top, right: EDGE_PAD + insets.right };
1524
1542
  const pointerEvents = visible ? "box-none" : "none";
1525
- return /* @__PURE__ */ jsxs8(Fragment5, { children: [
1543
+ return /* @__PURE__ */ jsxs7(Fragment5, { children: [
1526
1544
  /* @__PURE__ */ jsx18(Animated2.View, { pointerEvents, style: [styles6.slot, backSlot, fadeStyle], testID: "match-chips-back", children: /* @__PURE__ */ jsx18(IconButton2, { icon: "arrow-left", iconColor, containerColor, style: styles6.chip, size: 24, onPress: onBack, accessibilityLabel: "Back" }) }),
1527
1545
  /* @__PURE__ */ jsx18(Animated2.View, { pointerEvents, style: [styles6.slot, settingsSlot, fadeStyle], testID: "match-chips-settings", children: /* @__PURE__ */ jsx18(IconButton2, { icon: "cog", iconColor, containerColor, style: styles6.chip, size: 24, onPress: onSettings, accessibilityLabel: "Settings" }) })
1528
1546
  ] });
@@ -1787,7 +1805,6 @@ export {
1787
1805
  GUEST_PLAYER_ID,
1788
1806
  HomeScreen,
1789
1807
  LoadoutRoute,
1790
- MATCH_CHIPS_DESKTOP_MIN_WIDTH,
1791
1808
  MATCH_CHIPS_FADE_MS,
1792
1809
  MatchChips,
1793
1810
  PREVIEW_ASPECT,
@@ -1825,6 +1842,7 @@ export {
1825
1842
  seatTwoFor,
1826
1843
  useActiveRound,
1827
1844
  useAppSettings,
1845
+ useAtComputer,
1828
1846
  useGameGuide,
1829
1847
  useOpponent,
1830
1848
  usePack,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/shell",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "The app around a pack of React Native games, two-seat or for one player: Home, the loadout, the stats screen, Profiles and Settings, the pack's Redux store and its root providers, driven by one PackDefinition",
5
5
  "keywords": [
6
6
  "react-native",
@@ -70,12 +70,12 @@
70
70
  "@expo/vector-icons": "^15.1.1",
71
71
  "@infinitetoken/eslint-config": "^0.2.1",
72
72
  "@infinitetoken/jest-config": "^0.5.2",
73
- "@infinitetoken/tsconfig": "^0.6.0",
73
+ "@infinitetoken/tsconfig": "^0.7.0",
74
74
  "@react-native-async-storage/async-storage": "2.2.0",
75
75
  "@react-native-masked-view/masked-view": "0.3.2",
76
76
  "@react-native/jest-preset": "^0.88.0",
77
77
  "@reduxjs/toolkit": "^2.14.0",
78
- "@rific/auto-paper": "^0.13.4",
78
+ "@rific/auto-paper": "^0.14.0",
79
79
  "@rific/core": "^0.5.0",
80
80
  "@rific/feedback-press": "^0.13.1",
81
81
  "@rific/focus-chain": "^0.4.8",
@@ -84,10 +84,10 @@
84
84
  "@rific/toaster": "^0.12.0",
85
85
  "@rific/updater": "^0.8.0",
86
86
  "@tastic/achievements": "^0.6.0",
87
- "@tastic/core": "^0.15.0",
87
+ "@tastic/core": "^0.16.0",
88
88
  "@tastic/edge-guard": "^0.1.5",
89
- "@tastic/hud": "^0.27.1",
90
- "@tastic/profile": "^0.17.0",
89
+ "@tastic/hud": "^0.29.0",
90
+ "@tastic/profile": "^0.18.0",
91
91
  "@tastic/split-screen": "^0.5.5",
92
92
  "@testing-library/react-native": "^14.1.0",
93
93
  "@types/jest": "30.0.0",
@@ -135,10 +135,10 @@
135
135
  "@rific/splash-gate": ">=0.2.3",
136
136
  "@rific/toaster": ">=0.10.1",
137
137
  "@tastic/achievements": ">=0.6.0",
138
- "@tastic/core": ">=0.15.0",
138
+ "@tastic/core": ">=0.16.0",
139
139
  "@tastic/edge-guard": ">=0.1.5",
140
- "@tastic/hud": ">=0.27.1",
141
- "@tastic/profile": ">=0.16.2",
140
+ "@tastic/hud": ">=0.29.0",
141
+ "@tastic/profile": ">=0.18.0",
142
142
  "@tastic/split-screen": ">=0.5.5",
143
143
  "expo-blur": ">=58.0.4",
144
144
  "expo-constants": ">=58.0.12",
@@ -1,13 +1,12 @@
1
1
  import { useAutoPaperTheme } from '@rific/auto-paper'
2
2
  import { IconButton } from '@rific/feedback-press'
3
3
  import { useEffect } from 'react'
4
- import { Platform, StyleSheet, useWindowDimensions } from 'react-native'
4
+ import { StyleSheet, useWindowDimensions } from 'react-native'
5
5
  import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'
6
6
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
7
7
 
8
- // A web window at least this wide is a computer: the chips stay up in the page's top corners all
9
- // match. The games' own desktop cutoff, the same number everywhere.
10
- export const MATCH_CHIPS_DESKTOP_MIN_WIDTH = 768
8
+ import { useAtComputer } from '../hooks/useAtComputer'
9
+
11
10
  // How long the chips take to fade out when play starts, and back in when it stops.
12
11
  export const MATCH_CHIPS_FADE_MS = 150
13
12
  // The chip's box: a 24 icon in IconButton's 48, with Paper's margin zeroed.
@@ -17,12 +16,12 @@ const EDGE_PAD = 8
17
16
  export interface MatchChipsProps {
18
17
  /** Whether play is on right now (a live puck, a shot in flight, a round being ridden). While it is,
19
18
  * the chips fade out and can't be pressed, so they never cover play; they come back the moment it
20
- * stops (between points, a round's card, Game Over). Ignored on desktop web, where they stay up. */
19
+ * stops (between points, a round's card, Game Over). Ignored at a computer, where they stay up. */
21
20
  playing: boolean
22
21
  /** Two players at opposite ends of one screen right now (2 Player face to face). The chips then sit
23
22
  * on the side edges at the line between the players, rather than in the top corners, which are the
24
- * far player's end. False for one human (vs CPU, solo) and for side by side. Ignored on desktop
25
- * web: both players share one upright screen there. */
23
+ * far player's end. False for one human (vs CPU, solo) and for side by side. Ignored at a
24
+ * computer: both players share one upright screen there. */
26
25
  faceToFace: boolean
27
26
  /** Where that line between the players is, as a y in the window. Defaults to the window's middle;
28
27
  * pass it when the board isn't centered (a band of score above it, say). */
@@ -34,13 +33,14 @@ export interface MatchChipsProps {
34
33
  /** The back and settings chips over a match, in every game of every pack: one rule for where they sit
35
34
  * and when they show. The window's top corners (the same spot as hud's CornerActionButtons on every
36
35
  * other screen), except 2 Player face to face, which puts them on the side edges at the line between
37
- * the players; faded out while play is on, except on desktop web. */
36
+ * the players; faded out while play is on, except at a computer (useAtComputer: the web without a
37
+ * touch-first screen), where they stay up in the page's corners. */
38
38
  export function MatchChips({ playing, faceToFace, centerLineY, onBack, onSettings }: MatchChipsProps) {
39
- const { width, height } = useWindowDimensions()
39
+ const { height } = useWindowDimensions()
40
40
  const insets = useSafeAreaInsets()
41
41
  const { dark } = useAutoPaperTheme()
42
- const desktopWeb = Platform.OS === 'web' && width >= MATCH_CHIPS_DESKTOP_MIN_WIDTH
43
- const visible = desktopWeb || !playing
42
+ const atComputer = useAtComputer()
43
+ const visible = atComputer || !playing
44
44
  const opacity = useSharedValue(visible ? 1 : 0)
45
45
  useEffect(() => {
46
46
  opacity.value = withTiming(visible ? 1 : 0, { duration: MATCH_CHIPS_FADE_MS })
@@ -49,7 +49,7 @@ export function MatchChips({ playing, faceToFace, centerLineY, onBack, onSetting
49
49
 
50
50
  const iconColor = dark ? '#FFFFFF' : '#000000'
51
51
  const containerColor = dark ? 'rgba(0,0,0,0.55)' : 'rgba(255,255,255,0.55)'
52
- const between = faceToFace && !desktopWeb
52
+ const between = faceToFace && !atComputer
53
53
  const top = between ? (centerLineY ?? height / 2) - CHIP_SIZE / 2 : EDGE_PAD + insets.top
54
54
  const backSlot = { top, left: EDGE_PAD + insets.left }
55
55
  const settingsSlot = { top, right: EDGE_PAD + insets.right }