@tastic/shell 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/shell",
3
- "version": "0.4.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",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "prettier": "@infinitetoken/eslint-config/prettier",
68
68
  "devDependencies": {
69
- "@babel/core": "^7.29.7",
69
+ "@babel/core": "^7.29.13",
70
70
  "@expo/vector-icons": "^15.1.1",
71
71
  "@infinitetoken/eslint-config": "^0.2.1",
72
72
  "@infinitetoken/jest-config": "^0.5.2",
@@ -74,7 +74,7 @@
74
74
  "@react-native-async-storage/async-storage": "2.2.0",
75
75
  "@react-native-masked-view/masked-view": "0.3.2",
76
76
  "@react-native/jest-preset": "^0.88.0",
77
- "@reduxjs/toolkit": "^2.13.0",
77
+ "@reduxjs/toolkit": "^2.14.0",
78
78
  "@rific/auto-paper": "^0.13.4",
79
79
  "@rific/core": "^0.5.0",
80
80
  "@rific/feedback-press": "^0.13.1",
@@ -84,7 +84,7 @@
84
84
  "@rific/toaster": "^0.12.0",
85
85
  "@rific/updater": "^0.8.0",
86
86
  "@tastic/achievements": "^0.6.0",
87
- "@tastic/core": "^0.14.0",
87
+ "@tastic/core": "^0.15.0",
88
88
  "@tastic/edge-guard": "^0.1.5",
89
89
  "@tastic/hud": "^0.27.1",
90
90
  "@tastic/profile": "^0.17.0",
@@ -95,34 +95,33 @@
95
95
  "eslint": "^9.39.5",
96
96
  "eslint-plugin-react-hooks": "^7.1.1",
97
97
  "eslint-plugin-react-native": "^5.0.0",
98
- "expo": "~58.0.19",
99
- "expo-audio": "~58.0.10",
98
+ "expo": "~58.0.23",
99
+ "expo-audio": "~58.0.11",
100
100
  "expo-blur": "~58.0.5",
101
- "expo-constants": "~58.0.13",
101
+ "expo-constants": "~58.0.15",
102
102
  "expo-font": "~58.0.10",
103
103
  "expo-haptics": "~58.0.7",
104
104
  "expo-keep-awake": "~58.0.3",
105
- "expo-router": "~58.0.26",
106
- "expo-sensors": "~58.0.4",
107
- "expo-splash-screen": "~58.0.11",
105
+ "expo-router": "~58.0.30",
106
+ "expo-splash-screen": "~58.0.13",
108
107
  "expo-system-ui": "~58.0.6",
109
- "expo-updates": "~58.0.18",
108
+ "expo-updates": "~58.0.21",
110
109
  "jest": "~30.5.2",
111
- "jest-expo": "~58.0.12",
110
+ "jest-expo": "~58.0.14",
112
111
  "prettier": "^3.8.3",
113
112
  "react": "19.3.0",
114
- "react-native": "0.88.0",
113
+ "react-native": "^0.88.1",
115
114
  "react-native-gesture-handler": "~3.3.0",
116
- "react-native-paper": "^5.15.3",
115
+ "react-native-paper": "^5.15.4",
117
116
  "react-native-reanimated": "4.7.1",
118
- "react-native-safe-area-context": "~5.10.1",
117
+ "react-native-safe-area-context": "~5.11.1",
119
118
  "react-native-screens": "~4.30.0",
120
119
  "react-native-worklets": "0.13.0",
121
120
  "react-redux": "^9.3.0",
122
121
  "redux": "^5.0.1",
123
122
  "redux-persist": "^6.0.0",
124
123
  "test-renderer": "^1.3.1",
125
- "tsup": "^8.0.0",
124
+ "tsup": "^9.0.0",
126
125
  "typescript": "~6.0.3"
127
126
  },
