@tastic/shell 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -8
- package/dist/index.js +44 -22
- package/dist/index.mjs +46 -24
- package/package.json +3 -3
- package/src/RootLayout.tsx +31 -19
- package/src/components/SeatTwoPanel.tsx +21 -10
- package/src/screens/LoadoutScreen.tsx +36 -9
- package/src/types.ts +12 -8
package/README.md
CHANGED
|
@@ -18,7 +18,9 @@ package ecosystem, on [`@tastic/hud`](https://github.com/jayrdeaton/react-native
|
|
|
18
18
|
|
|
19
19
|
- **`RootLayout`**: the root of the app (gesture root, safe area, fonts, Redux and its persist gate,
|
|
20
20
|
the theme, profiles, every game's own provider, the splash gates, the shared orientation reading,
|
|
21
|
-
toasts, How to Play, the OTA update dialog) around expo-router's stack.
|
|
21
|
+
toasts, How to Play, the OTA update dialog) around expo-router's stack. The stack follows the app's
|
|
22
|
+
own theme, not the phone's light or dark appearance: the status bar's text, the root window and the
|
|
23
|
+
navigation theme's background (what shows in a sliding screen's corners during a push or pop).
|
|
22
24
|
- **Screens** for the routes: `HomeScreen` (the pack's hero, seat 1's button, a row per game, or a
|
|
23
25
|
grid of preview tiles on a wide screen), `LoadoutRoute` and `PlayRoute` (`loadout/[game]`,
|
|
24
26
|
`play/[game]`), `ProfilesRoute` and `StatsScreen`.
|
|
@@ -109,25 +111,25 @@ PuckTastic and DuelTastic draw what they always did:
|
|
|
109
111
|
|
|
110
112
|
| Field | |
|
|
111
113
|
| --- | --- |
|
|
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
|
|
114
|
+
| `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. |
|
|
115
|
+
| `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
116
|
| `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
|
-
| `useScreenOptions?` | A hook returning stack screen options (`StackScreenOptions`), merged after the shell's own (a
|
|
117
|
+
| `useScreenOptions?` | A hook returning stack screen options (`StackScreenOptions`), merged after the shell's own (a status bar style of the pack's own, Reduce Motion's crossfade). The shell's already follow the app's theme, and stay dark over a `Backdrop`. |
|
|
116
118
|
| `useFeedbackSounds?` | A hook returning the two sounds every button plays (`FeedbackSounds`: `selection`, `notification`), in place of the shell's two wavs. |
|
|
117
119
|
| `Toaster?` | The pack's own toaster, drawn in a frame turned with the phone (with the safe-area insets turned onto its edges) in place of the shell's. The toast provider is given no Paper, so nothing portals it out of the frame. |
|
|
118
120
|
| `portraitWhileKeyboard?` | Every screen reads upright while the software keyboard is up (`@tastic/core`'s `OrientationProvider`), for naming a profile sideways. |
|
|
119
121
|
| `edgeGuard?` | `false` leaves Edge Guard out of Settings (no edge controls, no Edge Guard plugin). |
|
|
120
122
|
| `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
|
|
123
|
+
| `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
124
|
| `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. |
|
|
125
|
+
| `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
126
|
| `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
127
|
| `AchievementList?` | One achievement list across the pack's games, on the Achievements tab, with the game picker hidden there. |
|
|
126
128
|
| `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
129
|
| `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
130
|
| `statsGameTitles?` | `false` leaves each game's name off its sections on All Games, where the sections name it. |
|
|
129
131
|
| `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. |
|
|
132
|
+
| `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
133
|
|
|
132
134
|
### `GameDefinition`
|
|
133
135
|
|
|
@@ -204,7 +206,7 @@ became a package (the lower of the two where they differ).
|
|
|
204
206
|
| `@rific/feedback-press` | >=0.13.1 | haptic and sound settings, the UI sounds |
|
|
205
207
|
| `@rific/toaster` | >=0.10.1 | toasts (DuelTastic runs 0.12, PuckTastic 0.10) |
|
|
206
208
|
| `@tastic/core` | >=0.14.0 | orientation, rotation, the status bar and root background |
|
|
207
|
-
| `@tastic/hud` | >=0.
|
|
209
|
+
| `@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
210
|
| `@tastic/profile` | >=0.16.2 | profiles, the seat buttons, seat colors |
|
|
209
211
|
| `@tastic/achievements` | >=0.6.0 | `createGameStats`, `forgetProfileAchievements` |
|
|
210
212
|
| `@tastic/split-screen` | >=0.5.5 | the 2 Player loadout's two halves |
|
package/dist/index.js
CHANGED
|
@@ -484,8 +484,9 @@ function AppStack() {
|
|
|
484
484
|
const pack = usePack();
|
|
485
485
|
const { Backdrop } = pack;
|
|
486
486
|
const { dark } = (0, import_auto_paper2.useAutoPaperTheme)();
|
|
487
|
-
const
|
|
488
|
-
const
|
|
487
|
+
const darkRoot = Backdrop ? true : dark;
|
|
488
|
+
const rootBackground = (0, import_core6.useThemedRootBackground)(darkRoot, false);
|
|
489
|
+
const statusBar = (0, import_core6.useRotationAwareStatusBar)({ style: darkRoot ? "light" : "dark" });
|
|
489
490
|
const packOptions = (pack.useScreenOptions ?? noScreenOptions)();
|
|
490
491
|
const backdropOptions = Backdrop ? { contentStyle: TRANSPARENT_CONTENT, animation: import_react_native2.Platform.select({ android: "fade" }) } : void 0;
|
|
491
492
|
const playOptions = (0, import_react9.useCallback)(
|
|
@@ -495,15 +496,19 @@ function AppStack() {
|
|
|
495
496
|
},
|
|
496
497
|
[pack.games]
|
|
497
498
|
);
|
|
498
|
-
const
|
|
499
|
-
const
|
|
499
|
+
const navigationBackground = Backdrop ? TRANSPARENT_CONTENT.backgroundColor : rootBackground.contentStyle.backgroundColor;
|
|
500
|
+
const navigationTheme = (0, import_react9.useMemo)(() => {
|
|
501
|
+
const base = darkRoot ? import_expo_router.DarkTheme : import_expo_router.DefaultTheme;
|
|
502
|
+
return { ...base, colors: { ...base.colors, background: navigationBackground } };
|
|
503
|
+
}, [darkRoot, navigationBackground]);
|
|
504
|
+
const stack = /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_expo_router.ThemeProvider, { value: navigationTheme, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_expo_router.Stack, { screenOptions: { ...rootBackground, ...statusBar, ...backdropOptions, ...packOptions }, children: [
|
|
500
505
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_expo_router.Stack.Screen, { name: "index" }),
|
|
501
506
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_expo_router.Stack.Screen, { name: "play/[game]", options: playOptions })
|
|
502
|
-
] });
|
|
503
|
-
if (!Backdrop
|
|
507
|
+
] }) });
|
|
508
|
+
if (!Backdrop) return stack;
|
|
504
509
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_react_native2.View, { style: styles.root, children: [
|
|
505
510
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Backdrop, {}),
|
|
506
|
-
|
|
511
|
+
stack
|
|
507
512
|
] });
|
|
508
513
|
}
|
|
509
514
|
function OrientationBridge({ children }) {
|
|
@@ -594,8 +599,9 @@ var CPU_ICON = "robot";
|
|
|
594
599
|
function getSeatTwoPopoverId(idPrefix) {
|
|
595
600
|
return `${idPrefix}-seat`;
|
|
596
601
|
}
|
|
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 }) {
|
|
602
|
+
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 }) {
|
|
598
603
|
const isPerson = seatTwo === "person";
|
|
604
|
+
const drawn = previewValue ? previewValue(color) : color;
|
|
599
605
|
const selectedProfile = isPerson ? profiles.find((p) => p.id === selectedProfileId) ?? null : null;
|
|
600
606
|
const cpuOptions = cpuDifficulty?.options;
|
|
601
607
|
const options = (0, import_react10.useMemo)(() => {
|
|
@@ -604,10 +610,10 @@ function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDi
|
|
|
604
610
|
if (choices.includes("nobody")) list.push({ value: NOBODY, label: "Nobody", icon: NOBODY_ICON });
|
|
605
611
|
if (choices.includes("person")) {
|
|
606
612
|
list.push({ value: GUEST, label: "Player 2", icon: GUEST_ICON });
|
|
607
|
-
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 })));
|
|
613
|
+
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 })));
|
|
608
614
|
}
|
|
609
615
|
return list;
|
|
610
|
-
}, [choices, cpuOptions, profiles]);
|
|
616
|
+
}, [choices, cpuOptions, profiles, previewValue]);
|
|
611
617
|
const value = seatTwo === "cpu" ? `${CPU_PREFIX}${cpuDifficulty?.value ?? ""}` : seatTwo === "nobody" ? NOBODY : selectedProfile ? `${PROFILE_PREFIX}${selectedProfile.id}` : GUEST;
|
|
612
618
|
const icon = seatTwo === "cpu" ? CPU_ICON : seatTwo === "nobody" ? NOBODY_ICON : selectedProfile ? "account" : GUEST_ICON;
|
|
613
619
|
const handleChange = (next) => {
|
|
@@ -621,16 +627,17 @@ function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDi
|
|
|
621
627
|
onSeatTwoChange("person");
|
|
622
628
|
}
|
|
623
629
|
};
|
|
624
|
-
const colorSection = seatTwo === "nobody" ? void 0 : { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, caption: selectedProfile ? colorCaption : void 0 };
|
|
630
|
+
const colorSection = seatTwo === "nobody" ? void 0 : { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, caption: selectedProfile ? colorCaption : void 0, previewValue };
|
|
625
631
|
const pickerId = getSeatTwoPopoverId(idPrefix);
|
|
626
632
|
const size = (0, import_profile4.getOptionPickerContentSize)(options.length, colorSection);
|
|
627
|
-
const
|
|
633
|
+
const useAlign = usePickerAlign ?? import_lobby.useZoneClampedAlign;
|
|
634
|
+
const align = useAlign(host.openId === pickerId, size.width, size.height);
|
|
628
635
|
const ownPopoverOpen = host.openId?.startsWith(`${idPrefix}-`) ?? false;
|
|
629
636
|
const pickerRowOpen = secondPickerId !== void 0 && host.openId === secondPickerId;
|
|
630
637
|
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native3.View, { style: [styles2.panel, ownPopoverOpen && styles2.panelOpen], children: [
|
|
631
|
-
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_profile4.OptionPicker, { id: pickerId, host, options, value, onChange: handleChange, color, dark, icon, colorSection, alignOverride: align }),
|
|
638
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_profile4.OptionPicker, { id: pickerId, host, options, value, onChange: handleChange, color: drawn, dark, icon, colorSection, alignOverride: align, triggerStyle }),
|
|
632
639
|
isPerson && secondPicker && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native3.View, { style: [styles2.pickerRow, pickerRowOpen && styles2.pickerRowOpen], children: secondPicker }),
|
|
633
|
-
isPerson && showReadyButton && onToggleReady && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lobby.ReadyButton, { color, ready: ready ?? false, onToggleReady })
|
|
640
|
+
isPerson && showReadyButton && onToggleReady && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lobby.ReadyButton, { color: drawn, ready: ready ?? false, onToggleReady })
|
|
634
641
|
] });
|
|
635
642
|
}
|
|
636
643
|
var styles2 = import_react_native3.StyleSheet.create({
|
|
@@ -870,13 +877,20 @@ function LoadoutScreen({ game }) {
|
|
|
870
877
|
dispatch(liveplayActions.setActiveRound({ game: game.id, mode, settings: roundSettingsRef.current }));
|
|
871
878
|
import_expo_router4.router.push(playHref(game.id));
|
|
872
879
|
}, [ready, humanSeats, game.id, mode, dispatch]);
|
|
873
|
-
const bg = dark ? "#000000" : "#FFFFFF";
|
|
874
|
-
const fg = dark ? "#FFFFFF" : "#000000";
|
|
875
|
-
const fgMuted = dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
|
|
880
|
+
const bg = pack.Backdrop ? "transparent" : dark ? "#000000" : "#FFFFFF";
|
|
881
|
+
const fg = pack.foreground ?? (dark ? "#FFFFFF" : "#000000");
|
|
882
|
+
const fgMuted = (pack.foreground && halfOf(pack.foreground)) ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
|
|
883
|
+
const { previewColor } = pack;
|
|
884
|
+
const seatPreview = (0, import_react13.useMemo)(() => previewColor ? (hex) => previewColor(hex, dark) : void 0, [previewColor, dark]);
|
|
885
|
+
const p1Drawn = seatPreview ? seatPreview(p1Color) : p1Color;
|
|
886
|
+
const p2Drawn = seatPreview ? seatPreview(p2Color) : p2Color;
|
|
887
|
+
const triggerStyle = (0, import_react13.useMemo)(() => pack.Backdrop ? [styles3.pill, { backgroundColor: themeColors.surface }] : void 0, [pack.Backdrop, themeColors.surface]);
|
|
876
888
|
const isPortrait = panelLayout.orientationMode === "faceToFace";
|
|
877
889
|
const showMetaInSharedBand = (0, import_split_screen.needsSharedNeutralZone)(panelLayout.orientationMode, humanSeats.length);
|
|
878
890
|
const p2ZoneStyle = p2IsHuman ? isPortrait ? styles3.p2ZoneTop : panelLayout.p1OnRight ? styles3.p2ZoneLeft : styles3.p2ZoneRight : null;
|
|
879
891
|
const showReadyButton = p2IsHuman;
|
|
892
|
+
const seatPickerAlign = p2IsHuman ? void 0 : pack.usePickerAlign;
|
|
893
|
+
const layoutKey = p2IsHuman ? "zones" : "row";
|
|
880
894
|
const controlsHost = (0, import_chrome4.usePopoverHost)();
|
|
881
895
|
const p2PanelHost = (0, import_chrome4.usePopoverHost)();
|
|
882
896
|
const p2Host = p2IsHuman ? p2PanelHost : controlsHost;
|
|
@@ -895,10 +909,10 @@ function LoadoutScreen({ game }) {
|
|
|
895
909
|
const optionsPopoverOpen = controlsHost.openId !== null && loadout.optionIds.includes(controlsHost.openId);
|
|
896
910
|
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;
|
|
897
911
|
const controlLook = { accentColor: themeColors.tertiary, mutedColor: fgMuted, dark, rotation };
|
|
898
|
-
const p1Control = loadout.seatControl(1, controlsHost, { ...controlLook, seatColor:
|
|
899
|
-
const p1Panel = /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lobby2.PlayerSetupPanel, { idPrefix: "p1", host: controlsHost, color: p1Color, onColorChange: handleP1ColorChange, swatches: import_auto_paper4.defaultColors, takenColor: p2Exists ? p2Color : void 0, allowSwapTaken: mode === "vsCpu", colorCaption: pack.colorCaption, isHuman: true, ready: ready[1], onToggleReady: () => setReady((r) => ({ ...r, 1: !r[1] })), dark, showReadyButton, secondPicker: p1Control, secondPickerId: p1Control ? "p1-control" : void 0, profiles, selectedProfileId: lastSelected[1], takenProfileId: p2IsHuman ? lastSelected[2] : null, guestLabel: "P1", onProfileSelect: handleP1ProfileSelect, onManageProfiles: () => import_expo_router4.router.push(PROFILES_HREF) });
|
|
900
|
-
const p2Control = p2IsHuman ? loadout.seatControl(2, p2Host, { ...controlLook, seatColor:
|
|
901
|
-
const p2Panel = game.seatTwo && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SeatTwoPanel, { idPrefix: "p2", host: p2Host, choices: game.seatTwo, seatTwo, onSeatTwoChange: changeSeatTwo, cpuDifficulty: loadout.cpuDifficulty, profiles, selectedProfileId: lastSelected[2], onProfileSelect: handleP2ProfileSelect, color: p2Color, onColorChange: handleP2ColorChange, swatches: import_auto_paper4.defaultColors, takenColor: p1Color, allowSwapTaken: mode === "vsCpu", colorCaption: pack.colorCaption, dark, secondPicker: p2Control, secondPickerId: p2Control ? "p2-control" : void 0, ready: ready[2], onToggleReady: () => setReady((r) => ({ ...r, 2: !r[2] })), showReadyButton });
|
|
912
|
+
const p1Control = loadout.seatControl(1, controlsHost, { ...controlLook, seatColor: p1Drawn });
|
|
913
|
+
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}`);
|
|
914
|
+
const p2Control = p2IsHuman ? loadout.seatControl(2, p2Host, { ...controlLook, seatColor: p2Drawn }) : void 0;
|
|
915
|
+
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}`);
|
|
902
916
|
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native4.View, { style: [styles3.container, { backgroundColor: bg }], children: [
|
|
903
917
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_core10.FakeLandscapeView, { orientationMode: panelLayout.orientationMode, p1OnRight: panelLayout.p1OnRight, upsideDown: panelLayout.upsideDown, style: styles3.rotatable, children: [
|
|
904
918
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_chrome4.PressAwayOverlay, { active: controlsHost.openId !== null, onPress: controlsHost.close }),
|
|
@@ -922,12 +936,16 @@ function LoadoutScreen({ game }) {
|
|
|
922
936
|
!isPortrait && panelLayout.p1OnRight ? p2Panel : p1Panel,
|
|
923
937
|
!isPortrait && panelLayout.p1OnRight ? p1Panel : p2Panel
|
|
924
938
|
] }),
|
|
925
|
-
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lobby2.ReadyButton, { color:
|
|
939
|
+
/* @__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 })
|
|
926
940
|
] })
|
|
927
941
|
] }),
|
|
928
942
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SettingsDialog, { visible: settingsOpen, onDismiss: () => setSettingsOpen(false), rotation, onShowHowToPlay: () => openFor(game.id) })
|
|
929
943
|
] });
|
|
930
944
|
}
|
|
945
|
+
function halfOf(color) {
|
|
946
|
+
const rgb = (0, import_auto_paper4.getRgb)(color);
|
|
947
|
+
return rgb ? `rgba(${rgb.r},${rgb.g},${rgb.b},0.5)` : void 0;
|
|
948
|
+
}
|
|
931
949
|
var styles3 = import_react_native4.StyleSheet.create({
|
|
932
950
|
container: {
|
|
933
951
|
flex: 1
|
|
@@ -953,6 +971,10 @@ var styles3 = import_react_native4.StyleSheet.create({
|
|
|
953
971
|
right: 0,
|
|
954
972
|
top: 0
|
|
955
973
|
},
|
|
974
|
+
// A seat button's trigger on a surface over the pack's backdrop, as Home's.
|
|
975
|
+
pill: {
|
|
976
|
+
borderRadius: 999
|
|
977
|
+
},
|
|
956
978
|
// The one-human row: seat 1's panel and seat 2's button read as one evenly spaced row of buttons,
|
|
957
979
|
// at PlayerSetupPanel's own pickerRow gap (12).
|
|
958
980
|
playersRow: {
|
package/dist/index.mjs
CHANGED
|
@@ -29,7 +29,7 @@ import { OrientationProvider, useRotationAwareStatusBar, useThemedRootBackground
|
|
|
29
29
|
import { RotatedFrame } from "@tastic/hud/chrome";
|
|
30
30
|
import { ProfileColorProvider } from "@tastic/profile";
|
|
31
31
|
import * as Haptics from "expo-haptics";
|
|
32
|
-
import { DarkTheme, Stack, ThemeProvider as NavigationThemeProvider } from "expo-router";
|
|
32
|
+
import { DarkTheme, DefaultTheme, Stack, ThemeProvider as NavigationThemeProvider } from "expo-router";
|
|
33
33
|
import { DeviceMotion } from "expo-sensors";
|
|
34
34
|
import { useCallback as useCallback6, useMemo as useMemo3 } from "react";
|
|
35
35
|
import { Platform as Platform2, StyleSheet, View } from "react-native";
|
|
@@ -410,8 +410,9 @@ function AppStack() {
|
|
|
410
410
|
const pack = usePack();
|
|
411
411
|
const { Backdrop } = pack;
|
|
412
412
|
const { dark } = useAutoPaperTheme();
|
|
413
|
-
const
|
|
414
|
-
const
|
|
413
|
+
const darkRoot = Backdrop ? true : dark;
|
|
414
|
+
const rootBackground = useThemedRootBackground(darkRoot, false);
|
|
415
|
+
const statusBar = useRotationAwareStatusBar({ style: darkRoot ? "light" : "dark" });
|
|
415
416
|
const packOptions = (pack.useScreenOptions ?? noScreenOptions)();
|
|
416
417
|
const backdropOptions = Backdrop ? { contentStyle: TRANSPARENT_CONTENT, animation: Platform2.select({ android: "fade" }) } : void 0;
|
|
417
418
|
const playOptions = useCallback6(
|
|
@@ -421,15 +422,19 @@ function AppStack() {
|
|
|
421
422
|
},
|
|
422
423
|
[pack.games]
|
|
423
424
|
);
|
|
424
|
-
const
|
|
425
|
-
const
|
|
425
|
+
const navigationBackground = Backdrop ? TRANSPARENT_CONTENT.backgroundColor : rootBackground.contentStyle.backgroundColor;
|
|
426
|
+
const navigationTheme = useMemo3(() => {
|
|
427
|
+
const base = darkRoot ? DarkTheme : DefaultTheme;
|
|
428
|
+
return { ...base, colors: { ...base.colors, background: navigationBackground } };
|
|
429
|
+
}, [darkRoot, navigationBackground]);
|
|
430
|
+
const stack = /* @__PURE__ */ jsx9(NavigationThemeProvider, { value: navigationTheme, children: /* @__PURE__ */ jsxs2(Stack, { screenOptions: { ...rootBackground, ...statusBar, ...backdropOptions, ...packOptions }, children: [
|
|
426
431
|
/* @__PURE__ */ jsx9(Stack.Screen, { name: "index" }),
|
|
427
432
|
/* @__PURE__ */ jsx9(Stack.Screen, { name: "play/[game]", options: playOptions })
|
|
428
|
-
] });
|
|
429
|
-
if (!Backdrop
|
|
433
|
+
] }) });
|
|
434
|
+
if (!Backdrop) return stack;
|
|
430
435
|
return /* @__PURE__ */ jsxs2(View, { style: styles.root, children: [
|
|
431
436
|
/* @__PURE__ */ jsx9(Backdrop, {}),
|
|
432
|
-
|
|
437
|
+
stack
|
|
433
438
|
] });
|
|
434
439
|
}
|
|
435
440
|
function OrientationBridge({ children }) {
|
|
@@ -492,7 +497,7 @@ var STATS_HREF = "/stats";
|
|
|
492
497
|
var PROFILES_HREF = "/profiles";
|
|
493
498
|
|
|
494
499
|
// src/screens/LoadoutScreen.tsx
|
|
495
|
-
import { defaultColors, useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
|
|
500
|
+
import { defaultColors, getRgb, useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
|
|
496
501
|
import { FakeLandscapeView, getViewRotation, rotateInsets, useOrientationState } from "@tastic/core";
|
|
497
502
|
import { CornerActionButtons, PressAwayOverlay, usePopoverHost } from "@tastic/hud/chrome";
|
|
498
503
|
import { PlayerSetupPanel, ReadyButton as ReadyButton2, SharedActionBand } from "@tastic/hud/lobby";
|
|
@@ -520,8 +525,9 @@ var CPU_ICON = "robot";
|
|
|
520
525
|
function getSeatTwoPopoverId(idPrefix) {
|
|
521
526
|
return `${idPrefix}-seat`;
|
|
522
527
|
}
|
|
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 }) {
|
|
528
|
+
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 }) {
|
|
524
529
|
const isPerson = seatTwo === "person";
|
|
530
|
+
const drawn = previewValue ? previewValue(color) : color;
|
|
525
531
|
const selectedProfile = isPerson ? profiles.find((p) => p.id === selectedProfileId) ?? null : null;
|
|
526
532
|
const cpuOptions = cpuDifficulty?.options;
|
|
527
533
|
const options = useMemo4(() => {
|
|
@@ -530,10 +536,10 @@ function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDi
|
|
|
530
536
|
if (choices.includes("nobody")) list.push({ value: NOBODY, label: "Nobody", icon: NOBODY_ICON });
|
|
531
537
|
if (choices.includes("person")) {
|
|
532
538
|
list.push({ value: GUEST, label: "Player 2", icon: GUEST_ICON });
|
|
533
|
-
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 })));
|
|
539
|
+
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 })));
|
|
534
540
|
}
|
|
535
541
|
return list;
|
|
536
|
-
}, [choices, cpuOptions, profiles]);
|
|
542
|
+
}, [choices, cpuOptions, profiles, previewValue]);
|
|
537
543
|
const value = seatTwo === "cpu" ? `${CPU_PREFIX}${cpuDifficulty?.value ?? ""}` : seatTwo === "nobody" ? NOBODY : selectedProfile ? `${PROFILE_PREFIX}${selectedProfile.id}` : GUEST;
|
|
538
544
|
const icon = seatTwo === "cpu" ? CPU_ICON : seatTwo === "nobody" ? NOBODY_ICON : selectedProfile ? "account" : GUEST_ICON;
|
|
539
545
|
const handleChange = (next) => {
|
|
@@ -547,16 +553,17 @@ function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDi
|
|
|
547
553
|
onSeatTwoChange("person");
|
|
548
554
|
}
|
|
549
555
|
};
|
|
550
|
-
const colorSection = seatTwo === "nobody" ? void 0 : { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, caption: selectedProfile ? colorCaption : void 0 };
|
|
556
|
+
const colorSection = seatTwo === "nobody" ? void 0 : { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, caption: selectedProfile ? colorCaption : void 0, previewValue };
|
|
551
557
|
const pickerId = getSeatTwoPopoverId(idPrefix);
|
|
552
558
|
const size = getOptionPickerContentSize(options.length, colorSection);
|
|
553
|
-
const
|
|
559
|
+
const useAlign = usePickerAlign ?? useZoneClampedAlign;
|
|
560
|
+
const align = useAlign(host.openId === pickerId, size.width, size.height);
|
|
554
561
|
const ownPopoverOpen = host.openId?.startsWith(`${idPrefix}-`) ?? false;
|
|
555
562
|
const pickerRowOpen = secondPickerId !== void 0 && host.openId === secondPickerId;
|
|
556
563
|
return /* @__PURE__ */ jsxs3(View2, { style: [styles2.panel, ownPopoverOpen && styles2.panelOpen], children: [
|
|
557
|
-
/* @__PURE__ */ jsx10(OptionPicker, { id: pickerId, host, options, value, onChange: handleChange, color, dark, icon, colorSection, alignOverride: align }),
|
|
564
|
+
/* @__PURE__ */ jsx10(OptionPicker, { id: pickerId, host, options, value, onChange: handleChange, color: drawn, dark, icon, colorSection, alignOverride: align, triggerStyle }),
|
|
558
565
|
isPerson && secondPicker && /* @__PURE__ */ jsx10(View2, { style: [styles2.pickerRow, pickerRowOpen && styles2.pickerRowOpen], children: secondPicker }),
|
|
559
|
-
isPerson && showReadyButton && onToggleReady && /* @__PURE__ */ jsx10(ReadyButton, { color, ready: ready ?? false, onToggleReady })
|
|
566
|
+
isPerson && showReadyButton && onToggleReady && /* @__PURE__ */ jsx10(ReadyButton, { color: drawn, ready: ready ?? false, onToggleReady })
|
|
560
567
|
] });
|
|
561
568
|
}
|
|
562
569
|
var styles2 = StyleSheet2.create({
|
|
@@ -796,13 +803,20 @@ function LoadoutScreen({ game }) {
|
|
|
796
803
|
dispatch(liveplayActions.setActiveRound({ game: game.id, mode, settings: roundSettingsRef.current }));
|
|
797
804
|
router2.push(playHref(game.id));
|
|
798
805
|
}, [ready, humanSeats, game.id, mode, dispatch]);
|
|
799
|
-
const bg = dark ? "#000000" : "#FFFFFF";
|
|
800
|
-
const fg = dark ? "#FFFFFF" : "#000000";
|
|
801
|
-
const fgMuted = dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
|
|
806
|
+
const bg = pack.Backdrop ? "transparent" : dark ? "#000000" : "#FFFFFF";
|
|
807
|
+
const fg = pack.foreground ?? (dark ? "#FFFFFF" : "#000000");
|
|
808
|
+
const fgMuted = (pack.foreground && halfOf(pack.foreground)) ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
|
|
809
|
+
const { previewColor } = pack;
|
|
810
|
+
const seatPreview = useMemo5(() => previewColor ? (hex) => previewColor(hex, dark) : void 0, [previewColor, dark]);
|
|
811
|
+
const p1Drawn = seatPreview ? seatPreview(p1Color) : p1Color;
|
|
812
|
+
const p2Drawn = seatPreview ? seatPreview(p2Color) : p2Color;
|
|
813
|
+
const triggerStyle = useMemo5(() => pack.Backdrop ? [styles3.pill, { backgroundColor: themeColors.surface }] : void 0, [pack.Backdrop, themeColors.surface]);
|
|
802
814
|
const isPortrait = panelLayout.orientationMode === "faceToFace";
|
|
803
815
|
const showMetaInSharedBand = needsSharedNeutralZone(panelLayout.orientationMode, humanSeats.length);
|
|
804
816
|
const p2ZoneStyle = p2IsHuman ? isPortrait ? styles3.p2ZoneTop : panelLayout.p1OnRight ? styles3.p2ZoneLeft : styles3.p2ZoneRight : null;
|
|
805
817
|
const showReadyButton = p2IsHuman;
|
|
818
|
+
const seatPickerAlign = p2IsHuman ? void 0 : pack.usePickerAlign;
|
|
819
|
+
const layoutKey = p2IsHuman ? "zones" : "row";
|
|
806
820
|
const controlsHost = usePopoverHost();
|
|
807
821
|
const p2PanelHost = usePopoverHost();
|
|
808
822
|
const p2Host = p2IsHuman ? p2PanelHost : controlsHost;
|
|
@@ -821,10 +835,10 @@ function LoadoutScreen({ game }) {
|
|
|
821
835
|
const optionsPopoverOpen = controlsHost.openId !== null && loadout.optionIds.includes(controlsHost.openId);
|
|
822
836
|
const sharedControls = showMetaInSharedBand ? /* @__PURE__ */ jsx12(SharedActionBand, { onBack: safeBack, onSettings: () => setSettingsOpen(true), onRandomize: loadout.randomize, onReset: loadout.reset, fg, popoverOpen: optionsPopoverOpen, children: optionsRow }) : optionsRow;
|
|
823
837
|
const controlLook = { accentColor: themeColors.tertiary, mutedColor: fgMuted, dark, rotation };
|
|
824
|
-
const p1Control = loadout.seatControl(1, controlsHost, { ...controlLook, seatColor:
|
|
825
|
-
const p1Panel = /* @__PURE__ */ jsx12(PlayerSetupPanel, { idPrefix: "p1", host: controlsHost, color: p1Color, onColorChange: handleP1ColorChange, swatches: defaultColors, takenColor: p2Exists ? p2Color : void 0, allowSwapTaken: mode === "vsCpu", colorCaption: pack.colorCaption, isHuman: true, ready: ready[1], onToggleReady: () => setReady((r) => ({ ...r, 1: !r[1] })), dark, showReadyButton, secondPicker: p1Control, secondPickerId: p1Control ? "p1-control" : void 0, profiles, selectedProfileId: lastSelected[1], takenProfileId: p2IsHuman ? lastSelected[2] : null, guestLabel: "P1", onProfileSelect: handleP1ProfileSelect, onManageProfiles: () => router2.push(PROFILES_HREF) });
|
|
826
|
-
const p2Control = p2IsHuman ? loadout.seatControl(2, p2Host, { ...controlLook, seatColor:
|
|
827
|
-
const p2Panel = game.seatTwo && /* @__PURE__ */ jsx12(SeatTwoPanel, { idPrefix: "p2", host: p2Host, choices: game.seatTwo, seatTwo, onSeatTwoChange: changeSeatTwo, cpuDifficulty: loadout.cpuDifficulty, profiles, selectedProfileId: lastSelected[2], onProfileSelect: handleP2ProfileSelect, color: p2Color, onColorChange: handleP2ColorChange, swatches: defaultColors, takenColor: p1Color, allowSwapTaken: mode === "vsCpu", colorCaption: pack.colorCaption, dark, secondPicker: p2Control, secondPickerId: p2Control ? "p2-control" : void 0, ready: ready[2], onToggleReady: () => setReady((r) => ({ ...r, 2: !r[2] })), showReadyButton });
|
|
838
|
+
const p1Control = loadout.seatControl(1, controlsHost, { ...controlLook, seatColor: p1Drawn });
|
|
839
|
+
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}`);
|
|
840
|
+
const p2Control = p2IsHuman ? loadout.seatControl(2, p2Host, { ...controlLook, seatColor: p2Drawn }) : void 0;
|
|
841
|
+
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}`);
|
|
828
842
|
return /* @__PURE__ */ jsxs5(View3, { style: [styles3.container, { backgroundColor: bg }], children: [
|
|
829
843
|
/* @__PURE__ */ jsxs5(FakeLandscapeView, { orientationMode: panelLayout.orientationMode, p1OnRight: panelLayout.p1OnRight, upsideDown: panelLayout.upsideDown, style: styles3.rotatable, children: [
|
|
830
844
|
/* @__PURE__ */ jsx12(PressAwayOverlay, { active: controlsHost.openId !== null, onPress: controlsHost.close }),
|
|
@@ -848,12 +862,16 @@ function LoadoutScreen({ game }) {
|
|
|
848
862
|
!isPortrait && panelLayout.p1OnRight ? p2Panel : p1Panel,
|
|
849
863
|
!isPortrait && panelLayout.p1OnRight ? p1Panel : p2Panel
|
|
850
864
|
] }),
|
|
851
|
-
/* @__PURE__ */ jsx12(ReadyButton2, { color:
|
|
865
|
+
/* @__PURE__ */ jsx12(ReadyButton2, { color: p1Drawn, ready: ready[1], onToggleReady: () => setReady((r) => ({ ...r, 1: !r[1] })), style: anyPlayerPickerOpen && styles3.readyButtonHidden })
|
|
852
866
|
] })
|
|
853
867
|
] }),
|
|
854
868
|
/* @__PURE__ */ jsx12(SettingsDialog, { visible: settingsOpen, onDismiss: () => setSettingsOpen(false), rotation, onShowHowToPlay: () => openFor(game.id) })
|
|
855
869
|
] });
|
|
856
870
|
}
|
|
871
|
+
function halfOf(color) {
|
|
872
|
+
const rgb = getRgb(color);
|
|
873
|
+
return rgb ? `rgba(${rgb.r},${rgb.g},${rgb.b},0.5)` : void 0;
|
|
874
|
+
}
|
|
857
875
|
var styles3 = StyleSheet3.create({
|
|
858
876
|
container: {
|
|
859
877
|
flex: 1
|
|
@@ -879,6 +897,10 @@ var styles3 = StyleSheet3.create({
|
|
|
879
897
|
right: 0,
|
|
880
898
|
top: 0
|
|
881
899
|
},
|
|
900
|
+
// A seat button's trigger on a surface over the pack's backdrop, as Home's.
|
|
901
|
+
pill: {
|
|
902
|
+
borderRadius: 999
|
|
903
|
+
},
|
|
882
904
|
// The one-human row: seat 1's panel and seat 2's button read as one evenly spaced row of buttons,
|
|
883
905
|
// at PlayerSetupPanel's own pickerRow gap (12).
|
|
884
906
|
playersRow: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastic/shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
package/src/RootLayout.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { OrientationProvider, useRotationAwareStatusBar, useThemedRootBackground
|
|
|
5
5
|
import { RotatedFrame } from '@tastic/hud/chrome'
|
|
6
6
|
import { ProfileColorProvider } from '@tastic/profile'
|
|
7
7
|
import * as Haptics from 'expo-haptics'
|
|
8
|
-
import { DarkTheme, Stack, ThemeProvider as NavigationThemeProvider } from 'expo-router'
|
|
8
|
+
import { DarkTheme, DefaultTheme, Stack, ThemeProvider as NavigationThemeProvider } from 'expo-router'
|
|
9
9
|
import { DeviceMotion } from 'expo-sensors'
|
|
10
10
|
import { ReactNode, useCallback, useMemo } from 'react'
|
|
11
11
|
import { Platform, StyleSheet, View } from 'react-native'
|
|
@@ -50,13 +50,16 @@ import type { PackDefinition, PackGameDefinition } from './types'
|
|
|
50
50
|
const PLAY_OPTIONS = { statusBarHidden: true }
|
|
51
51
|
const KEEP_STATUS_BAR_OPTIONS = {}
|
|
52
52
|
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
53
|
+
// The navigation theme is the root background too. React Navigation paints its theme's
|
|
54
|
+
// `colors.background` on the layers around every screen (the ScreenStack's container on native, each
|
|
55
|
+
// screen's wrapper on the web), and expo-router hands its container no theme, so that was always
|
|
56
|
+
// DefaultTheme's light grey: on iOS it showed in a sliding screen's rounded corners during every push and
|
|
57
|
+
// pop, whatever the app's theme. So the stack always gets a theme whose background is the root
|
|
58
|
+
// background's own color, and a pack's Backdrop (PackDefinition.Backdrop), drawn once behind the whole
|
|
59
|
+
// stack, gets a transparent one so it shows through both layers (contentStyle alone left an opaque light
|
|
60
|
+
// grey slab over Solitaire's felt on the web). Nothing else reads the navigation theme (headers are
|
|
61
|
+
// hidden; Paper has its own). Built as the stack renders, never at import, where a pack's tests may stand
|
|
62
|
+
// in for expo-router without its themes.
|
|
60
63
|
const TRANSPARENT_CONTENT = { backgroundColor: 'transparent' }
|
|
61
64
|
|
|
62
65
|
// The pack's own screen options (PackDefinition.useScreenOptions), as a hook every pack calls: an
|
|
@@ -67,10 +70,13 @@ function AppStack() {
|
|
|
67
70
|
const pack = usePack()
|
|
68
71
|
const { Backdrop } = pack
|
|
69
72
|
const { dark } = useAutoPaperTheme()
|
|
70
|
-
// Over a backdrop the root
|
|
71
|
-
//
|
|
72
|
-
const
|
|
73
|
-
const
|
|
73
|
+
// Over a backdrop the root stays dark whatever the mode: a backdrop is a dark table (the felt), and a
|
|
74
|
+
// light root window would flash white around it during a push or pop.
|
|
75
|
+
const darkRoot = Backdrop ? true : dark
|
|
76
|
+
const rootBackground = useThemedRootBackground(darkRoot, false)
|
|
77
|
+
// The status bar's text follows the app's theme, not the phone's appearance (react-native-screens'
|
|
78
|
+
// default `auto`), which drew dark text on a dark theme's black Home with the phone set to light.
|
|
79
|
+
const statusBar = useRotationAwareStatusBar({ style: darkRoot ? 'light' : 'dark' })
|
|
74
80
|
const packOptions = (pack.useScreenOptions ?? noScreenOptions)()
|
|
75
81
|
// Over a backdrop every screen is transparent, and Android crossfades: its default push and pop hold
|
|
76
82
|
// the outgoing screen opaque for ~450ms while the new one slides over it, which would show Home
|
|
@@ -83,18 +89,24 @@ function AppStack() {
|
|
|
83
89
|
},
|
|
84
90
|
[pack.games]
|
|
85
91
|
)
|
|
86
|
-
const
|
|
92
|
+
const navigationBackground = Backdrop ? TRANSPARENT_CONTENT.backgroundColor : rootBackground.contentStyle.backgroundColor
|
|
93
|
+
const navigationTheme = useMemo(() => {
|
|
94
|
+
const base = darkRoot ? DarkTheme : DefaultTheme
|
|
95
|
+
return { ...base, colors: { ...base.colors, background: navigationBackground } }
|
|
96
|
+
}, [darkRoot, navigationBackground])
|
|
87
97
|
const stack = (
|
|
88
|
-
<
|
|
89
|
-
<Stack
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
<NavigationThemeProvider value={navigationTheme}>
|
|
99
|
+
<Stack screenOptions={{ ...rootBackground, ...statusBar, ...backdropOptions, ...packOptions }}>
|
|
100
|
+
<Stack.Screen name='index' />
|
|
101
|
+
<Stack.Screen name='play/[game]' options={playOptions} />
|
|
102
|
+
</Stack>
|
|
103
|
+
</NavigationThemeProvider>
|
|
92
104
|
)
|
|
93
|
-
if (!Backdrop
|
|
105
|
+
if (!Backdrop) return stack
|
|
94
106
|
return (
|
|
95
107
|
<View style={styles.root}>
|
|
96
108
|
<Backdrop />
|
|
97
|
-
|
|
109
|
+
{stack}
|
|
98
110
|
</View>
|
|
99
111
|
)
|
|
100
112
|
}
|
|
@@ -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,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).
|