@tastic/hud 0.14.2 → 0.16.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 +117 -80
- package/dist/chrome/index.d.mts +240 -0
- package/dist/chrome/index.d.ts +240 -0
- package/dist/chrome/index.js +772 -0
- package/dist/chrome/index.mjs +44 -0
- package/dist/chunk-JXRBXQLQ.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 +23 -23
- 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 +18 -10
- 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
|
@@ -11,11 +11,11 @@ import { AlignResult, PopoverRotation, useAutoAlign } from './useAutoAlign'
|
|
|
11
11
|
import { PopoverHost } from './usePopoverHost'
|
|
12
12
|
|
|
13
13
|
const MENU_BORDER_WIDTH = 1
|
|
14
|
-
// Matches triggerBox's own height/width below
|
|
14
|
+
// Matches triggerBox's own height/width below - passed to PopoverBody so its caret can point at
|
|
15
15
|
// this trigger's actual center, not the (usually wider) menu's midpoint.
|
|
16
16
|
const TRIGGER_SIZE = 44
|
|
17
17
|
const MENU_MIN_WIDTH = 200
|
|
18
|
-
// Fed into useAutoAlign as the popover's assumed width
|
|
18
|
+
// Fed into useAutoAlign as the popover's assumed width - a real render can come in narrower
|
|
19
19
|
// (shrink-to-fit content), but never wider, since `menu` below caps at this same value. Alignment
|
|
20
20
|
// math staying in sync with the actual rendered cap is what keeps it from ever being wrong in a way
|
|
21
21
|
// that lets the popover clip off-screen.
|
|
@@ -24,7 +24,7 @@ const MENU_ITEM_ICON_SIZE = 18
|
|
|
24
24
|
// Tuned by eye: wide enough that a short one-word-ish label stays on one line, narrow enough that a
|
|
25
25
|
// sentence-length description reliably wraps onto a second line instead.
|
|
26
26
|
const MENU_ITEM_LABEL_MAX_WIDTH = 150
|
|
27
|
-
// Estimated per-row heights fed into useAutoAlign's vertical flip
|
|
27
|
+
// Estimated per-row heights fed into useAutoAlign's vertical flip - approximate (item's own
|
|
28
28
|
// padding + a labelLarge line, give or take font metrics), not measured, for the same reason
|
|
29
29
|
// MENU_MAX_WIDTH is a fixed cap rather than a measured value: knowing it before the popover renders
|
|
30
30
|
// is what lets the flip decision happen in the same frame as opening, instead of a measure-then-
|
|
@@ -38,7 +38,7 @@ export interface MenuOption<T extends string | number> {
|
|
|
38
38
|
value: T
|
|
39
39
|
label: string
|
|
40
40
|
description?: string
|
|
41
|
-
// Optional per-option icon
|
|
41
|
+
// Optional per-option icon - when every option in a single-select section has one, the trigger
|
|
42
42
|
// shows the currently selected option's icon instead of the static fallback, so the value is
|
|
43
43
|
// readable at a glance without opening the dropdown. Unused by multi-select sections (there's no
|
|
44
44
|
// single "the" selected option to show on the trigger).
|
|
@@ -46,7 +46,7 @@ export interface MenuOption<T extends string | number> {
|
|
|
46
46
|
iconSize?: number
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// Exactly one of these can be true at a time
|
|
49
|
+
// Exactly one of these can be true at a time - radio-style. Tapping a row selects it; whether that
|
|
50
50
|
// also closes the popover is controlled by the dropdown's own `autoDismiss` prop.
|
|
51
51
|
export interface SingleSelectSection<T extends string | number> {
|
|
52
52
|
kind: 'single'
|
|
@@ -54,7 +54,7 @@ export interface SingleSelectSection<T extends string | number> {
|
|
|
54
54
|
options: MenuOption<T>[]
|
|
55
55
|
value: T
|
|
56
56
|
onChange: (value: T) => void
|
|
57
|
-
// Another seat's currently-selected value in this section, if any
|
|
57
|
+
// Another seat's currently-selected value in this section, if any - stays in `options` (so the
|
|
58
58
|
// trigger gauge below keeps indexing into the one list every seat shares, instead of each seat's
|
|
59
59
|
// gauge landing wherever its own filtered-down list happens to put the remaining values) but
|
|
60
60
|
// renders disabled and dimmed, unselectable. Same "shown, not removed" treatment as
|
|
@@ -62,7 +62,7 @@ export interface SingleSelectSection<T extends string | number> {
|
|
|
62
62
|
takenValue?: T
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
// Any number of these can be true at once
|
|
65
|
+
// Any number of these can be true at once - checkbox-style. Tapping a row toggles just that one and
|
|
66
66
|
// never closes the popover on its own (unlike a single-select row, picking one of several things
|
|
67
67
|
// isn't "done" the way picking the one value in a single-select is).
|
|
68
68
|
export interface MultiSelectSection<T extends string | number> {
|
|
@@ -71,7 +71,7 @@ export interface MultiSelectSection<T extends string | number> {
|
|
|
71
71
|
options: MenuOption<T>[]
|
|
72
72
|
value: T[]
|
|
73
73
|
onChange: (value: T[]) => void
|
|
74
|
-
// Renders an alternating All/Clear bulk-toggle button beneath this section's rows
|
|
74
|
+
// Renders an alternating All/Clear bulk-toggle button beneath this section's rows - label reflects
|
|
75
75
|
// what tapping it would do next (every option already selected -> "Clear"; anything else -> "All").
|
|
76
76
|
allClear?: boolean
|
|
77
77
|
}
|
|
@@ -82,18 +82,18 @@ export type MenuSection = SingleSelectSection<any> | MultiSelectSection<any>
|
|
|
82
82
|
interface Props {
|
|
83
83
|
id: string
|
|
84
84
|
host: PopoverHost
|
|
85
|
-
// Static fallback trigger icon
|
|
85
|
+
// Static fallback trigger icon - used whenever there isn't a single unambiguous "selected option"
|
|
86
86
|
// to show instead (see the trigger-icon logic below).
|
|
87
87
|
icon: string
|
|
88
88
|
accessibilityLabel: string
|
|
89
89
|
sections: MenuSection[]
|
|
90
90
|
accentColor: string
|
|
91
91
|
mutedColor: string
|
|
92
|
-
// Foreground for a selected row's icon/text, painted on top of accentColor's own fill
|
|
92
|
+
// Foreground for a selected row's icon/text, painted on top of accentColor's own fill - accentColor
|
|
93
93
|
// isn't guaranteed to be a fixed brand tone (a consumer may derive it from a user-chosen color), so
|
|
94
94
|
// a hardcoded black or a guessed contrast color both risk landing unreadable against it. Omit to
|
|
95
95
|
// derive one automatically (getColorRoles' onColor) from accentColor against this popover's own
|
|
96
|
-
// background
|
|
96
|
+
// background - pass a real design-system "on" color explicitly instead when you have one.
|
|
97
97
|
onAccentColor?: string
|
|
98
98
|
dark: boolean
|
|
99
99
|
// Forces a specific horizontal alignment instead of letting whichever placement result applies
|
|
@@ -101,14 +101,14 @@ interface Props {
|
|
|
101
101
|
// the trigger actually sits.
|
|
102
102
|
align?: 'left' | 'right' | 'center'
|
|
103
103
|
// Substitutes this package's own plain useAutoAlign-based placement wholesale (align,
|
|
104
|
-
// verticalAlign, maxHeight, measured, triggerRef) with a caller-supplied one
|
|
104
|
+
// verticalAlign, maxHeight, measured, triggerRef) with a caller-supplied one - e.g.
|
|
105
105
|
// useZoneClampedAlign's result, for a dropdown living inside a @tastic/split-screen zone, where
|
|
106
106
|
// the plain window-relative decision can pick a direction that overflows into the shared row
|
|
107
107
|
// instead of the zone's own boundary. Same shape LabeledDropdown's own alignOverride takes; omit
|
|
108
108
|
// for the ordinary case (a popover with nothing but the screen edge to avoid).
|
|
109
109
|
alignOverride?: AlignResult
|
|
110
110
|
// Explicit override for the rotation a @tastic/split-screen-style FakeLandscapeView (or any other
|
|
111
|
-
// ancestor rotated the same way
|
|
111
|
+
// ancestor rotated the same way - a bare CSS `transform: rotate()`, not an actual OS-level
|
|
112
112
|
// orientation change) is currently applying. Defaults to a live ambient read via @tastic/core's
|
|
113
113
|
// useRotation() when omitted, so most callers never need to pass this at all. Pass it explicitly
|
|
114
114
|
// only when a caller's own reading needs to differ from the ambient one (e.g. a fading dual-zone
|
|
@@ -120,11 +120,11 @@ interface Props {
|
|
|
120
120
|
rotation?: PopoverRotation
|
|
121
121
|
// Whether picking a value in a *single-select* section closes the popover. Defaults to true
|
|
122
122
|
// (the old, pre-press-away default: pick one thing, you're done). Multi-select rows never
|
|
123
|
-
// auto-close regardless of this
|
|
123
|
+
// auto-close regardless of this - see MultiSelectSection's own comment. Worth setting false for a
|
|
124
124
|
// dropdown with multiple sections, where picking one value while another's still being decided
|
|
125
125
|
// shouldn't snap the whole thing shut; press-away (or the trigger itself) closes it instead.
|
|
126
126
|
autoDismiss?: boolean
|
|
127
|
-
// Defaults to the system monospace font
|
|
127
|
+
// Defaults to the system monospace font - pass your own game's registered font to match its UI.
|
|
128
128
|
labelFontFamily?: string
|
|
129
129
|
allClearLabels?: { all: string; clear: string }
|
|
130
130
|
}
|
|
@@ -141,19 +141,19 @@ export function SectionedDropdown({ id, host, icon, accessibilityLabel, sections
|
|
|
141
141
|
const menuBg = dark ? '#000000' : '#FFFFFF'
|
|
142
142
|
// mutedColor is translucent (fine for the icon/text tints below, each a single paint), but the
|
|
143
143
|
// caret is deliberately drawn overlapping the box's own top border (PopoverBody's CARET_DIP,
|
|
144
|
-
// needed to avoid a subpixel seam between the two separately-drawn edges)
|
|
144
|
+
// needed to avoid a subpixel seam between the two separately-drawn edges) - with a translucent
|
|
145
145
|
// color that overlap double-blends into a visibly lighter patch, unlike every other (opaque-
|
|
146
146
|
// bordered) popover in the app. Flattened once against this menu's own solid background so the
|
|
147
147
|
// box border and caret ring paint identically instead of compounding.
|
|
148
148
|
const menuBorderColor = getBlendedColor(mutedColor, menuBg, 0.5)
|
|
149
149
|
const resolvedOnAccentColor = onAccentColor ?? getColorRoles(accentColor, menuBg).onColor
|
|
150
|
-
// A bold, always-legible foreground
|
|
150
|
+
// A bold, always-legible foreground - the trigger icon's job is category identity ("this is grid
|
|
151
151
|
// size"), not state, so it stays as bold as any other icon on screen (a back arrow, a settings
|
|
152
152
|
// gear) instead of dimmed. State lives entirely in TriggerGaugeHost's ring around it.
|
|
153
153
|
const fg = dark ? '#FFFFFF' : '#000000'
|
|
154
154
|
|
|
155
155
|
const open = host.openId === id
|
|
156
|
-
// Estimated, not measured
|
|
156
|
+
// Estimated, not measured - see MENU_ITEM_HEIGHT's own comment. Every row, plus a divider between
|
|
157
157
|
// sections, plus a footer row for whichever multi-select sections show one.
|
|
158
158
|
const estimatedHeight =
|
|
159
159
|
MENU_VERTICAL_PADDING +
|
|
@@ -163,7 +163,7 @@ export function SectionedDropdown({ id, host, icon, accessibilityLabel, sections
|
|
|
163
163
|
const divider = sectionIndex > 0 ? MENU_DIVIDER_HEIGHT : 0
|
|
164
164
|
return sum + rows + footer + divider
|
|
165
165
|
}, 0)
|
|
166
|
-
// Always called, even when alignOverride is supplied and this result goes unused
|
|
166
|
+
// Always called, even when alignOverride is supplied and this result goes unused - hooks can't be
|
|
167
167
|
// called conditionally. See LabeledDropdown's identical comment for the same tradeoff.
|
|
168
168
|
const auto = useAutoAlign(open, MENU_MAX_WIDTH, estimatedHeight, rotation)
|
|
169
169
|
const placement = alignOverride ?? auto
|
|
@@ -171,7 +171,7 @@ export function SectionedDropdown({ id, host, icon, accessibilityLabel, sections
|
|
|
171
171
|
const align = forcedAlign ?? placement.align
|
|
172
172
|
|
|
173
173
|
// Trigger reflects the selected option's own icon only when there's exactly one section and it's
|
|
174
|
-
// single-select
|
|
174
|
+
// single-select - any other shape (multi-select present, or more than one section) has no single
|
|
175
175
|
// unambiguous "the" value to show, so it falls back to the static icon instead.
|
|
176
176
|
const onlySection = sections.length === 1 ? sections[0] : null
|
|
177
177
|
const selectedOption = onlySection?.kind === 'single' ? onlySection.options.find((o) => o.value === onlySection.value) : undefined
|
|
@@ -180,14 +180,14 @@ export function SectionedDropdown({ id, host, icon, accessibilityLabel, sections
|
|
|
180
180
|
const hasMultiSection = sections.some((s) => s.kind === 'multi')
|
|
181
181
|
const anyMultiSelected = sections.some((s) => s.kind === 'multi' && s.value.length > 0)
|
|
182
182
|
// A single-select trigger always has exactly one active value, so there's no "off" state for its
|
|
183
|
-
// icon to distinguish
|
|
183
|
+
// icon to distinguish - plain fg, unconditionally. A multi-select one (powerups, say) genuinely
|
|
184
184
|
// can be fully off (nothing enabled), which is different information than *which* ones are on
|
|
185
|
-
// (the gauge ring's job)
|
|
185
|
+
// (the gauge ring's job) - worth a real on/off read on the icon itself, especially once a section
|
|
186
186
|
// like this gains a combined off/few/many dimension alongside its checkboxes.
|
|
187
187
|
const triggerIconColor = hasMultiSection ? (anyMultiSelected ? accentColor : fg) : fg
|
|
188
188
|
|
|
189
189
|
// Unlike the trigger-icon override above (which only makes sense for a single unambiguous "the"
|
|
190
|
-
// value), the gauge ring has no such restriction
|
|
190
|
+
// value), the gauge ring has no such restriction - it just needs a position per option and which
|
|
191
191
|
// ones are lit, so it flattens every section's options into one combined ring instead of only
|
|
192
192
|
// rendering when there's exactly one section. A two-section dropdown (say, board variant + a
|
|
193
193
|
// rink toggle riding along as its own single-item multi-select) reads as one ring where the first
|
|
@@ -219,12 +219,12 @@ export function SectionedDropdown({ id, host, icon, accessibilityLabel, sections
|
|
|
219
219
|
<IconButton icon={triggerIcon} iconColor={triggerIconColor} size={triggerIconSize} accessibilityLabel={accessibilityLabel} onPress={() => host.toggle(id)} />
|
|
220
220
|
</View>
|
|
221
221
|
|
|
222
|
-
{/* Gated on `measured`, not just `open`
|
|
222
|
+
{/* Gated on `measured`, not just `open` - see useAutoAlign's own comment. Without it, the
|
|
223
223
|
popover mounts for one frame at whatever align/verticalAlign the *previous* open (or the
|
|
224
224
|
initial center/below guess) left behind, then visibly jumps once this open's own measurement
|
|
225
|
-
lands
|
|
225
|
+
lands - most noticeable near a screen edge, where the guess is furthest from correct. */}
|
|
226
226
|
<PopoverBody visible={open && measured} align={align} verticalAlign={verticalAlign} caretColor={menuBg} caretBorderColor={menuBorderColor} caretBorderWidth={MENU_BORDER_WIDTH} triggerSize={TRIGGER_SIZE}>
|
|
227
|
-
{/* maxHeight is the room useAutoAlign actually found in whichever direction it picked
|
|
227
|
+
{/* maxHeight is the room useAutoAlign actually found in whichever direction it picked - on
|
|
228
228
|
a screen tall enough for the estimated content, this never kicks in (estimatedHeight itself
|
|
229
229
|
is always <= maxHeight then, so nothing scrolls); on a short landscape screen where neither
|
|
230
230
|
direction has enough room, this is what keeps the menu from just running off the screen
|
|
@@ -232,7 +232,7 @@ export function SectionedDropdown({ id, host, icon, accessibilityLabel, sections
|
|
|
232
232
|
<ScrollView style={[styles.menu, menuStyle]} contentContainerStyle={styles.menuContent} showsVerticalScrollIndicator={false}>
|
|
233
233
|
{sections.map((section, sectionIndex) => (
|
|
234
234
|
<View key={section.id}>
|
|
235
|
-
{/* Every section after the first gets a rule above it
|
|
235
|
+
{/* Every section after the first gets a rule above it - the divider belongs to the
|
|
236
236
|
boundary between sections, not to either section itself, so it's keyed off position
|
|
237
237
|
rather than each section carrying its own "divider below me" flag. */}
|
|
238
238
|
{sectionIndex > 0 && <View style={[styles.divider, dividerStyle]} />}
|
|
@@ -331,7 +331,7 @@ function MultiSection({
|
|
|
331
331
|
{section.options.map((option, i) => {
|
|
332
332
|
const selected = section.value.includes(option.value)
|
|
333
333
|
const rowColor = selected ? onAccentColor : mutedColor
|
|
334
|
-
// Adjacent rows sit flush against each other (no gap
|
|
334
|
+
// Adjacent rows sit flush against each other (no gap - see styles.item's own lack of
|
|
335
335
|
// margin), so two selected neighbors otherwise each carve a small rounded notch out of what
|
|
336
336
|
// should read as one continuous block. Squaring off just the touching edge is what makes a
|
|
337
337
|
// run of several selected options look like one seamless shape instead of a stack of
|
|
@@ -372,7 +372,7 @@ const styles = StyleSheet.create({
|
|
|
372
372
|
position: 'relative'
|
|
373
373
|
},
|
|
374
374
|
// React Native Web gives every position:'relative' view an explicit zIndex (0, not 'auto'),
|
|
375
|
-
// which makes each one its own stacking context
|
|
375
|
+
// which makes each one its own stacking context - so a zIndex set only on the popover content
|
|
376
376
|
// below only wins comparisons within this anchor's own box, not against this anchor's own later
|
|
377
377
|
// siblings (e.g. a Ready button), which are otherwise painted on top by DOM-order tiebreak.
|
|
378
378
|
// Elevating the anchor itself, only while its popover is open, fixes that at the right level.
|
|
@@ -399,7 +399,7 @@ const styles = StyleSheet.create({
|
|
|
399
399
|
flexDirection: 'row',
|
|
400
400
|
gap: 8
|
|
401
401
|
},
|
|
402
|
-
// Same opacity as ProfilePicker's rowDisabled / InlineColorPicker's swatchTaken
|
|
402
|
+
// Same opacity as ProfilePicker's rowDisabled / InlineColorPicker's swatchTaken - the fleet's
|
|
403
403
|
// established "shown, not removed" treatment for another seat's own current pick.
|
|
404
404
|
itemTaken: {
|
|
405
405
|
opacity: 0.35
|
|
@@ -415,7 +415,7 @@ const styles = StyleSheet.create({
|
|
|
415
415
|
shadowOpacity: 0.3,
|
|
416
416
|
shadowRadius: 8
|
|
417
417
|
},
|
|
418
|
-
// Separate from `menu` itself
|
|
418
|
+
// Separate from `menu` itself - a ScrollView's own style sets the scrolling frame's bounds
|
|
419
419
|
// (including the maxHeight that actually caps it), while padding/gap belong to its
|
|
420
420
|
// contentContainerStyle, which sizes to the *unclamped* content instead of the frame.
|
|
421
421
|
menuContent: {
|
package/src/SharedActionBand.tsx
CHANGED
|
@@ -6,30 +6,30 @@ interface Props {
|
|
|
6
6
|
onBack: () => void
|
|
7
7
|
onSettings: () => void
|
|
8
8
|
// Optional, and expected to be passed together in practice (either a screen has both a randomize
|
|
9
|
-
// and a reset action, or neither)
|
|
9
|
+
// and a reset action, or neither) - kept as two independently-optional props rather than one pair
|
|
10
10
|
// flag so a caller with only one of the two isn't forced to fake the other.
|
|
11
11
|
onRandomize?: () => void
|
|
12
12
|
onReset?: () => void
|
|
13
13
|
fg: string
|
|
14
14
|
// Whether one of THIS band's own popovers (i.e. something rendered inside `children`) is
|
|
15
|
-
// currently open
|
|
15
|
+
// currently open - the caller computes this (typically `host.openId !== null &&
|
|
16
16
|
// OWN_IDS.includes(host.openId)`), since only the caller knows which popover ids belong to its
|
|
17
|
-
// own children. Elevates the whole column above sibling panels when true
|
|
17
|
+
// own children. Elevates the whole column above sibling panels when true - React Native Web gives
|
|
18
18
|
// every position:'relative' view its own stacking context, so a popover escaping `children` needs
|
|
19
19
|
// this wrapper itself elevated, not just the popover content, to paint above a later sibling
|
|
20
20
|
// (e.g. a player panel) rather than underneath it.
|
|
21
21
|
popoverOpen: boolean
|
|
22
|
-
// The screen's own per-round option row (grid size, arena, etc.)
|
|
22
|
+
// The screen's own per-round option row (grid size, arena, etc.) - rendered directly below the
|
|
23
23
|
// action row, inside the same elevating column.
|
|
24
24
|
children: ReactNode
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// The reachability fix for a two-player screen where the plain top-left/top-right corners (see
|
|
28
|
-
// CornerActionButtons) would land inside one player's own rotated zone
|
|
28
|
+
// CornerActionButtons) would land inside one player's own rotated zone - most concretely, two-
|
|
29
29
|
// player face-to-face, where the far player's zone always covers the top half of the screen. Folds
|
|
30
30
|
// back/randomize/reset/settings into one row that sits on neutral ground instead (the shared band
|
|
31
31
|
// between the two players' zones), directly above the caller's own per-round option row. The
|
|
32
|
-
// caller decides *when* this applies (it owns gameMode/orientationMode)
|
|
32
|
+
// caller decides *when* this applies (it owns gameMode/orientationMode) - this component only
|
|
33
33
|
// renders the result once that decision's already been made.
|
|
34
34
|
export function SharedActionBand({ onBack, onSettings, onRandomize, onReset, fg, popoverOpen, children }: Props) {
|
|
35
35
|
return (
|
package/src/SkiaGate.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import type { ComponentType, ReactNode } from 'react'
|
|
|
2
2
|
import { useEffect, useState } from 'react'
|
|
3
3
|
|
|
4
4
|
export interface SkiaGateProps<P extends object> {
|
|
5
|
-
// A dynamic import, e.g. `() => import('./Board')`
|
|
5
|
+
// A dynamic import, e.g. `() => import('./Board')` - hoist to module scope at the call site
|
|
6
6
|
// (never an inline closure written directly in JSX) so its identity stays stable across
|
|
7
7
|
// renders; it's this component's own useEffect dependency below.
|
|
8
8
|
getComponent: () => Promise<{ default: ComponentType<P> }>
|
|
@@ -11,20 +11,20 @@ export interface SkiaGateProps<P extends object> {
|
|
|
11
11
|
fallback?: ReactNode
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
// Native Skia renders through JSI
|
|
14
|
+
// Native Skia renders through JSI - no WASM/CanvasKit fetch to wait on - so this file just
|
|
15
15
|
// lazily mounts whatever getComponent() resolves to, once it resolves. The .web.tsx sibling
|
|
16
16
|
// (picked by Metro/webpack's own platform-extension resolution for web bundles) is a different
|
|
17
17
|
// implementation entirely: a direct re-export of @shopify/react-native-skia's own WithSkiaWeb,
|
|
18
|
-
// which gates the same mount behind LoadSkiaWeb() first
|
|
18
|
+
// which gates the same mount behind LoadSkiaWeb() first - CanvasKit's WASM build has to actually
|
|
19
19
|
// finish loading before anything that imports '@shopify/react-native-skia' can evaluate its own
|
|
20
20
|
// top-level `Skia` binding without crashing (see loadSkiaWeb.web.ts's own comment).
|
|
21
21
|
//
|
|
22
22
|
// No React.lazy()/Suspense here, deliberately: lazy() can't be called inline during render
|
|
23
|
-
// (react-hooks/static-components
|
|
23
|
+
// (react-hooks/static-components - a getComponent prop that isn't referentially stable would
|
|
24
24
|
// give a new component identity, and therefore an unwanted remount, on every render that passes
|
|
25
25
|
// a fresh one) and can't be cached in a ref either (react-hooks/refs forbids reading or writing
|
|
26
26
|
// ref.current during render, only inside an effect). Awaiting the import directly and setting
|
|
27
|
-
// state from the resolved module sidesteps both
|
|
27
|
+
// state from the resolved module sidesteps both - setState only ever runs from the promise
|
|
28
28
|
// callback, never synchronously in render or in the effect body itself.
|
|
29
29
|
export function SkiaGate<P extends object>({ getComponent, componentProps, fallback = null }: SkiaGateProps<P>) {
|
|
30
30
|
const [Component, setComponent] = useState<ComponentType<P> | null>(null)
|
package/src/SkiaGate.web.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// Web needs CanvasKit's WASM loaded before any Skia <Canvas> evaluates
|
|
1
|
+
// Web needs CanvasKit's WASM loaded before any Skia <Canvas> evaluates - see loadSkiaWeb.web.ts's
|
|
2
2
|
// own comment for why a plain useEffect-based loader can't fix this (it's a module-evaluation-
|
|
3
3
|
// order problem, not a component-lifecycle one). Only this file (picked by Metro/webpack's
|
|
4
4
|
// platform-extension resolution for web bundles) may import from
|
|
5
|
-
// '@shopify/react-native-skia/src/web'
|
|
5
|
+
// '@shopify/react-native-skia/src/web' - a static import of that subpath from a native-reachable
|
|
6
6
|
// file pulls canvaskit-wasm's `require('fs')` into the native bundle and breaks iOS/Android
|
|
7
7
|
// builds; this file vs. SkiaGate.tsx is what actually keeps it out, the same split
|
|
8
8
|
// loadSkiaWeb.web.ts/loadSkiaWeb.ts already use.
|
package/src/StaggeredWord.tsx
CHANGED
|
@@ -7,12 +7,12 @@ const LETTER_DURATION_MS = 320
|
|
|
7
7
|
|
|
8
8
|
// The one formula every consumer's own follow-on animation (a paddle rally, a bouncing puck, a
|
|
9
9
|
// looping trail) needs in order to time its own entrance off "once the letters have mostly
|
|
10
|
-
// landed"
|
|
10
|
+
// landed" - five apps (AirHockey, BoxHockey, Pong, LightCycles, Snake) had each independently
|
|
11
11
|
// re-derived `(letterCount - 1) * STAGGER_MS + LETTER_DURATION_MS` locally as their own
|
|
12
12
|
// `TOTAL_STAGGER_MS` constant. Exported as a pure function, not a component return value, so a
|
|
13
13
|
// caller can compute it once, outside render, the same way every one of those apps already did.
|
|
14
14
|
// `letterCount` is the TOTAL number of letters actually animated, not necessarily a single
|
|
15
|
-
// StaggeredWord's own `word.length`
|
|
15
|
+
// StaggeredWord's own `word.length` - see `startIndex` below for a multi-word wordmark, where a
|
|
16
16
|
// second word's letters continue the same overall stagger sequence.
|
|
17
17
|
export function getStaggeredWordDuration(letterCount: number): number {
|
|
18
18
|
return (letterCount - 1) * STAGGER_MS + LETTER_DURATION_MS
|
|
@@ -57,13 +57,13 @@ export interface StaggeredWordProps {
|
|
|
57
57
|
fontFamily: string
|
|
58
58
|
fontSize: number
|
|
59
59
|
lineHeight: number
|
|
60
|
-
// Whether to skip the stagger entirely and render every letter already fully settled
|
|
60
|
+
// Whether to skip the stagger entirely and render every letter already fully settled - wire
|
|
61
61
|
// straight from react-native-reanimated's own useReducedMotion() at the call site rather than
|
|
62
62
|
// this component calling it internally, so a caller composing several StaggeredWords (see
|
|
63
63
|
// `startIndex` below) only needs the one hook call, not one per word.
|
|
64
64
|
reducedMotion: boolean
|
|
65
65
|
// Continues the stagger sequence from a larger overall wordmark this word is only one line/part
|
|
66
|
-
// of
|
|
66
|
+
// of - this word's own first letter animates at index `startIndex`, its second at
|
|
67
67
|
// `startIndex + 1`, and so on. Defaults to 0 for a standalone single-word wordmark (Snake's
|
|
68
68
|
// "SNAKE", Pong's "PONG"); a caller stacking multiple words (AirHockey's "AIR"/"HOCKEY",
|
|
69
69
|
// BoxHockey's "BOX"/"HOCKEY", LightCycles' "Light"/"Cycles") passes the running total of letters
|
|
@@ -71,15 +71,15 @@ export interface StaggeredWordProps {
|
|
|
71
71
|
// instead of each word restarting its own stagger from zero.
|
|
72
72
|
startIndex?: number
|
|
73
73
|
// Every consumer this component was extracted from hardcoded bold letters (baked into a fixed
|
|
74
|
-
// StyleSheet entry, not a prop)
|
|
74
|
+
// StyleSheet entry, not a prop) - kept as the default here rather than a fixed style, so the one
|
|
75
75
|
// consumer that doesn't want it (LightCycles, whose own registered display font is already
|
|
76
76
|
// sufficiently bold on its own) can opt out with `fontWeight='normal'` instead of this component
|
|
77
77
|
// forcing it unconditionally.
|
|
78
78
|
fontWeight?: TextStyle['fontWeight']
|
|
79
|
-
// Unset by default
|
|
79
|
+
// Unset by default - only AirHockey/BoxHockey/Pong's shared MONO_FONT-based letter style actually
|
|
80
80
|
// wants extra tracking; Snake's/LightCycles' own theme fonts don't.
|
|
81
81
|
letterSpacing?: number
|
|
82
|
-
// The wrapping row's own style
|
|
82
|
+
// The wrapping row's own style - e.g. LightCycles' own vertical offset for its second word
|
|
83
83
|
// beneath its first.
|
|
84
84
|
style?: StyleProp<ViewStyle>
|
|
85
85
|
// Lets a caller measure this word's own rendered box (e.g. to size/position a follow-on canvas
|
|
@@ -89,11 +89,11 @@ export interface StaggeredWordProps {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// One row of letters staggering in on mount (or rendered already fully settled under reduced
|
|
92
|
-
// motion)
|
|
92
|
+
// motion) - the "AnimatedLetter" building block five apps' own hero-title screens
|
|
93
93
|
// (AirHockey/BoxHockey/Pong/LightCycles/Snake's own HeroTitle.tsx/AnimatedHeroTitle.tsx) had each
|
|
94
94
|
// hand-rolled independently, identical down to the same 45ms stagger, 320ms per-letter duration,
|
|
95
95
|
// and back-ease overshoot. A single word/line on its own, or one of several stacked/side-by-side
|
|
96
|
-
// words sharing one overall stagger sequence via `startIndex`
|
|
96
|
+
// words sharing one overall stagger sequence via `startIndex` - this component only ever renders
|
|
97
97
|
// one row of letters; a caller composing more than one word renders more than one StaggeredWord
|
|
98
98
|
// (see `startIndex`'s own doc), the same way every one of those five apps' own `Word` helper
|
|
99
99
|
// already did internally.
|
package/src/StatRow.tsx
CHANGED
|
@@ -5,19 +5,19 @@ import { Text } from 'react-native-paper'
|
|
|
5
5
|
interface Props {
|
|
6
6
|
label: string
|
|
7
7
|
value: string
|
|
8
|
-
// Independently optional overrides for this row's own text colors
|
|
8
|
+
// Independently optional overrides for this row's own text colors - default to the same
|
|
9
9
|
// useAutoPaperTheme()-derived formula as BaseStatsScreen (see that component's own doc for why)
|
|
10
10
|
// when omitted, so every existing caller renders identically to before.
|
|
11
11
|
fg?: string
|
|
12
12
|
fgMuted?: string
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
// A single labeled stat
|
|
15
|
+
// A single labeled stat - label on the left (muted), value on the right (bold, full-contrast).
|
|
16
16
|
// Extracted from LightCycles' own achievements.tsx, where this exact row shape backed every
|
|
17
17
|
// section (Overall/Vs CPU/Two Player/Activity). fg/fgMuted default to the same
|
|
18
18
|
// useAutoPaperTheme()-derived formula as BaseStatsScreen (see that component's own doc for why),
|
|
19
19
|
// each independently overridable via the matching prop for a caller with its own chrome palette.
|
|
20
|
-
// Deliberately just label+value text, not a color swatch or profile chip
|
|
20
|
+
// Deliberately just label+value text, not a color swatch or profile chip - an app needing a row
|
|
21
21
|
// with a leading visual builds its own on top of this same dark-mode formula (see LightCycles' own
|
|
22
22
|
// ColorStatRow/ProfileRankingRow for an example), since what that visual even is varies per game.
|
|
23
23
|
export function StatRow({ label, value, fg: fgOverride, fgMuted: fgMutedOverride }: Props) {
|
package/src/StatSection.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { Text } from 'react-native-paper'
|
|
|
5
5
|
|
|
6
6
|
interface Props {
|
|
7
7
|
label: string
|
|
8
|
-
// Independently optional overrides for this section's own label color/background
|
|
8
|
+
// Independently optional overrides for this section's own label color/background - default to
|
|
9
9
|
// the same useAutoPaperTheme()-derived formula as StatRow/BaseStatsScreen (see BaseStatsScreen's
|
|
10
10
|
// own doc for why) when omitted, so every existing caller renders identically to before.
|
|
11
11
|
fg?: string
|
|
@@ -13,7 +13,7 @@ interface Props {
|
|
|
13
13
|
children: ReactNode
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
// A labeled, tinted-background grouping box
|
|
16
|
+
// A labeled, tinted-background grouping box - the "OVERALL"/"VS CPU"/"ACTIVITY"-style section
|
|
17
17
|
// shape from LightCycles' own achievements.tsx, generalized to any content (typically a stack of
|
|
18
18
|
// StatRow). fg/sectionBg default to the same useAutoPaperTheme()-derived formula as
|
|
19
19
|
// StatRow/BaseStatsScreen (see BaseStatsScreen's own doc for why), each independently overridable
|
|
@@ -36,7 +36,7 @@ export function StatSection({ label, fg: fgOverride, sectionBg: sectionBgOverrid
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const styles = StyleSheet.create({
|
|
39
|
-
// Bold and full-contrast (fg), not the original fontless/fgMuted treatment
|
|
39
|
+
// Bold and full-contrast (fg), not the original fontless/fgMuted treatment - that read at the
|
|
40
40
|
// same visual weight as StatRow's own row labels below it (both used the identical muted color;
|
|
41
41
|
// letterSpacing/variant alone didn't separate them enough to read as "this is the group header"
|
|
42
42
|
// at a glance). Matches AchievementRow's own title styling (bold + fg) below it in the same list.
|