@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
|
@@ -9,7 +9,7 @@ import { PopoverBody } from './PopoverBody'
|
|
|
9
9
|
import { AlignResult, PopoverRotation, useAutoAlign } from './useAutoAlign'
|
|
10
10
|
import { PopoverHost } from './usePopoverHost'
|
|
11
11
|
|
|
12
|
-
// Presence check, not a full-string "is this exactly one emoji" validation
|
|
12
|
+
// Presence check, not a full-string "is this exactly one emoji" validation - this only decides a
|
|
13
13
|
// font-size ratio for the trigger's own `tag`, so it doesn't need that precision, just "does this
|
|
14
14
|
// tag contain a pictograph at all."
|
|
15
15
|
const EMOJI_PATTERN = /\p{Extended_Pictographic}/u
|
|
@@ -19,14 +19,14 @@ const SWATCH_SIZE = 28
|
|
|
19
19
|
const SWATCHES_PADDING = 8
|
|
20
20
|
const SWATCHES_GAP = 6
|
|
21
21
|
const SWATCHES_BORDER_WIDTH = 2
|
|
22
|
-
// Clamped to exactly 4 or 5, not a wider 3-6 range
|
|
22
|
+
// Clamped to exactly 4 or 5, not a wider 3-6 range - `defaultColors` (this component's own default
|
|
23
23
|
// `swatches`, and what every consumer actually ends up passing) is exactly 20 colors, so 4 or 5
|
|
24
24
|
// columns are the only counts that fill every row completely (4x5 or 5x4) instead of leaving a
|
|
25
25
|
// ragged, off-center last row. A caller passing a custom, non-20-length `swatches` list still gets
|
|
26
|
-
// a reasonable grid either way
|
|
26
|
+
// a reasonable grid either way - just not guaranteed to be a full rectangle.
|
|
27
27
|
const MIN_COLUMNS = 4
|
|
28
28
|
const MAX_COLUMNS = 5
|
|
29
|
-
// How much screen width the grid leaves alone on either side when computing how many columns fit
|
|
29
|
+
// How much screen width the grid leaves alone on either side when computing how many columns fit -
|
|
30
30
|
// this trigger can sit anywhere (including flush against a player's own screen edge in split-screen
|
|
31
31
|
// mode), so the budget is against the whole window, not this trigger's own local space.
|
|
32
32
|
const SCREEN_MARGIN = 40
|
|
@@ -35,13 +35,13 @@ function widthForColumns(columns: number) {
|
|
|
35
35
|
return SWATCHES_BORDER_WIDTH * 2 + SWATCHES_PADDING * 2 + SWATCH_SIZE * columns + SWATCHES_GAP * (columns - 1)
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// Exported so a caller that needs to independently compute this popover's own content size
|
|
38
|
+
// Exported so a caller that needs to independently compute this popover's own content size - to
|
|
39
39
|
// feed useZoneClampedAlign's contentWidth/contentHeight params with, ahead of producing a
|
|
40
|
-
// colorAlignOverride for a picker living inside a @tastic/split-screen zone
|
|
40
|
+
// colorAlignOverride for a picker living inside a @tastic/split-screen zone - can match this
|
|
41
41
|
// component's real rendered size exactly, rather than re-deriving or hardcoding its internal
|
|
42
42
|
// column/row math. Same job as LabeledDropdown's own getLabeledDropdownContentHeight, but this
|
|
43
43
|
// picker's size also depends on window width (via the auto-column clamp below), so windowWidth is
|
|
44
|
-
// a required parameter here rather than an ambient useWindowDimensions() read
|
|
44
|
+
// a required parameter here rather than an ambient useWindowDimensions() read - a caller computing
|
|
45
45
|
// this ahead of render (to decide alignment before the picker itself has even mounted) has no
|
|
46
46
|
// component instance to read that hook from, so it has to pass the value in instead. The component
|
|
47
47
|
// below calls this same function with its own live useWindowDimensions() read, so there is exactly
|
|
@@ -60,20 +60,20 @@ interface Props {
|
|
|
60
60
|
host: PopoverHost
|
|
61
61
|
value: string
|
|
62
62
|
onChange: (hex: string) => void
|
|
63
|
-
// Transforms only what's *rendered*
|
|
64
|
-
// colors derived from them
|
|
63
|
+
// Transforms only what's *rendered* - trigger fill, swatch fill, and the contrast/caret/border
|
|
64
|
+
// colors derived from them - leaving `value`/`swatch.value` themselves untouched for `selected`/
|
|
65
65
|
// `taken` matching and for what onChange actually commits. For a host whose own theme mutes or
|
|
66
66
|
// tints a raw seed color before it's actually used elsewhere (e.g. blending it toward a base
|
|
67
67
|
// palette for a card-back tint), so the picker can preview that real result per-swatch instead of
|
|
68
68
|
// the too-vibrant seed hue, without changing what gets stored.
|
|
69
69
|
previewValue?: (hex: string) => string
|
|
70
70
|
swatches?: SeedColor[]
|
|
71
|
-
// The other player's current color, if any
|
|
71
|
+
// The other player's current color, if any - stays visible in the grid (so the full palette
|
|
72
72
|
// reads consistently for both players) but renders disabled with an X, rather than being
|
|
73
|
-
// silently removed from the list. Unless allowSwapTaken is set
|
|
73
|
+
// silently removed from the list. Unless allowSwapTaken is set - see that prop.
|
|
74
74
|
takenValue?: string
|
|
75
75
|
// Lets a tap on the taken swatch swap the two colors (this picker takes it, the other slot takes
|
|
76
|
-
// this picker's old color) instead of being disabled
|
|
76
|
+
// this picker's old color) instead of being disabled - useful when the "other" slot is CPU-
|
|
77
77
|
// controlled, not a second real person's choice to step on. Leave unset when the other slot is a
|
|
78
78
|
// human player: swapping their color out from under them without their input isn't the same
|
|
79
79
|
// tradeoff.
|
|
@@ -83,41 +83,41 @@ interface Props {
|
|
|
83
83
|
// (see alignOverride below) decide.
|
|
84
84
|
align?: 'left' | 'right' | 'center'
|
|
85
85
|
// Substitutes this package's own plain useAutoAlign-based placement wholesale (align,
|
|
86
|
-
// verticalAlign, maxHeight, measured, triggerRef) with a caller-supplied one
|
|
86
|
+
// verticalAlign, maxHeight, measured, triggerRef) with a caller-supplied one - e.g.
|
|
87
87
|
// useZoneClampedAlign's result, for a picker living inside a @tastic/split-screen zone, where the
|
|
88
88
|
// plain window-relative decision can pick a direction that overflows into the shared row instead
|
|
89
89
|
// of the zone's own boundary. Same shape LabeledDropdown's/SectionedDropdown's own alignOverride
|
|
90
90
|
// takes; omit for the ordinary case (a popover with nothing but the screen edge to avoid).
|
|
91
91
|
alignOverride?: AlignResult
|
|
92
92
|
// Explicit override for the rotation a @tastic/split-screen-style FakeLandscapeView (or
|
|
93
|
-
// equivalent) ancestor is currently applying
|
|
93
|
+
// equivalent) ancestor is currently applying - see SectionedDropdown's identical prop for the
|
|
94
94
|
// full reasoning. Defaults to a live ambient read via @tastic/core's useRotation() when omitted.
|
|
95
95
|
rotation?: PopoverRotation
|
|
96
|
-
// Trigger glyph
|
|
96
|
+
// Trigger glyph - defaults to a plain palette. Pass a distinct icon per slot (e.g. a face for a
|
|
97
97
|
// human, a robot for CPU) to convey identity through the icon itself. Overridden by `tag` below
|
|
98
98
|
// when present.
|
|
99
99
|
icon?: string
|
|
100
100
|
// A short identity mark (an initial, an emoji, a couple of characters) shown as text on the
|
|
101
|
-
// trigger instead of `icon`
|
|
101
|
+
// trigger instead of `icon` - e.g. a saved player profile's own tag. Falls back to `icon` when
|
|
102
102
|
// empty/omitted, same as a seat with no such identity selected.
|
|
103
103
|
tag?: string
|
|
104
|
-
// Same convention as SectionedDropdown/ReadyButton's identical prop
|
|
104
|
+
// Same convention as SectionedDropdown/ReadyButton's identical prop - defaults to this package's
|
|
105
105
|
// own MONO_FONT rather than inheriting whatever font the host app's theme happens to be using,
|
|
106
106
|
// so this trigger's tag reads consistently with the rest of this package's own chrome text.
|
|
107
107
|
labelFontFamily?: string
|
|
108
|
-
// Whether picking a swatch closes the popover. Defaults to true
|
|
108
|
+
// Whether picking a swatch closes the popover. Defaults to true - a color pick is a single,
|
|
109
109
|
// complete choice the same way a single-select dropdown row is (see SectionedDropdown's identical
|
|
110
110
|
// prop), so press-away isn't the only way out, but doesn't have to be either.
|
|
111
111
|
autoDismiss?: boolean
|
|
112
112
|
// Overrides the auto-computed column count (see MIN_COLUMNS/MAX_COLUMNS/SCREEN_MARGIN above).
|
|
113
113
|
columns?: number
|
|
114
|
-
// Trigger circle's diameter
|
|
114
|
+
// Trigger circle's diameter - defaults to DEFAULT_SIZE. Overridable so a caller sharing a row
|
|
115
115
|
// with other fixed-size elements can line this trigger up with them instead of standing out at a
|
|
116
116
|
// mismatched size.
|
|
117
117
|
size?: number
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
// Renders inline rather than as a full-screen modal, scoped to its own panel
|
|
120
|
+
// Renders inline rather than as a full-screen modal, scoped to its own panel - a modal color
|
|
121
121
|
// picker would block the whole screen for one player while another can't touch their own panel at
|
|
122
122
|
// the same time, which defeats the point of a split-screen lobby.
|
|
123
123
|
export function InlineColorPicker({ id, host, value, onChange, previewValue, swatches = defaultColors, takenValue, allowSwapTaken, dark, align: forcedAlign, alignOverride, rotation: rotationOverride, icon = 'palette', tag, labelFontFamily = MONO_FONT, autoDismiss = true, columns, size = DEFAULT_SIZE }: Props) {
|
|
@@ -128,7 +128,7 @@ export function InlineColorPicker({ id, host, value, onChange, previewValue, swa
|
|
|
128
128
|
const { width: swatchesWidth, height: swatchesHeight } = getInlineColorPickerContentSize(swatches.length, windowWidth, columns)
|
|
129
129
|
|
|
130
130
|
const open = host.openId === id
|
|
131
|
-
// Always called, even when alignOverride is supplied and this result goes unused
|
|
131
|
+
// Always called, even when alignOverride is supplied and this result goes unused - hooks can't be
|
|
132
132
|
// called conditionally. See LabeledDropdown's identical comment for the same tradeoff.
|
|
133
133
|
const auto = useAutoAlign(open, swatchesWidth, swatchesHeight, rotation)
|
|
134
134
|
const placement = alignOverride ?? auto
|
|
@@ -139,7 +139,7 @@ export function InlineColorPicker({ id, host, value, onChange, previewValue, swa
|
|
|
139
139
|
const contrastColor = getContrastColor(displayValue)
|
|
140
140
|
// An emoji glyph reads visually smaller than a bold letter at the same fontSize (the system emoji
|
|
141
141
|
// font leaves more of its own em-box empty), so a plain-text ratio that looks right for an initial
|
|
142
|
-
// still looks small for an emoji tag at the identical size
|
|
142
|
+
// still looks small for an emoji tag at the identical size - this bumps emoji up to the icon's own
|
|
143
143
|
// ratio instead, since a single emoji is closer in visual weight to an icon glyph than to text.
|
|
144
144
|
const tagFontSize = size * (tag && EMOJI_PATTERN.test(tag) ? 0.6 : 0.4)
|
|
145
145
|
|
|
@@ -160,14 +160,14 @@ export function InlineColorPicker({ id, host, value, onChange, previewValue, swa
|
|
|
160
160
|
</View>
|
|
161
161
|
</TouchableRipple>
|
|
162
162
|
|
|
163
|
-
{/* Gated on `measured`, not just `open`
|
|
163
|
+
{/* Gated on `measured`, not just `open` - see useAutoAlign's own comment and
|
|
164
164
|
SectionedDropdown's identical fix: without it, the popover mounts for one frame at a stale or
|
|
165
165
|
guessed alignment and visibly jumps once this open's own measurement lands. */}
|
|
166
166
|
<PopoverBody visible={open && measured} align={align} verticalAlign={verticalAlign} caretColor={menuBg} caretBorderColor={displayValue} caretBorderWidth={SWATCHES_BORDER_WIDTH} triggerSize={size}>
|
|
167
|
-
{/* Border matches this trigger's own current color (not a neutral gray)
|
|
167
|
+
{/* Border matches this trigger's own current color (not a neutral gray) - two triggers can
|
|
168
168
|
sit close together, so the popover needs a clear visual tie back to which one opened it,
|
|
169
169
|
not just its screen position. maxHeight (see useAutoAlign) only actually clamps on a screen
|
|
170
|
-
short enough that the full grid can't fit above or below the trigger either way
|
|
170
|
+
short enough that the full grid can't fit above or below the trigger either way - a short
|
|
171
171
|
landscape screen with the trigger row near the top, same case SectionedDropdown's own
|
|
172
172
|
ScrollView exists for. */}
|
|
173
173
|
<ScrollView style={[styles.swatches, { backgroundColor: menuBg, borderColor: displayValue, width: swatchesWidth, maxHeight }]} contentContainerStyle={styles.swatchesContent} showsVerticalScrollIndicator={false}>
|
|
@@ -201,7 +201,7 @@ const styles = StyleSheet.create({
|
|
|
201
201
|
anchor: {
|
|
202
202
|
position: 'relative'
|
|
203
203
|
},
|
|
204
|
-
// See SectionedDropdown's identical comment
|
|
204
|
+
// See SectionedDropdown's identical comment - React Native Web gives every position:'relative' view
|
|
205
205
|
// its own stacking context, so the elevation has to live on the anchor itself, not just the
|
|
206
206
|
// popover content nested inside it, to correctly paint above this anchor's own later siblings.
|
|
207
207
|
anchorOpen: {
|
|
@@ -229,12 +229,12 @@ const styles = StyleSheet.create({
|
|
|
229
229
|
shadowOffset: { height: 2, width: 0 },
|
|
230
230
|
shadowOpacity: 0.3,
|
|
231
231
|
shadowRadius: 8
|
|
232
|
-
// width is set inline above
|
|
232
|
+
// width is set inline above - border-box sizing means it has to include the border too
|
|
233
233
|
// (2*SWATCHES_BORDER_WIDTH), not just padding+content, or exactly enough room goes missing that
|
|
234
234
|
// the last column silently wraps to a new row. Anything wider than the exact sum just shows as
|
|
235
235
|
// dead space on the right edge of each row instead.
|
|
236
236
|
},
|
|
237
|
-
// Separate from `swatches` itself
|
|
237
|
+
// Separate from `swatches` itself - see SectionedDropdown's identical menu/menuContent split.
|
|
238
238
|
// The scroll frame's own bounds (including maxHeight) live on the ScrollView; the actual grid
|
|
239
239
|
// wrapping belongs to its contentContainerStyle, which sizes to the unclamped content instead.
|
|
240
240
|
swatchesContent: {
|
|
@@ -247,7 +247,7 @@ const styles = StyleSheet.create({
|
|
|
247
247
|
fontWeight: '700',
|
|
248
248
|
letterSpacing: 0.5
|
|
249
249
|
},
|
|
250
|
-
// borderRadius/height/width come from the `size` prop instead (see render-site override)
|
|
250
|
+
// borderRadius/height/width come from the `size` prop instead (see render-site override) - no
|
|
251
251
|
// fixed default here, since this style object is shared by every caller regardless of size.
|
|
252
252
|
trigger: {
|
|
253
253
|
alignItems: 'center',
|
|
@@ -255,7 +255,7 @@ const styles = StyleSheet.create({
|
|
|
255
255
|
justifyContent: 'center'
|
|
256
256
|
},
|
|
257
257
|
// Wraps just the icon/tag, inside the TouchableRipple, purely so useAutoAlign has a plain View
|
|
258
|
-
// to attach its measurement ref to
|
|
258
|
+
// to attach its measurement ref to - TouchableRipple itself doesn't forward a ref to a
|
|
259
259
|
// measurable native node. Padded horizontally so a multi-character tag doesn't butt right up
|
|
260
260
|
// against the trigger's own circular edge (an icon alone doesn't need it, but this style is
|
|
261
261
|
// shared by both).
|
package/src/LabeledDropdown.tsx
CHANGED
|
@@ -12,8 +12,8 @@ const POPOVER_WIDTH = 180
|
|
|
12
12
|
const ROW_HEIGHT = 36
|
|
13
13
|
const LIST_PADDING = 8
|
|
14
14
|
|
|
15
|
-
// Exported so a caller that needs to independently compute this popover's own placement
|
|
16
|
-
// alignOverride with, say, a @tastic/split-screen-zone-aware hook's result
|
|
15
|
+
// Exported so a caller that needs to independently compute this popover's own placement - to feed
|
|
16
|
+
// alignOverride with, say, a @tastic/split-screen-zone-aware hook's result - can match its real
|
|
17
17
|
// content dimensions exactly, rather than hardcoding or re-deriving this component's own internal
|
|
18
18
|
// sizing (which alignOverride itself deliberately doesn't take a dependency on any particular
|
|
19
19
|
// zone-awareness package to compute).
|
|
@@ -40,7 +40,7 @@ interface Props<T extends string> {
|
|
|
40
40
|
// (see alignOverride below) decide. Matches PopoverBody's own align prop meaning.
|
|
41
41
|
align?: 'left' | 'right' | 'center'
|
|
42
42
|
// Substitutes this package's own plain useAutoAlign-based placement wholesale (align,
|
|
43
|
-
// verticalAlign, maxHeight, measured, triggerRef) with a caller-supplied one
|
|
43
|
+
// verticalAlign, maxHeight, measured, triggerRef) with a caller-supplied one - e.g. a popover
|
|
44
44
|
// living inside a @tastic/split-screen zone, where the plain window-relative decision can pick a
|
|
45
45
|
// direction that overflows into the shared row instead of the actual zone boundary. Omit for the
|
|
46
46
|
// ordinary case (a popover with nothing but the screen edge to avoid).
|
|
@@ -48,8 +48,8 @@ interface Props<T extends string> {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// A name-trigger + selection popover in the same visual style as @tastic/profile's own
|
|
51
|
-
// ProfilePicker trigger
|
|
52
|
-
// fills entirely with the seat's own accent color
|
|
51
|
+
// ProfilePicker trigger - an uppercase text label + chevron, opening a list whose selected row
|
|
52
|
+
// fills entirely with the seat's own accent color - rather than this package's own
|
|
53
53
|
// SectionedDropdown, whose trigger is a fixed icon with a gauge ring around it. For a choice that
|
|
54
54
|
// reads more like "who/what you're playing" than "a game setting" (a CPU seat's own difficulty,
|
|
55
55
|
// occupying the same slot a human seat's ProfilePicker would), the always-visible text label
|
|
@@ -61,7 +61,7 @@ export function LabeledDropdown<T extends string>({ id, host, options, value, on
|
|
|
61
61
|
const open = host.openId === id
|
|
62
62
|
const selected = options.find((o) => o.value === value) ?? null
|
|
63
63
|
const contentHeight = LIST_PADDING * 2 + options.length * ROW_HEIGHT
|
|
64
|
-
// Always called, even when alignOverride is supplied and this result goes unused
|
|
64
|
+
// Always called, even when alignOverride is supplied and this result goes unused - hooks can't be
|
|
65
65
|
// called conditionally. A caller passing alignOverride (e.g. this package's own
|
|
66
66
|
// useZoneClampedAlign, which already wraps this same hook) does end up measuring the trigger
|
|
67
67
|
// twice; an acceptable tradeoff for keeping this component's own hook usage unconditional and
|
|
@@ -87,7 +87,7 @@ export function LabeledDropdown<T extends string>({ id, host, options, value, on
|
|
|
87
87
|
</View>
|
|
88
88
|
</TouchableRipple>
|
|
89
89
|
|
|
90
|
-
{/* Gated on `measured`, not just `open`
|
|
90
|
+
{/* Gated on `measured`, not just `open` - see InlineColorPicker's identical fix. */}
|
|
91
91
|
<PopoverBody visible={open && measured} align={align} verticalAlign={verticalAlign} caretColor={menuBg} caretBorderColor={color} caretBorderWidth={2} triggerSize={TRIGGER_HEIGHT}>
|
|
92
92
|
<ScrollView style={[styles.menu, { backgroundColor: menuBg, borderColor: color, width: POPOVER_WIDTH, maxHeight }]} contentContainerStyle={styles.menuContent} showsVerticalScrollIndicator={false}>
|
|
93
93
|
{options.map((option) => {
|
|
@@ -115,7 +115,7 @@ const styles = StyleSheet.create({
|
|
|
115
115
|
position: 'relative'
|
|
116
116
|
},
|
|
117
117
|
// React Native Web gives every position:'relative' view its own stacking context, so the
|
|
118
|
-
// elevation has to live on the anchor itself
|
|
118
|
+
// elevation has to live on the anchor itself - see PopoverBody's own stacking-context notes.
|
|
119
119
|
anchorOpen: {
|
|
120
120
|
zIndex: 100
|
|
121
121
|
},
|
package/src/PlayerSetupPanel.tsx
CHANGED
|
@@ -10,12 +10,12 @@ import { ReadyButton } from './ReadyButton'
|
|
|
10
10
|
import { AlignResult, PopoverRotation } from './useAutoAlign'
|
|
11
11
|
import { PopoverHost, usePopoverHost } from './usePopoverHost'
|
|
12
12
|
|
|
13
|
-
// This panel's own internal color/CPU-difficulty popover ids, derived from idPrefix
|
|
13
|
+
// This panel's own internal color/CPU-difficulty popover ids, derived from idPrefix - exported so a
|
|
14
14
|
// caller that needs to know whether one of them is currently open (e.g. to compute the `open`
|
|
15
15
|
// argument useZoneClampedAlign's own first parameter needs, ahead of handing its result to
|
|
16
16
|
// colorAlignOverride/cpuDifficultyAlignOverride below) doesn't have to hardcode or guess this
|
|
17
17
|
// panel's own internal naming convention. Matches secondPickerId's own job for a caller's own
|
|
18
|
-
// externally-constructed picker
|
|
18
|
+
// externally-constructed picker - this is the equivalent for the two pickers this panel owns
|
|
19
19
|
// itself, which have no prop of their own to name their id with.
|
|
20
20
|
export function getColorPopoverId(idPrefix: string): string {
|
|
21
21
|
return `${idPrefix}-color`
|
|
@@ -31,43 +31,43 @@ export interface PlayerSetupPanelProps<C extends string = string> {
|
|
|
31
31
|
// Shared popover host, when this panel's own popovers should be mutually exclusive with another
|
|
32
32
|
// panel's or a shared-controls row's (e.g. vs-CPU: only one human is ever driving both slots, so
|
|
33
33
|
// having both YOU's and CPU's pickers open at once is just clutter, not a useful simultaneous-edit
|
|
34
|
-
// case). Omit to fall back to this panel's own independent host
|
|
34
|
+
// case). Omit to fall back to this panel's own independent host - the two-player case, where two
|
|
35
35
|
// real people editing at once is the point.
|
|
36
36
|
host?: PopoverHost
|
|
37
37
|
label?: string
|
|
38
38
|
// Applied only to the plain `label` fallback text below (ProfilePicker/LabeledDropdown render
|
|
39
|
-
// their own fixed chrome regardless of this prop)
|
|
39
|
+
// their own fixed chrome regardless of this prop) - omit to inherit whichever font the calling
|
|
40
40
|
// screen's own theme resolves for a labelSmall Text, rather than this package forcing its own
|
|
41
41
|
// MONO_FONT onto what's really the host app's own copy.
|
|
42
42
|
labelFontFamily?: string
|
|
43
43
|
color: string
|
|
44
44
|
onColorChange: (hex: string) => void
|
|
45
45
|
swatches: SeedColor[]
|
|
46
|
-
// The other player's current color
|
|
47
|
-
// removed from it entirely. Unless allowSwapTaken is set
|
|
46
|
+
// The other player's current color - stays visible in the swatch grid but disabled, rather than
|
|
47
|
+
// removed from it entirely. Unless allowSwapTaken is set - see InlineColorPicker's own doc.
|
|
48
48
|
takenColor?: string
|
|
49
49
|
allowSwapTaken?: boolean
|
|
50
|
-
// Passed straight through to the internal InlineColorPicker's own `alignOverride`
|
|
50
|
+
// Passed straight through to the internal InlineColorPicker's own `alignOverride` - this panel
|
|
51
51
|
// has no way for a caller to reach that picker directly (it's not a ReactNode slot the way
|
|
52
52
|
// secondPicker is, see that prop's own doc), so a caller inside a @tastic/split-screen zone
|
|
53
53
|
// needs this to substitute useZoneClampedAlign's result for the color picker's own popover.
|
|
54
54
|
colorAlignOverride?: AlignResult
|
|
55
|
-
// Passed straight through to the internal InlineColorPicker's own `rotation`
|
|
55
|
+
// Passed straight through to the internal InlineColorPicker's own `rotation` - same reasoning as
|
|
56
56
|
// colorAlignOverride above (no other way for a caller to reach that picker directly), for a
|
|
57
57
|
// caller whose own rotation reading needs to differ from InlineColorPicker's default ambient
|
|
58
58
|
// useRotation() read (see that component's own `rotation` doc for when that's worth doing).
|
|
59
59
|
colorRotation?: PopoverRotation
|
|
60
60
|
isHuman: boolean
|
|
61
|
-
// Whatever sits beside the color picker in pickerRow, if anything
|
|
61
|
+
// Whatever sits beside the color picker in pickerRow, if anything - a ControlSchemePicker, a raw
|
|
62
62
|
// SectionedDropdown, or nothing at all. The five apps this component was extracted from each
|
|
63
63
|
// duplicated this entire panel and differed only in what (if anything) occupied this one slot,
|
|
64
64
|
// each wanting a genuinely different concept there (which physical input drives a seat, vs. a
|
|
65
65
|
// pull/push "feel" preference, vs. no such concept at all for a game with no per-seat setting to
|
|
66
|
-
// pick)
|
|
66
|
+
// pick) - a ReactNode slot the caller renders its own picker into is what actually generalizes
|
|
67
67
|
// across all of them, rather than this component baking in one shape (e.g. ControlSchemePicker's
|
|
68
68
|
// own prop set) that most of its consumers don't need at all.
|
|
69
69
|
secondPicker?: ReactNode
|
|
70
|
-
// The popover id secondPicker opens under, if it opens one at all
|
|
70
|
+
// The popover id secondPicker opens under, if it opens one at all - lets pickerRow's own
|
|
71
71
|
// "elevate above later siblings while one of my own popovers is open" logic (see pickerRowOpen)
|
|
72
72
|
// react to secondPicker's popover the same way it already does for the color picker's, without
|
|
73
73
|
// this component needing any opinion on what secondPicker actually renders. Omit when secondPicker
|
|
@@ -82,24 +82,24 @@ export interface PlayerSetupPanelProps<C extends string = string> {
|
|
|
82
82
|
profiles?: Profile[]
|
|
83
83
|
selectedProfileId?: string | null
|
|
84
84
|
takenProfileId?: string | null
|
|
85
|
-
// What the name trigger shows before any profile's selected
|
|
85
|
+
// What the name trigger shows before any profile's selected - this seat's own 'P1'/'P2' (see
|
|
86
86
|
// ProfilePicker's own doc for why it's never "Player"). Required whenever profiles is provided,
|
|
87
87
|
// since the trigger always needs some idle text.
|
|
88
88
|
guestLabel?: string
|
|
89
89
|
onProfileSelect?: (profile: Profile | null) => void
|
|
90
|
-
// Navigates to a profile-management screen
|
|
90
|
+
// Navigates to a profile-management screen - typically only ever wired up for one seat (a rotated
|
|
91
91
|
// second zone often can't host a working text keyboard).
|
|
92
92
|
onManageProfiles?: () => void
|
|
93
93
|
// Generic over the calling app's own CPU-difficulty union, matching LabeledDropdownOption<T>'s own
|
|
94
|
-
// genericness
|
|
94
|
+
// genericness - this package has no opinion on what difficulties exist.
|
|
95
95
|
cpuDifficulty?: C
|
|
96
96
|
cpuDifficultyOptions?: LabeledDropdownOption<C>[]
|
|
97
97
|
onCpuDifficultyChange?: (value: C) => void
|
|
98
|
-
// Passed straight through to the internal LabeledDropdown's own `alignOverride`
|
|
98
|
+
// Passed straight through to the internal LabeledDropdown's own `alignOverride` - same reasoning
|
|
99
99
|
// as colorAlignOverride above, for the CPU-difficulty picker instead of the color picker.
|
|
100
100
|
cpuDifficultyAlignOverride?: AlignResult
|
|
101
101
|
// InlineColorPicker's own `icon` for a human vs. a non-human seat (overridden by a selected
|
|
102
|
-
// profile's own `tag`, same precedence InlineColorPicker itself applies)
|
|
102
|
+
// profile's own `tag`, same precedence InlineColorPicker itself applies) - defaults match every
|
|
103
103
|
// consumer's own convention. A caller with its own extra CPU states (e.g. a "no CPU" slot) computes
|
|
104
104
|
// its own icon string and passes it through cpuIcon rather than this component needing any opinion
|
|
105
105
|
// on what those states are.
|
|
@@ -115,23 +115,23 @@ export interface PlayerSetupPanelProps<C extends string = string> {
|
|
|
115
115
|
// Snake), which had all independently converged on this exact skeleton.
|
|
116
116
|
//
|
|
117
117
|
// Owns its own popover host shared by its own color/profile/difficulty pickers unless a `host` is
|
|
118
|
-
// passed in to share with another panel (or a shared-controls row) instead
|
|
118
|
+
// passed in to share with another panel (or a shared-controls row) instead - same convention as
|
|
119
119
|
// every popover-hosting component in this package.
|
|
120
120
|
export function PlayerSetupPanel<C extends string = string>({ idPrefix, host, label, labelFontFamily, color, onColorChange, swatches, takenColor, allowSwapTaken, colorAlignOverride, colorRotation, isHuman, secondPicker, secondPickerId, ready, onToggleReady, dark, showReadyButton = true, profiles, selectedProfileId, takenProfileId, guestLabel, onProfileSelect, onManageProfiles, cpuDifficulty, cpuDifficultyOptions, onCpuDifficultyChange, cpuDifficultyAlignOverride, humanIcon = 'face-man', cpuIcon = 'robot' }: PlayerSetupPanelProps<C>) {
|
|
121
121
|
const ownHost = usePopoverHost()
|
|
122
122
|
const popover = host ?? ownHost
|
|
123
|
-
// Checking *this panel's own* ids, not just "is anything open on the host"
|
|
123
|
+
// Checking *this panel's own* ids, not just "is anything open on the host" - when `host` is
|
|
124
124
|
// shared, another panel's (or a shared-controls row's) popovers also live on it, and elevating
|
|
125
125
|
// this panel for those too would tie it with whichever of them actually has the open popover,
|
|
126
126
|
// letting DOM order (wrongly) decide which one paints on top.
|
|
127
127
|
const ownPopoverOpen = popover.openId?.startsWith(`${idPrefix}-`) ?? false
|
|
128
|
-
// Narrower than ownPopoverOpen on purpose
|
|
128
|
+
// Narrower than ownPopoverOpen on purpose - only true for a popover actually inside pickerRow
|
|
129
129
|
// itself (color, or secondPicker's own popover when it has one and secondPickerId names it). See
|
|
130
130
|
// pickerRowOpen's own style comment for why pickerRow needs this in addition to panelOpen above.
|
|
131
131
|
const pickerRowPopoverOpen = popover.openId === getColorPopoverId(idPrefix) || (secondPickerId !== undefined && popover.openId === secondPickerId)
|
|
132
132
|
const showProfilePicker = isHuman && profiles !== undefined && !!onProfileSelect
|
|
133
133
|
const showCpuDifficulty = !isHuman && cpuDifficulty !== undefined && cpuDifficultyOptions !== undefined && !!onCpuDifficultyChange
|
|
134
|
-
// The color button's own tag display (see InlineColorPicker)
|
|
134
|
+
// The color button's own tag display (see InlineColorPicker) - read live from the selected
|
|
135
135
|
// profile rather than snapshotted at selection time, since a caller typically has no other
|
|
136
136
|
// editing surface for it on this same screen.
|
|
137
137
|
const selectedProfile = profiles?.find((p) => p.id === selectedProfileId) ?? null
|
|
@@ -152,7 +152,7 @@ export function PlayerSetupPanel<C extends string = string>({ idPrefix, host, la
|
|
|
152
152
|
|
|
153
153
|
<View style={[styles.pickerRow, pickerRowPopoverOpen && styles.pickerRowOpen]}>
|
|
154
154
|
{/* Human slots show the selected profile's own tag when it has one, falling back to
|
|
155
|
-
humanIcon; a non-human slot always falls back to cpuIcon
|
|
155
|
+
humanIcon; a non-human slot always falls back to cpuIcon - this is what actually
|
|
156
156
|
distinguishes "you" from "the CPU" now, not a fixed icon. */}
|
|
157
157
|
<InlineColorPicker id={getColorPopoverId(idPrefix)} host={popover} value={color} onChange={onColorChange} swatches={swatches} takenValue={takenColor} allowSwapTaken={allowSwapTaken} alignOverride={colorAlignOverride} rotation={colorRotation} dark={dark} tag={isHuman ? selectedProfile?.tag : undefined} icon={isHuman ? humanIcon : cpuIcon} />
|
|
158
158
|
|
|
@@ -169,13 +169,13 @@ const styles = StyleSheet.create({
|
|
|
169
169
|
alignItems: 'center',
|
|
170
170
|
gap: 12
|
|
171
171
|
},
|
|
172
|
-
// See SectionedDropdown's anchorOpen comment
|
|
172
|
+
// See SectionedDropdown's anchorOpen comment - this panel is typically a sibling of another
|
|
173
173
|
// panel (and/or a shared-controls row), so a popover escaping this panel's bounds needs the panel
|
|
174
174
|
// itself elevated, not just the popover content, to paint above a later sibling.
|
|
175
175
|
panelOpen: {
|
|
176
176
|
zIndex: 100
|
|
177
177
|
},
|
|
178
|
-
// Same gap as `panel`'s own vertical rhythm, reused horizontally
|
|
178
|
+
// Same gap as `panel`'s own vertical rhythm, reused horizontally - the color picker and (when
|
|
179
179
|
// present) secondPicker sit side by side within this row instead of stacked in the panel's own
|
|
180
180
|
// column.
|
|
181
181
|
pickerRow: {
|
|
@@ -183,10 +183,10 @@ const styles = StyleSheet.create({
|
|
|
183
183
|
flexDirection: 'row',
|
|
184
184
|
gap: 12
|
|
185
185
|
},
|
|
186
|
-
// A popover is an absolutely-positioned sibling of its own trigger
|
|
186
|
+
// A popover is an absolutely-positioned sibling of its own trigger - so a tall one (the color
|
|
187
187
|
// swatch grid) can extend down far enough to overlap the ReadyButton below, which paints on top of
|
|
188
188
|
// it by default DOM order since it's a later sibling within `panel`. `panel`'s own panelOpen
|
|
189
|
-
// elevation only helps this panel paint above OTHER panels (see its own comment)
|
|
189
|
+
// elevation only helps this panel paint above OTHER panels (see its own comment) - it does nothing
|
|
190
190
|
// for stacking *within* this panel, since React Native Web gives each View its own stacking
|
|
191
191
|
// context. This is what actually fixes that: elevate pickerRow itself above its own later sibling
|
|
192
192
|
// whenever one of its own popovers is open.
|
package/src/PopoverBody.tsx
CHANGED
|
@@ -3,27 +3,27 @@ import { StyleSheet, View, ViewStyle } from 'react-native'
|
|
|
3
3
|
|
|
4
4
|
const CARET_SIZE = 7
|
|
5
5
|
// How far the fill triangle's base sits past the border triangle's own base, producing the ring of
|
|
6
|
-
// caretBorderColor around it
|
|
6
|
+
// caretBorderColor around it - kept fixed regardless of caretBorderWidth; tuned by eye.
|
|
7
7
|
const CARET_RING_OFFSET = 2
|
|
8
8
|
// How far both triangles dip past the box's own edge (top edge when below the trigger, bottom edge
|
|
9
|
-
// when above it
|
|
10
|
-
// lets it paint over the box's own border there instead of butting up against it
|
|
9
|
+
// when above it - see verticalAlign). The caret renders after (on top of) the box, so this overlap
|
|
10
|
+
// lets it paint over the box's own border there instead of butting up against it - a seam shows if
|
|
11
11
|
// the two edges only touch, since neither is guaranteed to land on the exact same subpixel.
|
|
12
12
|
const CARET_DIP = 1
|
|
13
13
|
|
|
14
14
|
interface Props {
|
|
15
15
|
visible: boolean
|
|
16
16
|
children: ReactNode
|
|
17
|
-
// Which edge of the trigger the popover aligns to
|
|
17
|
+
// Which edge of the trigger the popover aligns to - 'center' (default) centers the popover
|
|
18
18
|
// under/over its trigger regardless of either one's width; 'left'/'right' instead align that
|
|
19
19
|
// edge flush with the trigger's matching edge, growing away from it. A trigger sitting right at
|
|
20
20
|
// the screen's edge (e.g. the rightmost of a row of icons, or a right-side player's panel) needs
|
|
21
|
-
// an explicit 'left' or 'right'
|
|
21
|
+
// an explicit 'left' or 'right' - centering alone can still overflow there - to guarantee which
|
|
22
22
|
// direction it grows.
|
|
23
23
|
align?: 'left' | 'right' | 'center'
|
|
24
|
-
// Whether the popover opens below the trigger (default
|
|
24
|
+
// Whether the popover opens below the trigger (default - the usual case) or above it. Flips both
|
|
25
25
|
// the content's own position and the caret's direction, so it still reads as pointing back at the
|
|
26
|
-
// trigger either way. Meant for a trigger that doesn't have enough room below it to fit
|
|
26
|
+
// trigger either way. Meant for a trigger that doesn't have enough room below it to fit - a short
|
|
27
27
|
// landscape screen with the trigger row near the top is the case this actually shows up in.
|
|
28
28
|
verticalAlign?: 'below' | 'above'
|
|
29
29
|
// Small triangle pointing back at the trigger, reinforcing the visual link beyond just position
|
|
@@ -31,22 +31,22 @@ interface Props {
|
|
|
31
31
|
// caretColor (a solid, borderless caret) when only caretColor is given.
|
|
32
32
|
caretColor?: string
|
|
33
33
|
caretBorderColor?: string
|
|
34
|
-
// Must match the popover box's own borderWidth
|
|
34
|
+
// Must match the popover box's own borderWidth - a fixed ring thickness would only happen to
|
|
35
35
|
// match whichever box first used it, and look like a mismatched outline on any other consumer.
|
|
36
36
|
caretBorderWidth?: number
|
|
37
|
-
// The trigger's own width, in px
|
|
37
|
+
// The trigger's own width, in px - required whenever a caret is shown, so the caret can be pinned
|
|
38
38
|
// to the trigger's actual center via a fixed offset rather than the (usually much wider) content
|
|
39
39
|
// box's own midpoint, which only coincides with the trigger's center by coincidence, if at all.
|
|
40
40
|
triggerSize?: number
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// Rendered inline as an absolutely-positioned sibling of its own trigger (never via Portal), so it
|
|
44
|
-
// inherits any rotation transform the trigger's zone applies
|
|
44
|
+
// inherits any rotation transform the trigger's zone applies - a Portal-based popover renders at
|
|
45
45
|
// the app root, outside that transform, and would end up upside-down relative to its own trigger
|
|
46
46
|
// once a face-to-face zone gets flipped 180°. Positioned via percentage (`top:'100%'`/`bottom:'100%'`)
|
|
47
47
|
// against the trigger's own wrapper rather than a measured pixel rect, so no onLayout/measurement
|
|
48
48
|
// plumbing is needed here and it resolves correctly regardless of how deep the trigger sits in the
|
|
49
|
-
// tree
|
|
49
|
+
// tree - the caller (useAutoAlign) is the one that actually measures, to decide which side to grow
|
|
50
50
|
// toward in the first place.
|
|
51
51
|
export function PopoverBody({ visible, children, align = 'center', verticalAlign = 'below', caretColor, caretBorderColor, caretBorderWidth = 1, triggerSize = 0 }: Props) {
|
|
52
52
|
if (!visible) return null
|
|
@@ -60,13 +60,13 @@ export function PopoverBody({ visible, children, align = 'center', verticalAlign
|
|
|
60
60
|
// pinning its *center* at some offset means placing its own `left`/`right` half a footprint short
|
|
61
61
|
// of that offset. Every align pins one edge of `content` flush with the matching edge of the
|
|
62
62
|
// trigger-sized anchor (contentCenter stretches left:0/right:0 to do this too, rather than
|
|
63
|
-
// centering via a percentage `transform`
|
|
63
|
+
// centering via a percentage `transform` - those need the child's own auto-resolved width fed
|
|
64
64
|
// back into the transform, which native has been unreliable about; plain flexbox centering
|
|
65
65
|
// doesn't), so triggerSize/2 from that shared edge always lands on the trigger's true center,
|
|
66
66
|
// however much wider the actual popover box grows.
|
|
67
67
|
const caretOffset = (halfFootprint: number): ViewStyle => (align === 'right' ? { right: triggerSize / 2 - halfFootprint } : { left: triggerSize / 2 - halfFootprint })
|
|
68
68
|
// Below the trigger: caret sits at content's top edge, apex pointing up (a filled border-bottom
|
|
69
|
-
// makes an apex-up triangle), dipping upward past that edge. Above the trigger: mirrored
|
|
69
|
+
// makes an apex-up triangle), dipping upward past that edge. Above the trigger: mirrored - caret
|
|
70
70
|
// sits at content's bottom edge, apex pointing down (filled border-top), dipping downward past it.
|
|
71
71
|
const caretFill = above ? 'borderTopColor' : 'borderBottomColor'
|
|
72
72
|
const caretWidthProp = above ? 'borderTopWidth' : 'borderBottomWidth'
|
|
@@ -75,7 +75,7 @@ export function PopoverBody({ visible, children, align = 'center', verticalAlign
|
|
|
75
75
|
return (
|
|
76
76
|
<View style={[styles.content, alignStyle, verticalStyle]}>
|
|
77
77
|
{children}
|
|
78
|
-
{/* Painted after (on top of) the box above, dipping into its edge
|
|
78
|
+
{/* Painted after (on top of) the box above, dipping into its edge - see CARET_DIP - so it
|
|
79
79
|
reads as one continuous outline flowing from the trigger into the box, not a separate chip
|
|
80
80
|
butted up against it. */}
|
|
81
81
|
{caretColor && (
|
|
@@ -95,7 +95,7 @@ const styles = StyleSheet.create({
|
|
|
95
95
|
height: 0,
|
|
96
96
|
position: 'absolute',
|
|
97
97
|
width: 0,
|
|
98
|
-
// Explicit, not left to DOM order
|
|
98
|
+
// Explicit, not left to DOM order - the box it dips into is itself a position:'relative' view,
|
|
99
99
|
// which React Native Web always gives an implicit zIndex:0 (see PopoverBody's stacking-context
|
|
100
100
|
// notes elsewhere in this codebase), so painting on top needs a real zIndex to beat that
|
|
101
101
|
// reliably rather than relying on being the later sibling.
|
|
@@ -114,7 +114,7 @@ const styles = StyleSheet.create({
|
|
|
114
114
|
top: '100%'
|
|
115
115
|
},
|
|
116
116
|
// Stretched to the anchor's own (trigger) width, with alignItems centering the actual (usually
|
|
117
|
-
// much wider) popover box inside via plain flexbox
|
|
117
|
+
// much wider) popover box inside via plain flexbox - not `left:'50%'` plus a percentage
|
|
118
118
|
// `transform`, which depends on native resolving the box's own auto width before applying the
|
|
119
119
|
// transform and has proven unreliable there, visibly shifting the box off-center.
|
|
120
120
|
contentCenter: {
|
package/src/PressAwayOverlay.tsx
CHANGED
|
@@ -3,13 +3,13 @@ import { Pressable, StyleProp, StyleSheet, ViewStyle } from 'react-native'
|
|
|
3
3
|
interface Props {
|
|
4
4
|
active: boolean
|
|
5
5
|
onPress: () => void
|
|
6
|
-
// Defaults to filling the parent entirely
|
|
6
|
+
// Defaults to filling the parent entirely - pass a narrower rect (e.g. just one player's half of
|
|
7
7
|
// the screen) to scope how far "away" reaches for that player's own popovers.
|
|
8
8
|
style?: StyleProp<ViewStyle>
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
// Invisible full-bleed tap catcher for "press away to close." Render one per popover host (or per
|
|
12
|
-
// player's own screen zone
|
|
12
|
+
// player's own screen zone - see the package README for the split-screen wiring pattern), as an
|
|
13
13
|
// early sibling of that zone's real content: plain paint order then keeps every real control
|
|
14
14
|
// directly tappable (later siblings paint on top of this), while a tap on genuinely empty space
|
|
15
15
|
// falls through to this and closes whatever's open. Unmounts entirely rather than always rendering
|
package/src/ReadyButton.tsx
CHANGED
|
@@ -10,15 +10,15 @@ interface Props {
|
|
|
10
10
|
onToggleReady: () => void
|
|
11
11
|
// Lets a caller fade/hide a standalone Ready button (e.g. while a popover is open elsewhere on
|
|
12
12
|
// screen, so it doesn't visually collide with the popover's own positioning) without reflowing
|
|
13
|
-
// layout around it
|
|
13
|
+
// layout around it - pass `{ opacity: 0, pointerEvents: 'none' }` rather than conditionally
|
|
14
14
|
// unmounting.
|
|
15
15
|
style?: StyleProp<ViewStyle>
|
|
16
|
-
// Defaults to the system monospace font
|
|
16
|
+
// Defaults to the system monospace font - pass your own game's registered font to match its UI.
|
|
17
17
|
labelFontFamily?: string
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// Standalone toggle so it can render either embedded in a per-player panel or centered on its own
|
|
21
|
-
// below a shared row
|
|
21
|
+
// below a shared row - e.g. a vs-CPU mode with only one human player needs just one Ready toggle,
|
|
22
22
|
// not one tucked under each slot.
|
|
23
23
|
export function ReadyButton({ color, ready, onToggleReady, style, labelFontFamily = MONO_FONT }: Props) {
|
|
24
24
|
const readyButtonStyle = [styles.readyButton, { borderColor: color }, ready && { backgroundColor: color }, style]
|