@tastic/hud 0.16.0 → 0.18.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 +57 -3
- package/dist/{TriggerGauge-2OJOOF6I.mjs → TriggerGauge-GIHKGAZE.mjs} +5 -5
- package/dist/chrome/index.d.mts +39 -10
- package/dist/chrome/index.d.ts +39 -10
- package/dist/chrome/index.js +309 -196
- package/dist/chrome/index.mjs +19 -7
- package/dist/{chunk-32U2OIRW.mjs → chunk-PUF6TABL.mjs} +76 -16
- package/dist/{chunk-JXRBXQLQ.mjs → chunk-Z2WUUYTE.mjs} +164 -113
- package/dist/guide/index.d.mts +3 -0
- package/dist/guide/index.d.ts +3 -0
- package/dist/guide/index.js +136 -72
- package/dist/guide/index.mjs +25 -13
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +480 -367
- package/dist/index.mjs +19 -7
- package/package.json +10 -10
- package/src/BaseSettingsDialog.tsx +203 -129
- package/src/ConfirmDialog.tsx +21 -26
- package/src/DialogHeader.tsx +72 -0
- package/src/OverlaySurface.tsx +70 -0
- package/src/TriggerGauge.tsx +9 -9
- package/src/chrome/index.ts +4 -1
- package/src/guide/HowToPlayDialog.tsx +37 -17
- package/src/index.ts +4 -1
- package/src/overlayCard.ts +11 -29
- package/src/useCompactDialog.ts +23 -0
package/dist/index.mjs
CHANGED
|
@@ -17,13 +17,19 @@ import {
|
|
|
17
17
|
StatRow,
|
|
18
18
|
StatSection,
|
|
19
19
|
UpdateDialog,
|
|
20
|
-
getStaggeredWordDuration
|
|
21
|
-
|
|
20
|
+
getStaggeredWordDuration,
|
|
21
|
+
useCompactDialog
|
|
22
|
+
} from "./chunk-Z2WUUYTE.mjs";
|
|
22
23
|
import {
|
|
23
|
-
|
|
24
|
+
COMPACT_DIALOG_HEIGHT,
|
|
25
|
+
DialogHeader,
|
|
26
|
+
OverlayBackdrop,
|
|
27
|
+
OverlayCard,
|
|
28
|
+
isCompactDialogHeight,
|
|
24
29
|
overlayActionStyles,
|
|
25
|
-
overlayStyles
|
|
26
|
-
|
|
30
|
+
overlayStyles,
|
|
31
|
+
useOverlayCardColors
|
|
32
|
+
} from "./chunk-PUF6TABL.mjs";
|
|
27
33
|
|
|
28
34
|
// src/AchievementUnlockList.tsx
|
|
29
35
|
import { getContrastColor, useAutoPaperTheme } from "@rific/auto-paper";
|
|
@@ -148,7 +154,7 @@ var styles2 = StyleSheet2.create({
|
|
|
148
154
|
|
|
149
155
|
// src/TriggerGaugeHost.tsx
|
|
150
156
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
151
|
-
var getTriggerGauge = () => import("./TriggerGauge-
|
|
157
|
+
var getTriggerGauge = () => import("./TriggerGauge-GIHKGAZE.mjs").then((mod) => ({ default: mod.TriggerGauge }));
|
|
152
158
|
function TriggerGaugeHost(props) {
|
|
153
159
|
return /* @__PURE__ */ jsx3(SkiaGate, { getComponent: getTriggerGauge, componentProps: props });
|
|
154
160
|
}
|
|
@@ -874,16 +880,20 @@ export {
|
|
|
874
880
|
ActivityStatSection,
|
|
875
881
|
BaseSettingsDialog,
|
|
876
882
|
BaseStatsScreen,
|
|
883
|
+
COMPACT_DIALOG_HEIGHT,
|
|
877
884
|
ConfirmDialog,
|
|
878
885
|
ContentGutter,
|
|
879
886
|
ControlSchemePicker,
|
|
880
887
|
CornerActionButtons,
|
|
881
888
|
CornerStatusBadges,
|
|
889
|
+
DialogHeader,
|
|
882
890
|
InlineColorPicker,
|
|
883
891
|
LABELED_DROPDOWN_POPOVER_WIDTH,
|
|
884
892
|
LOCKED_BADGE_COLOR,
|
|
885
893
|
LabeledDropdown,
|
|
886
894
|
MONO_FONT,
|
|
895
|
+
OverlayBackdrop,
|
|
896
|
+
OverlayCard,
|
|
887
897
|
PlayerSetupPanel,
|
|
888
898
|
PopoverBody,
|
|
889
899
|
PressAwayOverlay,
|
|
@@ -901,12 +911,14 @@ export {
|
|
|
901
911
|
getCpuDifficultyPopoverId,
|
|
902
912
|
getInlineColorPickerContentSize,
|
|
903
913
|
getLabeledDropdownContentHeight,
|
|
904
|
-
getOverlayCardColors,
|
|
905
914
|
getStaggeredWordDuration,
|
|
915
|
+
isCompactDialogHeight,
|
|
906
916
|
loadSkiaWeb,
|
|
907
917
|
overlayActionStyles,
|
|
908
918
|
overlayStyles,
|
|
909
919
|
useAutoAlign,
|
|
920
|
+
useCompactDialog,
|
|
921
|
+
useOverlayCardColors,
|
|
910
922
|
usePopoverHost,
|
|
911
923
|
useQuitConfirmation,
|
|
912
924
|
useZoneClampedAlign
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastic/hud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Visual component kit for local-multiplayer React Native games: inline (non-modal) popovers, dropdowns, color pickers, gauges, ready buttons, and dialogs, built to stay scoped to one player's own zone on a shared screen",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -87,14 +87,14 @@
|
|
|
87
87
|
"@infinitetoken/eslint-config": "^0.2.0",
|
|
88
88
|
"@infinitetoken/jest-config": "^0.4.0",
|
|
89
89
|
"@infinitetoken/tsconfig": "^0.5.0",
|
|
90
|
-
"@rific/auto-paper": "^0.
|
|
91
|
-
"@rific/core": "^0.
|
|
92
|
-
"@rific/feedback-press": "^0.
|
|
90
|
+
"@rific/auto-paper": "^0.13.0",
|
|
91
|
+
"@rific/core": "^0.3.0",
|
|
92
|
+
"@rific/feedback-press": "^0.13.0",
|
|
93
93
|
"@rific/updater": "^0.6.0",
|
|
94
94
|
"@shopify/react-native-skia": "^2.6.2",
|
|
95
|
-
"@tastic/achievements": "^0.
|
|
96
|
-
"@tastic/core": "^0.
|
|
97
|
-
"@tastic/profile": "^0.
|
|
95
|
+
"@tastic/achievements": "^0.5.0",
|
|
96
|
+
"@tastic/core": "^0.13.0",
|
|
97
|
+
"@tastic/profile": "^0.12.0",
|
|
98
98
|
"@tastic/split-screen": "^0.5.0",
|
|
99
99
|
"@testing-library/dom": "^10.4.1",
|
|
100
100
|
"@testing-library/react": "^16.3.2",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"prettier": "^3.8.3",
|
|
109
109
|
"react": "^19.0.0",
|
|
110
110
|
"react-dom": "^19.0.0",
|
|
111
|
-
"react-native": "^0.
|
|
111
|
+
"react-native": "^0.86.3",
|
|
112
112
|
"react-native-paper": "^5.15.2",
|
|
113
113
|
"react-native-reanimated": "^4.5.1",
|
|
114
114
|
"react-native-safe-area-context": "^5.10.0",
|
|
@@ -116,10 +116,10 @@
|
|
|
116
116
|
"typescript": "^6.0.3"
|
|
117
117
|
},
|
|
118
118
|
"peerDependencies": {
|
|
119
|
-
"@rific/auto-paper": "
|
|
119
|
+
"@rific/auto-paper": "^0.13.0",
|
|
120
120
|
"@rific/feedback-press": ">=0.10.2",
|
|
121
121
|
"@rific/updater": ">=0.4.0",
|
|
122
|
-
"@shopify/react-native-skia": ">=
|
|
122
|
+
"@shopify/react-native-skia": ">=2.6.0",
|
|
123
123
|
"@tastic/achievements": ">=0.2.0",
|
|
124
124
|
"@tastic/core": ">=0.12.0",
|
|
125
125
|
"@tastic/profile": ">=0.4.0",
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { AutoAppearancePicker, Dialog, useAutoPaperTheme } from '@rific/auto-paper'
|
|
1
|
+
import { AutoAppearancePicker, Dialog, FadingScrollView, useAutoPaperTheme, useBlurModule, useThemeSettings } from '@rific/auto-paper'
|
|
2
2
|
import { Button, SoundContext, TouchableRipple, useHapticSettings, useSoundSettings, useVibration } from '@rific/feedback-press'
|
|
3
3
|
import { useUpdater } from '@rific/updater'
|
|
4
|
-
import { useIsTouchPrimaryDevice, useRotation, type ViewRotation } from '@tastic/core'
|
|
4
|
+
import { rotateInsets, useIsTouchPrimaryDevice, useRotation, type ViewRotation } from '@tastic/core'
|
|
5
5
|
import { ReactNode, useContext, useState } from 'react'
|
|
6
|
-
import { Platform,
|
|
6
|
+
import { Platform, StyleSheet, useWindowDimensions, View } from 'react-native'
|
|
7
7
|
import { Icon, Portal, Text } from 'react-native-paper'
|
|
8
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
8
9
|
|
|
9
10
|
import { ConfirmDialog } from './ConfirmDialog'
|
|
11
|
+
import { DialogHeader, isCompactDialogHeight } from './DialogHeader'
|
|
12
|
+
import { overlayActionStyles, overlayStyles } from './overlayCard'
|
|
10
13
|
import { RotatedFrame } from './RotatedFrame'
|
|
11
14
|
import { useUpdateConfirmBridge } from './useUpdateConfirmBridge'
|
|
12
15
|
|
|
@@ -91,6 +94,18 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
91
94
|
const { colors } = useAutoPaperTheme()
|
|
92
95
|
const ambientRotation = useRotation()
|
|
93
96
|
const rotation = rotationOverride ?? ambientRotation
|
|
97
|
+
// The sheet lays out in a RotatedFrame turned by `rotation` (below), so its height is the window's
|
|
98
|
+
// width when turned a quarter: the same compact rule as every other dialog's header.
|
|
99
|
+
const window = useWindowDimensions()
|
|
100
|
+
const turned = rotation === 90 || rotation === -90
|
|
101
|
+
const compactHeader = isCompactDialogHeight(turned ? window.width : window.height)
|
|
102
|
+
// The overlay cards' width (ConfirmDialog, the guide, every app's result card): 24pt from the
|
|
103
|
+
// frame's edges, never under a notch, capped at their 360 (overlayStyles.card). auto-paper's Dialog
|
|
104
|
+
// would give the sheet a 32pt margin and a 400 cap, 16pt narrower than a card on a phone, which
|
|
105
|
+
// showed when Settings and a confirm opened one after the other. The card is centered, so the
|
|
106
|
+
// Dialog's own 32pt padding is simply overhung.
|
|
107
|
+
const insets = rotateInsets(useSafeAreaInsets(), rotation)
|
|
108
|
+
const sheetWidth = Math.min(overlayStyles.card.maxWidth, (turned ? window.height : window.width) - 2 * Math.max(insets.left, insets.right, SHEET_GUTTER))
|
|
94
109
|
// Also gates the Lock Orientation row below (alongside showLockOrientation itself) - true
|
|
95
110
|
// unconditionally on native (see the hook's own doc), so this only actually excludes a
|
|
96
111
|
// desktop/laptop browser, where a mouse-driven window has no physical orientation to lock
|
|
@@ -106,6 +121,8 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
106
121
|
// manually here instead, gated on the NEW value so it only plays when switching that channel on.
|
|
107
122
|
const sound = useContext(SoundContext)
|
|
108
123
|
const { forceShort: forceHapticFeedback } = useVibration()
|
|
124
|
+
const { settings: themeSettings, set: setThemeSettings } = useThemeSettings()
|
|
125
|
+
const expoBlur = useBlurModule()
|
|
109
126
|
// Covers check()'s three purely-informational cases (dev-mode disabled, web unsupported, no
|
|
110
127
|
// update found) via onInfo below - a retro card overlay instead of the native Alert.alert those
|
|
111
128
|
// cases fall back to by default.
|
|
@@ -139,6 +156,8 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
139
156
|
const showHaptics = Platform.OS !== 'web' && !hideHaptics
|
|
140
157
|
const showSound = !hideSound
|
|
141
158
|
const showAppearance = !hideAppearance
|
|
159
|
+
// See the Blur row's own comment below for why it hides on Android and without expo-blur.
|
|
160
|
+
const showBlur = showAppearance && Platform.OS !== 'android' && expoBlur !== undefined && expoBlur !== null
|
|
142
161
|
const showUpdateCheck = Platform.OS !== 'web' && !hideUpdateCheck
|
|
143
162
|
|
|
144
163
|
// Laid out in its own turned frame rather than rotated in place. Paper's Portal mounts at the app
|
|
@@ -155,169 +174,205 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
155
174
|
return (
|
|
156
175
|
<Portal>
|
|
157
176
|
<RotatedFrame rotation={rotation} rotatedSafeArea portalHost passThrough>
|
|
158
|
-
<Dialog visible={visible} onDismiss={onDismiss} style={styles.dialog}>
|
|
159
|
-
<Dialog.Title>Settings</Dialog.Title>
|
|
177
|
+
<Dialog visible={visible} onDismiss={onDismiss} style={[styles.dialog, { width: sheetWidth }]}>
|
|
160
178
|
{/* react-native-paper's Dialog.ScrollArea has a fixed 24px marginBottom baked in (meant to
|
|
161
|
-
reserve room for a Dialog.Actions row below it) - overridden to 0
|
|
162
|
-
|
|
179
|
+
reserve room for a Dialog.Actions row below it) - overridden to 0: the Done row below
|
|
180
|
+
carries its own spacing. It is the sheet's first child, so it runs to the card's top edge. */}
|
|
163
181
|
<Dialog.ScrollArea style={styles.scrollArea}>
|
|
164
|
-
<
|
|
165
|
-
{/*
|
|
182
|
+
<FadingScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
|
183
|
+
{/* The fleet's one dialog header and Done button (DialogHeader, overlayActionStyles' single
|
|
184
|
+
button), so the sheet reads as one family with ConfirmDialog, the guide and every app's
|
|
185
|
+
result cards (Jay, 2026-10-15). The header scrolls away with the rows, so a long sheet
|
|
186
|
+
gives them the whole card once scrolled; Done stays pinned below. Tapping outside still
|
|
187
|
+
closes it too. */}
|
|
188
|
+
<DialogHeader icon='cog' title='Settings' compact={compactHeader} />
|
|
189
|
+
<View style={styles.content}>
|
|
190
|
+
{/* Grouped tightly together (styles.toggleGroup's small internal gap, not the 24px gap
|
|
166
191
|
between sections below) - same bare-row shape for all of them, no segmented control
|
|
167
192
|
and no section heading (each row's own label already says what it is). Haptics only
|
|
168
193
|
joins on native - there's nothing for it to control on web. */}
|
|
169
|
-
|
|
170
|
-
|
|
194
|
+
<View style={styles.toggleGroup}>
|
|
195
|
+
{/* Omitted entirely for a portrait-only game (no lockOrientation/onLockOrientationChange
|
|
171
196
|
passed at all - nothing to lock), and hidden (not just disabled) on a desktop/laptop
|
|
172
197
|
browser even when they are - see isTouchPrimary's own doc above. Still shown on
|
|
173
198
|
native and on a touch-capable mobile browser, where a rotated window really does
|
|
174
199
|
reflect a rotated device. */}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
200
|
+
{showLockOrientation && (
|
|
201
|
+
<TouchableRipple onPress={() => onLockOrientationChange!(!lockOrientation)} style={styles.toggleButton} accessibilityLabel={`Lock orientation ${lockOrientation ? 'on' : 'off'}`}>
|
|
202
|
+
<View style={styles.toggleContent}>
|
|
203
|
+
<SettingIcon source={lockOrientation ? 'lock' : 'lock-open-variant-outline'} color={lockOrientation ? colors.tertiary : colors.onSurfaceVariant} containerColor={lockOrientation ? colors.tertiaryContainer : colors.surfaceVariant} />
|
|
204
|
+
<View style={styles.flexShrink}>
|
|
205
|
+
<Text variant='bodyLarge' style={{ color: colors.onSurface }}>
|
|
206
|
+
Lock Orientation
|
|
207
|
+
</Text>
|
|
208
|
+
<Text variant='bodySmall' numberOfLines={1} style={{ color: colors.onSurfaceVariant }}>
|
|
209
|
+
Pin current layout
|
|
210
|
+
</Text>
|
|
211
|
+
</View>
|
|
186
212
|
</View>
|
|
187
|
-
</
|
|
188
|
-
|
|
189
|
-
)}
|
|
213
|
+
</TouchableRipple>
|
|
214
|
+
)}
|
|
190
215
|
|
|
191
|
-
|
|
216
|
+
{/* iOS only - mirrors into UserDefaults for @tastic/edge-guard's own config plugin
|
|
192
217
|
swizzle to read. Guards both edges - the top (Notification Center/Control Center)
|
|
193
218
|
and the bottom (home-indicator swipe, incidentally Reachability too), per that
|
|
194
219
|
package's own doc. Off by default: a deliberate opt-in for whoever's actually hit an
|
|
195
220
|
accidental-swipe-near-an-edge problem mid-game and come looking for a fix, not a
|
|
196
221
|
surprise every player gets by default. */}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
222
|
+
{showEdgeGuard && (
|
|
223
|
+
<TouchableRipple onPress={() => onDeferBottomEdgeGestures!(!deferBottomEdgeGestures)} style={styles.toggleButton} accessibilityLabel={`Edge guard ${deferBottomEdgeGestures ? 'on' : 'off'}`}>
|
|
224
|
+
<View style={styles.toggleContent}>
|
|
225
|
+
<SettingIcon source={deferBottomEdgeGestures ? 'shield-check-outline' : 'shield-off-outline'} color={deferBottomEdgeGestures ? colors.tertiary : colors.onSurfaceVariant} containerColor={deferBottomEdgeGestures ? colors.tertiaryContainer : colors.surfaceVariant} />
|
|
226
|
+
<View style={styles.flexShrink}>
|
|
227
|
+
<Text variant='bodyLarge' style={{ color: colors.onSurface }}>
|
|
228
|
+
Edge Guard
|
|
229
|
+
</Text>
|
|
230
|
+
<Text variant='bodySmall' numberOfLines={1} style={{ color: colors.onSurfaceVariant }}>
|
|
231
|
+
Require second swipe
|
|
232
|
+
</Text>
|
|
233
|
+
</View>
|
|
208
234
|
</View>
|
|
209
|
-
</
|
|
210
|
-
|
|
211
|
-
)}
|
|
235
|
+
</TouchableRipple>
|
|
236
|
+
)}
|
|
212
237
|
|
|
213
|
-
|
|
238
|
+
{/* Side by side, not stacked - neither needs a full row's width (single-line label,
|
|
214
239
|
no description under it the way Lock Orientation has), so sharing one row reads
|
|
215
240
|
just as clearly and takes half the vertical space. toggleRow carries the same
|
|
216
241
|
edge-alignment negative margin toggleButton normally carries itself; the buttons
|
|
217
242
|
inside it use toggleButtonInRow (flex: 1) instead so the two don't double up on it.
|
|
218
243
|
The row itself only renders when at least one of the two will - an empty flex row
|
|
219
244
|
still eats its own gap/margin otherwise. */}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
245
|
+
{(showSound || showHaptics) && (
|
|
246
|
+
<View style={styles.toggleRow}>
|
|
247
|
+
{showSound && (
|
|
248
|
+
<TouchableRipple
|
|
249
|
+
soundDisabled
|
|
250
|
+
onPress={() => {
|
|
251
|
+
const enabled = !soundSettings.enabled
|
|
252
|
+
setSoundSettings({ enabled })
|
|
253
|
+
if (enabled) sound.selection?.()
|
|
254
|
+
}}
|
|
255
|
+
style={styles.toggleButtonInRow}
|
|
256
|
+
accessibilityLabel={`Sound ${soundSettings.enabled ? 'on' : 'off'}`}
|
|
257
|
+
>
|
|
258
|
+
<View style={styles.toggleContent}>
|
|
259
|
+
<SettingIcon source={soundSettings.enabled ? 'volume-high' : 'volume-off'} color={soundSettings.enabled ? colors.tertiary : colors.onSurfaceVariant} containerColor={soundSettings.enabled ? colors.tertiaryContainer : colors.surfaceVariant} />
|
|
260
|
+
<Text variant='bodyLarge' style={{ color: colors.onSurface }}>
|
|
261
|
+
Sound
|
|
262
|
+
</Text>
|
|
263
|
+
</View>
|
|
264
|
+
</TouchableRipple>
|
|
265
|
+
)}
|
|
266
|
+
|
|
267
|
+
{/* Already native-only (see showHaptics's own definition) - hideHaptics can hide
|
|
268
|
+
it there too, for a game that's deliberately silent on vibration. */}
|
|
269
|
+
{showHaptics && (
|
|
270
|
+
<TouchableRipple
|
|
271
|
+
hapticDisabled
|
|
272
|
+
onPress={() => {
|
|
273
|
+
const vibrate = !hapticSettings.vibrate
|
|
274
|
+
setHapticSettings({ vibrate })
|
|
275
|
+
if (vibrate) forceHapticFeedback()
|
|
276
|
+
}}
|
|
277
|
+
style={styles.toggleButtonInRow}
|
|
278
|
+
accessibilityLabel={`Haptics ${hapticSettings.vibrate ? 'on' : 'off'}`}
|
|
279
|
+
>
|
|
280
|
+
<View style={styles.toggleContent}>
|
|
281
|
+
<SettingIcon source={hapticSettings.vibrate ? 'vibrate' : 'vibrate-off'} color={hapticSettings.vibrate ? colors.tertiary : colors.onSurfaceVariant} containerColor={hapticSettings.vibrate ? colors.tertiaryContainer : colors.surfaceVariant} />
|
|
282
|
+
<Text variant='bodyLarge' style={{ color: colors.onSurface }}>
|
|
283
|
+
Haptics
|
|
284
|
+
</Text>
|
|
285
|
+
</View>
|
|
286
|
+
</TouchableRipple>
|
|
287
|
+
)}
|
|
288
|
+
</View>
|
|
289
|
+
)}
|
|
290
|
+
|
|
291
|
+
{/* Not a toggle, but it reads like one of the rows above and belongs with them rather than
|
|
292
|
+
down in `children` (which sits below Appearance) - this is the "replay the intro"
|
|
293
|
+
entry point every game's own how-to-play flow shares. Dismisses this dialog first so
|
|
294
|
+
the guide's own overlay never opens on top of a still-visible Settings card. */}
|
|
295
|
+
{onShowHowToPlay && (
|
|
296
|
+
<TouchableRipple
|
|
297
|
+
onPress={() => {
|
|
298
|
+
onDismiss()
|
|
299
|
+
onShowHowToPlay()
|
|
300
|
+
}}
|
|
301
|
+
style={styles.toggleButton}
|
|
302
|
+
accessibilityLabel='How to Play'
|
|
303
|
+
>
|
|
304
|
+
<View style={styles.toggleContent}>
|
|
305
|
+
<SettingIcon source='help-circle-outline' color={colors.secondary} containerColor={colors.secondaryContainer} />
|
|
306
|
+
<View style={styles.flexShrink}>
|
|
235
307
|
<Text variant='bodyLarge' style={{ color: colors.onSurface }}>
|
|
236
|
-
|
|
308
|
+
How to Play
|
|
309
|
+
</Text>
|
|
310
|
+
<Text variant='bodySmall' numberOfLines={1} style={{ color: colors.onSurfaceVariant }}>
|
|
311
|
+
Replay quick intro
|
|
237
312
|
</Text>
|
|
238
313
|
</View>
|
|
239
|
-
</
|
|
240
|
-
|
|
314
|
+
</View>
|
|
315
|
+
</TouchableRipple>
|
|
316
|
+
)}
|
|
317
|
+
</View>
|
|
241
318
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
{
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
319
|
+
{showAppearance && (
|
|
320
|
+
<View style={styles.section}>
|
|
321
|
+
<Text variant='labelMedium' style={[styles.sectionLabel, { color: colors.onSurfaceVariant }]}>
|
|
322
|
+
APPEARANCE
|
|
323
|
+
</Text>
|
|
324
|
+
<AutoAppearancePicker showLabels={false} />
|
|
325
|
+
{/* Blur (Jay, 2026-10-15): one fleet-wide switch for auto-paper's own `blur` setting,
|
|
326
|
+
which every blur-capable surface follows on its own (this sheet, and the
|
|
327
|
+
OverlayCard behind ConfirmDialog, UpdateDialog and the guide). Off, or wherever
|
|
328
|
+
blur can't happen, those surfaces paint the theme's raised surface instead. So the
|
|
329
|
+
row only shows where turning it on would change something: not on Android
|
|
330
|
+
(auto-paper's BlurView needs a blur target there, which these cards don't have) and
|
|
331
|
+
not in an app that never handed expo-blur to auto-paper's Provider. */}
|
|
332
|
+
{showBlur && (
|
|
333
|
+
<TouchableRipple onPress={() => setThemeSettings({ blur: !themeSettings.blur })} style={styles.toggleButton} accessibilityLabel={`Blur ${themeSettings.blur ? 'on' : 'off'}`}>
|
|
255
334
|
<View style={styles.toggleContent}>
|
|
256
|
-
<SettingIcon source={
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
335
|
+
<SettingIcon source={themeSettings.blur ? 'blur' : 'blur-off'} color={themeSettings.blur ? colors.tertiary : colors.onSurfaceVariant} containerColor={themeSettings.blur ? colors.tertiaryContainer : colors.surfaceVariant} />
|
|
336
|
+
{/* The one-word label alone didn't say what blurs (Jay, 2026-10-15). One line
|
|
337
|
+
even on a 360pt phone (216pt of room). */}
|
|
338
|
+
<View style={styles.flexShrink}>
|
|
339
|
+
<Text variant='bodyLarge' style={{ color: colors.onSurface }}>
|
|
340
|
+
Blur
|
|
341
|
+
</Text>
|
|
342
|
+
<Text variant='bodySmall' numberOfLines={1} style={{ color: colors.onSurfaceVariant }}>
|
|
343
|
+
Frost behind dialogs
|
|
344
|
+
</Text>
|
|
345
|
+
</View>
|
|
260
346
|
</View>
|
|
261
347
|
</TouchableRipple>
|
|
262
348
|
)}
|
|
263
349
|
</View>
|
|
264
350
|
)}
|
|
265
351
|
|
|
266
|
-
{
|
|
267
|
-
down in `children` (which sits below Appearance) - this is the "replay the intro"
|
|
268
|
-
entry point every game's own how-to-play flow shares. Dismisses this dialog first so
|
|
269
|
-
the guide's own overlay never opens on top of a still-visible Settings card. */}
|
|
270
|
-
{onShowHowToPlay && (
|
|
271
|
-
<TouchableRipple
|
|
272
|
-
onPress={() => {
|
|
273
|
-
onDismiss()
|
|
274
|
-
onShowHowToPlay()
|
|
275
|
-
}}
|
|
276
|
-
style={styles.toggleButton}
|
|
277
|
-
accessibilityLabel='How to Play'
|
|
278
|
-
>
|
|
279
|
-
<View style={styles.toggleContent}>
|
|
280
|
-
<SettingIcon source='help-circle-outline' color={colors.secondary} containerColor={colors.secondaryContainer} />
|
|
281
|
-
<View style={styles.flexShrink}>
|
|
282
|
-
<Text variant='bodyLarge' style={{ color: colors.onSurface }}>
|
|
283
|
-
How to Play
|
|
284
|
-
</Text>
|
|
285
|
-
<Text variant='bodySmall' numberOfLines={1} style={{ color: colors.onSurfaceVariant }}>
|
|
286
|
-
Replay the quick intro
|
|
287
|
-
</Text>
|
|
288
|
-
</View>
|
|
289
|
-
</View>
|
|
290
|
-
</TouchableRipple>
|
|
291
|
-
)}
|
|
292
|
-
</View>
|
|
293
|
-
|
|
294
|
-
{showAppearance && (
|
|
295
|
-
<View style={styles.section}>
|
|
296
|
-
<Text variant='labelMedium' style={[styles.sectionLabel, { color: colors.onSurfaceVariant }]}>
|
|
297
|
-
APPEARANCE
|
|
298
|
-
</Text>
|
|
299
|
-
<AutoAppearancePicker showLabels={false} />
|
|
300
|
-
</View>
|
|
301
|
-
)}
|
|
302
|
-
|
|
303
|
-
{children}
|
|
352
|
+
{children}
|
|
304
353
|
|
|
305
|
-
|
|
354
|
+
{/* Native only (a web build has no OTA update concept at all, desktop or mobile browser
|
|
306
355
|
alike) - hideUpdateCheck can hide it there too, for a native app that isn't wired up
|
|
307
356
|
for EAS/OTA updates at all. */}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
357
|
+
{showUpdateCheck && (
|
|
358
|
+
<View style={styles.section}>
|
|
359
|
+
<Text variant='labelMedium' style={[styles.sectionLabel, { color: colors.onSurfaceVariant }]}>
|
|
360
|
+
VERSION {version}
|
|
361
|
+
{updateReady ? ' · UPDATE READY' : ''}
|
|
362
|
+
</Text>
|
|
363
|
+
<Button mode='outlined' onPress={check} loading={checking} disabled={checking}>
|
|
364
|
+
Check for Updates
|
|
365
|
+
</Button>
|
|
366
|
+
</View>
|
|
367
|
+
)}
|
|
368
|
+
</View>
|
|
369
|
+
</FadingScrollView>
|
|
320
370
|
</Dialog.ScrollArea>
|
|
371
|
+
<View style={[overlayActionStyles.actions, styles.footer]}>
|
|
372
|
+
<Button mode='contained' onPress={onDismiss} style={overlayActionStyles.actionButtonSingle} buttonColor={colors.primary} textColor={colors.onPrimary}>
|
|
373
|
+
Done
|
|
374
|
+
</Button>
|
|
375
|
+
</View>
|
|
321
376
|
</Dialog>
|
|
322
377
|
<ConfirmDialog visible={!!updateManifest} title='Update Available' message={updateManifestDate ? `Released ${updateManifestDate.toLocaleDateString()} at ${updateManifestDate.toLocaleTimeString()}` : ''} confirmLabel='Restart' cancelLabel='Later' icon='update' rotation={0} onConfirm={() => respondToUpdateConfirm(true)} onCancel={() => respondToUpdateConfirm(false)} />
|
|
323
378
|
<ConfirmDialog visible={!!infoMessage} title={infoMessage?.title ?? ''} message={infoMessage?.message ?? ''} confirmLabel='OK' icon='information-outline' rotation={0} onConfirm={() => setInfoMessage(null)} />
|
|
@@ -326,10 +381,13 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
326
381
|
)
|
|
327
382
|
}
|
|
328
383
|
|
|
384
|
+
// The backdrop gutter every overlay card keeps from the screen's edges.
|
|
385
|
+
const SHEET_GUTTER = 24
|
|
386
|
+
|
|
329
387
|
const styles = StyleSheet.create({
|
|
388
|
+
// The rows' sections, 24pt apart.
|
|
330
389
|
content: {
|
|
331
|
-
gap: 24
|
|
332
|
-
paddingVertical: 20
|
|
390
|
+
gap: 24
|
|
333
391
|
},
|
|
334
392
|
// Caps the card so it never grows past the screen - without this the dialog just keeps
|
|
335
393
|
// growing to fit its content and the overflow gets clipped by the screen edge, which is
|
|
@@ -341,6 +399,11 @@ const styles = StyleSheet.create({
|
|
|
341
399
|
flexShrink: {
|
|
342
400
|
flexShrink: 1
|
|
343
401
|
},
|
|
402
|
+
// The overlay cards' 24pt padding, as the sheet's own insets (its scroll area pads 24 sideways).
|
|
403
|
+
footer: {
|
|
404
|
+
paddingBottom: 24,
|
|
405
|
+
paddingHorizontal: 24
|
|
406
|
+
},
|
|
344
407
|
iconBadge: {
|
|
345
408
|
alignItems: 'center',
|
|
346
409
|
borderRadius: 10,
|
|
@@ -348,9 +411,20 @@ const styles = StyleSheet.create({
|
|
|
348
411
|
justifyContent: 'center',
|
|
349
412
|
width: 36
|
|
350
413
|
},
|
|
414
|
+
// No hairline dividers above and below the scroll area (Paper's MD2-era borders): none of the
|
|
415
|
+
// fleet's other dialogs draws one, so the sheet matches them (Jay, 2026-10-15).
|
|
351
416
|
scrollArea: {
|
|
417
|
+
borderBottomWidth: 0,
|
|
418
|
+
borderTopWidth: 0,
|
|
352
419
|
marginBottom: 0
|
|
353
420
|
},
|
|
421
|
+
// The header and the rows scroll together: the card's 24pt inset above the header, the overlay
|
|
422
|
+
// cards' 16pt gap between it and the rows, and 16pt below the last row before the Done row.
|
|
423
|
+
scrollContent: {
|
|
424
|
+
gap: 16,
|
|
425
|
+
paddingBottom: 16,
|
|
426
|
+
paddingTop: 24
|
|
427
|
+
},
|
|
354
428
|
section: {
|
|
355
429
|
gap: 12
|
|
356
430
|
},
|