@tastic/hud 0.14.2 → 0.15.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 +110 -79
- package/dist/chrome/index.d.mts +239 -0
- package/dist/chrome/index.d.ts +239 -0
- package/dist/chrome/index.js +772 -0
- package/dist/chrome/index.mjs +44 -0
- package/dist/chunk-OTMMST5M.mjs +645 -0
- package/dist/guide/index.js +3 -3
- package/dist/guide/index.mjs +3 -3
- package/dist/index.d.mts +21 -252
- package/dist/index.d.ts +21 -252
- package/dist/index.js +20 -20
- package/dist/index.mjs +186 -791
- package/package.json +8 -1
- package/src/AchievementCatalogSection.tsx +1 -1
- package/src/AchievementRow.tsx +9 -9
- package/src/BaseSettingsDialog.tsx +31 -31
- package/src/BaseStatsScreen.tsx +13 -13
- package/src/ConfirmDialog.tsx +2 -2
- package/src/ControlSchemePicker.tsx +9 -9
- package/src/CornerActionButtons.tsx +6 -6
- package/src/CornerStatusBadges.tsx +8 -8
- package/src/InlineColorPicker.tsx +30 -30
- package/src/LabeledDropdown.tsx +8 -8
- package/src/PlayerSetupPanel.tsx +25 -25
- package/src/PopoverBody.tsx +16 -16
- package/src/PressAwayOverlay.tsx +2 -2
- package/src/ReadyButton.tsx +3 -3
- package/src/SectionedDropdown.tsx +31 -31
- package/src/SharedActionBand.tsx +6 -6
- package/src/SkiaGate.tsx +5 -5
- package/src/SkiaGate.web.tsx +2 -2
- package/src/StaggeredWord.tsx +9 -9
- package/src/StatRow.tsx +3 -3
- package/src/StatSection.tsx +3 -3
- package/src/TriggerGauge.tsx +54 -54
- package/src/TriggerGaugeHost.tsx +2 -2
- package/src/chrome/index.ts +22 -0
- package/src/fonts.ts +1 -1
- package/src/guide/DirectionKeysHint.tsx +3 -3
- package/src/guide/GuideProvider.tsx +7 -7
- package/src/guide/HowToPlayDialog.tsx +7 -7
- package/src/guide/SeatDiagram.tsx +3 -3
- package/src/guide/SwipeHint.tsx +4 -4
- package/src/guide/guideSlice.ts +7 -7
- package/src/guide/index.ts +4 -3
- package/src/guide/types.ts +1 -1
- package/src/index.ts +1 -1
- package/src/loadSkiaWeb.ts +2 -2
- package/src/loadSkiaWeb.web.ts +2 -2
- package/src/overlayCard.ts +3 -3
- package/src/useAutoAlign.ts +23 -23
- package/src/usePopoverHost.ts +1 -1
- package/src/useZoneClampedAlign.ts +23 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastic/hud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|
|
@@ -34,6 +34,13 @@
|
|
|
34
34
|
"import": "./dist/index.mjs",
|
|
35
35
|
"require": "./dist/index.js"
|
|
36
36
|
},
|
|
37
|
+
"./chrome": {
|
|
38
|
+
"react-native": "./src/chrome/index.ts",
|
|
39
|
+
"browser": "./src/chrome/index.ts",
|
|
40
|
+
"types": "./dist/chrome/index.d.ts",
|
|
41
|
+
"import": "./dist/chrome/index.mjs",
|
|
42
|
+
"require": "./dist/chrome/index.js"
|
|
43
|
+
},
|
|
37
44
|
"./guide": {
|
|
38
45
|
"react-native": "./src/guide/index.ts",
|
|
39
46
|
"browser": "./src/guide/index.ts",
|
|
@@ -8,7 +8,7 @@ import { AchievementRow, LOCKED_BADGE_COLOR } from './AchievementRow'
|
|
|
8
8
|
import { MONO_FONT } from './fonts'
|
|
9
9
|
|
|
10
10
|
// AchievementCatalogRow now comes straight from @tastic/achievements' own getAchievementCatalogRows
|
|
11
|
-
// (added in the same 2026-09-18 achievements-scaffolding pass this component belongs to)
|
|
11
|
+
// (added in the same 2026-09-18 achievements-scaffolding pass this component belongs to) - no
|
|
12
12
|
// longer duplicated locally now that package has actually published the export (confirmed the two
|
|
13
13
|
// shapes match exactly before wiring this real, type-only peer dependency).
|
|
14
14
|
|
package/src/AchievementRow.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { useAutoPaperTheme } from '@rific/auto-paper'
|
|
|
2
2
|
import { StyleSheet, View } from 'react-native'
|
|
3
3
|
import { Icon, ProgressBar, Text } from 'react-native-paper'
|
|
4
4
|
|
|
5
|
-
// A neutral gray for a locked badge's own background
|
|
5
|
+
// A neutral gray for a locked badge's own background - exported so a caller computing its own
|
|
6
6
|
// badge color (unlocked ? tierColor : LOCKED_BADGE_COLOR, matching LightCycles' own achievements.tsx)
|
|
7
7
|
// doesn't need to invent or duplicate this exact tone.
|
|
8
8
|
export const LOCKED_BADGE_COLOR = 'rgba(128,128,128,0.3)'
|
|
@@ -11,28 +11,28 @@ interface Props {
|
|
|
11
11
|
icon: string
|
|
12
12
|
title: string
|
|
13
13
|
description: string
|
|
14
|
-
// The badge's own background
|
|
14
|
+
// The badge's own background - pass the achievement's tier color when unlocked, or
|
|
15
15
|
// LOCKED_BADGE_COLOR (this package's own export, above) when it isn't; this component doesn't
|
|
16
16
|
// infer that from unlockedLabel itself, since a locked-vs-unlocked badge color scheme is a
|
|
17
17
|
// caller-level design choice (see checkColor below for the identical reasoning on the status
|
|
18
18
|
// icon's own color).
|
|
19
19
|
badgeColor: string
|
|
20
20
|
// undefined = locked. A precomputed display string (e.g. "Unlocked 2 days ago"), not a raw
|
|
21
|
-
// timestamp
|
|
21
|
+
// timestamp - this component has no opinion on date formatting or streak conventions, which vary
|
|
22
22
|
// per app (see LightCycles' own calendar-day-difference convention in its achievements.tsx).
|
|
23
23
|
unlockedLabel?: string
|
|
24
|
-
// Color for the unlocked check-circle icon/badge glyph
|
|
24
|
+
// Color for the unlocked check-circle icon/badge glyph - typically the same tier color passed to
|
|
25
25
|
// badgeColor, kept as its own prop rather than reused internally so a caller can special-case it
|
|
26
26
|
// independently if its own tier scheme ever needs to (matching LightCycles' own call site, which
|
|
27
27
|
// does pass the identical value to both today).
|
|
28
28
|
checkColor?: string
|
|
29
|
-
// Only rendered while locked (unlockedLabel is undefined)
|
|
29
|
+
// Only rendered while locked (unlockedLabel is undefined) - 0..1.
|
|
30
30
|
progress?: number
|
|
31
31
|
// The "Same for every profile" note LightCycles shows for a device-scoped achievement viewed
|
|
32
|
-
// from a specific profile's own tab
|
|
32
|
+
// from a specific profile's own tab - optional since only an app with per-profile stat views has
|
|
33
33
|
// this concept at all.
|
|
34
34
|
deviceMarker?: boolean
|
|
35
|
-
// Independently optional overrides for this row's own text/background colors
|
|
35
|
+
// Independently optional overrides for this row's own text/background colors - each defaults to
|
|
36
36
|
// the same dark-mode-derived formula as StatRow/StatSection/BaseStatsScreen (see that component's
|
|
37
37
|
// own doc for why) when omitted, so every existing caller renders identically to before. A caller
|
|
38
38
|
// with its own app-wide chrome palette passes whichever of the three it needs.
|
|
@@ -41,14 +41,14 @@ interface Props {
|
|
|
41
41
|
sectionBg?: string
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// One row per achievement
|
|
44
|
+
// One row per achievement - a tier-colored (or locked-gray) badge, title/description, an optional
|
|
45
45
|
// progress bar while locked, and either an unlocked check-mark+label or a lock icon. Extracted
|
|
46
46
|
// verbatim from LightCycles' own achievements.tsx, where this exact row shape backed its entire
|
|
47
47
|
// "ALL ACHIEVEMENTS" list. fg/fgMuted/its own section background default to the same
|
|
48
48
|
// useAutoPaperTheme()-derived formula as StatRow/StatSection/BaseStatsScreen (see BaseStatsScreen's
|
|
49
49
|
// own doc for why), each independently overridable via the matching prop for a caller with its own
|
|
50
50
|
// chrome palette. Has no opinion on what an achievement actually IS (id, unlock predicate, catalog)
|
|
51
|
-
//
|
|
51
|
+
// - that stays entirely app-local; this is purely the presentational row.
|
|
52
52
|
export function AchievementRow({ icon, title, description, badgeColor, unlockedLabel, checkColor, progress, deviceMarker, fg: fgOverride, fgMuted: fgMutedOverride, sectionBg: sectionBgOverride }: Props) {
|
|
53
53
|
const { dark } = useAutoPaperTheme()
|
|
54
54
|
const fg = fgOverride ?? (dark ? '#FFFFFF' : '#000000')
|
|
@@ -16,7 +16,7 @@ interface SettingIconProps {
|
|
|
16
16
|
containerColor: string
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
// Small colored badge per row (icon tinted on its own MD3 container color)
|
|
19
|
+
// Small colored badge per row (icon tinted on its own MD3 container color) - echoes the
|
|
20
20
|
// primary/secondary/tertiary triad that's already each consuming app's own identity, so the
|
|
21
21
|
// settings list picks up that same palette instead of introducing new colors of its own. Every
|
|
22
22
|
// on/off row (Lock Orientation, Edge Guard, Sound, Haptics) is tertiary while on, and an app adding
|
|
@@ -38,50 +38,50 @@ export interface BaseSettingsDialogProps {
|
|
|
38
38
|
// Defaults to a live ambient read via useRotation() when omitted. Pass 0 when the dialog already
|
|
39
39
|
// renders under a Portal.Host inside a turned frame of the caller's own (Solitaire's screens).
|
|
40
40
|
rotation?: ViewRotation
|
|
41
|
-
// Each app's own OTA version (e.g. release.otaVersion)
|
|
41
|
+
// Each app's own OTA version (e.g. release.otaVersion) - not something this package can read
|
|
42
42
|
// itself, since every consuming app tracks its own release.ts independently. Accepts a number too
|
|
43
43
|
// since that's what release.otaVersion actually is in most apps (a bare incrementing counter, not
|
|
44
|
-
// a formatted string)
|
|
44
|
+
// a formatted string) - this just interpolates it into the VERSION label same as the original
|
|
45
45
|
// inline JSX did.
|
|
46
46
|
version: string | number
|
|
47
|
-
// Optional, and always passed as a pair
|
|
47
|
+
// Optional, and always passed as a pair - omit both entirely for a game that's portrait-only (or
|
|
48
48
|
// otherwise has no orientation-based reflow at all), where there's nothing to lock against.
|
|
49
49
|
lockOrientation?: boolean
|
|
50
50
|
onLockOrientationChange?: (value: boolean) => void
|
|
51
|
-
// Optional, and always passed as a pair
|
|
51
|
+
// Optional, and always passed as a pair - omit both entirely for a game with no full-screen edge-
|
|
52
52
|
// anchored swipe controls (nothing to guard). iOS-only regardless of whether the pair is passed:
|
|
53
53
|
// @tastic/edge-guard's own swizzle is a no-op on every other platform.
|
|
54
54
|
deferBottomEdgeGestures?: boolean
|
|
55
55
|
onDeferBottomEdgeGestures?: (value: boolean) => void
|
|
56
56
|
// Explicit opt-outs for the rows that pull their own state straight from @rific/feedback-press,
|
|
57
57
|
// @rific/auto-paper, and @rific/updater's own shared contexts/hooks rather than from anything the
|
|
58
|
-
// caller passes in
|
|
58
|
+
// caller passes in - unlike Lock Orientation/Edge Guard above, there's no "value" to omit for
|
|
59
59
|
// these, so hiding one takes its own explicit flag instead. All default to shown, matching every
|
|
60
60
|
// app's existing behavior before this prop existed.
|
|
61
61
|
hideSound?: boolean
|
|
62
|
-
// Haptics is already native-only (see the row's own Platform.OS check)
|
|
62
|
+
// Haptics is already native-only (see the row's own Platform.OS check) - this hides it even
|
|
63
63
|
// there, for a game that's deliberately silent on vibration.
|
|
64
64
|
hideHaptics?: boolean
|
|
65
65
|
hideAppearance?: boolean
|
|
66
|
-
// Check for Updates is already native-only (a web build has no OTA update concept at all)
|
|
66
|
+
// Check for Updates is already native-only (a web build has no OTA update concept at all) - this
|
|
67
67
|
// hides it even there, for a native app that isn't wired up for EAS/OTA updates at all.
|
|
68
68
|
hideUpdateCheck?: boolean
|
|
69
|
-
// Surfaces a failed update check (e.g. via @rific/toaster's error())
|
|
69
|
+
// Surfaces a failed update check (e.g. via @rific/toaster's error()) - omit for an app with no
|
|
70
70
|
// toast system wired up; the check itself still runs and still reports success/no-update through
|
|
71
71
|
// the ordinary info overlay either way. Meaningless (never called) when hideUpdateCheck is set.
|
|
72
72
|
onUpdateError?: (message: string) => void
|
|
73
73
|
// Omit to hide the row entirely (a game with no how-to-play flow, or a screen where replaying one
|
|
74
|
-
// would be disruptive
|
|
74
|
+
// would be disruptive - e.g. over a live match). Called AFTER this dialog's own onDismiss, so the
|
|
75
75
|
// two overlays never stack: wire it straight to @tastic/hud/guide's `useGuide().open`.
|
|
76
76
|
onShowHowToPlay?: () => void
|
|
77
77
|
// App-specific extra settings sections (board options, CPU difficulty, stats backup, ...),
|
|
78
|
-
// rendered between Appearance and Check for Updates
|
|
78
|
+
// rendered between Appearance and Check for Updates - this component only owns the settings
|
|
79
79
|
// every game shares, never anything about how a particular game plays.
|
|
80
80
|
children?: ReactNode
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
// The settings shell every @tastic game shares
|
|
84
|
-
// sound/haptics, appearance, and update checking
|
|
83
|
+
// The settings shell every @tastic game shares - orientation lock, the optional edge-guard toggle,
|
|
84
|
+
// sound/haptics, appearance, and update checking - extracted after the same six rows were
|
|
85
85
|
// independently copied into LightCycles, AirHockey, BoxHockey, Pong, and Snake and then needed
|
|
86
86
|
// hand-editing in every one of them, twice, for what were meant to be identical fixes. `children`
|
|
87
87
|
// is the seam for whatever isn't shared: a game with its own board/difficulty settings (or a stats
|
|
@@ -91,7 +91,7 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
91
91
|
const { colors } = useAutoPaperTheme()
|
|
92
92
|
const ambientRotation = useRotation()
|
|
93
93
|
const rotation = rotationOverride ?? ambientRotation
|
|
94
|
-
// Also gates the Lock Orientation row below (alongside showLockOrientation itself)
|
|
94
|
+
// Also gates the Lock Orientation row below (alongside showLockOrientation itself) - true
|
|
95
95
|
// unconditionally on native (see the hook's own doc), so this only actually excludes a
|
|
96
96
|
// desktop/laptop browser, where a mouse-driven window has no physical orientation to lock
|
|
97
97
|
// against; a resize there would just make the setting silently stop reflowing the layout instead
|
|
@@ -100,21 +100,21 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
100
100
|
const { settings: hapticSettings, set: setHapticSettings } = useHapticSettings()
|
|
101
101
|
const { settings: soundSettings, set: setSoundSettings } = useSoundSettings()
|
|
102
102
|
// Sound/Haptics toggle themselves: the ripple's automatic press feedback fires on onPressIn,
|
|
103
|
-
// before onPress applies the toggle, so it reflects the OLD enabled value
|
|
103
|
+
// before onPress applies the toggle, so it reflects the OLD enabled value - backwards from what
|
|
104
104
|
// a settings toggle should confirm (turning off would click/buzz, turning on would go silent).
|
|
105
105
|
// Both rows disable their own automatic channel below (soundDisabled/hapticDisabled) and fire it
|
|
106
106
|
// manually here instead, gated on the NEW value so it only plays when switching that channel on.
|
|
107
107
|
const sound = useContext(SoundContext)
|
|
108
108
|
const { forceShort: forceHapticFeedback } = useVibration()
|
|
109
109
|
// Covers check()'s three purely-informational cases (dev-mode disabled, web unsupported, no
|
|
110
|
-
// update found) via onInfo below
|
|
110
|
+
// update found) via onInfo below - a retro card overlay instead of the native Alert.alert those
|
|
111
111
|
// cases fall back to by default.
|
|
112
112
|
const [infoMessage, setInfoMessage] = useState<{ title: string; message: string } | null>(null)
|
|
113
|
-
// autoCheck: false
|
|
113
|
+
// autoCheck: false - every consuming app's own root layout already runs the background check via
|
|
114
114
|
// its own useUpdater() instance; a second instance with autoCheck's default (true) would set up a
|
|
115
115
|
// second AppState listener and double every foreground-resume update check. This instance only
|
|
116
116
|
// ever checks on an explicit tap of the button below. Deliberately its own useUpdater() instance
|
|
117
|
-
// rather than rendering UpdateDialog directly here, for the same reason
|
|
117
|
+
// rather than rendering UpdateDialog directly here, for the same reason - UpdateDialog owns a
|
|
118
118
|
// second AppState listener of its own.
|
|
119
119
|
const { manifest: updateManifest, onConfirm: onUpdateConfirm, respond: respondToUpdateConfirm } = useUpdateConfirmBridge()
|
|
120
120
|
const { check, checking, updateReady } = useUpdater({
|
|
@@ -158,18 +158,18 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
158
158
|
<Dialog visible={visible} onDismiss={onDismiss} style={styles.dialog}>
|
|
159
159
|
<Dialog.Title>Settings</Dialog.Title>
|
|
160
160
|
{/* 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)
|
|
161
|
+
reserve room for a Dialog.Actions row below it) - overridden to 0 since this dialog has
|
|
162
162
|
no actions row, and that gap otherwise reads as unexplained empty footer space. */}
|
|
163
163
|
<Dialog.ScrollArea style={styles.scrollArea}>
|
|
164
164
|
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
|
|
165
165
|
{/* Grouped tightly together (styles.toggleGroup's small internal gap, not the 24px gap
|
|
166
|
-
between sections below)
|
|
166
|
+
between sections below) - same bare-row shape for all of them, no segmented control
|
|
167
167
|
and no section heading (each row's own label already says what it is). Haptics only
|
|
168
|
-
joins on native
|
|
168
|
+
joins on native - there's nothing for it to control on web. */}
|
|
169
169
|
<View style={styles.toggleGroup}>
|
|
170
170
|
{/* Omitted entirely for a portrait-only game (no lockOrientation/onLockOrientationChange
|
|
171
|
-
passed at all
|
|
172
|
-
browser even when they are
|
|
171
|
+
passed at all - nothing to lock), and hidden (not just disabled) on a desktop/laptop
|
|
172
|
+
browser even when they are - see isTouchPrimary's own doc above. Still shown on
|
|
173
173
|
native and on a touch-capable mobile browser, where a rotated window really does
|
|
174
174
|
reflect a rotated device. */}
|
|
175
175
|
{showLockOrientation && (
|
|
@@ -188,8 +188,8 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
188
188
|
</TouchableRipple>
|
|
189
189
|
)}
|
|
190
190
|
|
|
191
|
-
{/* iOS only
|
|
192
|
-
swizzle to read. Guards both edges
|
|
191
|
+
{/* iOS only - mirrors into UserDefaults for @tastic/edge-guard's own config plugin
|
|
192
|
+
swizzle to read. Guards both edges - the top (Notification Center/Control Center)
|
|
193
193
|
and the bottom (home-indicator swipe, incidentally Reachability too), per that
|
|
194
194
|
package's own doc. Off by default: a deliberate opt-in for whoever's actually hit an
|
|
195
195
|
accidental-swipe-near-an-edge problem mid-game and come looking for a fix, not a
|
|
@@ -210,12 +210,12 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
210
210
|
</TouchableRipple>
|
|
211
211
|
)}
|
|
212
212
|
|
|
213
|
-
{/* Side by side, not stacked
|
|
213
|
+
{/* Side by side, not stacked - neither needs a full row's width (single-line label,
|
|
214
214
|
no description under it the way Lock Orientation has), so sharing one row reads
|
|
215
215
|
just as clearly and takes half the vertical space. toggleRow carries the same
|
|
216
216
|
edge-alignment negative margin toggleButton normally carries itself; the buttons
|
|
217
217
|
inside it use toggleButtonInRow (flex: 1) instead so the two don't double up on it.
|
|
218
|
-
The row itself only renders when at least one of the two will
|
|
218
|
+
The row itself only renders when at least one of the two will - an empty flex row
|
|
219
219
|
still eats its own gap/margin otherwise. */}
|
|
220
220
|
{(showSound || showHaptics) && (
|
|
221
221
|
<View style={styles.toggleRow}>
|
|
@@ -239,7 +239,7 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
239
239
|
</TouchableRipple>
|
|
240
240
|
)}
|
|
241
241
|
|
|
242
|
-
{/* Already native-only (see showHaptics's own definition)
|
|
242
|
+
{/* Already native-only (see showHaptics's own definition) - hideHaptics can hide
|
|
243
243
|
it there too, for a game that's deliberately silent on vibration. */}
|
|
244
244
|
{showHaptics && (
|
|
245
245
|
<TouchableRipple
|
|
@@ -264,7 +264,7 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
264
264
|
)}
|
|
265
265
|
|
|
266
266
|
{/* Not a toggle, but it reads like one of the rows above and belongs with them rather than
|
|
267
|
-
down in `children` (which sits below Appearance)
|
|
267
|
+
down in `children` (which sits below Appearance) - this is the "replay the intro"
|
|
268
268
|
entry point every game's own how-to-play flow shares. Dismisses this dialog first so
|
|
269
269
|
the guide's own overlay never opens on top of a still-visible Settings card. */}
|
|
270
270
|
{onShowHowToPlay && (
|
|
@@ -303,7 +303,7 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
303
303
|
{children}
|
|
304
304
|
|
|
305
305
|
{/* Native only (a web build has no OTA update concept at all, desktop or mobile browser
|
|
306
|
-
alike)
|
|
306
|
+
alike) - hideUpdateCheck can hide it there too, for a native app that isn't wired up
|
|
307
307
|
for EAS/OTA updates at all. */}
|
|
308
308
|
{showUpdateCheck && (
|
|
309
309
|
<View style={styles.section}>
|
|
@@ -331,7 +331,7 @@ const styles = StyleSheet.create({
|
|
|
331
331
|
gap: 24,
|
|
332
332
|
paddingVertical: 20
|
|
333
333
|
},
|
|
334
|
-
// Caps the card so it never grows past the screen
|
|
334
|
+
// Caps the card so it never grows past the screen - without this the dialog just keeps
|
|
335
335
|
// growing to fit its content and the overflow gets clipped by the screen edge, which is
|
|
336
336
|
// what happened in landscape where there's less height to work with. Dialog.ScrollArea +
|
|
337
337
|
// ScrollView below then take over and let the content scroll within that bound.
|
package/src/BaseStatsScreen.tsx
CHANGED
|
@@ -7,22 +7,22 @@ import { Text } from 'react-native-paper'
|
|
|
7
7
|
|
|
8
8
|
import { ConfirmDialog } from './ConfirmDialog'
|
|
9
9
|
|
|
10
|
-
// react-native-paper's own MD3 Text variant names
|
|
10
|
+
// react-native-paper's own MD3 Text variant names - mirrored locally rather than importing its
|
|
11
11
|
// internal VariantProp/MD3TypescaleKey (not exported from the package's public entry point) just
|
|
12
12
|
// to type one optional prop.
|
|
13
13
|
type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall'
|
|
14
14
|
|
|
15
15
|
interface Props {
|
|
16
|
-
// Explicit override for the live physical-hold rotation (see @tastic/core's useRotation)
|
|
16
|
+
// Explicit override for the live physical-hold rotation (see @tastic/core's useRotation) - only
|
|
17
17
|
// affects this component's own Portal-rendered reset-confirm dialog (via ConfirmDialog's own
|
|
18
18
|
// rotation prop), which (like any Portal content) is unaffected by whichever screen's own
|
|
19
19
|
// FakeLandscapeView wraps the trigger and so has to rotate itself. Defaults to a live ambient
|
|
20
|
-
// read via useRotation() when omitted
|
|
20
|
+
// read via useRotation() when omitted - note this does NOT make the header/scrollable content
|
|
21
21
|
// below rotate; the caller's own screen still needs its own FakeLandscapeView wrap for that
|
|
22
22
|
// (this component has no transform on its own main body).
|
|
23
23
|
rotation?: ViewRotation
|
|
24
24
|
title?: string
|
|
25
|
-
// 'headlineSmall' (this screen's own default, below)
|
|
25
|
+
// 'headlineSmall' (this screen's own default, below) - was originally 'displaySmall' (the full
|
|
26
26
|
// display tier's own smallest step), then 'headlineLarge', both still read as oversized for a
|
|
27
27
|
// header that's paired with a small 24px back-button glyph rather than standing alone the way a
|
|
28
28
|
// title screen's own wordmark does; headlineSmall sits close to that icon's own size instead of
|
|
@@ -31,7 +31,7 @@ interface Props {
|
|
|
31
31
|
titleVariant?: MD3TextVariant
|
|
32
32
|
onBack: () => void
|
|
33
33
|
insets: { top: number; left: number; bottom: number; right: number }
|
|
34
|
-
// Optional, and always passed as a pair
|
|
34
|
+
// Optional, and always passed as a pair - a caller with nothing resettable (or that deliberately
|
|
35
35
|
// doesn't want a reset action on this screen) omits both rather than one alone. Confirmation copy
|
|
36
36
|
// is caller-owned since "erases all stats and achievements" is only accurate for a caller that
|
|
37
37
|
// actually tracks both; a caller with only high scores, say, would want its own wording.
|
|
@@ -39,7 +39,7 @@ interface Props {
|
|
|
39
39
|
resetLabel?: string
|
|
40
40
|
resetConfirmTitle?: string
|
|
41
41
|
resetConfirmBody?: string
|
|
42
|
-
// Independently optional overrides for this screen's own header/background colors
|
|
42
|
+
// Independently optional overrides for this screen's own header/background colors - each
|
|
43
43
|
// defaults to the literal-black/white-by-appearance formula described below when omitted, so
|
|
44
44
|
// every existing caller (none of which pass these) renders identically to before. A caller with
|
|
45
45
|
// its own app-wide chrome palette (e.g. a tinted background rather than neutral black/white)
|
|
@@ -49,10 +49,10 @@ interface Props {
|
|
|
49
49
|
// reset-confirm dialog's own icon/title/card. Both are gone: the reset-confirm step used to be a
|
|
50
50
|
// hand-rolled lookalike of ConfirmDialog with its own copy of the overlay/card/button styling,
|
|
51
51
|
// which is exactly how it drifted from ConfirmDialog's real look in the first place (different
|
|
52
|
-
// button layout, different Cancel styling, and
|
|
52
|
+
// button layout, different Cancel styling, and - via cardBg - a different, caller-tintable card
|
|
53
53
|
// background no other ConfirmDialog in the fleet has). It now renders a real ConfirmDialog
|
|
54
54
|
// instance below instead of its own copy, which by construction can never drift from every other
|
|
55
|
-
// confirm prompt in the app again
|
|
55
|
+
// confirm prompt in the app again - at the cost of losing that per-caller tinting, which is the
|
|
56
56
|
// right trade: a "you're about to erase everything" prompt should look and read identically
|
|
57
57
|
// everywhere, not take on whichever screen happened to trigger it.
|
|
58
58
|
fg?: string
|
|
@@ -60,15 +60,15 @@ interface Props {
|
|
|
60
60
|
children: ReactNode
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
// The stats/achievements screen shell every @tastic game shares
|
|
63
|
+
// The stats/achievements screen shell every @tastic game shares - back button + title header,
|
|
64
64
|
// scrollable content area, and an optional reset-everything action with its own confirm-before-
|
|
65
|
-
// destroying dialog
|
|
65
|
+
// destroying dialog - extracted after the same header/scroll/reset-confirm shape was independently
|
|
66
66
|
// built into LightCycles' own achievements.tsx. Colors default to the same literal-black/white-by-
|
|
67
67
|
// appearance formula BaseSettingsDialog hardcodes internally (same as this package's own
|
|
68
|
-
// BaseSettingsDialog)
|
|
68
|
+
// BaseSettingsDialog) - zero-config for every existing caller - but fg/bg are each independently
|
|
69
69
|
// overridable for a caller whose own app-wide chrome palette isn't that literal black/white
|
|
70
70
|
// convention. `children` is the seam for whatever isn't shared: this component has no opinion on
|
|
71
|
-
// what a "stat" is or how achievements are catalogued
|
|
71
|
+
// what a "stat" is or how achievements are catalogued - see StatRow/StatSection/AchievementRow for
|
|
72
72
|
// the smaller presentational pieces built to go inside it.
|
|
73
73
|
export function BaseStatsScreen({ rotation: rotationOverride, title = 'Achievements', titleVariant = 'headlineSmall', onBack, insets, onReset, resetLabel = 'Reset All Stats', resetConfirmTitle = 'Reset Everything?', resetConfirmBody = 'This permanently erases all stats and achievements. This cannot be undone.', fg: fgOverride, bg: bgOverride, children }: Props) {
|
|
74
74
|
const { dark, colors } = useAutoPaperTheme()
|
|
@@ -139,7 +139,7 @@ const styles = StyleSheet.create({
|
|
|
139
139
|
resetButton: {
|
|
140
140
|
marginTop: 16
|
|
141
141
|
},
|
|
142
|
-
// Bounds the ScrollView to the space `container`'s flex:1 actually gives it
|
|
142
|
+
// Bounds the ScrollView to the space `container`'s flex:1 actually gives it - without this, a
|
|
143
143
|
// ScrollView with only a contentContainerStyle isn't reliably height-constrained on native (it
|
|
144
144
|
// can render at its full unclipped content height instead of the screen's), which leaves the
|
|
145
145
|
// header's back button touch target unreliable underneath it.
|
package/src/ConfirmDialog.tsx
CHANGED
|
@@ -76,14 +76,14 @@ const styles = StyleSheet.create({
|
|
|
76
76
|
textAlign: 'center'
|
|
77
77
|
},
|
|
78
78
|
// Additive on top of overlayCard's own shared `card` shape (borderRadius/borderWidth/gap/maxWidth/
|
|
79
|
-
// padding)
|
|
79
|
+
// padding) - claims its full allowance instead of just maxWidth alone, which only caps how wide the
|
|
80
80
|
// card is allowed to get - without an explicit width, a flex-centered child sizes to its own content
|
|
81
81
|
// instead of stretching, so the card (and therefore actions below) can end up narrower than the
|
|
82
82
|
// screen actually has room for, needlessly forcing the action row to wrap sooner than it has to.
|
|
83
83
|
card: {
|
|
84
84
|
width: '100%'
|
|
85
85
|
},
|
|
86
|
-
// Additive on top of overlayCard's own shared `overlay` backdrop
|
|
86
|
+
// Additive on top of overlayCard's own shared `overlay` backdrop - gives the card room to breathe
|
|
87
87
|
// from the screen edges now that it reliably stretches to fill its full width allowance (see
|
|
88
88
|
// card's own width comment) - without this, width: '100%' would run the card edge-to-edge on a
|
|
89
89
|
// screen narrower than maxWidth's own 360.
|
|
@@ -11,7 +11,7 @@ export interface ControlSchemePickerProps<T extends string | number> {
|
|
|
11
11
|
value: T
|
|
12
12
|
onChange: (value: T) => void
|
|
13
13
|
options: MenuOption<T>[]
|
|
14
|
-
// Another seat's currently-selected scheme, if any
|
|
14
|
+
// Another seat's currently-selected scheme, if any - passed straight through as the underlying
|
|
15
15
|
// section's own takenValue (see SectionedDropdown's own doc: stays in the list, shown disabled,
|
|
16
16
|
// rather than filtered out). Two local seats sharing one physical keyboard (and, depending on your
|
|
17
17
|
// own option set, one mouse) can't both be assigned the same scheme at once; this is what a caller
|
|
@@ -22,11 +22,11 @@ export interface ControlSchemePickerProps<T extends string | number> {
|
|
|
22
22
|
onAccentColor?: string
|
|
23
23
|
dark: boolean
|
|
24
24
|
align?: 'left' | 'right' | 'center'
|
|
25
|
-
// Passed straight through to the underlying SectionedDropdown
|
|
25
|
+
// Passed straight through to the underlying SectionedDropdown - see that component's own
|
|
26
26
|
// `alignOverride` doc. Lets a caller inside a @tastic/split-screen zone substitute
|
|
27
27
|
// useZoneClampedAlign's result for this picker's own popover placement.
|
|
28
28
|
alignOverride?: AlignResult
|
|
29
|
-
// Passed straight through to the underlying SectionedDropdown
|
|
29
|
+
// Passed straight through to the underlying SectionedDropdown - see that component's own
|
|
30
30
|
// `rotation` doc. Defaults to a live ambient useRotation() read when omitted, same as
|
|
31
31
|
// SectionedDropdown itself.
|
|
32
32
|
rotation?: PopoverRotation
|
|
@@ -35,23 +35,23 @@ export interface ControlSchemePickerProps<T extends string | number> {
|
|
|
35
35
|
|
|
36
36
|
// A single-select SectionedDropdown, opinionated for exactly one job: letting a local seat pick
|
|
37
37
|
// which physical input drives it in a couch-multiplayer game. Fixes the trigger icon and
|
|
38
|
-
// accessibility label so every game's control-scheme picker looks and reads the same, and
|
|
38
|
+
// accessibility label so every game's control-scheme picker looks and reads the same, and - the
|
|
39
39
|
// actual reason this exists as its own component rather than a usage snippet in each app's own
|
|
40
|
-
// docs
|
|
40
|
+
// docs - bakes in the "does this even mean anything right now" gate every consumer was independently
|
|
41
41
|
// re-deriving: a physical keyboard scheme has no meaning on a touch-primary device (there's nothing
|
|
42
42
|
// to pick a *keyboard* layout for), and this repo's own components target React Native Web, where
|
|
43
43
|
// `Platform.OS !== 'web'` means native/touch already, so keying off `useIsTouchPrimaryDevice` alone
|
|
44
|
-
// isn't enough on its own
|
|
44
|
+
// isn't enough on its own - a native touch device still needs the `Platform.OS` check to be excluded
|
|
45
45
|
// the same way a desktop browser needs the touch check. Renders nothing at all when either
|
|
46
46
|
// condition fails, rather than a disabled/empty picker, so a caller can mount this unconditionally
|
|
47
47
|
// inside its own per-seat layout (a `View` row alongside a color picker, say) without its own
|
|
48
|
-
// `Platform`/`useIsTouchPrimaryDevice` branch
|
|
48
|
+
// `Platform`/`useIsTouchPrimaryDevice` branch - an always-rendered-but-sometimes-null child is a
|
|
49
49
|
// no-op in a flex layout, so the surrounding row collapses to just its other children automatically.
|
|
50
50
|
//
|
|
51
51
|
// Deliberately generic over `T` (matching SectionedDropdown's own `MenuOption<T>`) rather than
|
|
52
|
-
// hardcoding a fixed scheme union: this repo has no opinion on which schemes exist
|
|
52
|
+
// hardcoding a fixed scheme union: this repo has no opinion on which schemes exist - the raw
|
|
53
53
|
// physical-key layouts (wasd/arrows/ijkl/numpad) live in `@tastic/input`, and any extra values with
|
|
54
|
-
// no keys of their own (a "keep using pointer input" option, say) are purely a per-app label
|
|
54
|
+
// no keys of their own (a "keep using pointer input" option, say) are purely a per-app label - so a
|
|
55
55
|
// caller always supplies its own `options` (and typically its own `T`) rather than this component
|
|
56
56
|
// importing `@tastic/input` and choosing a set for every consumer.
|
|
57
57
|
export function ControlSchemePicker<T extends string | number>({ id, host, value, onChange, options, takenValue, accentColor, mutedColor, onAccentColor, dark, align, alignOverride, rotation, labelFontFamily }: ControlSchemePickerProps<T>) {
|
|
@@ -4,29 +4,29 @@ import { StyleSheet } from 'react-native'
|
|
|
4
4
|
interface Props {
|
|
5
5
|
onBack: () => void
|
|
6
6
|
onSettings: () => void
|
|
7
|
-
// Full-contrast foreground
|
|
7
|
+
// Full-contrast foreground - same convention as every other component keying off a `dark` prop
|
|
8
8
|
// (BaseSettingsDialog, SectionedDropdown, etc.), just resolved by the caller since this component
|
|
9
9
|
// never reads theme state itself.
|
|
10
10
|
fg: string
|
|
11
11
|
// Safe-area insets already remapped onto the screen's real visual edges (see
|
|
12
|
-
// @tastic/split-screen's rotateInsets)
|
|
12
|
+
// @tastic/split-screen's rotateInsets) - this component just adds the fixed 8px margin on top,
|
|
13
13
|
// it doesn't know anything about physical device orientation itself.
|
|
14
14
|
insets: { top: number; left: number; right: number }
|
|
15
15
|
// Overrides the top-left button's icon (a react-native-paper/MaterialCommunityIcons name) and its
|
|
16
|
-
// accessibility label
|
|
16
|
+
// accessibility label - default 'arrow-left'/'Back'. Not every screen's top-left control is
|
|
17
17
|
// actually "undo the last navigation": a flat menu/detail screen with nothing deeper to pop back
|
|
18
18
|
// through often wants a literal home/menu affordance driving an absolute navigation instead (see
|
|
19
19
|
// e.g. Solitaire's GameChrome, whose onBack navigates straight to its root route). Pass both
|
|
20
|
-
// together
|
|
20
|
+
// together - an icon without a matching label (or vice versa) misleads screen readers/tooltips.
|
|
21
21
|
backIcon?: string
|
|
22
22
|
backLabel?: string
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
// Back (top-left) + Settings (top-right) icon buttons, fixed at the screen's rotated corners
|
|
25
|
+
// Back (top-left) + Settings (top-right) icon buttons, fixed at the screen's rotated corners - the
|
|
26
26
|
// default reachability answer for a two-player screen: correct any time both players share one
|
|
27
27
|
// screen but only one of them (or neither, in vs-CPU) is on the far side of a rotated zone. Not the
|
|
28
28
|
// right answer for a screen where the top corners themselves fall *inside* one player's own rotated
|
|
29
|
-
// zone (two-player face-to-face)
|
|
29
|
+
// zone (two-player face-to-face) - see SharedActionBand for that case instead, which callers select
|
|
30
30
|
// between based on their own game-mode/orientation state (this component has no opinion on which).
|
|
31
31
|
export function CornerActionButtons({ onBack, onSettings, fg, insets, backIcon = 'arrow-left', backLabel = 'Back' }: Props) {
|
|
32
32
|
return (
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { StyleSheet, View } from 'react-native'
|
|
2
2
|
import { Icon } from 'react-native-paper'
|
|
3
3
|
|
|
4
|
-
// Mirrors @tastic/split-screen's own type of the same name, duplicated here rather than imported
|
|
4
|
+
// Mirrors @tastic/split-screen's own type of the same name, duplicated here rather than imported -
|
|
5
5
|
// this package has never taken a dependency on @tastic/split-screen; every orientation/rotation-
|
|
6
6
|
// derived value it touches (ConfirmDialog's and BaseSettingsDialog's own `rotation` props, this
|
|
7
7
|
// component's orientationMode/p1OnRight below) is always a plain value the caller already resolved
|
|
@@ -9,7 +9,7 @@ import { Icon } from 'react-native-paper'
|
|
|
9
9
|
export type OrientationMode = 'faceToFace' | 'sideBySide'
|
|
10
10
|
|
|
11
11
|
export interface CornerStatusBadgeSeat {
|
|
12
|
-
// Any react-native-paper Icon `source` name. Omit to still render the badge itself, empty
|
|
12
|
+
// Any react-native-paper Icon `source` name. Omit to still render the badge itself, empty - a
|
|
13
13
|
// seat with nothing to show right now is different from no seat at all (see `p1`/`p2` on
|
|
14
14
|
// CornerStatusBadgesProps below), and the empty badge is itself the answer ("nothing"), not a
|
|
15
15
|
// reason to hide the corner.
|
|
@@ -18,11 +18,11 @@ export interface CornerStatusBadgeSeat {
|
|
|
18
18
|
|
|
19
19
|
export interface CornerStatusBadgesProps {
|
|
20
20
|
orientationMode: OrientationMode
|
|
21
|
-
// Same "is this seat on the right" convention as @tastic/split-screen's own p1OnRight
|
|
21
|
+
// Same "is this seat on the right" convention as @tastic/split-screen's own p1OnRight - only
|
|
22
22
|
// meaningful when orientationMode is 'sideBySide'; ignored in 'faceToFace', where seat 1 always
|
|
23
23
|
// owns the bottom half of a shared screen and seat 2 the top half, regardless of physical rotation.
|
|
24
24
|
p1OnRight: boolean
|
|
25
|
-
// Independently optional
|
|
25
|
+
// Independently optional - omit a seat entirely to skip rendering its badge at all (e.g. a solo
|
|
26
26
|
// mode with no second seat to show anything for). Pass an object (even `{}`) to render that
|
|
27
27
|
// seat's badge with nothing in it.
|
|
28
28
|
p1?: CornerStatusBadgeSeat
|
|
@@ -31,21 +31,21 @@ export interface CornerStatusBadgesProps {
|
|
|
31
31
|
|
|
32
32
|
const BADGE_SIZE = 30
|
|
33
33
|
|
|
34
|
-
// Deliberately understated (small, low-opacity, no per-seat color)
|
|
34
|
+
// Deliberately understated (small, low-opacity, no per-seat color) - a quiet corner landmark, not
|
|
35
35
|
// something that should compete for attention with whatever's on screen underneath it.
|
|
36
36
|
function StatusBadge({ icon }: { icon?: string }) {
|
|
37
37
|
return <View style={styles.badge}>{icon && <Icon source={icon} size={16} color='rgba(255,255,255,0.85)' />}</View>
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// Generic per-seat corner status badge: at most one caller-supplied icon per seat (or none), each
|
|
41
|
-
// pinned to that seat's own screen corner. Has zero opinion on what the icon means
|
|
41
|
+
// pinned to that seat's own screen corner. Has zero opinion on what the icon means - originally
|
|
42
42
|
// extracted from LightCycles' PowerupHud.tsx and Snake's SnakePowerupHud.tsx, which both used this
|
|
43
43
|
// exact positioning and styling to reveal a held power-up, but this component only ever sees a
|
|
44
44
|
// resolved icon name (or undefined) per seat, never the domain value behind it, so it's equally
|
|
45
45
|
// suited to any other per-seat status a caller wants glanceable in a screen corner (connection
|
|
46
46
|
// state, a turn-order marker, anything that's a single icon or nothing). Corner picked per seat
|
|
47
|
-
// from orientationMode/p1OnRight
|
|
48
|
-
// @tastic/split-screen's own consumers already use for zone/rotation decisions
|
|
47
|
+
// from orientationMode/p1OnRight - the same "is this seat on the right" convention
|
|
48
|
+
// @tastic/split-screen's own consumers already use for zone/rotation decisions - so each badge
|
|
49
49
|
// always sits in that seat's own zone rather than a side fixed regardless of which way the device
|
|
50
50
|
// is being held. In 'faceToFace', column doesn't matter (each seat's zone spans the full width):
|
|
51
51
|
// seat 1 stays bottom, seat 2 stays top, regardless of p1OnRight.
|