@tastic/shell 0.2.0 → 0.3.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/README.md +6 -6
- package/dist/index.js +58 -38
- package/dist/index.mjs +40 -20
- package/package.json +3 -3
- package/src/components/SeatTwoPanel.tsx +21 -10
- package/src/screens/GameRoute.tsx +11 -4
- package/src/screens/LoadoutScreen.tsx +36 -9
- package/src/types.ts +12 -8
package/README.md
CHANGED
|
@@ -109,8 +109,8 @@ PuckTastic and DuelTastic draw what they always did:
|
|
|
109
109
|
|
|
110
110
|
| Field | |
|
|
111
111
|
| --- | --- |
|
|
112
|
-
| `Backdrop?` | A table drawn once behind the whole stack (Solitaire's felt). The stack's two layers go transparent (the screens' `contentStyle` and the navigation theme's `colors.background`, which React Navigation paints around every screen), the root window stays dark so light mode doesn't flash white on a push, Android's push and pop crossfade (its default holds the outgoing screen opaque), and Home, the stats screen and Profiles draw transparent, with their buttons and tabs on the theme's surface. |
|
|
113
|
-
| `foreground?` | The color of what those
|
|
112
|
+
| `Backdrop?` | A table drawn once behind the whole stack (Solitaire's felt). The stack's two layers go transparent (the screens' `contentStyle` and the navigation theme's `colors.background`, which React Navigation paints around every screen), the root window stays dark so light mode doesn't flash white on a push, Android's push and pop crossfade (its default holds the outgoing screen opaque), and Home, each loadout, the stats screen and Profiles draw transparent, with their buttons and tabs on the theme's surface. |
|
|
113
|
+
| `foreground?` | The color of what those screens draw straight on the background (the hero, the corner buttons, headers), and at half strength a loadout's muted options. Unset is black or white with the theme. |
|
|
114
114
|
| `Provider?` | Mounted once around How to Play and the stack, inside the store, the theme, profiles and every game's `Provider` (one stats context across the pack's games). |
|
|
115
115
|
| `useScreenOptions?` | A hook returning stack screen options (`StackScreenOptions`), merged after the shell's own (a light status bar, Reduce Motion's crossfade). |
|
|
116
116
|
| `useFeedbackSounds?` | A hook returning the two sounds every button plays (`FeedbackSounds`: `selection`, `notification`), in place of the shell's two wavs. |
|
|
@@ -118,16 +118,16 @@ PuckTastic and DuelTastic draw what they always did:
|
|
|
118
118
|
| `portraitWhileKeyboard?` | Every screen reads upright while the software keyboard is up (`@tastic/core`'s `OrientationProvider`), for naming a profile sideways. |
|
|
119
119
|
| `edgeGuard?` | `false` leaves Edge Guard out of Settings (no edge controls, no Edge Guard plugin). |
|
|
120
120
|
| `seatTheme?` | `(colors, dark) => ({ color, harmony })`: the theme the seats' colors make (a muted seed from seat 1's color), applied whenever they or dark mode change and as a fresh install's theme, in place of a triad of the raw colors. With it, `useSeatColors` reads each seat's color from where it's remembered (`seatColors`) rather than from the theme. |
|
|
121
|
-
| `previewColor?` | `(hex, dark) => string`: how every profile color is drawn (seat
|
|
121
|
+
| `previewColor?` | `(hex, dark) => string`: how every profile color is drawn (the seat buttons and their swatches on Home and in the loadouts, a loadout's Ready button and seat controls, each `ProfileChip`), mounted as `@tastic/profile`'s `ProfileColorProvider`. Saved colors stay as picked. |
|
|
122
122
|
| `guide?` | The pack's own How to Play (`PackGuide`: `version`, `selectVersionSeen`, `markSeen`, `getSteps({ touch })`), for cards that teach every game at once: shown on Home's first launch (never while a cold launch resumes a game), replayed from Home's Settings, never in a play screen. Its seen version is a key of the pack's own. |
|
|
123
|
-
| `guestLabel?`, `nullLabel?` | Seat 1's button: the chip's label for a guest (default `'P1'`) and the guest row's in its menu. |
|
|
123
|
+
| `guestLabel?`, `nullLabel?` | Seat 1's button on Home and in every loadout: the chip's label for a guest (default `'P1'`) and the guest row's in its menu. |
|
|
124
124
|
| `guestStats?` | The stats screen keeps a guest's stats under `GUEST_PLAYER_ID` (`'guest'`): a Guest entry between Everyone and the profiles once a profile exists, and the screen opens on seat 1's player. Without it the screen opens on Everyone, where guests count. |
|
|
125
125
|
| `AchievementList?` | One achievement list across the pack's games, on the Achievements tab, with the game picker hidden there. |
|
|
126
126
|
| `statsTabs?` | Tabs of the pack's own between Stats and Achievements (`PackStatsTab`: `value`, `label`, `Content`, handed both filters). With them every tab is as wide as its label, with no icons, so three fit a phone. |
|
|
127
127
|
| `resetStats?` | The stats screen's Reset where the pack resets stats itself (`PackStatsReset`: a `useReset` hook returning `(profileId) => void`, and the `labels` for a player's name, Guest, or `null` for Everyone). It follows the player picker, not the game picker. |
|
|
128
128
|
| `statsGameTitles?` | `false` leaves each game's name off its sections on All Games, where the sections name it. |
|
|
129
129
|
| `newProfileColor?` | A new profile's starting color on Profiles. Unset is auto-paper's first swatch. |
|
|
130
|
-
| `usePickerAlign?` | `(open, contentWidth, contentHeight, options?) => AlignResult` (`PickerAlign`), a hook: where seat 1's menu on Home, the stats screen's two pickers and a profile's color swatch open. hud's own placement measures a turned button against the upright window, so with the phone held sideways it can run a menu off the screen (seen on the iOS simulator, 2026-10-29); a pack whose screens turn hands in a frame-aware one (Solitaire's `useFrameAutoAlign`). Profiles passes `{ maxContentWidth: 640 }`, its roster's column. Unset is hud's placement. |
|
|
130
|
+
| `usePickerAlign?` | `(open, contentWidth, contentHeight, options?) => AlignResult` (`PickerAlign`), a hook: where seat 1's menu on Home, both seat buttons' menus in a loadout's one-human row (2 Player keeps each menu in its own player's half), the stats screen's two pickers and a profile's color swatch open. hud's own placement measures a turned button against the upright window, so with the phone held sideways it can run a menu off the screen (seen on the iOS simulator, 2026-10-29); a pack whose screens turn hands in a frame-aware one (Solitaire's `useFrameAutoAlign`). Profiles passes `{ maxContentWidth: 640 }`, its roster's column. Unset is hud's placement. |
|
|
131
131
|
|
|
132
132
|
### `GameDefinition`
|
|
133
133
|
|
|
@@ -204,7 +204,7 @@ became a package (the lower of the two where they differ).
|
|
|
204
204
|
| `@rific/feedback-press` | >=0.13.1 | haptic and sound settings, the UI sounds |
|
|
205
205
|
| `@rific/toaster` | >=0.10.1 | toasts (DuelTastic runs 0.12, PuckTastic 0.10) |
|
|
206
206
|
| `@tastic/core` | >=0.14.0 | orientation, rotation, the status bar and root background |
|
|
207
|
-
| `@tastic/hud` | >=0.
|
|
207
|
+
| `@tastic/hud` | >=0.27.1 | Settings, the stats screen, the loadout's panels (its Skia-free `/lobby` entry, new in 0.26.0; 0.27.1's options for a pack drawn on its own backdrop), How to Play |
|
|
208
208
|
| `@tastic/profile` | >=0.16.2 | profiles, the seat buttons, seat colors |
|
|
209
209
|
| `@tastic/achievements` | >=0.6.0 | `createGameStats`, `forgetProfileAchievements` |
|
|
210
210
|
| `@tastic/split-screen` | >=0.5.5 | the 2 Player loadout's two halves |
|
package/dist/index.js
CHANGED
|
@@ -557,6 +557,7 @@ var styles = import_react_native2.StyleSheet.create({
|
|
|
557
557
|
// src/screens/GameRoute.tsx
|
|
558
558
|
var import_expo_router5 = require("expo-router");
|
|
559
559
|
var import_react14 = require("react");
|
|
560
|
+
var import_react_native5 = require("react-native");
|
|
560
561
|
|
|
561
562
|
// src/routes.ts
|
|
562
563
|
var playHref = (game) => ({ pathname: "/play/[game]", params: { game } });
|
|
@@ -593,8 +594,9 @@ var CPU_ICON = "robot";
|
|
|
593
594
|
function getSeatTwoPopoverId(idPrefix) {
|
|
594
595
|
return `${idPrefix}-seat`;
|
|
595
596
|
}
|
|
596
|
-
function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDifficulty, profiles, selectedProfileId, onProfileSelect, color, onColorChange, swatches, takenColor, allowSwapTaken, colorCaption, dark, secondPicker, secondPickerId, ready, onToggleReady, showReadyButton = true }) {
|
|
597
|
+
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 }) {
|
|
597
598
|
const isPerson = seatTwo === "person";
|
|
599
|
+
const drawn = previewValue ? previewValue(color) : color;
|
|
598
600
|
const selectedProfile = isPerson ? profiles.find((p) => p.id === selectedProfileId) ?? null : null;
|
|
599
601
|
const cpuOptions = cpuDifficulty?.options;
|
|
600
602
|
const options = (0, import_react10.useMemo)(() => {
|
|
@@ -603,10 +605,10 @@ function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDi
|
|
|
603
605
|
if (choices.includes("nobody")) list.push({ value: NOBODY, label: "Nobody", icon: NOBODY_ICON });
|
|
604
606
|
if (choices.includes("person")) {
|
|
605
607
|
list.push({ value: GUEST, label: "Player 2", icon: GUEST_ICON });
|
|
606
|
-
list.push(...[...profiles].sort((a, b) => a.name.localeCompare(b.name)).map((p) => ({ value: `${PROFILE_PREFIX}${p.id}`, label: p.name, icon: "account", color: p.color })));
|
|
608
|
+
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 })));
|
|
607
609
|
}
|
|
608
610
|
return list;
|
|
609
|
-
}, [choices, cpuOptions, profiles]);
|
|
611
|
+
}, [choices, cpuOptions, profiles, previewValue]);
|
|
610
612
|
const value = seatTwo === "cpu" ? `${CPU_PREFIX}${cpuDifficulty?.value ?? ""}` : seatTwo === "nobody" ? NOBODY : selectedProfile ? `${PROFILE_PREFIX}${selectedProfile.id}` : GUEST;
|
|
611
613
|
const icon = seatTwo === "cpu" ? CPU_ICON : seatTwo === "nobody" ? NOBODY_ICON : selectedProfile ? "account" : GUEST_ICON;
|
|
612
614
|
const handleChange = (next) => {
|
|
@@ -620,16 +622,17 @@ function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDi
|
|
|
620
622
|
onSeatTwoChange("person");
|
|
621
623
|
}
|
|
622
624
|
};
|
|
623
|
-
const colorSection = seatTwo === "nobody" ? void 0 : { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, caption: selectedProfile ? colorCaption : void 0 };
|
|
625
|
+
const colorSection = seatTwo === "nobody" ? void 0 : { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, caption: selectedProfile ? colorCaption : void 0, previewValue };
|
|
624
626
|
const pickerId = getSeatTwoPopoverId(idPrefix);
|
|
625
627
|
const size = (0, import_profile4.getOptionPickerContentSize)(options.length, colorSection);
|
|
626
|
-
const
|
|
628
|
+
const useAlign = usePickerAlign ?? import_lobby.useZoneClampedAlign;
|
|
629
|
+
const align = useAlign(host.openId === pickerId, size.width, size.height);
|
|
627
630
|
const ownPopoverOpen = host.openId?.startsWith(`${idPrefix}-`) ?? false;
|
|
628
631
|
const pickerRowOpen = secondPickerId !== void 0 && host.openId === secondPickerId;
|
|
629
632
|
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native3.View, { style: [styles2.panel, ownPopoverOpen && styles2.panelOpen], children: [
|
|
630
|
-
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_profile4.OptionPicker, { id: pickerId, host, options, value, onChange: handleChange, color, dark, icon, colorSection, alignOverride: align }),
|
|
633
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_profile4.OptionPicker, { id: pickerId, host, options, value, onChange: handleChange, color: drawn, dark, icon, colorSection, alignOverride: align, triggerStyle }),
|
|
631
634
|
isPerson && secondPicker && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native3.View, { style: [styles2.pickerRow, pickerRowOpen && styles2.pickerRowOpen], children: secondPicker }),
|
|
632
|
-
isPerson && showReadyButton && onToggleReady && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lobby.ReadyButton, { color, ready: ready ?? false, onToggleReady })
|
|
635
|
+
isPerson && showReadyButton && onToggleReady && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lobby.ReadyButton, { color: drawn, ready: ready ?? false, onToggleReady })
|
|
633
636
|
] });
|
|
634
637
|
}
|
|
635
638
|
var styles2 = import_react_native3.StyleSheet.create({
|
|
@@ -869,13 +872,20 @@ function LoadoutScreen({ game }) {
|
|
|
869
872
|
dispatch(liveplayActions.setActiveRound({ game: game.id, mode, settings: roundSettingsRef.current }));
|
|
870
873
|
import_expo_router4.router.push(playHref(game.id));
|
|
871
874
|
}, [ready, humanSeats, game.id, mode, dispatch]);
|
|
872
|
-
const bg = dark ? "#000000" : "#FFFFFF";
|
|
873
|
-
const fg = dark ? "#FFFFFF" : "#000000";
|
|
874
|
-
const fgMuted = dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
|
|
875
|
+
const bg = pack.Backdrop ? "transparent" : dark ? "#000000" : "#FFFFFF";
|
|
876
|
+
const fg = pack.foreground ?? (dark ? "#FFFFFF" : "#000000");
|
|
877
|
+
const fgMuted = (pack.foreground && halfOf(pack.foreground)) ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
|
|
878
|
+
const { previewColor } = pack;
|
|
879
|
+
const seatPreview = (0, import_react13.useMemo)(() => previewColor ? (hex) => previewColor(hex, dark) : void 0, [previewColor, dark]);
|
|
880
|
+
const p1Drawn = seatPreview ? seatPreview(p1Color) : p1Color;
|
|
881
|
+
const p2Drawn = seatPreview ? seatPreview(p2Color) : p2Color;
|
|
882
|
+
const triggerStyle = (0, import_react13.useMemo)(() => pack.Backdrop ? [styles3.pill, { backgroundColor: themeColors.surface }] : void 0, [pack.Backdrop, themeColors.surface]);
|
|
875
883
|
const isPortrait = panelLayout.orientationMode === "faceToFace";
|
|
876
884
|
const showMetaInSharedBand = (0, import_split_screen.needsSharedNeutralZone)(panelLayout.orientationMode, humanSeats.length);
|
|
877
885
|
const p2ZoneStyle = p2IsHuman ? isPortrait ? styles3.p2ZoneTop : panelLayout.p1OnRight ? styles3.p2ZoneLeft : styles3.p2ZoneRight : null;
|
|
878
886
|
const showReadyButton = p2IsHuman;
|
|
887
|
+
const seatPickerAlign = p2IsHuman ? void 0 : pack.usePickerAlign;
|
|
888
|
+
const layoutKey = p2IsHuman ? "zones" : "row";
|
|
879
889
|
const controlsHost = (0, import_chrome4.usePopoverHost)();
|
|
880
890
|
const p2PanelHost = (0, import_chrome4.usePopoverHost)();
|
|
881
891
|
const p2Host = p2IsHuman ? p2PanelHost : controlsHost;
|
|
@@ -894,10 +904,10 @@ function LoadoutScreen({ game }) {
|
|
|
894
904
|
const optionsPopoverOpen = controlsHost.openId !== null && loadout.optionIds.includes(controlsHost.openId);
|
|
895
905
|
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;
|
|
896
906
|
const controlLook = { accentColor: themeColors.tertiary, mutedColor: fgMuted, dark, rotation };
|
|
897
|
-
const p1Control = loadout.seatControl(1, controlsHost, { ...controlLook, seatColor:
|
|
898
|
-
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) });
|
|
899
|
-
const p2Control = p2IsHuman ? loadout.seatControl(2, p2Host, { ...controlLook, seatColor:
|
|
900
|
-
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 });
|
|
907
|
+
const p1Control = loadout.seatControl(1, controlsHost, { ...controlLook, seatColor: p1Drawn });
|
|
908
|
+
const p1Panel = /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lobby2.PlayerSetupPanel, { idPrefix: "p1", host: controlsHost, color: p1Color, onColorChange: handleP1ColorChange, swatches: import_auto_paper4.defaultColors, takenColor: p2Exists ? p2Color : void 0, allowSwapTaken: mode === "vsCpu", colorCaption: pack.colorCaption, isHuman: true, ready: ready[1], onToggleReady: () => setReady((r) => ({ ...r, 1: !r[1] })), dark, showReadyButton, secondPicker: p1Control, secondPickerId: p1Control ? "p1-control" : void 0, profiles, selectedProfileId: lastSelected[1], takenProfileId: p2IsHuman ? lastSelected[2] : null, guestLabel: pack.guestLabel ?? "P1", nullLabel: pack.nullLabel, onProfileSelect: handleP1ProfileSelect, onManageProfiles: () => import_expo_router4.router.push(PROFILES_HREF), previewValue: seatPreview, triggerStyle, useSeatPickerAlign: seatPickerAlign }, `p1-${layoutKey}`);
|
|
909
|
+
const p2Control = p2IsHuman ? loadout.seatControl(2, p2Host, { ...controlLook, seatColor: p2Drawn }) : void 0;
|
|
910
|
+
const p2Panel = game.seatTwo && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SeatTwoPanel, { idPrefix: "p2", host: p2Host, choices: game.seatTwo, seatTwo, onSeatTwoChange: changeSeatTwo, cpuDifficulty: loadout.cpuDifficulty, profiles, selectedProfileId: lastSelected[2], onProfileSelect: handleP2ProfileSelect, color: p2Color, onColorChange: handleP2ColorChange, swatches: import_auto_paper4.defaultColors, takenColor: p1Color, allowSwapTaken: mode === "vsCpu", colorCaption: pack.colorCaption, dark, secondPicker: p2Control, secondPickerId: p2Control ? "p2-control" : void 0, ready: ready[2], onToggleReady: () => setReady((r) => ({ ...r, 2: !r[2] })), showReadyButton, previewValue: seatPreview, triggerStyle, usePickerAlign: seatPickerAlign }, `p2-${layoutKey}`);
|
|
901
911
|
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native4.View, { style: [styles3.container, { backgroundColor: bg }], children: [
|
|
902
912
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_core10.FakeLandscapeView, { orientationMode: panelLayout.orientationMode, p1OnRight: panelLayout.p1OnRight, upsideDown: panelLayout.upsideDown, style: styles3.rotatable, children: [
|
|
903
913
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_chrome4.PressAwayOverlay, { active: controlsHost.openId !== null, onPress: controlsHost.close }),
|
|
@@ -921,12 +931,16 @@ function LoadoutScreen({ game }) {
|
|
|
921
931
|
!isPortrait && panelLayout.p1OnRight ? p2Panel : p1Panel,
|
|
922
932
|
!isPortrait && panelLayout.p1OnRight ? p1Panel : p2Panel
|
|
923
933
|
] }),
|
|
924
|
-
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lobby2.ReadyButton, { color:
|
|
934
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lobby2.ReadyButton, { color: p1Drawn, ready: ready[1], onToggleReady: () => setReady((r) => ({ ...r, 1: !r[1] })), style: anyPlayerPickerOpen && styles3.readyButtonHidden })
|
|
925
935
|
] })
|
|
926
936
|
] }),
|
|
927
937
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SettingsDialog, { visible: settingsOpen, onDismiss: () => setSettingsOpen(false), rotation, onShowHowToPlay: () => openFor(game.id) })
|
|
928
938
|
] });
|
|
929
939
|
}
|
|
940
|
+
function halfOf(color) {
|
|
941
|
+
const rgb = (0, import_auto_paper4.getRgb)(color);
|
|
942
|
+
return rgb ? `rgba(${rgb.r},${rgb.g},${rgb.b},0.5)` : void 0;
|
|
943
|
+
}
|
|
930
944
|
var styles3 = import_react_native4.StyleSheet.create({
|
|
931
945
|
container: {
|
|
932
946
|
flex: 1
|
|
@@ -952,6 +966,10 @@ var styles3 = import_react_native4.StyleSheet.create({
|
|
|
952
966
|
right: 0,
|
|
953
967
|
top: 0
|
|
954
968
|
},
|
|
969
|
+
// A seat button's trigger on a surface over the pack's backdrop, as Home's.
|
|
970
|
+
pill: {
|
|
971
|
+
borderRadius: 999
|
|
972
|
+
},
|
|
955
973
|
// The one-human row: seat 1's panel and seat 2's button read as one evenly spaced row of buttons,
|
|
956
974
|
// at PlayerSetupPanel's own pickerRow gap (12).
|
|
957
975
|
playersRow: {
|
|
@@ -1006,7 +1024,9 @@ function useResumePoint(gameId) {
|
|
|
1006
1024
|
(0, import_react14.useEffect)(() => {
|
|
1007
1025
|
if (!gameId) return;
|
|
1008
1026
|
setLastActiveGame(gameId);
|
|
1009
|
-
return
|
|
1027
|
+
return () => {
|
|
1028
|
+
if (import_react_native5.AppState.currentState === "active") clearLastActiveGame();
|
|
1029
|
+
};
|
|
1010
1030
|
}, [gameId, setLastActiveGame, clearLastActiveGame]);
|
|
1011
1031
|
}
|
|
1012
1032
|
|
|
@@ -1018,7 +1038,7 @@ var import_chrome5 = require("@tastic/hud/chrome");
|
|
|
1018
1038
|
var import_profile7 = require("@tastic/profile");
|
|
1019
1039
|
var import_expo_router6 = require("expo-router");
|
|
1020
1040
|
var import_react16 = require("react");
|
|
1021
|
-
var
|
|
1041
|
+
var import_react_native6 = require("react-native");
|
|
1022
1042
|
var import_react_native_paper = require("react-native-paper");
|
|
1023
1043
|
|
|
1024
1044
|
// src/hooks/useFrozenWhileHidden.ts
|
|
@@ -1124,9 +1144,9 @@ function HomeMenu() {
|
|
|
1124
1144
|
const [settingsOpen, setSettingsOpen] = (0, import_react16.useState)(false);
|
|
1125
1145
|
const [rulesGame, setRulesGame] = (0, import_react16.useState)(null);
|
|
1126
1146
|
const isFocused = (0, import_expo_router6.useIsFocused)();
|
|
1127
|
-
const [appState, setAppState] = (0, import_react16.useState)(
|
|
1147
|
+
const [appState, setAppState] = (0, import_react16.useState)(import_react_native6.AppState.currentState);
|
|
1128
1148
|
(0, import_react16.useEffect)(() => {
|
|
1129
|
-
const subscription =
|
|
1149
|
+
const subscription = import_react_native6.AppState.addEventListener("change", setAppState);
|
|
1130
1150
|
return () => subscription.remove();
|
|
1131
1151
|
}, []);
|
|
1132
1152
|
const heroActive = isFocused && appState === "active";
|
|
@@ -1161,20 +1181,20 @@ function HomeMenu() {
|
|
|
1161
1181
|
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 });
|
|
1162
1182
|
const pressAway = /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_chrome5.PressAwayOverlay, { active: pickerHost.openId !== null, onPress: pickerHost.close, style: styles4.pressAway });
|
|
1163
1183
|
const RulesSheet = rulesGame?.Rules;
|
|
1164
|
-
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
|
|
1184
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native6.View, { style: [styles4.container, { backgroundColor: bg }], children: [
|
|
1165
1185
|
/* @__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: [
|
|
1166
1186
|
isWide ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
|
|
1167
1187
|
pressAway,
|
|
1168
|
-
/* @__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)(
|
|
1169
|
-
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
|
|
1188
|
+
/* @__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)) }) }),
|
|
1189
|
+
/* @__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: [
|
|
1170
1190
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(pack.HeroTitle, { words: pack.wordmark, color: fg, p1Color, p2Color, active: heroActive, fit: layout.heroFit }),
|
|
1171
|
-
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
1191
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native6.View, { style: styles4.widePicker, children: picker })
|
|
1172
1192
|
] })
|
|
1173
1193
|
] }) : /* @__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: [
|
|
1174
1194
|
pressAway,
|
|
1175
1195
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(pack.HeroTitle, { words: pack.wordmark, color: fg, p1Color, p2Color, active: heroActive }),
|
|
1176
|
-
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
1177
|
-
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
1196
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native6.View, { style: [styles4.picker, pickerHost.openId !== null && styles4.pickerOpen], children: picker }),
|
|
1197
|
+
/* @__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)) })
|
|
1178
1198
|
] }),
|
|
1179
1199
|
/* @__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" }),
|
|
1180
1200
|
/* @__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" }),
|
|
@@ -1197,10 +1217,10 @@ function GameRow({ game, dark, p1Color, p2Color, previewRevealed, onPress, onRul
|
|
|
1197
1217
|
return (
|
|
1198
1218
|
// The rules button sits beside the row's own button, laid over its end, not inside it: a button
|
|
1199
1219
|
// inside a button is invalid on the web.
|
|
1200
|
-
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
|
|
1220
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native6.View, { children: [
|
|
1201
1221
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_feedback_press3.Pressable, { onPress, style: [styles4.row, { backgroundColor: well.bg }], accessibilityRole: "button", accessibilityLabel: game.title, children: [
|
|
1202
|
-
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)(
|
|
1203
|
-
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
|
|
1222
|
+
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 }) }),
|
|
1223
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native6.View, { style: styles4.rowText, children: [
|
|
1204
1224
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper.Text, { variant: "titleLarge", style: [styles4.rowTitle, { color: well.fg }], numberOfLines: 1, children: game.title }),
|
|
1205
1225
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper.Text, { variant: "bodyMedium", style: { color: well.fgMuted }, numberOfLines: 2, children: game.blurb })
|
|
1206
1226
|
] })
|
|
@@ -1211,7 +1231,7 @@ function GameRow({ game, dark, p1Color, p2Color, previewRevealed, onPress, onRul
|
|
|
1211
1231
|
}
|
|
1212
1232
|
function GameTile({ game, width, previewWidth, dark, p1Color, p2Color, previewRevealed, onPress, onRules }) {
|
|
1213
1233
|
const well = (0, import_chrome5.useWellColors)();
|
|
1214
|
-
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
|
|
1234
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native6.View, { style: { width }, children: [
|
|
1215
1235
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_feedback_press3.Pressable, { onPress, style: [styles4.tile, { backgroundColor: well.bg }], accessibilityRole: "button", accessibilityLabel: game.title, children: [
|
|
1216
1236
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(PreviewBox, { game, width: previewWidth, revealed: previewRevealed, dark, p1Color, p2Color }),
|
|
1217
1237
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper.Text, { variant: "titleMedium", style: [styles4.rowTitle, { color: well.fg }], numberOfLines: 1, children: game.title }),
|
|
@@ -1224,13 +1244,13 @@ function PreviewBox({ game, width, revealed, dark, p1Color, p2Color }) {
|
|
|
1224
1244
|
const height = Math.round(width / PREVIEW_ASPECT);
|
|
1225
1245
|
const { Preview, icon: GameIcon } = game;
|
|
1226
1246
|
const iconSize = Math.min(height, ROW_ICON_SIZE);
|
|
1227
|
-
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
1247
|
+
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 }) }) });
|
|
1228
1248
|
}
|
|
1229
1249
|
var ROW_ICON_SIZE = 56;
|
|
1230
1250
|
var ICON_LIGHT_BG = "#FAFAFA";
|
|
1231
1251
|
var ICON_DARK_BG = "#050505";
|
|
1232
1252
|
var RULES_BUTTON_SPACE = 44;
|
|
1233
|
-
var styles4 =
|
|
1253
|
+
var styles4 = import_react_native6.StyleSheet.create({
|
|
1234
1254
|
container: {
|
|
1235
1255
|
flex: 1
|
|
1236
1256
|
},
|
|
@@ -1377,7 +1397,7 @@ var import_auto_paper7 = require("@rific/auto-paper");
|
|
|
1377
1397
|
var import_chrome7 = require("@tastic/hud/chrome");
|
|
1378
1398
|
var import_profile9 = require("@tastic/profile");
|
|
1379
1399
|
var import_react17 = require("react");
|
|
1380
|
-
var
|
|
1400
|
+
var import_react_native7 = require("react-native");
|
|
1381
1401
|
var import_react_native_paper2 = require("react-native-paper");
|
|
1382
1402
|
var import_react_native_safe_area_context4 = require("react-native-safe-area-context");
|
|
1383
1403
|
var import_react_redux10 = require("react-redux");
|
|
@@ -1400,7 +1420,7 @@ function StatsScreen() {
|
|
|
1400
1420
|
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 }) }) });
|
|
1401
1421
|
}
|
|
1402
1422
|
function StatsPressAway({ host, children }) {
|
|
1403
|
-
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
1423
|
+
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 });
|
|
1404
1424
|
}
|
|
1405
1425
|
var useNoPackReset = () => null;
|
|
1406
1426
|
var noPickerAlign2 = () => void 0;
|
|
@@ -1437,7 +1457,7 @@ function StatsContent({ pickerHost }) {
|
|
|
1437
1457
|
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: [
|
|
1438
1458
|
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_chrome7.PressAwayOverlay, { active: pickerHost.openId !== null, onPress: pickerHost.close, style: styles5.pressAway }),
|
|
1439
1459
|
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_paper2.SegmentedButtons, { value: tab, onValueChange: setTab, buttons: tabButtons, style: [styles5.tabs, surface] }),
|
|
1440
|
-
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
|
|
1460
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native7.View, { style: [styles5.filters, pickerHost.openId !== null && styles5.filtersOpen], onStartShouldSetResponder: () => pickerHost.openId !== null, children: [
|
|
1441
1461
|
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 }),
|
|
1442
1462
|
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 })
|
|
1443
1463
|
] }),
|
|
@@ -1458,12 +1478,12 @@ function GameStats({ game, tab, profileId, showTitle, titleColor }) {
|
|
|
1458
1478
|
const { StatsSections, AchievementList } = game;
|
|
1459
1479
|
const Sections = tab === STATS ? StatsSections : AchievementList;
|
|
1460
1480
|
if (!Sections) return null;
|
|
1461
|
-
return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
|
|
1481
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native7.View, { style: styles5.game, children: [
|
|
1462
1482
|
showTitle && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_paper2.Text, { variant: "titleLarge", style: [styles5.gameTitle, { color: titleColor }], children: game.title }),
|
|
1463
1483
|
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Sections, { profileId })
|
|
1464
1484
|
] });
|
|
1465
1485
|
}
|
|
1466
|
-
var styles5 =
|
|
1486
|
+
var styles5 = import_react_native7.StyleSheet.create({
|
|
1467
1487
|
filters: {
|
|
1468
1488
|
flexDirection: "row",
|
|
1469
1489
|
flexWrap: "wrap",
|
|
@@ -1497,11 +1517,11 @@ var styles5 = import_react_native6.StyleSheet.create({
|
|
|
1497
1517
|
});
|
|
1498
1518
|
|
|
1499
1519
|
// src/components/imageGameIcon.tsx
|
|
1500
|
-
var
|
|
1520
|
+
var import_react_native8 = require("react-native");
|
|
1501
1521
|
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
1502
1522
|
function imageGameIcon(images) {
|
|
1503
1523
|
return function ImageGameIcon({ size, dark }) {
|
|
1504
|
-
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
1524
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native8.Image, { source: dark ? images.dark : images.light, style: { height: size, width: size }, accessibilityIgnoresInvertColors: true });
|
|
1505
1525
|
};
|
|
1506
1526
|
}
|
|
1507
1527
|
|
package/dist/index.mjs
CHANGED
|
@@ -483,6 +483,7 @@ var styles = StyleSheet.create({
|
|
|
483
483
|
// src/screens/GameRoute.tsx
|
|
484
484
|
import { Redirect, useLocalSearchParams } from "expo-router";
|
|
485
485
|
import { useEffect as useEffect5 } from "react";
|
|
486
|
+
import { AppState } from "react-native";
|
|
486
487
|
|
|
487
488
|
// src/routes.ts
|
|
488
489
|
var playHref = (game) => ({ pathname: "/play/[game]", params: { game } });
|
|
@@ -491,7 +492,7 @@ var STATS_HREF = "/stats";
|
|
|
491
492
|
var PROFILES_HREF = "/profiles";
|
|
492
493
|
|
|
493
494
|
// src/screens/LoadoutScreen.tsx
|
|
494
|
-
import { defaultColors, useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
|
|
495
|
+
import { defaultColors, getRgb, useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
|
|
495
496
|
import { FakeLandscapeView, getViewRotation, rotateInsets, useOrientationState } from "@tastic/core";
|
|
496
497
|
import { CornerActionButtons, PressAwayOverlay, usePopoverHost } from "@tastic/hud/chrome";
|
|
497
498
|
import { PlayerSetupPanel, ReadyButton as ReadyButton2, SharedActionBand } from "@tastic/hud/lobby";
|
|
@@ -519,8 +520,9 @@ var CPU_ICON = "robot";
|
|
|
519
520
|
function getSeatTwoPopoverId(idPrefix) {
|
|
520
521
|
return `${idPrefix}-seat`;
|
|
521
522
|
}
|
|
522
|
-
function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDifficulty, profiles, selectedProfileId, onProfileSelect, color, onColorChange, swatches, takenColor, allowSwapTaken, colorCaption, dark, secondPicker, secondPickerId, ready, onToggleReady, showReadyButton = true }) {
|
|
523
|
+
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 }) {
|
|
523
524
|
const isPerson = seatTwo === "person";
|
|
525
|
+
const drawn = previewValue ? previewValue(color) : color;
|
|
524
526
|
const selectedProfile = isPerson ? profiles.find((p) => p.id === selectedProfileId) ?? null : null;
|
|
525
527
|
const cpuOptions = cpuDifficulty?.options;
|
|
526
528
|
const options = useMemo4(() => {
|
|
@@ -529,10 +531,10 @@ function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDi
|
|
|
529
531
|
if (choices.includes("nobody")) list.push({ value: NOBODY, label: "Nobody", icon: NOBODY_ICON });
|
|
530
532
|
if (choices.includes("person")) {
|
|
531
533
|
list.push({ value: GUEST, label: "Player 2", icon: GUEST_ICON });
|
|
532
|
-
list.push(...[...profiles].sort((a, b) => a.name.localeCompare(b.name)).map((p) => ({ value: `${PROFILE_PREFIX}${p.id}`, label: p.name, icon: "account", color: p.color })));
|
|
534
|
+
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 })));
|
|
533
535
|
}
|
|
534
536
|
return list;
|
|
535
|
-
}, [choices, cpuOptions, profiles]);
|
|
537
|
+
}, [choices, cpuOptions, profiles, previewValue]);
|
|
536
538
|
const value = seatTwo === "cpu" ? `${CPU_PREFIX}${cpuDifficulty?.value ?? ""}` : seatTwo === "nobody" ? NOBODY : selectedProfile ? `${PROFILE_PREFIX}${selectedProfile.id}` : GUEST;
|
|
537
539
|
const icon = seatTwo === "cpu" ? CPU_ICON : seatTwo === "nobody" ? NOBODY_ICON : selectedProfile ? "account" : GUEST_ICON;
|
|
538
540
|
const handleChange = (next) => {
|
|
@@ -546,16 +548,17 @@ function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDi
|
|
|
546
548
|
onSeatTwoChange("person");
|
|
547
549
|
}
|
|
548
550
|
};
|
|
549
|
-
const colorSection = seatTwo === "nobody" ? void 0 : { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, caption: selectedProfile ? colorCaption : void 0 };
|
|
551
|
+
const colorSection = seatTwo === "nobody" ? void 0 : { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, caption: selectedProfile ? colorCaption : void 0, previewValue };
|
|
550
552
|
const pickerId = getSeatTwoPopoverId(idPrefix);
|
|
551
553
|
const size = getOptionPickerContentSize(options.length, colorSection);
|
|
552
|
-
const
|
|
554
|
+
const useAlign = usePickerAlign ?? useZoneClampedAlign;
|
|
555
|
+
const align = useAlign(host.openId === pickerId, size.width, size.height);
|
|
553
556
|
const ownPopoverOpen = host.openId?.startsWith(`${idPrefix}-`) ?? false;
|
|
554
557
|
const pickerRowOpen = secondPickerId !== void 0 && host.openId === secondPickerId;
|
|
555
558
|
return /* @__PURE__ */ jsxs3(View2, { style: [styles2.panel, ownPopoverOpen && styles2.panelOpen], children: [
|
|
556
|
-
/* @__PURE__ */ jsx10(OptionPicker, { id: pickerId, host, options, value, onChange: handleChange, color, dark, icon, colorSection, alignOverride: align }),
|
|
559
|
+
/* @__PURE__ */ jsx10(OptionPicker, { id: pickerId, host, options, value, onChange: handleChange, color: drawn, dark, icon, colorSection, alignOverride: align, triggerStyle }),
|
|
557
560
|
isPerson && secondPicker && /* @__PURE__ */ jsx10(View2, { style: [styles2.pickerRow, pickerRowOpen && styles2.pickerRowOpen], children: secondPicker }),
|
|
558
|
-
isPerson && showReadyButton && onToggleReady && /* @__PURE__ */ jsx10(ReadyButton, { color, ready: ready ?? false, onToggleReady })
|
|
561
|
+
isPerson && showReadyButton && onToggleReady && /* @__PURE__ */ jsx10(ReadyButton, { color: drawn, ready: ready ?? false, onToggleReady })
|
|
559
562
|
] });
|
|
560
563
|
}
|
|
561
564
|
var styles2 = StyleSheet2.create({
|
|
@@ -795,13 +798,20 @@ function LoadoutScreen({ game }) {
|
|
|
795
798
|
dispatch(liveplayActions.setActiveRound({ game: game.id, mode, settings: roundSettingsRef.current }));
|
|
796
799
|
router2.push(playHref(game.id));
|
|
797
800
|
}, [ready, humanSeats, game.id, mode, dispatch]);
|
|
798
|
-
const bg = dark ? "#000000" : "#FFFFFF";
|
|
799
|
-
const fg = dark ? "#FFFFFF" : "#000000";
|
|
800
|
-
const fgMuted = dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
|
|
801
|
+
const bg = pack.Backdrop ? "transparent" : dark ? "#000000" : "#FFFFFF";
|
|
802
|
+
const fg = pack.foreground ?? (dark ? "#FFFFFF" : "#000000");
|
|
803
|
+
const fgMuted = (pack.foreground && halfOf(pack.foreground)) ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
|
|
804
|
+
const { previewColor } = pack;
|
|
805
|
+
const seatPreview = useMemo5(() => previewColor ? (hex) => previewColor(hex, dark) : void 0, [previewColor, dark]);
|
|
806
|
+
const p1Drawn = seatPreview ? seatPreview(p1Color) : p1Color;
|
|
807
|
+
const p2Drawn = seatPreview ? seatPreview(p2Color) : p2Color;
|
|
808
|
+
const triggerStyle = useMemo5(() => pack.Backdrop ? [styles3.pill, { backgroundColor: themeColors.surface }] : void 0, [pack.Backdrop, themeColors.surface]);
|
|
801
809
|
const isPortrait = panelLayout.orientationMode === "faceToFace";
|
|
802
810
|
const showMetaInSharedBand = needsSharedNeutralZone(panelLayout.orientationMode, humanSeats.length);
|
|
803
811
|
const p2ZoneStyle = p2IsHuman ? isPortrait ? styles3.p2ZoneTop : panelLayout.p1OnRight ? styles3.p2ZoneLeft : styles3.p2ZoneRight : null;
|
|
804
812
|
const showReadyButton = p2IsHuman;
|
|
813
|
+
const seatPickerAlign = p2IsHuman ? void 0 : pack.usePickerAlign;
|
|
814
|
+
const layoutKey = p2IsHuman ? "zones" : "row";
|
|
805
815
|
const controlsHost = usePopoverHost();
|
|
806
816
|
const p2PanelHost = usePopoverHost();
|
|
807
817
|
const p2Host = p2IsHuman ? p2PanelHost : controlsHost;
|
|
@@ -820,10 +830,10 @@ function LoadoutScreen({ game }) {
|
|
|
820
830
|
const optionsPopoverOpen = controlsHost.openId !== null && loadout.optionIds.includes(controlsHost.openId);
|
|
821
831
|
const sharedControls = showMetaInSharedBand ? /* @__PURE__ */ jsx12(SharedActionBand, { onBack: safeBack, onSettings: () => setSettingsOpen(true), onRandomize: loadout.randomize, onReset: loadout.reset, fg, popoverOpen: optionsPopoverOpen, children: optionsRow }) : optionsRow;
|
|
822
832
|
const controlLook = { accentColor: themeColors.tertiary, mutedColor: fgMuted, dark, rotation };
|
|
823
|
-
const p1Control = loadout.seatControl(1, controlsHost, { ...controlLook, seatColor:
|
|
824
|
-
const p1Panel = /* @__PURE__ */ jsx12(PlayerSetupPanel, { idPrefix: "p1", host: controlsHost, color: p1Color, onColorChange: handleP1ColorChange, swatches: defaultColors, takenColor: p2Exists ? p2Color : void 0, allowSwapTaken: mode === "vsCpu", colorCaption: pack.colorCaption, isHuman: true, ready: ready[1], onToggleReady: () => setReady((r) => ({ ...r, 1: !r[1] })), dark, showReadyButton, secondPicker: p1Control, secondPickerId: p1Control ? "p1-control" : void 0, profiles, selectedProfileId: lastSelected[1], takenProfileId: p2IsHuman ? lastSelected[2] : null, guestLabel: "P1", onProfileSelect: handleP1ProfileSelect, onManageProfiles: () => router2.push(PROFILES_HREF) });
|
|
825
|
-
const p2Control = p2IsHuman ? loadout.seatControl(2, p2Host, { ...controlLook, seatColor:
|
|
826
|
-
const p2Panel = game.seatTwo && /* @__PURE__ */ jsx12(SeatTwoPanel, { idPrefix: "p2", host: p2Host, choices: game.seatTwo, seatTwo, onSeatTwoChange: changeSeatTwo, cpuDifficulty: loadout.cpuDifficulty, profiles, selectedProfileId: lastSelected[2], onProfileSelect: handleP2ProfileSelect, color: p2Color, onColorChange: handleP2ColorChange, swatches: defaultColors, takenColor: p1Color, allowSwapTaken: mode === "vsCpu", colorCaption: pack.colorCaption, dark, secondPicker: p2Control, secondPickerId: p2Control ? "p2-control" : void 0, ready: ready[2], onToggleReady: () => setReady((r) => ({ ...r, 2: !r[2] })), showReadyButton });
|
|
833
|
+
const p1Control = loadout.seatControl(1, controlsHost, { ...controlLook, seatColor: p1Drawn });
|
|
834
|
+
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}`);
|
|
835
|
+
const p2Control = p2IsHuman ? loadout.seatControl(2, p2Host, { ...controlLook, seatColor: p2Drawn }) : void 0;
|
|
836
|
+
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}`);
|
|
827
837
|
return /* @__PURE__ */ jsxs5(View3, { style: [styles3.container, { backgroundColor: bg }], children: [
|
|
828
838
|
/* @__PURE__ */ jsxs5(FakeLandscapeView, { orientationMode: panelLayout.orientationMode, p1OnRight: panelLayout.p1OnRight, upsideDown: panelLayout.upsideDown, style: styles3.rotatable, children: [
|
|
829
839
|
/* @__PURE__ */ jsx12(PressAwayOverlay, { active: controlsHost.openId !== null, onPress: controlsHost.close }),
|
|
@@ -847,12 +857,16 @@ function LoadoutScreen({ game }) {
|
|
|
847
857
|
!isPortrait && panelLayout.p1OnRight ? p2Panel : p1Panel,
|
|
848
858
|
!isPortrait && panelLayout.p1OnRight ? p1Panel : p2Panel
|
|
849
859
|
] }),
|
|
850
|
-
/* @__PURE__ */ jsx12(ReadyButton2, { color:
|
|
860
|
+
/* @__PURE__ */ jsx12(ReadyButton2, { color: p1Drawn, ready: ready[1], onToggleReady: () => setReady((r) => ({ ...r, 1: !r[1] })), style: anyPlayerPickerOpen && styles3.readyButtonHidden })
|
|
851
861
|
] })
|
|
852
862
|
] }),
|
|
853
863
|
/* @__PURE__ */ jsx12(SettingsDialog, { visible: settingsOpen, onDismiss: () => setSettingsOpen(false), rotation, onShowHowToPlay: () => openFor(game.id) })
|
|
854
864
|
] });
|
|
855
865
|
}
|
|
866
|
+
function halfOf(color) {
|
|
867
|
+
const rgb = getRgb(color);
|
|
868
|
+
return rgb ? `rgba(${rgb.r},${rgb.g},${rgb.b},0.5)` : void 0;
|
|
869
|
+
}
|
|
856
870
|
var styles3 = StyleSheet3.create({
|
|
857
871
|
container: {
|
|
858
872
|
flex: 1
|
|
@@ -878,6 +892,10 @@ var styles3 = StyleSheet3.create({
|
|
|
878
892
|
right: 0,
|
|
879
893
|
top: 0
|
|
880
894
|
},
|
|
895
|
+
// A seat button's trigger on a surface over the pack's backdrop, as Home's.
|
|
896
|
+
pill: {
|
|
897
|
+
borderRadius: 999
|
|
898
|
+
},
|
|
881
899
|
// The one-human row: seat 1's panel and seat 2's button read as one evenly spaced row of buttons,
|
|
882
900
|
// at PlayerSetupPanel's own pickerRow gap (12).
|
|
883
901
|
playersRow: {
|
|
@@ -932,7 +950,9 @@ function useResumePoint(gameId) {
|
|
|
932
950
|
useEffect5(() => {
|
|
933
951
|
if (!gameId) return;
|
|
934
952
|
setLastActiveGame(gameId);
|
|
935
|
-
return
|
|
953
|
+
return () => {
|
|
954
|
+
if (AppState.currentState === "active") clearLastActiveGame();
|
|
955
|
+
};
|
|
936
956
|
}, [gameId, setLastActiveGame, clearLastActiveGame]);
|
|
937
957
|
}
|
|
938
958
|
|
|
@@ -944,7 +964,7 @@ import { PressAwayOverlay as PressAwayOverlay2, usePopoverHost as usePopoverHost
|
|
|
944
964
|
import { getProfilePickerContentSize, ProfilePicker } from "@tastic/profile";
|
|
945
965
|
import { Redirect as Redirect2, router as router3, useIsFocused } from "expo-router";
|
|
946
966
|
import { useCallback as useCallback10, useEffect as useEffect6, useMemo as useMemo6, useState as useState6 } from "react";
|
|
947
|
-
import { AppState, StyleSheet as StyleSheet4, View as View4 } from "react-native";
|
|
967
|
+
import { AppState as AppState2, StyleSheet as StyleSheet4, View as View4 } from "react-native";
|
|
948
968
|
import { Icon, Portal, Text } from "react-native-paper";
|
|
949
969
|
|
|
950
970
|
// src/hooks/useFrozenWhileHidden.ts
|
|
@@ -1050,9 +1070,9 @@ function HomeMenu() {
|
|
|
1050
1070
|
const [settingsOpen, setSettingsOpen] = useState6(false);
|
|
1051
1071
|
const [rulesGame, setRulesGame] = useState6(null);
|
|
1052
1072
|
const isFocused = useIsFocused();
|
|
1053
|
-
const [appState, setAppState] = useState6(
|
|
1073
|
+
const [appState, setAppState] = useState6(AppState2.currentState);
|
|
1054
1074
|
useEffect6(() => {
|
|
1055
|
-
const subscription =
|
|
1075
|
+
const subscription = AppState2.addEventListener("change", setAppState);
|
|
1056
1076
|
return () => subscription.remove();
|
|
1057
1077
|
}, []);
|
|
1058
1078
|
const heroActive = isFocused && appState === "active";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastic/shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"@tastic/achievements": "^0.6.0",
|
|
87
87
|
"@tastic/core": "^0.14.0",
|
|
88
88
|
"@tastic/edge-guard": "^0.1.5",
|
|
89
|
-
"@tastic/hud": "^0.
|
|
89
|
+
"@tastic/hud": "^0.27.1",
|
|
90
90
|
"@tastic/profile": "^0.17.0",
|
|
91
91
|
"@tastic/split-screen": "^0.5.5",
|
|
92
92
|
"@testing-library/react-native": "^14.1.0",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
"@tastic/achievements": ">=0.6.0",
|
|
139
139
|
"@tastic/core": ">=0.14.0",
|
|
140
140
|
"@tastic/edge-guard": ">=0.1.5",
|
|
141
|
-
"@tastic/hud": ">=0.
|
|
141
|
+
"@tastic/hud": ">=0.27.1",
|
|
142
142
|
"@tastic/profile": ">=0.16.2",
|
|
143
143
|
"@tastic/split-screen": ">=0.5.5",
|
|
144
144
|
"expo-blur": ">=58.0.4",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { SeedColor } from '@rific/auto-paper'
|
|
2
|
-
import type { PopoverHost } from '@tastic/hud/chrome'
|
|
2
|
+
import type { AlignResult, PopoverHost } from '@tastic/hud/chrome'
|
|
3
3
|
import { type MenuOption, ReadyButton, useZoneClampedAlign } from '@tastic/hud/lobby'
|
|
4
4
|
import { getOptionPickerContentSize, OptionPicker, type OptionPickerOption, type Profile, type ProfilePickerColorSection } from '@tastic/profile'
|
|
5
5
|
import { ReactNode, useMemo } from 'react'
|
|
6
|
-
import { StyleSheet, View } from 'react-native'
|
|
6
|
+
import { type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'
|
|
7
7
|
|
|
8
8
|
import type { SeatTwo } from '../types'
|
|
9
9
|
|
|
@@ -57,10 +57,19 @@ export interface SeatTwoPanelProps {
|
|
|
57
57
|
ready?: boolean
|
|
58
58
|
onToggleReady?: () => void
|
|
59
59
|
showReadyButton?: boolean
|
|
60
|
+
// How the seat's color, and each profile's in the menu, is drawn (PackDefinition.previewColor), as
|
|
61
|
+
// PlayerSetupPanel's own previewValue: `color` and what onColorChange receives stay the saved values.
|
|
62
|
+
previewValue?: (hex: string) => string
|
|
63
|
+
// The button's trigger style (a surface behind its pill over the pack's backdrop).
|
|
64
|
+
triggerStyle?: StyleProp<ViewStyle>
|
|
65
|
+
// A placement hook of the pack's own for the menu, in place of the zone clamp (PlayerSetupPanel's
|
|
66
|
+
// useSeatPickerAlign): left unset inside a split-screen zone, and the same one for the panel's life.
|
|
67
|
+
usePickerAlign?: (open: boolean, contentWidth: number, contentHeight: number) => AlignResult
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
export function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDifficulty, profiles, selectedProfileId, onProfileSelect, color, onColorChange, swatches, takenColor, allowSwapTaken, colorCaption, dark, secondPicker, secondPickerId, ready, onToggleReady, showReadyButton = true }: SeatTwoPanelProps) {
|
|
70
|
+
export 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 }: SeatTwoPanelProps) {
|
|
63
71
|
const isPerson = seatTwo === 'person'
|
|
72
|
+
const drawn = previewValue ? previewValue(color) : color
|
|
64
73
|
const selectedProfile = isPerson ? (profiles.find((p) => p.id === selectedProfileId) ?? null) : null
|
|
65
74
|
|
|
66
75
|
const cpuOptions = cpuDifficulty?.options
|
|
@@ -70,10 +79,10 @@ export function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange
|
|
|
70
79
|
if (choices.includes('nobody')) list.push({ value: NOBODY, label: 'Nobody', icon: NOBODY_ICON })
|
|
71
80
|
if (choices.includes('person')) {
|
|
72
81
|
list.push({ value: GUEST, label: 'Player 2', icon: GUEST_ICON })
|
|
73
|
-
list.push(...[...profiles].sort((a, b) => a.name.localeCompare(b.name)).map((p) => ({ value: `${PROFILE_PREFIX}${p.id}`, label: p.name, icon: 'account', color: p.color })))
|
|
82
|
+
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 })))
|
|
74
83
|
}
|
|
75
84
|
return list
|
|
76
|
-
}, [choices, cpuOptions, profiles])
|
|
85
|
+
}, [choices, cpuOptions, profiles, previewValue])
|
|
77
86
|
|
|
78
87
|
const value = seatTwo === 'cpu' ? `${CPU_PREFIX}${cpuDifficulty?.value ?? ''}` : seatTwo === 'nobody' ? NOBODY : selectedProfile ? `${PROFILE_PREFIX}${selectedProfile.id}` : GUEST
|
|
79
88
|
const icon = seatTwo === 'cpu' ? CPU_ICON : seatTwo === 'nobody' ? NOBODY_ICON : selectedProfile ? 'account' : GUEST_ICON
|
|
@@ -91,21 +100,23 @@ export function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange
|
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
// Nobody has no color to pick: there's nothing of theirs on the board.
|
|
94
|
-
const colorSection: ProfilePickerColorSection | undefined = seatTwo === 'nobody' ? undefined : { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, caption: selectedProfile ? colorCaption : undefined }
|
|
103
|
+
const colorSection: ProfilePickerColorSection | undefined = seatTwo === 'nobody' ? undefined : { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, caption: selectedProfile ? colorCaption : undefined, previewValue }
|
|
95
104
|
|
|
96
105
|
const pickerId = getSeatTwoPopoverId(idPrefix)
|
|
97
106
|
const size = getOptionPickerContentSize(options.length, colorSection)
|
|
98
|
-
// Kept inside this player's half in 2 Player, like PlayerSetupPanel's own seat button
|
|
99
|
-
|
|
107
|
+
// Kept inside this player's half in 2 Player, like PlayerSetupPanel's own seat button, or placed by
|
|
108
|
+
// the pack's own hook where it's given one.
|
|
109
|
+
const useAlign = usePickerAlign ?? useZoneClampedAlign
|
|
110
|
+
const align = useAlign(host.openId === pickerId, size.width, size.height)
|
|
100
111
|
|
|
101
112
|
const ownPopoverOpen = host.openId?.startsWith(`${idPrefix}-`) ?? false
|
|
102
113
|
const pickerRowOpen = secondPickerId !== undefined && host.openId === secondPickerId
|
|
103
114
|
|
|
104
115
|
return (
|
|
105
116
|
<View style={[styles.panel, ownPopoverOpen && styles.panelOpen]}>
|
|
106
|
-
<OptionPicker id={pickerId} host={host} options={options} value={value} onChange={handleChange} color={
|
|
117
|
+
<OptionPicker id={pickerId} host={host} options={options} value={value} onChange={handleChange} color={drawn} dark={dark} icon={icon} colorSection={colorSection} alignOverride={align} triggerStyle={triggerStyle} />
|
|
107
118
|
{isPerson && secondPicker && <View style={[styles.pickerRow, pickerRowOpen && styles.pickerRowOpen]}>{secondPicker}</View>}
|
|
108
|
-
{isPerson && showReadyButton && onToggleReady && <ReadyButton color={
|
|
119
|
+
{isPerson && showReadyButton && onToggleReady && <ReadyButton color={drawn} ready={ready ?? false} onToggleReady={onToggleReady} />}
|
|
109
120
|
</View>
|
|
110
121
|
)
|
|
111
122
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Redirect, useLocalSearchParams } from 'expo-router'
|
|
2
2
|
import { useEffect } from 'react'
|
|
3
|
+
import { AppState } from 'react-native'
|
|
3
4
|
|
|
4
5
|
import { useLastActiveGame } from '../hooks/useLastActiveGame'
|
|
5
6
|
import { usePackGame } from '../PackContext'
|
|
@@ -36,14 +37,20 @@ export function PlayRoute() {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
// A resumable game's play screen (GameDefinition.resumable) is where a cold launch comes back to:
|
|
39
|
-
// remembered as it mounts, and forgotten when
|
|
40
|
-
//
|
|
41
|
-
// so it stays remembered.
|
|
40
|
+
// remembered as it mounts, and forgotten when the player backs out of it (its own back, hardware back
|
|
41
|
+
// or a browser back), which unmounts it while the app is in use. Backgrounding the app leaves the
|
|
42
|
+
// screen mounted, so it stays remembered. An unmount with the app NOT active keeps it too: when iOS
|
|
43
|
+
// terminates an app it hasn't suspended yet (swiped away from the app switcher soon after leaving
|
|
44
|
+
// it), React Native's renderer stops every surface (RCTSurfacePresenter's will-terminate handler),
|
|
45
|
+
// which unmounts the whole tree, and clearing then sent every such relaunch to Home. Runs once per
|
|
46
|
+
// play screen: the context's functions never change.
|
|
42
47
|
function useResumePoint(gameId: string | null) {
|
|
43
48
|
const { setLastActiveGame, clearLastActiveGame } = useLastActiveGame()
|
|
44
49
|
useEffect(() => {
|
|
45
50
|
if (!gameId) return
|
|
46
51
|
setLastActiveGame(gameId)
|
|
47
|
-
return
|
|
52
|
+
return () => {
|
|
53
|
+
if (AppState.currentState === 'active') clearLastActiveGame()
|
|
54
|
+
}
|
|
48
55
|
}, [gameId, setLastActiveGame, clearLastActiveGame])
|
|
49
56
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defaultColors, useAutoPaperTheme } from '@rific/auto-paper'
|
|
1
|
+
import { defaultColors, getRgb, useAutoPaperTheme } from '@rific/auto-paper'
|
|
2
2
|
import { FakeLandscapeView, getViewRotation, rotateInsets, useOrientationState } from '@tastic/core'
|
|
3
3
|
import { CornerActionButtons, PressAwayOverlay, usePopoverHost } from '@tastic/hud/chrome'
|
|
4
4
|
import { PlayerSetupPanel, ReadyButton, SharedActionBand } from '@tastic/hud/lobby'
|
|
@@ -122,9 +122,19 @@ export function LoadoutScreen({ game }: { game: LoadoutGame }) {
|
|
|
122
122
|
router.push(playHref(game.id))
|
|
123
123
|
}, [ready, humanSeats, game.id, mode, dispatch])
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
125
|
+
// Over the pack's Backdrop the loadout draws nothing of its own behind its controls, in the pack's
|
|
126
|
+
// foreground, as Home does.
|
|
127
|
+
const bg = pack.Backdrop ? 'transparent' : dark ? '#000000' : '#FFFFFF'
|
|
128
|
+
const fg = pack.foreground ?? (dark ? '#FFFFFF' : '#000000')
|
|
129
|
+
const fgMuted = (pack.foreground && halfOf(pack.foreground)) ?? (dark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)')
|
|
130
|
+
// Each seat's color as the pack draws profile colors (PackDefinition.previewColor), and the seat
|
|
131
|
+
// buttons on the theme's surface over a backdrop, as Home's seat 1 button is. The saved colors stay
|
|
132
|
+
// what the Color rows pick and compare.
|
|
133
|
+
const { previewColor } = pack
|
|
134
|
+
const seatPreview = useMemo(() => (previewColor ? (hex: string) => previewColor(hex, dark) : undefined), [previewColor, dark])
|
|
135
|
+
const p1Drawn = seatPreview ? seatPreview(p1Color) : p1Color
|
|
136
|
+
const p2Drawn = seatPreview ? seatPreview(p2Color) : p2Color
|
|
137
|
+
const triggerStyle = useMemo(() => (pack.Backdrop ? [styles.pill, { backgroundColor: themeColors.surface }] : undefined), [pack.Backdrop, themeColors.surface])
|
|
128
138
|
|
|
129
139
|
const isPortrait = panelLayout.orientationMode === 'faceToFace'
|
|
130
140
|
// See @tastic/split-screen's needsSharedNeutralZone: whether back/settings/randomize/reset fold
|
|
@@ -138,6 +148,12 @@ export function LoadoutScreen({ game }: { game: LoadoutGame }) {
|
|
|
138
148
|
|
|
139
149
|
// With one human, the one Ready button sits under both panels instead of in each.
|
|
140
150
|
const showReadyButton = p2IsHuman
|
|
151
|
+
// The seat buttons' menus are placed by the pack where it places them itself (PackDefinition.
|
|
152
|
+
// usePickerAlign), as on Home, but only in the one-human row: 2 Player's halves keep each menu in
|
|
153
|
+
// its own player's half (hud's useZoneClampedAlign, the panels' own). The panels are keyed by layout,
|
|
154
|
+
// so a placement hook never changes under a mounted panel.
|
|
155
|
+
const seatPickerAlign = p2IsHuman ? undefined : pack.usePickerAlign
|
|
156
|
+
const layoutKey = p2IsHuman ? 'zones' : 'row'
|
|
141
157
|
// P1's panel and the match options share one host - they sit in the same near/bottom zone. With
|
|
142
158
|
// one human, seat 2's panel joins it too (one person drives both, so two open menus at once would
|
|
143
159
|
// be clutter). 2 Player gives P2 their own host so two people can pick at once, each press-away
|
|
@@ -171,11 +187,11 @@ export function LoadoutScreen({ game }: { game: LoadoutGame }) {
|
|
|
171
187
|
)
|
|
172
188
|
|
|
173
189
|
const controlLook = { accentColor: themeColors.tertiary, mutedColor: fgMuted, dark, rotation }
|
|
174
|
-
const p1Control = loadout.seatControl(1, controlsHost, { ...controlLook, seatColor:
|
|
175
|
-
const p1Panel = <PlayerSetupPanel idPrefix='p1' host={controlsHost} color={p1Color} onColorChange={handleP1ColorChange} swatches={defaultColors} takenColor={p2Exists ? p2Color : undefined} allowSwapTaken={mode === 'vsCpu'} colorCaption={pack.colorCaption} isHuman ready={ready[1]} onToggleReady={() => setReady((r) => ({ ...r, 1: !r[1] }))} dark={dark} showReadyButton={showReadyButton} secondPicker={p1Control} secondPickerId={p1Control ? 'p1-control' : undefined} profiles={profiles} selectedProfileId={lastSelected[1]} takenProfileId={p2IsHuman ? lastSelected[2] : null} guestLabel='P1' onProfileSelect={handleP1ProfileSelect} onManageProfiles={() => router.push(PROFILES_HREF)} />
|
|
176
|
-
const p2Control = p2IsHuman ? loadout.seatControl(2, p2Host, { ...controlLook, seatColor:
|
|
190
|
+
const p1Control = loadout.seatControl(1, controlsHost, { ...controlLook, seatColor: p1Drawn })
|
|
191
|
+
const p1Panel = <PlayerSetupPanel key={`p1-${layoutKey}`} idPrefix='p1' host={controlsHost} color={p1Color} onColorChange={handleP1ColorChange} swatches={defaultColors} takenColor={p2Exists ? p2Color : undefined} allowSwapTaken={mode === 'vsCpu'} colorCaption={pack.colorCaption} isHuman ready={ready[1]} onToggleReady={() => setReady((r) => ({ ...r, 1: !r[1] }))} dark={dark} showReadyButton={showReadyButton} secondPicker={p1Control} secondPickerId={p1Control ? 'p1-control' : undefined} profiles={profiles} selectedProfileId={lastSelected[1]} takenProfileId={p2IsHuman ? lastSelected[2] : null} guestLabel={pack.guestLabel ?? 'P1'} nullLabel={pack.nullLabel} onProfileSelect={handleP1ProfileSelect} onManageProfiles={() => router.push(PROFILES_HREF)} previewValue={seatPreview} triggerStyle={triggerStyle} useSeatPickerAlign={seatPickerAlign} />
|
|
192
|
+
const p2Control = p2IsHuman ? loadout.seatControl(2, p2Host, { ...controlLook, seatColor: p2Drawn }) : undefined
|
|
177
193
|
// A game for one player (no seatTwo) has no seat 2 button: seat 1's panel stands alone.
|
|
178
|
-
const p2Panel = game.seatTwo && <SeatTwoPanel idPrefix='p2' host={p2Host} choices={game.seatTwo} seatTwo={seatTwo} onSeatTwoChange={changeSeatTwo} cpuDifficulty={loadout.cpuDifficulty} profiles={profiles} selectedProfileId={lastSelected[2]} onProfileSelect={handleP2ProfileSelect} color={p2Color} onColorChange={handleP2ColorChange} swatches={defaultColors} takenColor={p1Color} allowSwapTaken={mode === 'vsCpu'} colorCaption={pack.colorCaption} dark={dark} secondPicker={p2Control} secondPickerId={p2Control ? 'p2-control' : undefined} ready={ready[2]} onToggleReady={() => setReady((r) => ({ ...r, 2: !r[2] }))} showReadyButton={showReadyButton} />
|
|
194
|
+
const p2Panel = game.seatTwo && <SeatTwoPanel key={`p2-${layoutKey}`} idPrefix='p2' host={p2Host} choices={game.seatTwo} seatTwo={seatTwo} onSeatTwoChange={changeSeatTwo} cpuDifficulty={loadout.cpuDifficulty} profiles={profiles} selectedProfileId={lastSelected[2]} onProfileSelect={handleP2ProfileSelect} color={p2Color} onColorChange={handleP2ColorChange} swatches={defaultColors} takenColor={p1Color} allowSwapTaken={mode === 'vsCpu'} colorCaption={pack.colorCaption} dark={dark} secondPicker={p2Control} secondPickerId={p2Control ? 'p2-control' : undefined} ready={ready[2]} onToggleReady={() => setReady((r) => ({ ...r, 2: !r[2] }))} showReadyButton={showReadyButton} previewValue={seatPreview} triggerStyle={triggerStyle} usePickerAlign={seatPickerAlign} />
|
|
179
195
|
|
|
180
196
|
return (
|
|
181
197
|
<View style={[styles.container, { backgroundColor: bg }]}>
|
|
@@ -212,7 +228,7 @@ export function LoadoutScreen({ game }: { game: LoadoutGame }) {
|
|
|
212
228
|
{!isPortrait && panelLayout.p1OnRight ? p2Panel : p1Panel}
|
|
213
229
|
{!isPortrait && panelLayout.p1OnRight ? p1Panel : p2Panel}
|
|
214
230
|
</Animated.View>
|
|
215
|
-
<ReadyButton color={
|
|
231
|
+
<ReadyButton color={p1Drawn} ready={ready[1]} onToggleReady={() => setReady((r) => ({ ...r, 1: !r[1] }))} style={anyPlayerPickerOpen && styles.readyButtonHidden} />
|
|
216
232
|
</View>
|
|
217
233
|
)}
|
|
218
234
|
</FakeLandscapeView>
|
|
@@ -222,6 +238,13 @@ export function LoadoutScreen({ game }: { game: LoadoutGame }) {
|
|
|
222
238
|
)
|
|
223
239
|
}
|
|
224
240
|
|
|
241
|
+
// A foreground at half strength: the loadout's muted text and icons, drawn on the pack's backdrop.
|
|
242
|
+
// Undefined for a color it can't read.
|
|
243
|
+
function halfOf(color: string): string | undefined {
|
|
244
|
+
const rgb = getRgb(color)
|
|
245
|
+
return rgb ? `rgba(${rgb.r},${rgb.g},${rgb.b},0.5)` : undefined
|
|
246
|
+
}
|
|
247
|
+
|
|
225
248
|
const styles = StyleSheet.create({
|
|
226
249
|
container: {
|
|
227
250
|
flex: 1
|
|
@@ -247,6 +270,10 @@ const styles = StyleSheet.create({
|
|
|
247
270
|
right: 0,
|
|
248
271
|
top: 0
|
|
249
272
|
},
|
|
273
|
+
// A seat button's trigger on a surface over the pack's backdrop, as Home's.
|
|
274
|
+
pill: {
|
|
275
|
+
borderRadius: 999
|
|
276
|
+
},
|
|
250
277
|
// The one-human row: seat 1's panel and seat 2's button read as one evenly spaced row of buttons,
|
|
251
278
|
// at PlayerSetupPanel's own pickerRow gap (12).
|
|
252
279
|
playersRow: {
|
package/src/types.ts
CHANGED
|
@@ -288,10 +288,12 @@ export interface PackDefinition {
|
|
|
288
288
|
StatsSections?: ComponentType<PackStatsSectionsProps>
|
|
289
289
|
|
|
290
290
|
// A table drawn once behind the whole stack (a felt), seen through every screen: the stack's own
|
|
291
|
-
// layers go transparent, and Home, the stats screen and Profiles draw on it in
|
|
291
|
+
// layers go transparent, and Home, each loadout, the stats screen and Profiles draw on it in
|
|
292
|
+
// `foreground`, with their buttons on the theme's surface.
|
|
292
293
|
Backdrop?: ComponentType
|
|
293
|
-
// The text and icon color of what Home, the stats screen and Profiles draw straight on
|
|
294
|
-
// background (the hero, the corner buttons, headers
|
|
294
|
+
// The text and icon color of what Home, the loadouts, the stats screen and Profiles draw straight on
|
|
295
|
+
// the screen's background (the hero, the corner buttons, headers; a loadout's options at half of
|
|
296
|
+
// it). Unset is black or white with the theme.
|
|
295
297
|
foreground?: string
|
|
296
298
|
// Mounted once around How to Play and the stack, inside the store, Theme, ProfilesProvider and every
|
|
297
299
|
// game's Provider: a context of the pack's own (one stats context across its games).
|
|
@@ -313,14 +315,15 @@ export interface PackDefinition {
|
|
|
313
315
|
// dark mode change and as a fresh install's theme, in place of a triad of the raw seat colors. With it,
|
|
314
316
|
// each seat's color is read from the seat colors rather than from the theme.
|
|
315
317
|
seatTheme?: (colors: Record<Seat, string>, dark: boolean) => Pick<ThemeSettings, 'color' | 'harmony'>
|
|
316
|
-
// How every profile color is drawn (the
|
|
317
|
-
//
|
|
318
|
+
// How every profile color is drawn (the seat buttons' chips on Home and in the loadouts, their
|
|
319
|
+
// swatches, a loadout's Ready button, each ProfileChip): mounted as @tastic/profile's
|
|
320
|
+
// ProfileColorProvider. The saved colors stay as picked.
|
|
318
321
|
previewColor?: (hex: string, dark: boolean) => string
|
|
319
322
|
// The pack's own How to Play: shown on Home's first launch (held back while a cold launch resumes a
|
|
320
323
|
// game) and from Home's Settings, never in a play screen.
|
|
321
324
|
guide?: PackGuide
|
|
322
|
-
// Seat 1's button: the chip's label for a guest (default 'P1') and the
|
|
323
|
-
// (default @tastic/profile's).
|
|
325
|
+
// Seat 1's button, on Home and in every loadout: the chip's label for a guest (default 'P1') and the
|
|
326
|
+
// guest row's in its menu (default @tastic/profile's).
|
|
324
327
|
guestLabel?: string
|
|
325
328
|
nullLabel?: string
|
|
326
329
|
// The stats screen keeps a guest's stats under GUEST_PLAYER_ID: a Guest entry in the player picker
|
|
@@ -336,7 +339,8 @@ export interface PackDefinition {
|
|
|
336
339
|
statsGameTitles?: boolean
|
|
337
340
|
// A new profile's starting color on Profiles. Unset is auto-paper's first swatch.
|
|
338
341
|
newProfileColor?: string
|
|
339
|
-
// Where a picker's menu opens (its alignOverride) on Home (seat 1's button),
|
|
342
|
+
// Where a picker's menu opens (its alignOverride) on Home (seat 1's button), a loadout's one-human row
|
|
343
|
+
// (both seat buttons; 2 Player keeps each menu in its own player's half), the stats screen (its two
|
|
340
344
|
// pickers) and Profiles (a profile's color swatch). hud's own placement measures a turned button
|
|
341
345
|
// against the upright window, so with the phone held sideways it can pick the side with no room and
|
|
342
346
|
// run the menu off the screen (Solitaire's useFrameAutoAlign places it in the turned frame instead).
|