@tastic/shell 0.2.1 → 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 +32 -15
- package/dist/index.mjs +33 -16
- package/package.json +3 -3
- package/src/components/SeatTwoPanel.tsx +21 -10
- 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
|
@@ -594,8 +594,9 @@ var CPU_ICON = "robot";
|
|
|
594
594
|
function getSeatTwoPopoverId(idPrefix) {
|
|
595
595
|
return `${idPrefix}-seat`;
|
|
596
596
|
}
|
|
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 }) {
|
|
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 }) {
|
|
598
598
|
const isPerson = seatTwo === "person";
|
|
599
|
+
const drawn = previewValue ? previewValue(color) : color;
|
|
599
600
|
const selectedProfile = isPerson ? profiles.find((p) => p.id === selectedProfileId) ?? null : null;
|
|
600
601
|
const cpuOptions = cpuDifficulty?.options;
|
|
601
602
|
const options = (0, import_react10.useMemo)(() => {
|
|
@@ -604,10 +605,10 @@ function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDi
|
|
|
604
605
|
if (choices.includes("nobody")) list.push({ value: NOBODY, label: "Nobody", icon: NOBODY_ICON });
|
|
605
606
|
if (choices.includes("person")) {
|
|
606
607
|
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 })));
|
|
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 })));
|
|
608
609
|
}
|
|
609
610
|
return list;
|
|
610
|
-
}, [choices, cpuOptions, profiles]);
|
|
611
|
+
}, [choices, cpuOptions, profiles, previewValue]);
|
|
611
612
|
const value = seatTwo === "cpu" ? `${CPU_PREFIX}${cpuDifficulty?.value ?? ""}` : seatTwo === "nobody" ? NOBODY : selectedProfile ? `${PROFILE_PREFIX}${selectedProfile.id}` : GUEST;
|
|
612
613
|
const icon = seatTwo === "cpu" ? CPU_ICON : seatTwo === "nobody" ? NOBODY_ICON : selectedProfile ? "account" : GUEST_ICON;
|
|
613
614
|
const handleChange = (next) => {
|
|
@@ -621,16 +622,17 @@ function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDi
|
|
|
621
622
|
onSeatTwoChange("person");
|
|
622
623
|
}
|
|
623
624
|
};
|
|
624
|
-
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 };
|
|
625
626
|
const pickerId = getSeatTwoPopoverId(idPrefix);
|
|
626
627
|
const size = (0, import_profile4.getOptionPickerContentSize)(options.length, colorSection);
|
|
627
|
-
const
|
|
628
|
+
const useAlign = usePickerAlign ?? import_lobby.useZoneClampedAlign;
|
|
629
|
+
const align = useAlign(host.openId === pickerId, size.width, size.height);
|
|
628
630
|
const ownPopoverOpen = host.openId?.startsWith(`${idPrefix}-`) ?? false;
|
|
629
631
|
const pickerRowOpen = secondPickerId !== void 0 && host.openId === secondPickerId;
|
|
630
632
|
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 }),
|
|
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 }),
|
|
632
634
|
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 })
|
|
635
|
+
isPerson && showReadyButton && onToggleReady && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lobby.ReadyButton, { color: drawn, ready: ready ?? false, onToggleReady })
|
|
634
636
|
] });
|
|
635
637
|
}
|
|
636
638
|
var styles2 = import_react_native3.StyleSheet.create({
|
|
@@ -870,13 +872,20 @@ function LoadoutScreen({ game }) {
|
|
|
870
872
|
dispatch(liveplayActions.setActiveRound({ game: game.id, mode, settings: roundSettingsRef.current }));
|
|
871
873
|
import_expo_router4.router.push(playHref(game.id));
|
|
872
874
|
}, [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)";
|
|
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]);
|
|
876
883
|
const isPortrait = panelLayout.orientationMode === "faceToFace";
|
|
877
884
|
const showMetaInSharedBand = (0, import_split_screen.needsSharedNeutralZone)(panelLayout.orientationMode, humanSeats.length);
|
|
878
885
|
const p2ZoneStyle = p2IsHuman ? isPortrait ? styles3.p2ZoneTop : panelLayout.p1OnRight ? styles3.p2ZoneLeft : styles3.p2ZoneRight : null;
|
|
879
886
|
const showReadyButton = p2IsHuman;
|
|
887
|
+
const seatPickerAlign = p2IsHuman ? void 0 : pack.usePickerAlign;
|
|
888
|
+
const layoutKey = p2IsHuman ? "zones" : "row";
|
|
880
889
|
const controlsHost = (0, import_chrome4.usePopoverHost)();
|
|
881
890
|
const p2PanelHost = (0, import_chrome4.usePopoverHost)();
|
|
882
891
|
const p2Host = p2IsHuman ? p2PanelHost : controlsHost;
|
|
@@ -895,10 +904,10 @@ function LoadoutScreen({ game }) {
|
|
|
895
904
|
const optionsPopoverOpen = controlsHost.openId !== null && loadout.optionIds.includes(controlsHost.openId);
|
|
896
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;
|
|
897
906
|
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 });
|
|
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}`);
|
|
902
911
|
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native4.View, { style: [styles3.container, { backgroundColor: bg }], children: [
|
|
903
912
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_core10.FakeLandscapeView, { orientationMode: panelLayout.orientationMode, p1OnRight: panelLayout.p1OnRight, upsideDown: panelLayout.upsideDown, style: styles3.rotatable, children: [
|
|
904
913
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_chrome4.PressAwayOverlay, { active: controlsHost.openId !== null, onPress: controlsHost.close }),
|
|
@@ -922,12 +931,16 @@ function LoadoutScreen({ game }) {
|
|
|
922
931
|
!isPortrait && panelLayout.p1OnRight ? p2Panel : p1Panel,
|
|
923
932
|
!isPortrait && panelLayout.p1OnRight ? p1Panel : p2Panel
|
|
924
933
|
] }),
|
|
925
|
-
/* @__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 })
|
|
926
935
|
] })
|
|
927
936
|
] }),
|
|
928
937
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SettingsDialog, { visible: settingsOpen, onDismiss: () => setSettingsOpen(false), rotation, onShowHowToPlay: () => openFor(game.id) })
|
|
929
938
|
] });
|
|
930
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
|
+
}
|
|
931
944
|
var styles3 = import_react_native4.StyleSheet.create({
|
|
932
945
|
container: {
|
|
933
946
|
flex: 1
|
|
@@ -953,6 +966,10 @@ var styles3 = import_react_native4.StyleSheet.create({
|
|
|
953
966
|
right: 0,
|
|
954
967
|
top: 0
|
|
955
968
|
},
|
|
969
|
+
// A seat button's trigger on a surface over the pack's backdrop, as Home's.
|
|
970
|
+
pill: {
|
|
971
|
+
borderRadius: 999
|
|
972
|
+
},
|
|
956
973
|
// The one-human row: seat 1's panel and seat 2's button read as one evenly spaced row of buttons,
|
|
957
974
|
// at PlayerSetupPanel's own pickerRow gap (12).
|
|
958
975
|
playersRow: {
|
package/dist/index.mjs
CHANGED
|
@@ -492,7 +492,7 @@ var STATS_HREF = "/stats";
|
|
|
492
492
|
var PROFILES_HREF = "/profiles";
|
|
493
493
|
|
|
494
494
|
// src/screens/LoadoutScreen.tsx
|
|
495
|
-
import { defaultColors, useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
|
|
495
|
+
import { defaultColors, getRgb, useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
|
|
496
496
|
import { FakeLandscapeView, getViewRotation, rotateInsets, useOrientationState } from "@tastic/core";
|
|
497
497
|
import { CornerActionButtons, PressAwayOverlay, usePopoverHost } from "@tastic/hud/chrome";
|
|
498
498
|
import { PlayerSetupPanel, ReadyButton as ReadyButton2, SharedActionBand } from "@tastic/hud/lobby";
|
|
@@ -520,8 +520,9 @@ var CPU_ICON = "robot";
|
|
|
520
520
|
function getSeatTwoPopoverId(idPrefix) {
|
|
521
521
|
return `${idPrefix}-seat`;
|
|
522
522
|
}
|
|
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 }) {
|
|
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 }) {
|
|
524
524
|
const isPerson = seatTwo === "person";
|
|
525
|
+
const drawn = previewValue ? previewValue(color) : color;
|
|
525
526
|
const selectedProfile = isPerson ? profiles.find((p) => p.id === selectedProfileId) ?? null : null;
|
|
526
527
|
const cpuOptions = cpuDifficulty?.options;
|
|
527
528
|
const options = useMemo4(() => {
|
|
@@ -530,10 +531,10 @@ function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDi
|
|
|
530
531
|
if (choices.includes("nobody")) list.push({ value: NOBODY, label: "Nobody", icon: NOBODY_ICON });
|
|
531
532
|
if (choices.includes("person")) {
|
|
532
533
|
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 })));
|
|
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 })));
|
|
534
535
|
}
|
|
535
536
|
return list;
|
|
536
|
-
}, [choices, cpuOptions, profiles]);
|
|
537
|
+
}, [choices, cpuOptions, profiles, previewValue]);
|
|
537
538
|
const value = seatTwo === "cpu" ? `${CPU_PREFIX}${cpuDifficulty?.value ?? ""}` : seatTwo === "nobody" ? NOBODY : selectedProfile ? `${PROFILE_PREFIX}${selectedProfile.id}` : GUEST;
|
|
538
539
|
const icon = seatTwo === "cpu" ? CPU_ICON : seatTwo === "nobody" ? NOBODY_ICON : selectedProfile ? "account" : GUEST_ICON;
|
|
539
540
|
const handleChange = (next) => {
|
|
@@ -547,16 +548,17 @@ function SeatTwoPanel({ idPrefix, host, choices, seatTwo, onSeatTwoChange, cpuDi
|
|
|
547
548
|
onSeatTwoChange("person");
|
|
548
549
|
}
|
|
549
550
|
};
|
|
550
|
-
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 };
|
|
551
552
|
const pickerId = getSeatTwoPopoverId(idPrefix);
|
|
552
553
|
const size = getOptionPickerContentSize(options.length, colorSection);
|
|
553
|
-
const
|
|
554
|
+
const useAlign = usePickerAlign ?? useZoneClampedAlign;
|
|
555
|
+
const align = useAlign(host.openId === pickerId, size.width, size.height);
|
|
554
556
|
const ownPopoverOpen = host.openId?.startsWith(`${idPrefix}-`) ?? false;
|
|
555
557
|
const pickerRowOpen = secondPickerId !== void 0 && host.openId === secondPickerId;
|
|
556
558
|
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 }),
|
|
559
|
+
/* @__PURE__ */ jsx10(OptionPicker, { id: pickerId, host, options, value, onChange: handleChange, color: drawn, dark, icon, colorSection, alignOverride: align, triggerStyle }),
|
|
558
560
|
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 })
|
|
561
|
+
isPerson && showReadyButton && onToggleReady && /* @__PURE__ */ jsx10(ReadyButton, { color: drawn, ready: ready ?? false, onToggleReady })
|
|
560
562
|
] });
|
|
561
563
|
}
|
|
562
564
|
var styles2 = StyleSheet2.create({
|
|
@@ -796,13 +798,20 @@ function LoadoutScreen({ game }) {
|
|
|
796
798
|
dispatch(liveplayActions.setActiveRound({ game: game.id, mode, settings: roundSettingsRef.current }));
|
|
797
799
|
router2.push(playHref(game.id));
|
|
798
800
|
}, [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)";
|
|
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]);
|
|
802
809
|
const isPortrait = panelLayout.orientationMode === "faceToFace";
|
|
803
810
|
const showMetaInSharedBand = needsSharedNeutralZone(panelLayout.orientationMode, humanSeats.length);
|
|
804
811
|
const p2ZoneStyle = p2IsHuman ? isPortrait ? styles3.p2ZoneTop : panelLayout.p1OnRight ? styles3.p2ZoneLeft : styles3.p2ZoneRight : null;
|
|
805
812
|
const showReadyButton = p2IsHuman;
|
|
813
|
+
const seatPickerAlign = p2IsHuman ? void 0 : pack.usePickerAlign;
|
|
814
|
+
const layoutKey = p2IsHuman ? "zones" : "row";
|
|
806
815
|
const controlsHost = usePopoverHost();
|
|
807
816
|
const p2PanelHost = usePopoverHost();
|
|
808
817
|
const p2Host = p2IsHuman ? p2PanelHost : controlsHost;
|
|
@@ -821,10 +830,10 @@ function LoadoutScreen({ game }) {
|
|
|
821
830
|
const optionsPopoverOpen = controlsHost.openId !== null && loadout.optionIds.includes(controlsHost.openId);
|
|
822
831
|
const sharedControls = showMetaInSharedBand ? /* @__PURE__ */ jsx12(SharedActionBand, { onBack: safeBack, onSettings: () => setSettingsOpen(true), onRandomize: loadout.randomize, onReset: loadout.reset, fg, popoverOpen: optionsPopoverOpen, children: optionsRow }) : optionsRow;
|
|
823
832
|
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 });
|
|
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}`);
|
|
828
837
|
return /* @__PURE__ */ jsxs5(View3, { style: [styles3.container, { backgroundColor: bg }], children: [
|
|
829
838
|
/* @__PURE__ */ jsxs5(FakeLandscapeView, { orientationMode: panelLayout.orientationMode, p1OnRight: panelLayout.p1OnRight, upsideDown: panelLayout.upsideDown, style: styles3.rotatable, children: [
|
|
830
839
|
/* @__PURE__ */ jsx12(PressAwayOverlay, { active: controlsHost.openId !== null, onPress: controlsHost.close }),
|
|
@@ -848,12 +857,16 @@ function LoadoutScreen({ game }) {
|
|
|
848
857
|
!isPortrait && panelLayout.p1OnRight ? p2Panel : p1Panel,
|
|
849
858
|
!isPortrait && panelLayout.p1OnRight ? p1Panel : p2Panel
|
|
850
859
|
] }),
|
|
851
|
-
/* @__PURE__ */ jsx12(ReadyButton2, { color:
|
|
860
|
+
/* @__PURE__ */ jsx12(ReadyButton2, { color: p1Drawn, ready: ready[1], onToggleReady: () => setReady((r) => ({ ...r, 1: !r[1] })), style: anyPlayerPickerOpen && styles3.readyButtonHidden })
|
|
852
861
|
] })
|
|
853
862
|
] }),
|
|
854
863
|
/* @__PURE__ */ jsx12(SettingsDialog, { visible: settingsOpen, onDismiss: () => setSettingsOpen(false), rotation, onShowHowToPlay: () => openFor(game.id) })
|
|
855
864
|
] });
|
|
856
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
|
+
}
|
|
857
870
|
var styles3 = StyleSheet3.create({
|
|
858
871
|
container: {
|
|
859
872
|
flex: 1
|
|
@@ -879,6 +892,10 @@ var styles3 = StyleSheet3.create({
|
|
|
879
892
|
right: 0,
|
|
880
893
|
top: 0
|
|
881
894
|
},
|
|
895
|
+
// A seat button's trigger on a surface over the pack's backdrop, as Home's.
|
|
896
|
+
pill: {
|
|
897
|
+
borderRadius: 999
|
|
898
|
+
},
|
|
882
899
|
// The one-human row: seat 1's panel and seat 2's button read as one evenly spaced row of buttons,
|
|
883
900
|
// at PlayerSetupPanel's own pickerRow gap (12).
|
|
884
901
|
playersRow: {
|
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,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).
|