128
127
  "peerDependencies": {
@@ -136,7 +135,7 @@
136
135
  "@rific/splash-gate": ">=0.2.3",
137
136
  "@rific/toaster": ">=0.10.1",
138
137
  "@tastic/achievements": ">=0.6.0",
139
- "@tastic/core": ">=0.14.0",
138
+ "@tastic/core": ">=0.15.0",
140
139
  "@tastic/edge-guard": ">=0.1.5",
141
140
  "@tastic/hud": ">=0.27.1",
142
141
  "@tastic/profile": ">=0.16.2",
@@ -147,7 +146,6 @@
147
146
  "expo-haptics": ">=58.0.7",
148
147
  "expo-keep-awake": ">=58.0.3",
149
148
  "expo-router": ">=58.0.23",
150
- "expo-sensors": ">=58.0.4",
151
149
  "expo-splash-screen": ">=58.0.10",
152
150
  "react": ">=19.3.0",
153
151
  "react-native": ">=0.88.0",
@@ -1,12 +1,11 @@
1
1
  import { useAutoPaperTheme } from '@rific/auto-paper'
2
2
  import { useHapticSettings } from '@rific/feedback-press'
3
3
  import { Toaster, ToastProvider } from '@rific/toaster'
4
- import { OrientationProvider, useRotationAwareStatusBar, useThemedRootBackground } from '@tastic/core'
4
+ import { useThemedRootBackground } from '@tastic/core'
5
5
  import { RotatedFrame } from '@tastic/hud/chrome'
6
6
  import { ProfileColorProvider } from '@tastic/profile'
7
7
  import * as Haptics from 'expo-haptics'
8
8
  import { DarkTheme, DefaultTheme, Stack, ThemeProvider as NavigationThemeProvider } from 'expo-router'
9
- import { DeviceMotion } from 'expo-sensors'
10
9
  import { ReactNode, useCallback, useMemo } from 'react'
11
10
  import { Platform, StyleSheet, View } from 'react-native'
12
11
  import { GestureHandlerRootView } from 'react-native-gesture-handler'
@@ -19,7 +18,6 @@ import { GuideHost } from './components/GuideHost'
19
18
  import { Providers } from './components/Providers'
20
19
  import { Theme } from './components/Theme'
21
20
  import { UpdateDialog } from './components/UpdateDialog'
22
- import { useAppSettings } from './hooks/useAppSettings'
23
21
  import { LastActiveGameProvider } from './hooks/useLastActiveGame'
24
22
  import { ProfilesProvider, useProfiles } from './hooks/useProfiles'
25
23
  import { PackProvider, usePack } from './PackContext'
@@ -38,15 +36,13 @@ import type { PackDefinition, PackGameDefinition } from './types'
38
36
  // a play screen and wins (confirmed on-device in LightCycles, where it kicked the player back to the title
39
37
  // screen mid-round instead of turning their cycle). Web has no such OS-level gesture.
40
38
  //
41
- // useRotationAwareStatusBar (@tastic/core) hides the OS status bar whenever the content is rotated
42
- // off portrait. It does it through screen options because that is the only way left: an app linked
43
- // against the iOS 27 SDK can't set the status bar app-wide, which is all React Native's StatusBar
44
- // (and expo-status-bar) does on iOS, and with @tastic/core's config plugin on, every mounted
45
- // StatusBar logs an error. So nothing in the app mounts one (Theme passes auto-paper's Provider
46
- // statusBar={false}). No `locked` argument: OrientationBridge already freezes the reading it
47
- // follows. A play screen hides the bar for the whole round, whatever the angle (a screen's own
48
- // options win over screenOptions); index is listed first so it stays the stack's first route.
49
- // A game with keepStatusBar (Solitaire's) keeps the screen options' rotation-aware bar instead.
39
+ // The status bar is set through screen options because that is the only way left: an app linked
40
+ // against the iOS 27 SDK can't set the status bar app-wide, which is all React Native's StatusBar (and
41
+ // expo-status-bar) does on iOS, and with UIViewControllerBasedStatusBarAppearance on (each app's
42
+ // ios.infoPlist), every mounted StatusBar logs an error. So nothing in the app mounts one (Theme passes
43
+ // auto-paper's Provider statusBar={false}). A play screen hides the bar for the whole round (a screen's
44
+ // own options win over screenOptions); index is listed first so it stays the stack's first route. A
45
+ // game with keepStatusBar (Solitaire's) keeps the stack's bar instead.
50
46
  const PLAY_OPTIONS = { statusBarHidden: true }
51
47
  const KEEP_STATUS_BAR_OPTIONS = {}
52
48
 
@@ -76,7 +72,7 @@ function AppStack() {
76
72
  const rootBackground = useThemedRootBackground(darkRoot, false)
77
73
  // The status bar's text follows the app's theme, not the phone's appearance (react-native-screens'
78
74
  // 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' })
75
+ const statusBarStyle = darkRoot ? ('light' as const) : ('dark' as const)
80
76
  const packOptions = (pack.useScreenOptions ?? noScreenOptions)()
81
77
  // Over a backdrop every screen is transparent, and Android crossfades: its default push and pop hold
82
78
  // the outgoing screen opaque for ~450ms while the new one slides over it, which would show Home
@@ -96,7 +92,7 @@ function AppStack() {
96
92
  }, [darkRoot, navigationBackground])
97
93
  const stack = (
98
94
  <NavigationThemeProvider value={navigationTheme}>
99
- <Stack screenOptions={{ ...rootBackground, ...statusBar, ...backdropOptions, ...packOptions }}>
95
+ <Stack screenOptions={{ ...rootBackground, statusBarStyle, ...backdropOptions, ...packOptions }}>
100
96
  <Stack.Screen name='index' />
101
97
  <Stack.Screen name='play/[game]' options={playOptions} />
102
98
  </Stack>
@@ -111,38 +107,6 @@ function AppStack() {
111
107
  )
112
108
  }
113
109
 
114
- // Wires the persisted Lock Orientation setting into @tastic/core's OrientationProvider. Its own
115
- // component because RootLayout renders the Redux store (inside Providers) and so can't call
116
- // useAppSettings() itself. Providers' PersistGate holds rendering until the store has rehydrated, so
117
- // lockInitialValue carries the saved lock from the very first render.
118
- //
119
- // freezeWhileLocked makes the lock app-wide: while appSettings.lockOrientation is on, the ONE shared
120
- // orientation reading freezes for every consumer (every screen, every board, hud dialogs, the
121
- // status bar), including anything that mounts after the lock, so no call site ever takes a `locked`
122
- // argument of its own (one would freeze on the pre-sensor default at mount). The Settings dialog
123
- // toggle only writes appSettings.lockOrientation, which reaches the provider as a changed
124
- // lockInitialValue; the provider reports the angle it froze on through onLockChange (`rotation`,
125
- // cleared on unlock), persisted as appSettings.lockedRotation and handed back here so a relaunch reopens
126
- // in the locked orientation instead of portrait.
127
- //
128
- // Mounted once, here, rather than per screen: one app-lifetime sensor subscription is what lets the
129
- // committed orientation survive navigation. deviceMotion is the app's own expo-sensors import, handed
130
- // in because @tastic/core never imports expo-sensors itself (apps without tilt tracking would
131
- // otherwise need it installed just to bundle).
132
- //
133
- // portraitWhileKeyboard (PackDefinition.portraitWhileKeyboard): the software keyboard can't turn with a
134
- // faked rotation, so a pack that has the player type sideways (a profile's name) reads portrait while
135
- // it's up.
136
- function OrientationBridge({ children }: { children: ReactNode }) {
137
- const { settings, setSettings } = useAppSettings()
138
- const { portraitWhileKeyboard } = usePack()
139
- return (
140
- <OrientationProvider deviceMotion={DeviceMotion} freezeWhileLocked lockInitialValue={{ locked: settings.lockOrientation, rotation: settings.lockedRotation }} onLockChange={(next) => setSettings({ lockOrientation: next.locked, lockedRotation: next.rotation })} portraitWhileKeyboard={portraitWhileKeyboard}>
141
- {children}
142
- </OrientationProvider>
143
- )
144
- }
145
-
146
110
  // ProfilesProvider reconciles the local roster with the shared App Group one before `loaded` flips,
147
111
  // so a screen mounting earlier would read (and could persist) the wrong profiles. This holds
148
112
  // everything nested inside it, and the splash screen, until that's done, while ProfilesProvider sits
@@ -163,7 +127,7 @@ function GatedApp({ children }: { children: ReactNode }) {
163
127
  // either is ever shown. Must render inside Providers, whose FeedbackBridge provides the setting.
164
128
  //
165
129
  // A pack with its own Toaster gets no Paper here: with Paper the toaster draws through a Paper Portal,
166
- // which would take its toasts out of the turned frame PackToaster lays them in.
130
+ // which would take its toasts out of the frame PackToaster lays them in.
167
131
  function HapticGatedToastProvider({ children }: { children: ReactNode }) {
168
132
  const { settings } = useHapticSettings()
169
133
  const { Toaster: PackToasterComponent } = usePack()
@@ -186,10 +150,10 @@ function RotatingToaster() {
186
150
  return <Toaster historyButton={null} clearButton={null} limit={3} renderFrame={renderFrame} />
187
151
  }
188
152
 
189
- // The pack's own toaster (PackDefinition.Toaster), laid over the whole app in a frame turned with the
190
- // phone: passThrough, so only a toast claims a touch over the stack, and rotatedSafeArea, so the
191
- // toaster's own safe-area reading lands on the frame's edges. The absoluteFill wrapper takes the frame
192
- // out of the layout beside the stack.
153
+ // The pack's own toaster (PackDefinition.Toaster), laid over the whole app in hud's RotatedFrame, which
154
+ // nothing turns since @tastic/core 0.15: passThrough, so only a toast claims a touch over the stack, and
155
+ // rotatedSafeArea, so the toaster reads the safe area. The absoluteFill wrapper takes the frame out of
156
+ // the layout beside the stack.
193
157
  function PackToaster({ Toaster: PackToasterComponent }: { Toaster: NonNullable<PackDefinition['Toaster']> }) {
194
158
  return (
195
159
  <View style={StyleSheet.absoluteFill} pointerEvents='box-none'>
@@ -243,24 +207,21 @@ export function RootLayout({ pack, store, persistor }: RootLayoutProps) {
243
207
  <GameProviders games={pack.games}>
244
208
  <PackRoot pack={pack}>
245
209
  <GatedApp>
246
- <OrientationBridge>
247
- <HapticGatedToastProvider>
248
- {/* Wraps the stack (rather than sitting beside it like UpdateDialog)
210
+ <HapticGatedToastProvider>
211
+ {/* Wraps the stack (rather than sitting beside it like UpdateDialog)
249
212
  because Home's tiles and the loadout's How to Play row reach it through
250
- context (see GuideHost). Inside the toast provider and OrientationBridge
251
- because the card renders through a Portal and rotates with the shared
252
- reading, and inside the gate so it mounts together with Home rather than
253
- before it. */}
254
- <GuideHost>
255
- <AppStack />
256
- </GuideHost>
257
- {/* historyButton/clearButton off: there's no toast history to open, and a
213
+ context (see GuideHost). Inside the toast provider because the card renders
214
+ through a Portal, and inside the gate so it mounts together with Home rather
215
+ than before it. */}
216
+ <GuideHost>
217
+ <AppStack />
218
+ </GuideHost>
219
+ {/* historyButton/clearButton off: there's no toast history to open, and a
258
220
  control row for it above the toast stack would go nowhere. limit pins the
259
221
  package's current default (3) so a future toaster release can't change it. */}
260
- {pack.Toaster ? <PackToaster Toaster={pack.Toaster} /> : <RotatingToaster />}
261
- <UpdateDialog />
262
- </HapticGatedToastProvider>
263
- </OrientationBridge>
222
+ {pack.Toaster ? <PackToaster Toaster={pack.Toaster} /> : <RotatingToaster />}
223
+ <UpdateDialog />
224
+ </HapticGatedToastProvider>
264
225
  </GatedApp>
265
226
  </PackRoot>
266
227
  </GameProviders>
@@ -1,4 +1,4 @@
1
- import { useIsTouchPrimaryDevice, useRotation } from '@tastic/core'
1
+ import { useIsTouchPrimaryDevice } from '@tastic/core'
2
2
  import { GuideProvider, useGuide } from '@tastic/hud/guide'
3
3
  import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
4
4
  import { shallowEqual, useDispatch, useSelector, useStore } from 'react-redux'
@@ -38,9 +38,8 @@ interface Props {
38
38
  // the pack's own (PackDefinition.guide, cards that teach every game at once), or a game's. Each
39
39
  // game's cards come from its own registry entry (GameDefinition.guide), built from the live settings
40
40
  // and seat 2 so a replay after any loadout change describes the next match. Mounted around the stack
41
- // in RootLayout, inside the toast provider and OrientationBridge because the card renders through a
42
- // Portal and turns with the shared reading (`rotation`, the same one Home hands its Settings dialog),
43
- // and inside the profiles gate so it mounts together with Home.
41
+ // in RootLayout, inside the toast provider because the card renders through a Portal, and inside the
42
+ // profiles gate so it mounts together with Home.
44
43
  export function GuideHost({ children }: Props) {
45
44
  const pack = usePack()
46
45
  const [subject, setSubject] = useState(() => (pack.guide ? PACK_GUIDE : (pack.games.find((g) => g.guide)?.id ?? null)))
@@ -48,7 +47,6 @@ export function GuideHost({ children }: Props) {
48
47
  const game = subject === PACK_GUIDE ? undefined : pack.games.find((g) => g.id === subject)
49
48
  const gameGuide = game?.guide
50
49
 
51
- const rotation = useRotation()
52
50
  const touch = useIsTouchPrimaryDevice()
53
51
  const seatTwo = useSelector((state: ShellState) => state.opponent.seatTwo)
54
52
  const input = useSelector((state: ShellState) => (gameGuide ? gameGuide.selectInput(state) : NO_INPUT), shallowEqual)
@@ -68,7 +66,7 @@ export function GuideHost({ children }: Props) {
68
66
  )
69
67
 
70
68
  return (
71
- <GuideProvider steps={steps} currentVersion={(packGuide ?? gameGuide)?.version ?? 0} seenVersion={seenVersion} onSeen={onSeen} rotation={rotation}>
69
+ <GuideProvider steps={steps} currentVersion={(packGuide ?? gameGuide)?.version ?? 0} seenVersion={seenVersion} onSeen={onSeen}>
72
70
  <GameGuideBridge onPick={setSubject}>{children}</GameGuideBridge>
73
71
  </GuideProvider>
74
72
  )
@@ -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
+ })
@@ -1,5 +1,4 @@
1
1
  import { useUpdateErrorToast } from '@rific/toaster'
2
- import { ViewRotation } from '@tastic/core'
3
2
  import { BaseSettingsDialog } from '@tastic/hud/chrome'
4
3
  import type { ReactNode } from 'react'
5
4
 
@@ -9,11 +8,6 @@ import { usePack } from '../PackContext'
9
8
  export interface SettingsDialogProps {
10
9
  visible: boolean
11
10
  onDismiss: () => void
12
- // Live physical-hold rotation (see @tastic/core's useRotation). BaseSettingsDialog lays the sheet
13
- // out in its own frame turned by this (see @tastic/hud's RotatedFrame), so it reads right-side-up
14
- // and fits the short edge however the phone is held. Defaults to 0 for a call site that doesn't
15
- // track one.
16
- rotation?: ViewRotation
17
11
  // Shows the How to Play row, which opens this game's cards (a loadout passes its game's opener).
18
12
  // Left out on Home, where each tile's rules button does it, and in a match, where opening
19
13
  // Settings pauses play.
@@ -23,12 +17,12 @@ export interface SettingsDialogProps {
23
17
  children?: ReactNode
24
18
  }
25
19
 
26
- // The pack-wide Settings dialog: everything the shell puts in it applies to every game (Lock
27
- // Orientation, Edge Guard, theme, sound, haptics, Keep Awake), so it reads and writes the app
20
+ // The pack-wide Settings dialog: everything the shell puts in it applies to every game (Edge Guard,
21
+ // theme, sound, haptics, Keep Awake), so it reads and writes the app
28
22
  // settings itself. Under those, a play screen's own rows and then the pack's own sections
29
23
  // (PackDefinition.SettingsSections), in every Settings dialog the pack opens. A pack with edgeGuard
30
24
  // false gets no Edge Guard row: hud draws it on iOS whenever its props are passed.
31
- export function SettingsDialog({ visible, onDismiss, rotation = 0, onShowHowToPlay, children }: SettingsDialogProps) {
25
+ export function SettingsDialog({ visible, onDismiss, onShowHowToPlay, children }: SettingsDialogProps) {
32
26
  const onUpdateError = useUpdateErrorToast()
33
27
  const { settings, setSettings } = useAppSettings()
34
28
  // The pack's otaVersion (src/constants/release.ts, which the OTA tooling bumps).
@@ -36,7 +30,7 @@ export function SettingsDialog({ visible, onDismiss, rotation = 0, onShowHowToPl
36
30
  const edgeGuardProps = edgeGuard ? { deferBottomEdgeGestures: settings.deferBottomEdgeGestures, onDeferBottomEdgeGestures: (value: boolean) => setSettings({ deferBottomEdgeGestures: value }) } : {}
37
31
 
38
32
  return (
39
- <BaseSettingsDialog visible={visible} onDismiss={onDismiss} rotation={rotation} version={otaVersion} lockOrientation={settings.lockOrientation} onLockOrientationChange={(value) => setSettings({ lockOrientation: value })} {...edgeGuardProps} onUpdateError={onUpdateError} onShowHowToPlay={onShowHowToPlay}>
33
+ <BaseSettingsDialog visible={visible} onDismiss={onDismiss} version={otaVersion} {...edgeGuardProps} onUpdateError={onUpdateError} onShowHowToPlay={onShowHowToPlay}>
40
34
  {children}
41
35
  {SettingsSections && <SettingsSections />}
42
36
  </BaseSettingsDialog>
@@ -61,8 +61,9 @@ export function Theme({ children }: Props) {
61
61
  // iOS/Android (the Settings sheet's and Rules' rows above Done, @tastic/profile's dropdown lists
62
62
  // above Manage); without it they just scroll with a hard edge. Web needs nothing.
63
63
  // statusBar={false}: auto-paper renders no React Native StatusBar (here or in Appbar.Header). The
64
- // status bar is set per screen (_layout.tsx's useRotationAwareStatusBar), and with @tastic/core's
65
- // config plugin on, a mounted StatusBar logs an error (a red box in a dev build).
64
+ // status bar is set through the stack's screen options (RootLayout's AppStack), and with
65
+ // UIViewControllerBasedStatusBarAppearance on (each app's ios.infoPlist), a mounted StatusBar logs an
66
+ // error (a red box in a dev build).
66
67
  // reanimated is what makes Dialog's own animatedStyle prop do anything (Metro won't pick up a
67
68
  // require()-in-try/catch from this package's ESM build, so the module is handed in); nothing else in
68
69
  // auto-paper reads it. fontFamily is the pack's (PackDefinition.fontFamily): set, it goes on every
@@ -1,5 +1,4 @@
1
1
  import { useUpdateErrorToast } from '@rific/toaster'
2
- import { useRotation } from '@tastic/core'
3
2
  import { UpdateDialog as SharedUpdateDialog } from '@tastic/hud/chrome'
4
3
 
5
4
  // Thin bridge onto the fleet-shared @tastic/hud UpdateDialog, which now owns the useUpdater()
@@ -8,6 +7,5 @@ import { UpdateDialog as SharedUpdateDialog } from '@tastic/hud/chrome'
8
7
  // directly in _layout.tsx) purely so this app's own onError -> toast wiring has one place to live.
9
8
  export function UpdateDialog() {
10
9
  const onError = useUpdateErrorToast()
11
- const rotation = useRotation()
12
- return <SharedUpdateDialog onError={onError} rotation={rotation} />
10
+ return <SharedUpdateDialog onError={onError} />
13
11
  }
@@ -5,9 +5,9 @@ import { useState } from 'react'
5
5
  // Solitaire's components/useFrozenWhileHidden.ts as it was.
6
6
  //
7
7
  // Home holds its layout with it while another screen covers it (screens/HomeScreen.tsx): Home stays
8
- // mounted beneath every screen the stack pushes and keeps following the rotation, and crossing the
8
+ // mounted beneath every screen the stack pushes and keeps following the window, and crossing the
9
9
  // upright/wide threshold swaps its whole tree (every tile, the hero, seat 1's button), invisible work
10
- // landing in the same commit as the covering screen's own rotation update. While covered the layout is
10
+ // landing in the same commit as the covering screen's own update for the new window. While covered the layout is
11
11
  // held where it was; it catches up (one swap, visible) when Home is shown again.
12
12
  export function useFrozenWhileHidden<T>(value: T, hidden: boolean): T {
13
13
  // React's own sanctioned "adjust state during render" shape (see react.dev's guide of the same
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'
@@ -1,24 +1,13 @@
1
1
  import { createSettingsSlice } from '@rific/core'
2
- import type { ViewRotation } from '@tastic/core'
3
2
 
4
3
  // Settings that apply to the whole pack rather than one game. A game's own rules (board, score to
5
4
  // win, controls) live in that game's own settings slice.
6
5
  export interface AppSettings {
7
- // Lock Orientation: RootLayout's OrientationBridge hands it to @tastic/core's OrientationProvider
8
- // (freezeWhileLocked), which freezes the one shared orientation reading for every screen, every
9
- // board, dialogs and the status bar alike. Off by default: all of those just follow however the
10
- // device is actually being held.
11
- lockOrientation: boolean
12
- // The angle the lock is holding. Written by OrientationProvider's onLockChange (recorded on lock,
13
- // cleared on unlock) and handed back as lockInitialValue, so a relaunch reopens in the locked
14
- // orientation instead of portrait. Never set by the UI directly; absent while unlocked.
15
- lockedRotation?: ViewRotation
16
6
  // Edge Guard's opt-in (see Providers.tsx's EdgeGuardBridge).
17
7
  deferBottomEdgeGestures: boolean
18
8
  }
19
9
 
20
10
  export const DEFAULT_APP_SETTINGS: AppSettings = {
21
- lockOrientation: false,
22
11
  deferBottomEdgeGestures: false
23
12
  }
24
13
 
@@ -1,6 +1,5 @@
1
1
  import { defaultColors, FadingScrollView, useAutoPaperTheme } from '@rific/auto-paper'
2
2
  import { IconButton, Pressable } from '@rific/feedback-press'
3
- import { FakeLandscapeView, useRotation } from '@tastic/core'
4
3
  import { PressAwayOverlay, usePopoverHost, useWellColors, WELL_SHADOW } from '@tastic/hud/chrome'
5
4
  import { getProfilePickerContentSize, type Profile, ProfilePicker, type ProfilePickerColorSection } from '@tastic/profile'
6
5
  import { Redirect, router, useIsFocused } from 'expo-router'
@@ -53,7 +52,6 @@ export function HomeScreen() {
53
52
 
54
53
  function HomeMenu() {
55
54
  const pack = usePack()
56
- const rotation = useRotation()
57
55
  const layout = useHomeLayout()
58
56
  const { insets } = layout
59
57
  const { dark, colors } = useAutoPaperTheme()
@@ -79,7 +77,7 @@ function HomeMenu() {
79
77
 
80
78
  // Tiles: while any game in the pack has a preview, a wide screen lays the games out as a grid beside
81
79
  // the hero (homeLayout.ts). Home stays mounted beneath every screen the stack pushes and keeps
82
- // following the rotation, so while it's covered the layout is held where it was (see
80
+ // following the window, so while it's covered the layout is held where it was (see
83
81
  // useFrozenWhileHidden); it catches up when Home is shown again. Only the choice of layout is held:
84
82
  // the panes' numbers follow the window, unseen under the screen covering Home.
85
83
  const tiled = pack.games.some((game) => game.Preview)
@@ -95,13 +93,13 @@ function HomeMenu() {
95
93
  return () => cancelAnimationFrame(id)
96
94
  }, [tiled])
97
95
  // A popover is placed once, as it opens, from measurements of the frame it opens in. Turning the
98
- // phone (or any resize) moves the frame under it, and switching between the column and the two panes
96
+ // device (or any resize) moves the frame under it, and switching between the column and the two panes
99
97
  // unmounts the very button it hangs off, which would leave the press-away overlay up with nothing to
100
98
  // dismiss. So every layout change closes it.
101
99
  const closePicker = pickerHost.close
102
100
  useEffect(() => {
103
101
  closePicker()
104
- }, [isWide, rotation, layout.windowWidth, layout.windowHeight, closePicker])
102
+ }, [isWide, layout.windowWidth, layout.windowHeight, closePicker])
105
103
 
106
104
  // Over the pack's Backdrop Home draws nothing of its own behind the menu.
107
105
  const bg = pack.Backdrop ? 'transparent' : dark ? '#000000' : '#FFFFFF'
@@ -150,10 +148,9 @@ function HomeMenu() {
150
148
 
151
149
  return (
152
150
  <View style={[styles.container, { backgroundColor: bg }]}>
153
- {/* Reads correctly however the phone is held: see @tastic/core's FakeLandscapeView. The
154
- Settings dialog stays outside it: it renders through a Portal, turned by its own `rotation`. A
155
- game's rules sheet renders inside it instead (see RulesHost). */}
156
- <FakeLandscapeView style={styles.rotatable}>
151
+ {/* The Settings dialog stays outside this frame: it renders through a Portal. A game's rules
152
+ sheet renders inside it instead (see RulesHost). */}
153
+ <View style={styles.frame}>
157
154
  <RulesHost enabled={pack.games.some((game) => game.Rules)}>
158
155
  {isWide ? (
159
156
  <>
@@ -201,11 +198,11 @@ function HomeMenu() {
201
198
  <IconButton icon='cog' iconColor={fg} size={24} style={[styles.corner, { top: 8 + insets.top, right: 8 + insets.right }]} onPress={() => setSettingsOpen(true)} accessibilityLabel='Settings' />
202
199
  {RulesSheet && <RulesSheet visible onDismiss={() => setRulesGame(null)} />}
203
200
  </RulesHost>
204
- </FakeLandscapeView>
201
+ </View>
205
202
 
206
203
  {/* Home's How to Play row replays the pack's own cards (PackDefinition.guide); without them each
207
204
  tile's rules button shows its game's. */}
208
- <SettingsDialog visible={settingsOpen} onDismiss={() => setSettingsOpen(false)} rotation={rotation} onShowHowToPlay={pack.guide ? openPack : undefined} />
205
+ <SettingsDialog visible={settingsOpen} onDismiss={() => setSettingsOpen(false)} onShowHowToPlay={pack.guide ? openPack : undefined} />
209
206
  </View>
210
207
  )
211
208
  }
@@ -258,9 +255,8 @@ function hasRules(game: PackGameDefinition): boolean {
258
255
  return !!(game.Rules || game.guide)
259
256
  }
260
257
 
261
- // A game's rules sheet (GameDefinition.Rules) portals into a Paper host inside Home's turned frame, so
262
- // it's turned with the frame and laid out against it (a sheet laid out against the upright window ran
263
- // off a phone held sideways). Only in a pack with one: a pack without keeps the frame as it was.
258
+ // A game's rules sheet (GameDefinition.Rules) portals into a Paper host inside Home's frame, so it's
259
+ // laid out against Home's safe area. Only in a pack with one: a pack without keeps the frame as it was.
264
260
  function RulesHost({ enabled, children }: { enabled: boolean; children: ReactNode }) {
265
261
  return enabled ? <Portal.Host>{children}</Portal.Host> : <>{children}</>
266
262
  }
@@ -386,6 +382,9 @@ const styles = StyleSheet.create({
386
382
  corner: {
387
383
  position: 'absolute'
388
384
  },
385
+ frame: {
386
+ flex: 1
387
+ },
389
388
  // The wide layout's tiles: wrapped rows of tiles the layout sizes to fill the pane's width.
390
389
  grid: {
391
390
  flexDirection: 'row',
@@ -432,9 +431,6 @@ const styles = StyleSheet.create({
432
431
  previewIcon: {
433
432
  overflow: 'hidden'
434
433
  },
435
- rotatable: {
436
- flex: 1
437
- },
438
434
  // A well (@tastic/hud's wellSurface): the theme's surface on MD3's level-1 shadow, the same surface
439
435
  // as Solitaire's game rows, the stats screen's sections and every dialog.
440
436
  row: {