@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.
Files changed (53) hide show
  1. package/README.md +110 -79
  2. package/dist/chrome/index.d.mts +239 -0
  3. package/dist/chrome/index.d.ts +239 -0
  4. package/dist/chrome/index.js +772 -0
  5. package/dist/chrome/index.mjs +44 -0
  6. package/dist/chunk-OTMMST5M.mjs +645 -0
  7. package/dist/guide/index.js +3 -3
  8. package/dist/guide/index.mjs +3 -3
  9. package/dist/index.d.mts +21 -252
  10. package/dist/index.d.ts +21 -252
  11. package/dist/index.js +20 -20
  12. package/dist/index.mjs +186 -791
  13. package/package.json +8 -1
  14. package/src/AchievementCatalogSection.tsx +1 -1
  15. package/src/AchievementRow.tsx +9 -9
  16. package/src/BaseSettingsDialog.tsx +31 -31
  17. package/src/BaseStatsScreen.tsx +13 -13
  18. package/src/ConfirmDialog.tsx +2 -2
  19. package/src/ControlSchemePicker.tsx +9 -9
  20. package/src/CornerActionButtons.tsx +6 -6
  21. package/src/CornerStatusBadges.tsx +8 -8
  22. package/src/InlineColorPicker.tsx +30 -30
  23. package/src/LabeledDropdown.tsx +8 -8
  24. package/src/PlayerSetupPanel.tsx +25 -25
  25. package/src/PopoverBody.tsx +16 -16
  26. package/src/PressAwayOverlay.tsx +2 -2
  27. package/src/ReadyButton.tsx +3 -3
  28. package/src/SectionedDropdown.tsx +31 -31
  29. package/src/SharedActionBand.tsx +6 -6
  30. package/src/SkiaGate.tsx +5 -5
  31. package/src/SkiaGate.web.tsx +2 -2
  32. package/src/StaggeredWord.tsx +9 -9
  33. package/src/StatRow.tsx +3 -3
  34. package/src/StatSection.tsx +3 -3
  35. package/src/TriggerGauge.tsx +54 -54
  36. package/src/TriggerGaugeHost.tsx +2 -2
  37. package/src/chrome/index.ts +22 -0
  38. package/src/fonts.ts +1 -1
  39. package/src/guide/DirectionKeysHint.tsx +3 -3
  40. package/src/guide/GuideProvider.tsx +7 -7
  41. package/src/guide/HowToPlayDialog.tsx +7 -7
  42. package/src/guide/SeatDiagram.tsx +3 -3
  43. package/src/guide/SwipeHint.tsx +4 -4
  44. package/src/guide/guideSlice.ts +7 -7
  45. package/src/guide/index.ts +4 -3
  46. package/src/guide/types.ts +1 -1
  47. package/src/index.ts +1 -1
  48. package/src/loadSkiaWeb.ts +2 -2
  49. package/src/loadSkiaWeb.web.ts +2 -2
  50. package/src/overlayCard.ts +3 -3
  51. package/src/useAutoAlign.ts +23 -23
  52. package/src/usePopoverHost.ts +1 -1
  53. 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 — this only decides a
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 — `defaultColors` (this component's own default
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 — just not guaranteed to be a full rectangle.
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 — to
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 — can match this
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 — a caller computing
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* — trigger fill, swatch fill, and the contrast/caret/border
64
- // colors derived from them — leaving `value`/`swatch.value` themselves untouched for `selected`/
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 — stays visible in the grid (so the full palette
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 — see that prop.
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 — useful when the "other" slot is CPU-
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 — e.g.
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 — see SectionedDropdown's identical prop for the
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 — defaults to a plain palette. Pass a distinct icon per slot (e.g. a face for a
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` — e.g. a saved player profile's own tag. Falls back to `icon` when
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 — defaults to this package's
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 — a color pick is a single,
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 — defaults to DEFAULT_SIZE. Overridable so a caller sharing a row
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 — a modal color
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 — hooks can't be
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 — this bumps emoji up to the icon's own
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` — see useAutoAlign's own comment and
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) — two triggers can
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 — a short
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 — React Native Web gives every position:'relative' view
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 — border-box sizing means it has to include the border too
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 — see SectionedDropdown's identical menu/menuContent split.
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) — no
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 — TouchableRipple itself doesn't forward a ref to a
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).
@@ -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 — to feed
16
- // alignOverride with, say, a @tastic/split-screen-zone-aware hook's result — can match its real
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 — e.g. a popover
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 — 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
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 — hooks can't be
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` — see InlineColorPicker's identical fix. */}
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 — see PopoverBody's own stacking-context notes.
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
  },
@@ -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 — exported so a
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 — this is the equivalent for the two pickers this panel owns
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 — the two-player case, where two
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) — omit to inherit whichever font the calling
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 — stays visible in the swatch grid but disabled, rather than
47
- // removed from it entirely. Unless allowSwapTaken is set — see InlineColorPicker's own doc.
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` — this panel
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` — same reasoning as
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 — a ControlSchemePicker, a raw
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) — a ReactNode slot the caller renders its own picker into is what actually generalizes
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 — lets pickerRow's own
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 — this seat's own 'P1'/'P2' (see
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 — typically only ever wired up for one seat (a rotated
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 — this package has no opinion on what difficulties exist.
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` — same reasoning
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) — defaults match every
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 — same convention as
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" — when `host` is
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 — only true for a popover actually inside pickerRow
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) — read live from the selected
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 — this is what actually
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 — this panel is typically a sibling of another
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 — the color picker and (when
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 — so a tall one (the color
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) — it does nothing
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.
@@ -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 — kept fixed regardless of caretBorderWidth; tuned by eye.
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 — 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
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 — 'center' (default) centers the popover
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' — centering alone can still overflow there — to guarantee which
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 — the usual case) or above it. Flips both
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 — a short
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 — a fixed ring thickness would only happen to
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 — required whenever a caret is shown, so the caret can be pinned
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 — a Portal-based popover renders at
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 — the caller (useAutoAlign) is the one that actually measures, to decide which side to grow
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` — those need the child's own auto-resolved width fed
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 — caret
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 — see CARET_DIP — so it
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 — the box it dips into is itself a position:'relative' view,
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 — not `left:'50%'` plus a percentage
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: {
@@ -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 — pass a narrower rect (e.g. just one player's half of
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 — see the package README for the split-screen wiring pattern), as an
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
@@ -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 — pass `{ opacity: 0, pointerEvents: 'none' }` rather than conditionally
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 — pass your own game's registered font to match its UI.
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 — e.g. a vs-CPU mode with only one human player needs just one Ready toggle,
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]