@tastic/hud 0.17.0 → 0.19.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 +31 -16
- package/dist/{TriggerGauge-2OJOOF6I.mjs → TriggerGauge-GIHKGAZE.mjs} +5 -5
- package/dist/chrome/index.d.mts +23 -17
- package/dist/chrome/index.d.ts +23 -17
- package/dist/chrome/index.js +298 -211
- package/dist/chrome/index.mjs +11 -5
- package/dist/{chunk-27FIA26F.mjs → chunk-HGJKNLMD.mjs} +165 -114
- package/dist/{chunk-AX4GC5KU.mjs → chunk-PUF6TABL.mjs} +60 -24
- package/dist/guide/index.d.mts +3 -0
- package/dist/guide/index.d.ts +3 -0
- package/dist/guide/index.js +122 -74
- package/dist/guide/index.mjs +16 -8
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +471 -384
- package/dist/index.mjs +12 -6
- package/package.json +12 -12
- package/src/BaseSettingsDialog.tsx +224 -146
- package/src/ConfirmDialog.tsx +8 -15
- package/src/DialogHeader.tsx +72 -0
- package/src/OverlaySurface.tsx +21 -13
- package/src/TriggerGauge.tsx +9 -9
- package/src/chrome/index.ts +3 -1
- package/src/guide/HowToPlayDialog.tsx +23 -9
- package/src/index.ts +3 -1
- package/src/overlayCard.ts +11 -34
- package/src/useCompactDialog.ts +23 -0
package/dist/index.mjs
CHANGED
|
@@ -17,16 +17,19 @@ import {
|
|
|
17
17
|
StatRow,
|
|
18
18
|
StatSection,
|
|
19
19
|
UpdateDialog,
|
|
20
|
-
getStaggeredWordDuration
|
|
21
|
-
|
|
20
|
+
getStaggeredWordDuration,
|
|
21
|
+
useCompactDialog
|
|
22
|
+
} from "./chunk-HGJKNLMD.mjs";
|
|
22
23
|
import {
|
|
24
|
+
COMPACT_DIALOG_HEIGHT,
|
|
25
|
+
DialogHeader,
|
|
23
26
|
OverlayBackdrop,
|
|
24
27
|
OverlayCard,
|
|
25
|
-
|
|
28
|
+
isCompactDialogHeight,
|
|
26
29
|
overlayActionStyles,
|
|
27
30
|
overlayStyles,
|
|
28
31
|
useOverlayCardColors
|
|
29
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-PUF6TABL.mjs";
|
|
30
33
|
|
|
31
34
|
// src/AchievementUnlockList.tsx
|
|
32
35
|
import { getContrastColor, useAutoPaperTheme } from "@rific/auto-paper";
|
|
@@ -151,7 +154,7 @@ var styles2 = StyleSheet2.create({
|
|
|
151
154
|
|
|
152
155
|
// src/TriggerGaugeHost.tsx
|
|
153
156
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
154
|
-
var getTriggerGauge = () => import("./TriggerGauge-
|
|
157
|
+
var getTriggerGauge = () => import("./TriggerGauge-GIHKGAZE.mjs").then((mod) => ({ default: mod.TriggerGauge }));
|
|
155
158
|
function TriggerGaugeHost(props) {
|
|
156
159
|
return /* @__PURE__ */ jsx3(SkiaGate, { getComponent: getTriggerGauge, componentProps: props });
|
|
157
160
|
}
|
|
@@ -877,11 +880,13 @@ export {
|
|
|
877
880
|
ActivityStatSection,
|
|
878
881
|
BaseSettingsDialog,
|
|
879
882
|
BaseStatsScreen,
|
|
883
|
+
COMPACT_DIALOG_HEIGHT,
|
|
880
884
|
ConfirmDialog,
|
|
881
885
|
ContentGutter,
|
|
882
886
|
ControlSchemePicker,
|
|
883
887
|
CornerActionButtons,
|
|
884
888
|
CornerStatusBadges,
|
|
889
|
+
DialogHeader,
|
|
885
890
|
InlineColorPicker,
|
|
886
891
|
LABELED_DROPDOWN_POPOVER_WIDTH,
|
|
887
892
|
LOCKED_BADGE_COLOR,
|
|
@@ -906,12 +911,13 @@ export {
|
|
|
906
911
|
getCpuDifficultyPopoverId,
|
|
907
912
|
getInlineColorPickerContentSize,
|
|
908
913
|
getLabeledDropdownContentHeight,
|
|
909
|
-
getOverlayCardColors,
|
|
910
914
|
getStaggeredWordDuration,
|
|
915
|
+
isCompactDialogHeight,
|
|
911
916
|
loadSkiaWeb,
|
|
912
917
|
overlayActionStyles,
|
|
913
918
|
overlayStyles,
|
|
914
919
|
useAutoAlign,
|
|
920
|
+
useCompactDialog,
|
|
915
921
|
useOverlayCardColors,
|
|
916
922
|
usePopoverHost,
|
|
917
923
|
useQuitConfirmation,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastic/hud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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,15 +87,15 @@
|
|
|
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.
|
|
93
|
-
"@rific/updater": "^0.
|
|
90
|
+
"@rific/auto-paper": "^0.13.2",
|
|
91
|
+
"@rific/core": "^0.3.1",
|
|
92
|
+
"@rific/feedback-press": "^0.13.0",
|
|
93
|
+
"@rific/updater": "^0.7.1",
|
|
94
94
|
"@shopify/react-native-skia": "^2.6.2",
|
|
95
|
-
"@tastic/achievements": "^0.
|
|
96
|
-
"@tastic/core": "^0.
|
|
97
|
-
"@tastic/profile": "^0.
|
|
98
|
-
"@tastic/split-screen": "^0.5.
|
|
95
|
+
"@tastic/achievements": "^0.6.0",
|
|
96
|
+
"@tastic/core": "^0.14.0",
|
|
97
|
+
"@tastic/profile": "^0.12.0",
|
|
98
|
+
"@tastic/split-screen": "^0.5.2",
|
|
99
99
|
"@testing-library/dom": "^10.4.1",
|
|
100
100
|
"@testing-library/react": "^16.3.2",
|
|
101
101
|
"@types/jest": "^30.0.0",
|
|
@@ -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, useBlurModule, useThemeSettings } 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
|
|
|
@@ -53,6 +56,13 @@ export interface BaseSettingsDialogProps {
|
|
|
53
56
|
// @tastic/edge-guard's own swizzle is a no-op on every other platform.
|
|
54
57
|
deferBottomEdgeGestures?: boolean
|
|
55
58
|
onDeferBottomEdgeGestures?: (value: boolean) => void
|
|
59
|
+
// Optional, and always passed as a pair - omit both for an app that doesn't keep the screen awake.
|
|
60
|
+
// This dialog only shows and reports the toggle; the app itself keeps the screen on while it's set
|
|
61
|
+
// (expo-keep-awake on its match screen), so this package needs no native dependency for it. Shown
|
|
62
|
+
// on the same devices as Lock Orientation (see isTouchPrimary below): a desktop/laptop browser
|
|
63
|
+
// stays awake on its own while someone is playing with its keyboard or mouse.
|
|
64
|
+
keepAwake?: boolean
|
|
65
|
+
onKeepAwakeChange?: (value: boolean) => void
|
|
56
66
|
// Explicit opt-outs for the rows that pull their own state straight from @rific/feedback-press,
|
|
57
67
|
// @rific/auto-paper, and @rific/updater's own shared contexts/hooks rather than from anything the
|
|
58
68
|
// caller passes in - unlike Lock Orientation/Edge Guard above, there's no "value" to omit for
|
|
@@ -80,18 +90,30 @@ export interface BaseSettingsDialogProps {
|
|
|
80
90
|
children?: ReactNode
|
|
81
91
|
}
|
|
82
92
|
|
|
83
|
-
// The settings shell every @tastic game shares - orientation lock, the optional edge-guard
|
|
84
|
-
// sound/haptics, appearance, and update checking - extracted after the same six rows were
|
|
93
|
+
// The settings shell every @tastic game shares - orientation lock, the optional edge-guard and
|
|
94
|
+
// keep-awake toggles, sound/haptics, appearance, and update checking - extracted after the same six rows were
|
|
85
95
|
// independently copied into LightCycles, AirHockey, BoxHockey, Pong, and Snake and then needed
|
|
86
96
|
// hand-editing in every one of them, twice, for what were meant to be identical fixes. `children`
|
|
87
97
|
// is the seam for whatever isn't shared: a game with its own board/difficulty settings (or a stats
|
|
88
98
|
// backup flow, or anything else genuinely specific to it) renders those below Appearance, not by
|
|
89
99
|
// forking this component.
|
|
90
|
-
export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }: BaseSettingsDialogProps) {
|
|
100
|
+
export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, keepAwake, onKeepAwakeChange, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }: BaseSettingsDialogProps) {
|
|
91
101
|
const { colors } = useAutoPaperTheme()
|
|
92
102
|
const ambientRotation = useRotation()
|
|
93
103
|
const rotation = rotationOverride ?? ambientRotation
|
|
94
|
-
//
|
|
104
|
+
// The sheet lays out in a RotatedFrame turned by `rotation` (below), so its height is the window's
|
|
105
|
+
// width when turned a quarter: the same compact rule as every other dialog's header.
|
|
106
|
+
const window = useWindowDimensions()
|
|
107
|
+
const turned = rotation === 90 || rotation === -90
|
|
108
|
+
const compactHeader = isCompactDialogHeight(turned ? window.width : window.height)
|
|
109
|
+
// The overlay cards' width (ConfirmDialog, the guide, every app's result card): 24pt from the
|
|
110
|
+
// frame's edges, never under a notch, capped at their 360 (overlayStyles.card). auto-paper's Dialog
|
|
111
|
+
// would give the sheet a 32pt margin and a 400 cap, 16pt narrower than a card on a phone, which
|
|
112
|
+
// showed when Settings and a confirm opened one after the other. The card is centered, so the
|
|
113
|
+
// Dialog's own 32pt padding is simply overhung.
|
|
114
|
+
const insets = rotateInsets(useSafeAreaInsets(), rotation)
|
|
115
|
+
const sheetWidth = Math.min(overlayStyles.card.maxWidth, (turned ? window.height : window.width) - 2 * Math.max(insets.left, insets.right, SHEET_GUTTER))
|
|
116
|
+
// Also gates the Lock Orientation and Keep Awake rows below (alongside their own props) - true
|
|
95
117
|
// unconditionally on native (see the hook's own doc), so this only actually excludes a
|
|
96
118
|
// desktop/laptop browser, where a mouse-driven window has no physical orientation to lock
|
|
97
119
|
// against; a resize there would just make the setting silently stop reflowing the layout instead
|
|
@@ -138,6 +160,7 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
138
160
|
|
|
139
161
|
const showLockOrientation = isTouchPrimary && lockOrientation !== undefined && onLockOrientationChange !== undefined
|
|
140
162
|
const showEdgeGuard = Platform.OS === 'ios' && deferBottomEdgeGestures !== undefined && onDeferBottomEdgeGestures !== undefined
|
|
163
|
+
const showKeepAwake = isTouchPrimary && keepAwake !== undefined && onKeepAwakeChange !== undefined
|
|
141
164
|
const showHaptics = Platform.OS !== 'web' && !hideHaptics
|
|
142
165
|
const showSound = !hideSound
|
|
143
166
|
const showAppearance = !hideAppearance
|
|
@@ -159,186 +182,222 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
159
182
|
return (
|
|
160
183
|
<Portal>
|
|
161
184
|
<RotatedFrame rotation={rotation} rotatedSafeArea portalHost passThrough>
|
|
162
|
-
<Dialog visible={visible} onDismiss={onDismiss} style={styles.dialog}>
|
|
163
|
-
<Dialog.Title>Settings</Dialog.Title>
|
|
185
|
+
<Dialog visible={visible} onDismiss={onDismiss} style={[styles.dialog, { width: sheetWidth }]}>
|
|
164
186
|
{/* react-native-paper's Dialog.ScrollArea has a fixed 24px marginBottom baked in (meant to
|
|
165
|
-
reserve room for a Dialog.Actions row below it) - overridden to 0
|
|
166
|
-
|
|
187
|
+
reserve room for a Dialog.Actions row below it) - overridden to 0: the Done row below
|
|
188
|
+
carries its own spacing. It is the sheet's first child, so it runs to the card's top edge. */}
|
|
167
189
|
<Dialog.ScrollArea style={styles.scrollArea}>
|
|
168
|
-
<
|
|
169
|
-
{/*
|
|
190
|
+
<FadingScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
|
191
|
+
{/* The fleet's one dialog header and Done button (DialogHeader, overlayActionStyles' single
|
|
192
|
+
button), so the sheet reads as one family with ConfirmDialog, the guide and every app's
|
|
193
|
+
result cards (Jay, 2026-10-15). The header scrolls away with the rows, so a long sheet
|
|
194
|
+
gives them the whole card once scrolled; Done stays pinned below. Tapping outside still
|
|
195
|
+
closes it too. */}
|
|
196
|
+
<DialogHeader icon='cog' title='Settings' compact={compactHeader} />
|
|
197
|
+
<View style={styles.content}>
|
|
198
|
+
{/* Grouped tightly together (styles.toggleGroup's small internal gap, not the 24px gap
|
|
170
199
|
between sections below) - same bare-row shape for all of them, no segmented control
|
|
171
200
|
and no section heading (each row's own label already says what it is). Haptics only
|
|
172
201
|
joins on native - there's nothing for it to control on web. */}
|
|
173
|
-
|
|
174
|
-
|
|
202
|
+
<View style={styles.toggleGroup}>
|
|
203
|
+
{/* Omitted entirely for a portrait-only game (no lockOrientation/onLockOrientationChange
|
|
175
204
|
passed at all - nothing to lock), and hidden (not just disabled) on a desktop/laptop
|
|
176
205
|
browser even when they are - see isTouchPrimary's own doc above. Still shown on
|
|
177
206
|
native and on a touch-capable mobile browser, where a rotated window really does
|
|
178
207
|
reflect a rotated device. */}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
208
|
+
{showLockOrientation && (
|
|
209
|
+
<TouchableRipple onPress={() => onLockOrientationChange!(!lockOrientation)} style={styles.toggleButton} accessibilityLabel={`Lock orientation ${lockOrientation ? 'on' : 'off'}`}>
|
|
210
|
+
<View style={styles.toggleContent}>
|
|
211
|
+
<SettingIcon source={lockOrientation ? 'lock' : 'lock-open-variant-outline'} color={lockOrientation ? colors.tertiary : colors.onSurfaceVariant} containerColor={lockOrientation ? colors.tertiaryContainer : colors.surfaceVariant} />
|
|
212
|
+
<View style={styles.flexShrink}>
|
|
213
|
+
<Text variant='bodyLarge' style={{ color: colors.onSurface }}>
|
|
214
|
+
Lock Orientation
|
|
215
|
+
</Text>
|
|
216
|
+
<Text variant='bodySmall' numberOfLines={1} style={{ color: colors.onSurfaceVariant }}>
|
|
217
|
+
Pin current layout
|
|
218
|
+
</Text>
|
|
219
|
+
</View>
|
|
190
220
|
</View>
|
|
191
|
-
</
|
|
192
|
-
|
|
193
|
-
)}
|
|
221
|
+
</TouchableRipple>
|
|
222
|
+
)}
|
|
194
223
|
|
|
195
|
-
|
|
224
|
+
{/* iOS only - mirrors into UserDefaults for @tastic/edge-guard's own config plugin
|
|
196
225
|
swizzle to read. Guards both edges - the top (Notification Center/Control Center)
|
|
197
226
|
and the bottom (home-indicator swipe, incidentally Reachability too), per that
|
|
198
227
|
package's own doc. Off by default: a deliberate opt-in for whoever's actually hit an
|
|
199
228
|
accidental-swipe-near-an-edge problem mid-game and come looking for a fix, not a
|
|
200
229
|
surprise every player gets by default. */}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
230
|
+
{showEdgeGuard && (
|
|
231
|
+
<TouchableRipple onPress={() => onDeferBottomEdgeGestures!(!deferBottomEdgeGestures)} style={styles.toggleButton} accessibilityLabel={`Edge guard ${deferBottomEdgeGestures ? 'on' : 'off'}`}>
|
|
232
|
+
<View style={styles.toggleContent}>
|
|
233
|
+
<SettingIcon source={deferBottomEdgeGestures ? 'shield-check-outline' : 'shield-off-outline'} color={deferBottomEdgeGestures ? colors.tertiary : colors.onSurfaceVariant} containerColor={deferBottomEdgeGestures ? colors.tertiaryContainer : colors.surfaceVariant} />
|
|
234
|
+
<View style={styles.flexShrink}>
|
|
235
|
+
<Text variant='bodyLarge' style={{ color: colors.onSurface }}>
|
|
236
|
+
Edge Guard
|
|
237
|
+
</Text>
|
|
238
|
+
<Text variant='bodySmall' numberOfLines={1} style={{ color: colors.onSurfaceVariant }}>
|
|
239
|
+
Require second swipe
|
|
240
|
+
</Text>
|
|
241
|
+
</View>
|
|
212
242
|
</View>
|
|
213
|
-
</
|
|
214
|
-
|
|
215
|
-
)}
|
|
243
|
+
</TouchableRipple>
|
|
244
|
+
)}
|
|
216
245
|
|
|
217
|
-
|
|
246
|
+
{/* See the keepAwake prop's own doc: the app does the keeping awake, this only toggles it. */}
|
|
247
|
+
{showKeepAwake && (
|
|
248
|
+
<TouchableRipple onPress={() => onKeepAwakeChange!(!keepAwake)} style={styles.toggleButton} accessibilityLabel={`Keep awake ${keepAwake ? 'on' : 'off'}`}>
|
|
249
|
+
<View style={styles.toggleContent}>
|
|
250
|
+
<SettingIcon source={keepAwake ? 'eye' : 'eye-closed'} color={keepAwake ? colors.tertiary : colors.onSurfaceVariant} containerColor={keepAwake ? colors.tertiaryContainer : colors.surfaceVariant} />
|
|
251
|
+
<View style={styles.flexShrink}>
|
|
252
|
+
<Text variant='bodyLarge' style={{ color: colors.onSurface }}>
|
|
253
|
+
Keep Awake
|
|
254
|
+
</Text>
|
|
255
|
+
<Text variant='bodySmall' numberOfLines={1} style={{ color: colors.onSurfaceVariant }}>
|
|
256
|
+
Stop auto-lock mid-match
|
|
257
|
+
</Text>
|
|
258
|
+
</View>
|
|
259
|
+
</View>
|
|
260
|
+
</TouchableRipple>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{/* Side by side, not stacked - neither needs a full row's width (single-line label,
|
|
218
264
|
no description under it the way Lock Orientation has), so sharing one row reads
|
|
219
265
|
just as clearly and takes half the vertical space. toggleRow carries the same
|
|
220
266
|
edge-alignment negative margin toggleButton normally carries itself; the buttons
|
|
221
267
|
inside it use toggleButtonInRow (flex: 1) instead so the two don't double up on it.
|
|
222
268
|
The row itself only renders when at least one of the two will - an empty flex row
|
|
223
269
|
still eats its own gap/margin otherwise. */}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
270
|
+
{(showSound || showHaptics) && (
|
|
271
|
+
<View style={styles.toggleRow}>
|
|
272
|
+
{showSound && (
|
|
273
|
+
<TouchableRipple
|
|
274
|
+
soundDisabled
|
|
275
|
+
onPress={() => {
|
|
276
|
+
const enabled = !soundSettings.enabled
|
|
277
|
+
setSoundSettings({ enabled })
|
|
278
|
+
if (enabled) sound.selection?.()
|
|
279
|
+
}}
|
|
280
|
+
style={styles.toggleButtonInRow}
|
|
281
|
+
accessibilityLabel={`Sound ${soundSettings.enabled ? 'on' : 'off'}`}
|
|
282
|
+
>
|
|
283
|
+
<View style={styles.toggleContent}>
|
|
284
|
+
<SettingIcon source={soundSettings.enabled ? 'volume-high' : 'volume-off'} color={soundSettings.enabled ? colors.tertiary : colors.onSurfaceVariant} containerColor={soundSettings.enabled ? colors.tertiaryContainer : colors.surfaceVariant} />
|
|
285
|
+
<Text variant='bodyLarge' style={{ color: colors.onSurface }}>
|
|
286
|
+
Sound
|
|
287
|
+
</Text>
|
|
288
|
+
</View>
|
|
289
|
+
</TouchableRipple>
|
|
290
|
+
)}
|
|
245
291
|
|
|
246
|
-
|
|
292
|
+
{/* Already native-only (see showHaptics's own definition) - hideHaptics can hide
|
|
247
293
|
it there too, for a game that's deliberately silent on vibration. */}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
294
|
+
{showHaptics && (
|
|
295
|
+
<TouchableRipple
|
|
296
|
+
hapticDisabled
|
|
297
|
+
onPress={() => {
|
|
298
|
+
const vibrate = !hapticSettings.vibrate
|
|
299
|
+
setHapticSettings({ vibrate })
|
|
300
|
+
if (vibrate) forceHapticFeedback()
|
|
301
|
+
}}
|
|
302
|
+
style={styles.toggleButtonInRow}
|
|
303
|
+
accessibilityLabel={`Haptics ${hapticSettings.vibrate ? 'on' : 'off'}`}
|
|
304
|
+
>
|
|
305
|
+
<View style={styles.toggleContent}>
|
|
306
|
+
<SettingIcon source={hapticSettings.vibrate ? 'vibrate' : 'vibrate-off'} color={hapticSettings.vibrate ? colors.tertiary : colors.onSurfaceVariant} containerColor={hapticSettings.vibrate ? colors.tertiaryContainer : colors.surfaceVariant} />
|
|
307
|
+
<Text variant='bodyLarge' style={{ color: colors.onSurface }}>
|
|
308
|
+
Haptics
|
|
309
|
+
</Text>
|
|
310
|
+
</View>
|
|
311
|
+
</TouchableRipple>
|
|
312
|
+
)}
|
|
313
|
+
</View>
|
|
314
|
+
)}
|
|
269
315
|
|
|
270
|
-
|
|
316
|
+
{/* Not a toggle, but it reads like one of the rows above and belongs with them rather than
|
|
271
317
|
down in `children` (which sits below Appearance) - this is the "replay the intro"
|
|
272
318
|
entry point every game's own how-to-play flow shares. Dismisses this dialog first so
|
|
273
319
|
the guide's own overlay never opens on top of a still-visible Settings card. */}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
320
|
+
{onShowHowToPlay && (
|
|
321
|
+
<TouchableRipple
|
|
322
|
+
onPress={() => {
|
|
323
|
+
onDismiss()
|
|
324
|
+
onShowHowToPlay()
|
|
325
|
+
}}
|
|
326
|
+
style={styles.toggleButton}
|
|
327
|
+
accessibilityLabel='How to Play'
|
|
328
|
+
>
|
|
329
|
+
<View style={styles.toggleContent}>
|
|
330
|
+
<SettingIcon source='help-circle-outline' color={colors.secondary} containerColor={colors.secondaryContainer} />
|
|
331
|
+
<View style={styles.flexShrink}>
|
|
332
|
+
<Text variant='bodyLarge' style={{ color: colors.onSurface }}>
|
|
333
|
+
How to Play
|
|
334
|
+
</Text>
|
|
335
|
+
<Text variant='bodySmall' numberOfLines={1} style={{ color: colors.onSurfaceVariant }}>
|
|
336
|
+
Replay quick intro
|
|
337
|
+
</Text>
|
|
338
|
+
</View>
|
|
292
339
|
</View>
|
|
293
|
-
</
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
</View>
|
|
340
|
+
</TouchableRipple>
|
|
341
|
+
)}
|
|
342
|
+
</View>
|
|
297
343
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
344
|
+
{showAppearance && (
|
|
345
|
+
<View style={styles.section}>
|
|
346
|
+
<Text variant='labelMedium' style={[styles.sectionLabel, { color: colors.onSurfaceVariant }]}>
|
|
347
|
+
APPEARANCE
|
|
348
|
+
</Text>
|
|
349
|
+
<AutoAppearancePicker showLabels={false} />
|
|
350
|
+
{/* Blur (Jay, 2026-10-15): one fleet-wide switch for auto-paper's own `blur` setting,
|
|
305
351
|
which every blur-capable surface follows on its own (this sheet, and the
|
|
306
352
|
OverlayCard behind ConfirmDialog, UpdateDialog and the guide). Off, or wherever
|
|
307
353
|
blur can't happen, those surfaces paint the theme's raised surface instead. So the
|
|
308
354
|
row only shows where turning it on would change something: not on Android
|
|
309
355
|
(auto-paper's BlurView needs a blur target there, which these cards don't have) and
|
|
310
356
|
not in an app that never handed expo-blur to auto-paper's Provider. */}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
357
|
+
{showBlur && (
|
|
358
|
+
<TouchableRipple onPress={() => setThemeSettings({ blur: !themeSettings.blur })} style={styles.toggleButton} accessibilityLabel={`Blur ${themeSettings.blur ? 'on' : 'off'}`}>
|
|
359
|
+
<View style={styles.toggleContent}>
|
|
360
|
+
<SettingIcon source={themeSettings.blur ? 'blur' : 'blur-off'} color={themeSettings.blur ? colors.tertiary : colors.onSurfaceVariant} containerColor={themeSettings.blur ? colors.tertiaryContainer : colors.surfaceVariant} />
|
|
361
|
+
{/* The one-word label alone didn't say what blurs (Jay, 2026-10-15). One line
|
|
362
|
+
even on a 360pt phone (216pt of room). */}
|
|
363
|
+
<View style={styles.flexShrink}>
|
|
364
|
+
<Text variant='bodyLarge' style={{ color: colors.onSurface }}>
|
|
365
|
+
Blur
|
|
366
|
+
</Text>
|
|
367
|
+
<Text variant='bodySmall' numberOfLines={1} style={{ color: colors.onSurfaceVariant }}>
|
|
368
|
+
Frost behind dialogs
|
|
369
|
+
</Text>
|
|
370
|
+
</View>
|
|
371
|
+
</View>
|
|
372
|
+
</TouchableRipple>
|
|
373
|
+
)}
|
|
374
|
+
</View>
|
|
375
|
+
)}
|
|
323
376
|
|
|
324
|
-
|
|
377
|
+
{children}
|
|
325
378
|
|
|
326
|
-
|
|
379
|
+
{/* Native only (a web build has no OTA update concept at all, desktop or mobile browser
|
|
327
380
|
alike) - hideUpdateCheck can hide it there too, for a native app that isn't wired up
|
|
328
381
|
for EAS/OTA updates at all. */}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
382
|
+
{showUpdateCheck && (
|
|
383
|
+
<View style={styles.section}>
|
|
384
|
+
<Text variant='labelMedium' style={[styles.sectionLabel, { color: colors.onSurfaceVariant }]}>
|
|
385
|
+
VERSION {version}
|
|
386
|
+
{updateReady ? ' · UPDATE READY' : ''}
|
|
387
|
+
</Text>
|
|
388
|
+
<Button mode='outlined' onPress={check} loading={checking} disabled={checking}>
|
|
389
|
+
Check for Updates
|
|
390
|
+
</Button>
|
|
391
|
+
</View>
|
|
392
|
+
)}
|
|
393
|
+
</View>
|
|
394
|
+
</FadingScrollView>
|
|
341
395
|
</Dialog.ScrollArea>
|
|
396
|
+
<View style={[overlayActionStyles.actions, styles.footer]}>
|
|
397
|
+
<Button mode='contained' onPress={onDismiss} style={overlayActionStyles.actionButtonSingle} buttonColor={colors.primary} textColor={colors.onPrimary}>
|
|
398
|
+
Done
|
|
399
|
+
</Button>
|
|
400
|
+
</View>
|
|
342
401
|
</Dialog>
|
|
343
402
|
<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)} />
|
|
344
403
|
<ConfirmDialog visible={!!infoMessage} title={infoMessage?.title ?? ''} message={infoMessage?.message ?? ''} confirmLabel='OK' icon='information-outline' rotation={0} onConfirm={() => setInfoMessage(null)} />
|
|
@@ -347,10 +406,13 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
347
406
|
)
|
|
348
407
|
}
|
|
349
408
|
|
|
409
|
+
// The backdrop gutter every overlay card keeps from the screen's edges.
|
|
410
|
+
const SHEET_GUTTER = 24
|
|
411
|
+
|
|
350
412
|
const styles = StyleSheet.create({
|
|
413
|
+
// The rows' sections, 24pt apart.
|
|
351
414
|
content: {
|
|
352
|
-
gap: 24
|
|
353
|
-
paddingVertical: 20
|
|
415
|
+
gap: 24
|
|
354
416
|
},
|
|
355
417
|
// Caps the card so it never grows past the screen - without this the dialog just keeps
|
|
356
418
|
// growing to fit its content and the overflow gets clipped by the screen edge, which is
|
|
@@ -362,6 +424,11 @@ const styles = StyleSheet.create({
|
|
|
362
424
|
flexShrink: {
|
|
363
425
|
flexShrink: 1
|
|
364
426
|
},
|
|
427
|
+
// The overlay cards' 24pt padding, as the sheet's own insets (its scroll area pads 24 sideways).
|
|
428
|
+
footer: {
|
|
429
|
+
paddingBottom: 24,
|
|
430
|
+
paddingHorizontal: 24
|
|
431
|
+
},
|
|
365
432
|
iconBadge: {
|
|
366
433
|
alignItems: 'center',
|
|
367
434
|
borderRadius: 10,
|
|
@@ -369,9 +436,20 @@ const styles = StyleSheet.create({
|
|
|
369
436
|
justifyContent: 'center',
|
|
370
437
|
width: 36
|
|
371
438
|
},
|
|
439
|
+
// No hairline dividers above and below the scroll area (Paper's MD2-era borders): none of the
|
|
440
|
+
// fleet's other dialogs draws one, so the sheet matches them (Jay, 2026-10-15).
|
|
372
441
|
scrollArea: {
|
|
442
|
+
borderBottomWidth: 0,
|
|
443
|
+
borderTopWidth: 0,
|
|
373
444
|
marginBottom: 0
|
|
374
445
|
},
|
|
446
|
+
// The header and the rows scroll together: the card's 24pt inset above the header, the overlay
|
|
447
|
+
// cards' 16pt gap between it and the rows, and 16pt below the last row before the Done row.
|
|
448
|
+
scrollContent: {
|
|
449
|
+
gap: 16,
|
|
450
|
+
paddingBottom: 16,
|
|
451
|
+
paddingTop: 24
|
|
452
|
+
},
|
|
375
453
|
section: {
|
|
376
454
|
gap: 12
|
|
377
455
|
},
|