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