@tastic/shell 0.5.0 → 0.6.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 +15 -0
- package/dist/index.d.mts +26 -2
- package/dist/index.d.ts +26 -2
- package/dist/index.js +62 -16
- package/dist/index.mjs +45 -2
- package/package.json +1 -1
- package/src/components/MatchChips.tsx +72 -0
- package/src/index.ts +1 -0
package/README.md
CHANGED
|
@@ -196,6 +196,21 @@ longer reads the accelerometer, draws a turn or offers Lock Orientation. A pack
|
|
|
196
196
|
- takes `"@tastic/core"` out of `app.json`'s plugins, sets `UIViewControllerBasedStatusBarAppearance: true`
|
|
197
197
|
in `ios.infoPlist` (the stack sets the status bar per screen), and makes a new native build.
|
|
198
198
|
|
|
199
|
+
### The chips over a match (0.6.0)
|
|
200
|
+
|
|
201
|
+
`MatchChips` is every game's back and settings chips during a match, so they sit and show the same
|
|
202
|
+
way in every game of every pack. A play screen renders it with `playing` (true while play is on: a
|
|
203
|
+
live puck, a shot in flight, a round being ridden), `faceToFace` (two players at opposite ends of the
|
|
204
|
+
screen right now) and `onBack`/`onSettings`, plus `centerLineY` when the line between the players isn't
|
|
205
|
+
the window's middle.
|
|
206
|
+
|
|
207
|
+
- **Where:** the window's top corners; 2 Player face to face, the side edges at the line between the
|
|
208
|
+
players (the top corners are the far player's end); desktop web (a browser window at least
|
|
209
|
+
`MATCH_CHIPS_DESKTOP_MIN_WIDTH`, 768, wide), the page's top corners always.
|
|
210
|
+
- **When:** faded out over `MATCH_CHIPS_FADE_MS` (150) and not pressable while `playing`, back the moment
|
|
211
|
+
it stops; up all match on desktop web.
|
|
212
|
+
- **Look:** a 24 icon in the theme's full contrast on a translucent chip of the page's color.
|
|
213
|
+
|
|
199
214
|
## Peer dependencies
|
|
200
215
|
|
|
201
216
|
None are bundled. The floors are the versions PuckTastic and DuelTastic ran the shell on when it
|
package/dist/index.d.mts
CHANGED
|
@@ -399,6 +399,30 @@ declare function imageGameIcon(images: {
|
|
|
399
399
|
dark: ImageSourcePropType;
|
|
400
400
|
}): ComponentType<GameIconProps>;
|
|
401
401
|
|
|
402
|
+
declare const MATCH_CHIPS_DESKTOP_MIN_WIDTH = 768;
|
|
403
|
+
declare const MATCH_CHIPS_FADE_MS = 150;
|
|
404
|
+
interface MatchChipsProps {
|
|
405
|
+
/** Whether play is on right now (a live puck, a shot in flight, a round being ridden). While it is,
|
|
406
|
+
* the chips fade out and can't be pressed, so they never cover play; they come back the moment it
|
|
407
|
+
* stops (between points, a round's card, Game Over). Ignored on desktop web, where they stay up. */
|
|
408
|
+
playing: boolean;
|
|
409
|
+
/** Two players at opposite ends of one screen right now (2 Player face to face). The chips then sit
|
|
410
|
+
* on the side edges at the line between the players, rather than in the top corners, which are the
|
|
411
|
+
* far player's end. False for one human (vs CPU, solo) and for side by side. Ignored on desktop
|
|
412
|
+
* web: both players share one upright screen there. */
|
|
413
|
+
faceToFace: boolean;
|
|
414
|
+
/** Where that line between the players is, as a y in the window. Defaults to the window's middle;
|
|
415
|
+
* pass it when the board isn't centered (a band of score above it, say). */
|
|
416
|
+
centerLineY?: number;
|
|
417
|
+
onBack: () => void;
|
|
418
|
+
onSettings: () => void;
|
|
419
|
+
}
|
|
420
|
+
/** The back and settings chips over a match, in every game of every pack: one rule for where they sit
|
|
421
|
+
* and when they show. The window's top corners (the same spot as hud's CornerActionButtons on every
|
|
422
|
+
* other screen), except 2 Player face to face, which puts them on the side edges at the line between
|
|
423
|
+
* the players; faded out while play is on, except on desktop web. */
|
|
424
|
+
declare function MatchChips({ playing, faceToFace, centerLineY, onBack, onSettings }: MatchChipsProps): react.JSX.Element;
|
|
425
|
+
|
|
402
426
|
interface SettingsDialogProps {
|
|
403
427
|
visible: boolean;
|
|
404
428
|
onDismiss: () => void;
|
|
@@ -551,5 +575,5 @@ declare function createPlayerSettings<S extends WithControls<C>, C>({ id, defaul
|
|
|
551
575
|
};
|
|
552
576
|
};
|
|
553
577
|
|
|
554
|
-
export { DEFAULT_APP_SETTINGS, DEFAULT_OPPONENT, GUEST_PLAYER_ID, HomeScreen, LoadoutRoute, PREVIEW_ASPECT, PackProvider, PlayRoute, ProfilesProvider, ProfilesRoute, RootLayout, SettingsDialog, StatsScreen, appSettingsActions, reducer$1 as appSettingsReducer, createGameGuideSlice, createGameStats, createPackStore, createPlayerSettings, createSeatColorsReducer, createShellReducers, defaultProfileSelectionState, defaultSeatColorsState, forgetProfileAchievements, humanSeatsFor, imageGameIcon, liveplayActions, reducer$3 as liveplayReducer, onProfileRemoved, opponentActions, reducer$2 as opponentReducer, profileSelectionActions, reducer as profileSelectionReducer, roundModeFor, seatColorsActions, seatPlayerKey, seatTwoFor, useActiveRound, useAppSettings, useGameGuide, useOpponent, usePack, usePackGame, useProfiles, useSeatColors };
|
|
555
|
-
export type { ActiveRound, AppSettings, FeedbackSounds, GameDefinition, GameGuide, GameGuideState, GameIconProps, GameLoadout, GamePreviewProps, GameRulesProps, GuidelessGameDefinition, HeroTitleProps, HomeTab, LiveplaySliceState, LoadoutOptionsProps, OpponentState, PackDefinition, PackGameDefinition, PackGuide, PackStatsReset, PackStatsSectionsProps, PackStatsTab, PackStore, PackStoreConfig, PackStoreSource, PickerAlign, PlayerControlsState, PlayerSettingsState, ProfileSelectionState, RecordResult, RoundMode, Seat, SeatColorsState, SeatControlLook, SeatTwo, SettingsDialogProps, ShellState, StackScreenOptions };
|
|
578
|
+
export { DEFAULT_APP_SETTINGS, DEFAULT_OPPONENT, GUEST_PLAYER_ID, HomeScreen, LoadoutRoute, MATCH_CHIPS_DESKTOP_MIN_WIDTH, MATCH_CHIPS_FADE_MS, MatchChips, PREVIEW_ASPECT, PackProvider, PlayRoute, ProfilesProvider, ProfilesRoute, RootLayout, SettingsDialog, StatsScreen, appSettingsActions, reducer$1 as appSettingsReducer, createGameGuideSlice, createGameStats, createPackStore, createPlayerSettings, createSeatColorsReducer, createShellReducers, defaultProfileSelectionState, defaultSeatColorsState, forgetProfileAchievements, humanSeatsFor, imageGameIcon, liveplayActions, reducer$3 as liveplayReducer, onProfileRemoved, opponentActions, reducer$2 as opponentReducer, profileSelectionActions, reducer as profileSelectionReducer, roundModeFor, seatColorsActions, seatPlayerKey, seatTwoFor, useActiveRound, useAppSettings, useGameGuide, useOpponent, usePack, usePackGame, useProfiles, useSeatColors };
|
|
579
|
+
export type { ActiveRound, AppSettings, FeedbackSounds, GameDefinition, GameGuide, GameGuideState, GameIconProps, GameLoadout, GamePreviewProps, GameRulesProps, GuidelessGameDefinition, HeroTitleProps, HomeTab, LiveplaySliceState, LoadoutOptionsProps, MatchChipsProps, OpponentState, PackDefinition, PackGameDefinition, PackGuide, PackStatsReset, PackStatsSectionsProps, PackStatsTab, PackStore, PackStoreConfig, PackStoreSource, PickerAlign, PlayerControlsState, PlayerSettingsState, ProfileSelectionState, RecordResult, RoundMode, Seat, SeatColorsState, SeatControlLook, SeatTwo, SettingsDialogProps, ShellState, StackScreenOptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -399,6 +399,30 @@ declare function imageGameIcon(images: {
|
|
|
399
399
|
dark: ImageSourcePropType;
|
|
400
400
|
}): ComponentType<GameIconProps>;
|
|
401
401
|
|
|
402
|
+
declare const MATCH_CHIPS_DESKTOP_MIN_WIDTH = 768;
|
|
403
|
+
declare const MATCH_CHIPS_FADE_MS = 150;
|
|
404
|
+
interface MatchChipsProps {
|
|
405
|
+
/** Whether play is on right now (a live puck, a shot in flight, a round being ridden). While it is,
|
|
406
|
+
* the chips fade out and can't be pressed, so they never cover play; they come back the moment it
|
|
407
|
+
* stops (between points, a round's card, Game Over). Ignored on desktop web, where they stay up. */
|
|
408
|
+
playing: boolean;
|
|
409
|
+
/** Two players at opposite ends of one screen right now (2 Player face to face). The chips then sit
|
|
410
|
+
* on the side edges at the line between the players, rather than in the top corners, which are the
|
|
411
|
+
* far player's end. False for one human (vs CPU, solo) and for side by side. Ignored on desktop
|
|
412
|
+
* web: both players share one upright screen there. */
|
|
413
|
+
faceToFace: boolean;
|
|
414
|
+
/** Where that line between the players is, as a y in the window. Defaults to the window's middle;
|
|
415
|
+
* pass it when the board isn't centered (a band of score above it, say). */
|
|
416
|
+
centerLineY?: number;
|
|
417
|
+
onBack: () => void;
|
|
418
|
+
onSettings: () => void;
|
|
419
|
+
}
|
|
420
|
+
/** The back and settings chips over a match, in every game of every pack: one rule for where they sit
|
|
421
|
+
* and when they show. The window's top corners (the same spot as hud's CornerActionButtons on every
|
|
422
|
+
* other screen), except 2 Player face to face, which puts them on the side edges at the line between
|
|
423
|
+
* the players; faded out while play is on, except on desktop web. */
|
|
424
|
+
declare function MatchChips({ playing, faceToFace, centerLineY, onBack, onSettings }: MatchChipsProps): react.JSX.Element;
|
|
425
|
+
|
|
402
426
|
interface SettingsDialogProps {
|
|
403
427
|
visible: boolean;
|
|
404
428
|
onDismiss: () => void;
|
|
@@ -551,5 +575,5 @@ declare function createPlayerSettings<S extends WithControls<C>, C>({ id, defaul
|
|
|
551
575
|
};
|
|
552
576
|
};
|
|
553
577
|
|
|
554
|
-
export { DEFAULT_APP_SETTINGS, DEFAULT_OPPONENT, GUEST_PLAYER_ID, HomeScreen, LoadoutRoute, PREVIEW_ASPECT, PackProvider, PlayRoute, ProfilesProvider, ProfilesRoute, RootLayout, SettingsDialog, StatsScreen, appSettingsActions, reducer$1 as appSettingsReducer, createGameGuideSlice, createGameStats, createPackStore, createPlayerSettings, createSeatColorsReducer, createShellReducers, defaultProfileSelectionState, defaultSeatColorsState, forgetProfileAchievements, humanSeatsFor, imageGameIcon, liveplayActions, reducer$3 as liveplayReducer, onProfileRemoved, opponentActions, reducer$2 as opponentReducer, profileSelectionActions, reducer as profileSelectionReducer, roundModeFor, seatColorsActions, seatPlayerKey, seatTwoFor, useActiveRound, useAppSettings, useGameGuide, useOpponent, usePack, usePackGame, useProfiles, useSeatColors };
|
|
555
|
-
export type { ActiveRound, AppSettings, FeedbackSounds, GameDefinition, GameGuide, GameGuideState, GameIconProps, GameLoadout, GamePreviewProps, GameRulesProps, GuidelessGameDefinition, HeroTitleProps, HomeTab, LiveplaySliceState, LoadoutOptionsProps, OpponentState, PackDefinition, PackGameDefinition, PackGuide, PackStatsReset, PackStatsSectionsProps, PackStatsTab, PackStore, PackStoreConfig, PackStoreSource, PickerAlign, PlayerControlsState, PlayerSettingsState, ProfileSelectionState, RecordResult, RoundMode, Seat, SeatColorsState, SeatControlLook, SeatTwo, SettingsDialogProps, ShellState, StackScreenOptions };
|
|
578
|
+
export { DEFAULT_APP_SETTINGS, DEFAULT_OPPONENT, GUEST_PLAYER_ID, HomeScreen, LoadoutRoute, MATCH_CHIPS_DESKTOP_MIN_WIDTH, MATCH_CHIPS_FADE_MS, MatchChips, PREVIEW_ASPECT, PackProvider, PlayRoute, ProfilesProvider, ProfilesRoute, RootLayout, SettingsDialog, StatsScreen, appSettingsActions, reducer$1 as appSettingsReducer, createGameGuideSlice, createGameStats, createPackStore, createPlayerSettings, createSeatColorsReducer, createShellReducers, defaultProfileSelectionState, defaultSeatColorsState, forgetProfileAchievements, humanSeatsFor, imageGameIcon, liveplayActions, reducer$3 as liveplayReducer, onProfileRemoved, opponentActions, reducer$2 as opponentReducer, profileSelectionActions, reducer as profileSelectionReducer, roundModeFor, seatColorsActions, seatPlayerKey, seatTwoFor, useActiveRound, useAppSettings, useGameGuide, useOpponent, usePack, usePackGame, useProfiles, useSeatColors };
|
|
579
|
+
export type { ActiveRound, AppSettings, FeedbackSounds, GameDefinition, GameGuide, GameGuideState, GameIconProps, GameLoadout, GamePreviewProps, GameRulesProps, GuidelessGameDefinition, HeroTitleProps, HomeTab, LiveplaySliceState, LoadoutOptionsProps, MatchChipsProps, OpponentState, PackDefinition, PackGameDefinition, PackGuide, PackStatsReset, PackStatsSectionsProps, PackStatsTab, PackStore, PackStoreConfig, PackStoreSource, PickerAlign, PlayerControlsState, PlayerSettingsState, ProfileSelectionState, RecordResult, RoundMode, Seat, SeatColorsState, SeatControlLook, SeatTwo, SettingsDialogProps, ShellState, StackScreenOptions };
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,9 @@ __export(index_exports, {
|
|
|
35
35
|
GUEST_PLAYER_ID: () => GUEST_PLAYER_ID,
|
|
36
36
|
HomeScreen: () => HomeScreen,
|
|
37
37
|
LoadoutRoute: () => LoadoutRoute,
|
|
38
|
+
MATCH_CHIPS_DESKTOP_MIN_WIDTH: () => MATCH_CHIPS_DESKTOP_MIN_WIDTH,
|
|
39
|
+
MATCH_CHIPS_FADE_MS: () => MATCH_CHIPS_FADE_MS,
|
|
40
|
+
MatchChips: () => MatchChips,
|
|
38
41
|
PREVIEW_ASPECT: () => PREVIEW_ASPECT,
|
|
39
42
|
PackProvider: () => PackProvider,
|
|
40
43
|
PlayRoute: () => PlayRoute,
|
|
@@ -1566,6 +1569,46 @@ function imageGameIcon(images) {
|
|
|
1566
1569
|
};
|
|
1567
1570
|
}
|
|
1568
1571
|
|
|
1572
|
+
// src/components/MatchChips.tsx
|
|
1573
|
+
var import_auto_paper8 = require("@rific/auto-paper");
|
|
1574
|
+
var import_feedback_press4 = require("@rific/feedback-press");
|
|
1575
|
+
var import_react18 = require("react");
|
|
1576
|
+
var import_react_native10 = require("react-native");
|
|
1577
|
+
var import_react_native_reanimated3 = __toESM(require("react-native-reanimated"));
|
|
1578
|
+
var import_react_native_safe_area_context5 = require("react-native-safe-area-context");
|
|
1579
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
1580
|
+
var MATCH_CHIPS_DESKTOP_MIN_WIDTH = 768;
|
|
1581
|
+
var MATCH_CHIPS_FADE_MS = 150;
|
|
1582
|
+
var CHIP_SIZE = 48;
|
|
1583
|
+
var EDGE_PAD = 8;
|
|
1584
|
+
function MatchChips({ playing, faceToFace, centerLineY, onBack, onSettings }) {
|
|
1585
|
+
const { width, height } = (0, import_react_native10.useWindowDimensions)();
|
|
1586
|
+
const insets = (0, import_react_native_safe_area_context5.useSafeAreaInsets)();
|
|
1587
|
+
const { dark } = (0, import_auto_paper8.useAutoPaperTheme)();
|
|
1588
|
+
const desktopWeb = import_react_native10.Platform.OS === "web" && width >= MATCH_CHIPS_DESKTOP_MIN_WIDTH;
|
|
1589
|
+
const visible = desktopWeb || !playing;
|
|
1590
|
+
const opacity = (0, import_react_native_reanimated3.useSharedValue)(visible ? 1 : 0);
|
|
1591
|
+
(0, import_react18.useEffect)(() => {
|
|
1592
|
+
opacity.value = (0, import_react_native_reanimated3.withTiming)(visible ? 1 : 0, { duration: MATCH_CHIPS_FADE_MS });
|
|
1593
|
+
}, [visible]);
|
|
1594
|
+
const fadeStyle = (0, import_react_native_reanimated3.useAnimatedStyle)(() => ({ opacity: opacity.value }));
|
|
1595
|
+
const iconColor = dark ? "#FFFFFF" : "#000000";
|
|
1596
|
+
const containerColor = dark ? "rgba(0,0,0,0.55)" : "rgba(255,255,255,0.55)";
|
|
1597
|
+
const between = faceToFace && !desktopWeb;
|
|
1598
|
+
const top = between ? (centerLineY ?? height / 2) - CHIP_SIZE / 2 : EDGE_PAD + insets.top;
|
|
1599
|
+
const backSlot = { top, left: EDGE_PAD + insets.left };
|
|
1600
|
+
const settingsSlot = { top, right: EDGE_PAD + insets.right };
|
|
1601
|
+
const pointerEvents = visible ? "box-none" : "none";
|
|
1602
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
|
|
1603
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native_reanimated3.default.View, { pointerEvents, style: [styles6.slot, backSlot, fadeStyle], testID: "match-chips-back", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_feedback_press4.IconButton, { icon: "arrow-left", iconColor, containerColor, style: styles6.chip, size: 24, onPress: onBack, accessibilityLabel: "Back" }) }),
|
|
1604
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native_reanimated3.default.View, { pointerEvents, style: [styles6.slot, settingsSlot, fadeStyle], testID: "match-chips-settings", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_feedback_press4.IconButton, { icon: "cog", iconColor, containerColor, style: styles6.chip, size: 24, onPress: onSettings, accessibilityLabel: "Settings" }) })
|
|
1605
|
+
] });
|
|
1606
|
+
}
|
|
1607
|
+
var styles6 = import_react_native10.StyleSheet.create({
|
|
1608
|
+
chip: { margin: 0 },
|
|
1609
|
+
slot: { position: "absolute" }
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1569
1612
|
// src/hooks/useActiveRound.ts
|
|
1570
1613
|
var import_react_redux11 = require("react-redux");
|
|
1571
1614
|
function useActiveRound(game, defaults) {
|
|
@@ -1581,12 +1624,12 @@ var import_toolkit = require("@reduxjs/toolkit");
|
|
|
1581
1624
|
var import_redux_persist = require("redux-persist");
|
|
1582
1625
|
|
|
1583
1626
|
// src/redux/shellState.ts
|
|
1584
|
-
var
|
|
1627
|
+
var import_auto_paper9 = require("@rific/auto-paper");
|
|
1585
1628
|
var import_core11 = require("@rific/core");
|
|
1586
|
-
var
|
|
1629
|
+
var import_feedback_press5 = require("@rific/feedback-press");
|
|
1587
1630
|
var import_profile10 = require("@tastic/profile");
|
|
1588
|
-
var initialSoundSettings = { ...
|
|
1589
|
-
var appSoundReducer = (state = initialSoundSettings, action) => (0,
|
|
1631
|
+
var initialSoundSettings = { ...import_feedback_press5.defaultSoundSettings, enabled: !__DEV__ };
|
|
1632
|
+
var appSoundReducer = (state = initialSoundSettings, action) => (0, import_feedback_press5.soundReducer)(state, action);
|
|
1590
1633
|
function createShellReducers({ seatColors, appearance = "system", seatTheme }) {
|
|
1591
1634
|
return {
|
|
1592
1635
|
// Explicit primary/secondary triad (rather than a single seed expanded via harmony) so the pack's
|
|
@@ -1598,8 +1641,8 @@ function createShellReducers({ seatColors, appearance = "system", seatTheme }) {
|
|
|
1598
1641
|
// createThemeReducer's own default, when it sets none).
|
|
1599
1642
|
// A pack with its own seatTheme starts from what seat 1's default color makes of it instead (dark
|
|
1600
1643
|
// only when the pack's appearance is; hooks/useSeatColors.ts re-applies it with the live mode).
|
|
1601
|
-
theme: (0,
|
|
1602
|
-
haptic:
|
|
1644
|
+
theme: (0, import_auto_paper9.createThemeReducer)(seatTheme ? { appearance, ...seatTheme(seatColors, appearance === "dark") } : { appearance, color: { primary: seatColors[1], secondary: seatColors[2], tertiary: (0, import_auto_paper9.getThirdColor)(seatColors[1], seatColors[2]) } }),
|
|
1645
|
+
haptic: import_feedback_press5.hapticReducer,
|
|
1603
1646
|
sound: appSoundReducer,
|
|
1604
1647
|
// @rific/core's Keep Awake setting (see Providers.tsx's KeepAwakeBridge), under the namespace its
|
|
1605
1648
|
// REHYDRATE handling expects.
|
|
@@ -1709,8 +1752,8 @@ function forgetProfileAchievements(dropStats) {
|
|
|
1709
1752
|
|
|
1710
1753
|
// src/gameStats.tsx
|
|
1711
1754
|
var import_achievements2 = require("@tastic/achievements");
|
|
1712
|
-
var
|
|
1713
|
-
var
|
|
1755
|
+
var import_react19 = require("react");
|
|
1756
|
+
var import_jsx_runtime19 = require("react/jsx-runtime");
|
|
1714
1757
|
var SEATS = [1, 2];
|
|
1715
1758
|
function createGameStats({ key, defaultStats, isValidStats, catalog, getProfileStatsView }) {
|
|
1716
1759
|
const { actions: actions6, reducer: sliceReducer } = (0, import_achievements2.createAchievementsSlice)({
|
|
@@ -1727,7 +1770,7 @@ function createGameStats({ key, defaultStats, isValidStats, catalog, getProfileS
|
|
|
1727
1770
|
};
|
|
1728
1771
|
const reducer6 = onProfileRemoved(sliceReducer, forgetProfileAchievements(dropProfileStats));
|
|
1729
1772
|
const profileViews = (stats) => Object.fromEntries(Object.entries(stats.profiles).map(([profileId, bucket]) => [profileId, getProfileStatsView(bucket)]));
|
|
1730
|
-
const Context = (0,
|
|
1773
|
+
const Context = (0, import_react19.createContext)(null);
|
|
1731
1774
|
function Provider({ children }) {
|
|
1732
1775
|
const { stats, unlockedAchievements, recordOutcome, resetAll } = (0, import_achievements2.useAchievements)({
|
|
1733
1776
|
catalog,
|
|
@@ -1735,18 +1778,18 @@ function createGameStats({ key, defaultStats, isValidStats, catalog, getProfileS
|
|
|
1735
1778
|
actions: actions6,
|
|
1736
1779
|
profileViews
|
|
1737
1780
|
});
|
|
1738
|
-
const record = (0,
|
|
1781
|
+
const record = (0, import_react19.useCallback)(
|
|
1739
1782
|
(update, profileIds) => {
|
|
1740
1783
|
const result = recordOutcome(update);
|
|
1741
1784
|
return { device: result.device, profiles: (0, import_achievements2.mapUnlocksBySeat)(SEATS, profileIds, result.profiles) };
|
|
1742
1785
|
},
|
|
1743
1786
|
[recordOutcome]
|
|
1744
1787
|
);
|
|
1745
|
-
const value = (0,
|
|
1746
|
-
return /* @__PURE__ */ (0,
|
|
1788
|
+
const value = (0, import_react19.useMemo)(() => ({ stats, unlockedAchievements, record, resetAll }), [stats, unlockedAchievements, record, resetAll]);
|
|
1789
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Context.Provider, { value, children });
|
|
1747
1790
|
}
|
|
1748
1791
|
function useStats() {
|
|
1749
|
-
const ctx = (0,
|
|
1792
|
+
const ctx = (0, import_react19.useContext)(Context);
|
|
1750
1793
|
if (!ctx) throw new Error(`The ${key} stats are read within their game's Provider`);
|
|
1751
1794
|
return ctx;
|
|
1752
1795
|
}
|
|
@@ -1754,7 +1797,7 @@ function createGameStats({ key, defaultStats, isValidStats, catalog, getProfileS
|
|
|
1754
1797
|
}
|
|
1755
1798
|
|
|
1756
1799
|
// src/playerSettings.ts
|
|
1757
|
-
var
|
|
1800
|
+
var import_react20 = require("react");
|
|
1758
1801
|
var import_react_redux12 = require("react-redux");
|
|
1759
1802
|
var SEATS2 = [1, 2];
|
|
1760
1803
|
function seatPlayerKey(selection, seat) {
|
|
@@ -1798,8 +1841,8 @@ function createPlayerSettings({ id, defaults, distinctControls }) {
|
|
|
1798
1841
|
const saved = (0, import_react_redux12.useSelector)((state) => state[settingsKey]);
|
|
1799
1842
|
const controls = (0, import_react_redux12.useSelector)((state) => state[controlsKey]);
|
|
1800
1843
|
const dispatch = (0, import_react_redux12.useDispatch)();
|
|
1801
|
-
const settings = (0,
|
|
1802
|
-
const setSettings = (0,
|
|
1844
|
+
const settings = (0, import_react20.useMemo)(() => compose(saved, controls, selection, seatTwoIsPerson), [saved, controls, selection, seatTwoIsPerson]);
|
|
1845
|
+
const setSettings = (0, import_react20.useCallback)(
|
|
1803
1846
|
(next) => {
|
|
1804
1847
|
const { controlScheme, ...changes } = next;
|
|
1805
1848
|
if (Object.keys(changes).length > 0) dispatch(patch(seatPlayerKey(selection, 1), changes));
|
|
@@ -1822,6 +1865,9 @@ function createPlayerSettings({ id, defaults, distinctControls }) {
|
|
|
1822
1865
|
GUEST_PLAYER_ID,
|
|
1823
1866
|
HomeScreen,
|
|
1824
1867
|
LoadoutRoute,
|
|
1868
|
+
MATCH_CHIPS_DESKTOP_MIN_WIDTH,
|
|
1869
|
+
MATCH_CHIPS_FADE_MS,
|
|
1870
|
+
MatchChips,
|
|
1825
1871
|
PREVIEW_ASPECT,
|
|
1826
1872
|
PackProvider,
|
|
1827
1873
|
PlayRoute,
|
package/dist/index.mjs
CHANGED
|
@@ -1492,6 +1492,46 @@ function imageGameIcon(images) {
|
|
|
1492
1492
|
};
|
|
1493
1493
|
}
|
|
1494
1494
|
|
|
1495
|
+
// src/components/MatchChips.tsx
|
|
1496
|
+
import { useAutoPaperTheme as useAutoPaperTheme6 } from "@rific/auto-paper";
|
|
1497
|
+
import { IconButton as IconButton2 } from "@rific/feedback-press";
|
|
1498
|
+
import { useEffect as useEffect7 } from "react";
|
|
1499
|
+
import { Platform as Platform3, StyleSheet as StyleSheet6, useWindowDimensions as useWindowDimensions2 } from "react-native";
|
|
1500
|
+
import Animated2, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
|
|
1501
|
+
import { useSafeAreaInsets as useSafeAreaInsets4 } from "react-native-safe-area-context";
|
|
1502
|
+
import { Fragment as Fragment5, jsx as jsx18, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1503
|
+
var MATCH_CHIPS_DESKTOP_MIN_WIDTH = 768;
|
|
1504
|
+
var MATCH_CHIPS_FADE_MS = 150;
|
|
1505
|
+
var CHIP_SIZE = 48;
|
|
1506
|
+
var EDGE_PAD = 8;
|
|
1507
|
+
function MatchChips({ playing, faceToFace, centerLineY, onBack, onSettings }) {
|
|
1508
|
+
const { width, height } = useWindowDimensions2();
|
|
1509
|
+
const insets = useSafeAreaInsets4();
|
|
1510
|
+
const { dark } = useAutoPaperTheme6();
|
|
1511
|
+
const desktopWeb = Platform3.OS === "web" && width >= MATCH_CHIPS_DESKTOP_MIN_WIDTH;
|
|
1512
|
+
const visible = desktopWeb || !playing;
|
|
1513
|
+
const opacity = useSharedValue(visible ? 1 : 0);
|
|
1514
|
+
useEffect7(() => {
|
|
1515
|
+
opacity.value = withTiming(visible ? 1 : 0, { duration: MATCH_CHIPS_FADE_MS });
|
|
1516
|
+
}, [visible]);
|
|
1517
|
+
const fadeStyle = useAnimatedStyle(() => ({ opacity: opacity.value }));
|
|
1518
|
+
const iconColor = dark ? "#FFFFFF" : "#000000";
|
|
1519
|
+
const containerColor = dark ? "rgba(0,0,0,0.55)" : "rgba(255,255,255,0.55)";
|
|
1520
|
+
const between = faceToFace && !desktopWeb;
|
|
1521
|
+
const top = between ? (centerLineY ?? height / 2) - CHIP_SIZE / 2 : EDGE_PAD + insets.top;
|
|
1522
|
+
const backSlot = { top, left: EDGE_PAD + insets.left };
|
|
1523
|
+
const settingsSlot = { top, right: EDGE_PAD + insets.right };
|
|
1524
|
+
const pointerEvents = visible ? "box-none" : "none";
|
|
1525
|
+
return /* @__PURE__ */ jsxs8(Fragment5, { children: [
|
|
1526
|
+
/* @__PURE__ */ jsx18(Animated2.View, { pointerEvents, style: [styles6.slot, backSlot, fadeStyle], testID: "match-chips-back", children: /* @__PURE__ */ jsx18(IconButton2, { icon: "arrow-left", iconColor, containerColor, style: styles6.chip, size: 24, onPress: onBack, accessibilityLabel: "Back" }) }),
|
|
1527
|
+
/* @__PURE__ */ jsx18(Animated2.View, { pointerEvents, style: [styles6.slot, settingsSlot, fadeStyle], testID: "match-chips-settings", children: /* @__PURE__ */ jsx18(IconButton2, { icon: "cog", iconColor, containerColor, style: styles6.chip, size: 24, onPress: onSettings, accessibilityLabel: "Settings" }) })
|
|
1528
|
+
] });
|
|
1529
|
+
}
|
|
1530
|
+
var styles6 = StyleSheet6.create({
|
|
1531
|
+
chip: { margin: 0 },
|
|
1532
|
+
slot: { position: "absolute" }
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1495
1535
|
// src/hooks/useActiveRound.ts
|
|
1496
1536
|
import { useSelector as useSelector8 } from "react-redux";
|
|
1497
1537
|
function useActiveRound(game, defaults) {
|
|
@@ -1636,7 +1676,7 @@ function forgetProfileAchievements(dropStats) {
|
|
|
1636
1676
|
// src/gameStats.tsx
|
|
1637
1677
|
import { createAchievementsSlice, mapUnlocksBySeat, useAchievements } from "@tastic/achievements";
|
|
1638
1678
|
import { createContext as createContext5, useCallback as useCallback11, useContext as useContext5, useMemo as useMemo8 } from "react";
|
|
1639
|
-
import { jsx as
|
|
1679
|
+
import { jsx as jsx19 } from "react/jsx-runtime";
|
|
1640
1680
|
var SEATS = [1, 2];
|
|
1641
1681
|
function createGameStats({ key, defaultStats, isValidStats, catalog, getProfileStatsView }) {
|
|
1642
1682
|
const { actions: actions6, reducer: sliceReducer } = createAchievementsSlice({
|
|
@@ -1669,7 +1709,7 @@ function createGameStats({ key, defaultStats, isValidStats, catalog, getProfileS
|
|
|
1669
1709
|
[recordOutcome]
|
|
1670
1710
|
);
|
|
1671
1711
|
const value = useMemo8(() => ({ stats, unlockedAchievements, record, resetAll }), [stats, unlockedAchievements, record, resetAll]);
|
|
1672
|
-
return /* @__PURE__ */
|
|
1712
|
+
return /* @__PURE__ */ jsx19(Context.Provider, { value, children });
|
|
1673
1713
|
}
|
|
1674
1714
|
function useStats() {
|
|
1675
1715
|
const ctx = useContext5(Context);
|
|
@@ -1747,6 +1787,9 @@ export {
|
|
|
1747
1787
|
GUEST_PLAYER_ID,
|
|
1748
1788
|
HomeScreen,
|
|
1749
1789
|
LoadoutRoute,
|
|
1790
|
+
MATCH_CHIPS_DESKTOP_MIN_WIDTH,
|
|
1791
|
+
MATCH_CHIPS_FADE_MS,
|
|
1792
|
+
MatchChips,
|
|
1750
1793
|
PREVIEW_ASPECT,
|
|
1751
1794
|
PackProvider,
|
|
1752
1795
|
PlayRoute,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastic/shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useAutoPaperTheme } from '@rific/auto-paper'
|
|
2
|
+
import { IconButton } from '@rific/feedback-press'
|
|
3
|
+
import { useEffect } from 'react'
|
|
4
|
+
import { Platform, StyleSheet, useWindowDimensions } from 'react-native'
|
|
5
|
+
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'
|
|
6
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
7
|
+
|
|
8
|
+
// A web window at least this wide is a computer: the chips stay up in the page's top corners all
|
|
9
|
+
// match. The games' own desktop cutoff, the same number everywhere.
|
|
10
|
+
export const MATCH_CHIPS_DESKTOP_MIN_WIDTH = 768
|
|
11
|
+
// How long the chips take to fade out when play starts, and back in when it stops.
|
|
12
|
+
export const MATCH_CHIPS_FADE_MS = 150
|
|
13
|
+
// The chip's box: a 24 icon in IconButton's 48, with Paper's margin zeroed.
|
|
14
|
+
const CHIP_SIZE = 48
|
|
15
|
+
const EDGE_PAD = 8
|
|
16
|
+
|
|
17
|
+
export interface MatchChipsProps {
|
|
18
|
+
/** Whether play is on right now (a live puck, a shot in flight, a round being ridden). While it is,
|
|
19
|
+
* the chips fade out and can't be pressed, so they never cover play; they come back the moment it
|
|
20
|
+
* stops (between points, a round's card, Game Over). Ignored on desktop web, where they stay up. */
|
|
21
|
+
playing: boolean
|
|
22
|
+
/** Two players at opposite ends of one screen right now (2 Player face to face). The chips then sit
|
|
23
|
+
* on the side edges at the line between the players, rather than in the top corners, which are the
|
|
24
|
+
* far player's end. False for one human (vs CPU, solo) and for side by side. Ignored on desktop
|
|
25
|
+
* web: both players share one upright screen there. */
|
|
26
|
+
faceToFace: boolean
|
|
27
|
+
/** Where that line between the players is, as a y in the window. Defaults to the window's middle;
|
|
28
|
+
* pass it when the board isn't centered (a band of score above it, say). */
|
|
29
|
+
centerLineY?: number
|
|
30
|
+
onBack: () => void
|
|
31
|
+
onSettings: () => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The back and settings chips over a match, in every game of every pack: one rule for where they sit
|
|
35
|
+
* and when they show. The window's top corners (the same spot as hud's CornerActionButtons on every
|
|
36
|
+
* other screen), except 2 Player face to face, which puts them on the side edges at the line between
|
|
37
|
+
* the players; faded out while play is on, except on desktop web. */
|
|
38
|
+
export function MatchChips({ playing, faceToFace, centerLineY, onBack, onSettings }: MatchChipsProps) {
|
|
39
|
+
const { width, height } = useWindowDimensions()
|
|
40
|
+
const insets = useSafeAreaInsets()
|
|
41
|
+
const { dark } = useAutoPaperTheme()
|
|
42
|
+
const desktopWeb = Platform.OS === 'web' && width >= MATCH_CHIPS_DESKTOP_MIN_WIDTH
|
|
43
|
+
const visible = desktopWeb || !playing
|
|
44
|
+
const opacity = useSharedValue(visible ? 1 : 0)
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
opacity.value = withTiming(visible ? 1 : 0, { duration: MATCH_CHIPS_FADE_MS })
|
|
47
|
+
}, [visible]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
48
|
+
const fadeStyle = useAnimatedStyle(() => ({ opacity: opacity.value }))
|
|
49
|
+
|
|
50
|
+
const iconColor = dark ? '#FFFFFF' : '#000000'
|
|
51
|
+
const containerColor = dark ? 'rgba(0,0,0,0.55)' : 'rgba(255,255,255,0.55)'
|
|
52
|
+
const between = faceToFace && !desktopWeb
|
|
53
|
+
const top = between ? (centerLineY ?? height / 2) - CHIP_SIZE / 2 : EDGE_PAD + insets.top
|
|
54
|
+
const backSlot = { top, left: EDGE_PAD + insets.left }
|
|
55
|
+
const settingsSlot = { top, right: EDGE_PAD + insets.right }
|
|
56
|
+
const pointerEvents = visible ? 'box-none' : 'none'
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<Animated.View pointerEvents={pointerEvents} style={[styles.slot, backSlot, fadeStyle]} testID='match-chips-back'>
|
|
60
|
+
<IconButton icon='arrow-left' iconColor={iconColor} containerColor={containerColor} style={styles.chip} size={24} onPress={onBack} accessibilityLabel='Back' />
|
|
61
|
+
</Animated.View>
|
|
62
|
+
<Animated.View pointerEvents={pointerEvents} style={[styles.slot, settingsSlot, fadeStyle]} testID='match-chips-settings'>
|
|
63
|
+
<IconButton icon='cog' iconColor={iconColor} containerColor={containerColor} style={styles.chip} size={24} onPress={onSettings} accessibilityLabel='Settings' />
|
|
64
|
+
</Animated.View>
|
|
65
|
+
</>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const styles = StyleSheet.create({
|
|
70
|
+
chip: { margin: 0 },
|
|
71
|
+
slot: { position: 'absolute' }
|
|
72
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { StatsScreen } from './screens/StatsScreen'
|
|
|
15
15
|
// What a game's own screens use.
|
|
16
16
|
export { useGameGuide } from './components/GuideHost'
|
|
17
17
|
export { imageGameIcon } from './components/imageGameIcon'
|
|
18
|
+
export { MATCH_CHIPS_DESKTOP_MIN_WIDTH, MATCH_CHIPS_FADE_MS, MatchChips, type MatchChipsProps } from './components/MatchChips'
|
|
18
19
|
export { SettingsDialog, type SettingsDialogProps } from './components/SettingsDialog'
|
|
19
20
|
export { useActiveRound } from './hooks/useActiveRound'
|
|
20
21
|
export { useAppSettings } from './hooks/useAppSettings'
|