@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
package/src/loadSkiaWeb.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Native counterpart to loadSkiaWeb.web.ts
|
|
1
|
+
// Native counterpart to loadSkiaWeb.web.ts - Skia on native (via JSI, no WASM) is ready as soon as
|
|
2
2
|
// the app's native module is linked, so there's nothing to actually wait on here. This file must
|
|
3
3
|
// never import '@shopify/react-native-skia/src/web' (even conditionally behind a Platform.OS check):
|
|
4
|
-
// that module pulls in canvaskit-wasm, which does `require('fs')`
|
|
4
|
+
// that module pulls in canvaskit-wasm, which does `require('fs')` - a runtime check doesn't stop
|
|
5
5
|
// Metro from statically resolving that import to build the native bundle graph, and Metro has no
|
|
6
6
|
// `fs` for iOS/Android, so the build fails outright. Metro's own platform-extension resolution (this
|
|
7
7
|
// file vs. loadSkiaWeb.web.ts) is what actually keeps the web-only module out of the native graph.
|
package/src/loadSkiaWeb.web.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { LoadSkiaWeb } from '@shopify/react-native-skia/src/web'
|
|
2
2
|
|
|
3
|
-
// Skia's web target renders through CanvasKit, a WASM build of Skia fetched at runtime
|
|
3
|
+
// Skia's web target renders through CanvasKit, a WASM build of Skia fetched at runtime - nothing
|
|
4
4
|
// using @shopify/react-native-skia's React components can draw a single frame until this resolves.
|
|
5
5
|
// Kept as a thin wrapper (rather than importing LoadSkiaWeb directly at call sites) purely so this
|
|
6
|
-
// file's `.ts` counterpart, loadSkiaWeb.ts, gives native an identically-shaped async no-op
|
|
6
|
+
// file's `.ts` counterpart, loadSkiaWeb.ts, gives native an identically-shaped async no-op - Metro's
|
|
7
7
|
// platform-extension resolution picks whichever one matches the bundle target, so canvaskit-wasm's
|
|
8
8
|
// `require('fs')' never even gets resolved into a native/iOS/Android bundle.
|
|
9
9
|
export function loadSkiaWeb(): Promise<void> {
|
package/src/overlayCard.ts
CHANGED
|
@@ -8,7 +8,7 @@ export interface OverlayCardColors {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
// The literal-black/white-by-appearance "retro" formula this package's own BaseSettingsDialog and
|
|
11
|
-
// ConfirmDialog each independently re-derived
|
|
11
|
+
// ConfirmDialog each independently re-derived - deliberately NOT auto-paper's own (seed-color-tinted)
|
|
12
12
|
// background role, see either component's own "High-contrast retro look" comment for why. Every
|
|
13
13
|
// fleet app's own Game-Over/Round-Over/Match-Over/Quit-confirmation overlay re-derives this same
|
|
14
14
|
// three-or-four-line formula again, which is what this export exists to replace.
|
|
@@ -21,7 +21,7 @@ export function getOverlayCardColors(dark: boolean): OverlayCardColors {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
// Only the shell shape every overlay card shares (backdrop + card geometry)
|
|
24
|
+
// Only the shell shape every overlay card shares (backdrop + card geometry) - deliberately not the
|
|
25
25
|
// title/body styling, which already legitimately varies by consumer (each app's own score/badge/zone
|
|
26
26
|
// content). A consumer applies `{backgroundColor: cardBg, borderColor: cardBorder}` from
|
|
27
27
|
// getOverlayCardColors on top of `card` itself, same as every existing call site already does. The
|
|
@@ -51,7 +51,7 @@ export const overlayStyles = StyleSheet.create({
|
|
|
51
51
|
// Back) on the left, a contained primary on the right, each growing to split the row but never
|
|
52
52
|
// narrower than 128. Moved here from ConfirmDialog when the how-to-play card needed the identical
|
|
53
53
|
// row, so the two can't drift into different button shapes (the guide briefly shipped text-mode
|
|
54
|
-
// Skip/Back
|
|
54
|
+
// Skip/Back - the only text buttons in the fleet).
|
|
55
55
|
export const overlayActionStyles = StyleSheet.create({
|
|
56
56
|
// minWidth (not flexBasis) is each button's real floor - whenever both fit side by side, flexGrow
|
|
57
57
|
// still splits any leftover width evenly beyond that floor, same visual result as a plain flex: 1
|
package/src/useAutoAlign.ts
CHANGED
|
@@ -2,18 +2,18 @@ import { type ViewRotation } from '@tastic/core'
|
|
|
2
2
|
import { RefObject, useCallback, useEffect, useRef, useState } from 'react'
|
|
3
3
|
import { useWindowDimensions, View } from 'react-native'
|
|
4
4
|
|
|
5
|
-
// Minimum breathing room between a popover's outer edge and the screen edge
|
|
5
|
+
// Minimum breathing room between a popover's outer edge and the screen edge - matches the general
|
|
6
6
|
// "don't butt content right up against the bezel" margin used elsewhere in this app.
|
|
7
7
|
const EDGE_MARGIN = 12
|
|
8
8
|
|
|
9
9
|
export type PopoverAlign = 'left' | 'right' | 'center'
|
|
10
10
|
export type PopoverVerticalAlign = 'below' | 'above'
|
|
11
|
-
// Same set of values as @tastic/core's own ViewRotation
|
|
11
|
+
// Same set of values as @tastic/core's own ViewRotation - re-exported under this package's existing
|
|
12
12
|
// name rather than switching every call site to import ViewRotation directly, since "popover
|
|
13
13
|
// rotation" reads clearer at these call sites than the more general core name.
|
|
14
14
|
export type PopoverRotation = ViewRotation
|
|
15
15
|
|
|
16
|
-
// Same shape useAutoAlign itself returns
|
|
16
|
+
// Same shape useAutoAlign itself returns - exported so a caller can substitute this package's own
|
|
17
17
|
// plain, screen-edge-aware placement wholesale via a component's own `alignOverride` prop (see
|
|
18
18
|
// LabeledDropdown's, SectionedDropdown's, and InlineColorPicker's own `alignOverride`), most often
|
|
19
19
|
// with useZoneClampedAlign's result for a popover living inside a @tastic/split-screen zone, where
|
|
@@ -31,13 +31,13 @@ export interface AlignResult {
|
|
|
31
31
|
|
|
32
32
|
// Reflects a rect from the PRE-rotation layout frame measureInWindow actually reports (see
|
|
33
33
|
// useAutoAlign's own comment below for why that's what it reports, not the post-rotation visual
|
|
34
|
-
// position) into its true, post-rotation position
|
|
34
|
+
// position) into its true, post-rotation position - needed for a trigger sitting inside a
|
|
35
35
|
// @tastic/split-screen-style FakeLandscapeView, or any other ancestor rotated the same way (a bare
|
|
36
36
|
// CSS `transform: rotate()`, not an actual OS-level orientation change).
|
|
37
37
|
//
|
|
38
38
|
// The pivot is always the window's own center, for every rotation amount: FakeLandscapeView (and
|
|
39
39
|
// this package's own doc-recommended equivalent for a caller that rolls its own) always centers its
|
|
40
|
-
// rotated container
|
|
40
|
+
// rotated container - width/height swapped first for ±90°, unchanged for 180° - within the original,
|
|
41
41
|
// un-rotated screen bounds before applying the CSS rotation, so the transform's own effective pivot
|
|
42
42
|
// lands at that shared center regardless of which of the three non-zero rotations it is.
|
|
43
43
|
export function rotateRect(x: number, y: number, width: number, height: number, rotation: PopoverRotation, windowWidth: number, windowHeight: number): { x: number; y: number; width: number; height: number } {
|
|
@@ -46,7 +46,7 @@ export function rotateRect(x: number, y: number, width: number, height: number,
|
|
|
46
46
|
const cy = windowHeight / 2
|
|
47
47
|
const dx = x + width / 2 - cx
|
|
48
48
|
const dy = y + height / 2 - cy
|
|
49
|
-
// Clockwise-positive about (cx, cy)
|
|
49
|
+
// Clockwise-positive about (cx, cy) - matches CSS's own rotate(deg) sign convention, which is
|
|
50
50
|
// where every rotation value this ever receives ultimately comes from (getViewRotation and
|
|
51
51
|
// FakeLandscapeView both already commit to that same convention).
|
|
52
52
|
const [newDx, newDy, newWidth, newHeight] = rotation === 180 ? [-dx, -dy, width, height] : rotation === 90 ? [-dy, dx, height, width] : [dy, -dx, height, width]
|
|
@@ -54,35 +54,35 @@ export function rotateRect(x: number, y: number, width: number, height: number,
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// Measures a trigger's on-screen position (via measureInWindow) and picks whichever alignment keeps
|
|
57
|
-
// a popover of `contentWidth`×`contentHeight` from overflowing a screen edge
|
|
58
|
-
// (left/right/center, preferring center) and vertically (below/above, preferring below
|
|
59
|
-
// case
|
|
57
|
+
// a popover of `contentWidth`×`contentHeight` from overflowing a screen edge - horizontally
|
|
58
|
+
// (left/right/center, preferring center) and vertically (below/above, preferring below - the usual
|
|
59
|
+
// case - but flipping to above when the trigger doesn't have enough room underneath it, which is
|
|
60
60
|
// what a short landscape screen with the trigger row near the top actually runs into).
|
|
61
61
|
//
|
|
62
|
-
// measureInWindow resolves the trigger's PRE-transform layout frame
|
|
62
|
+
// measureInWindow resolves the trigger's PRE-transform layout frame - confirmed against a real
|
|
63
63
|
// rotated screen, not just reasoned about: an earlier version of this comment claimed the opposite
|
|
64
64
|
// (that measureInWindow "resolves post-transform"), which is what let a real bug ship unnoticed,
|
|
65
65
|
// since nothing here ever actually exercised a rotated case to catch that claim being wrong (see
|
|
66
66
|
// this file's own test suite, which now does). A plain `onLayout` reports the same pre-transform
|
|
67
|
-
// frame
|
|
67
|
+
// frame - there is no plain RN layout API that reports a transformed descendant's true visual
|
|
68
68
|
// position, which is exactly why `rotation` below exists: it's the one piece of information this
|
|
69
69
|
// hook cannot recover from measurement alone, since Yoga's layout pass and a CSS `transform` are
|
|
70
70
|
// deliberately independent stages, so the caller has to supply it.
|
|
71
71
|
//
|
|
72
|
-
// `rotation` defaults to 0 (measureInWindow's raw result used as-is)
|
|
72
|
+
// `rotation` defaults to 0 (measureInWindow's raw result used as-is) - every existing caller that
|
|
73
73
|
// never sits inside a rotated ancestor keeps behaving identically; only a caller that both (a) can
|
|
74
74
|
// end up inside a @tastic/split-screen-style rotated zone and (b) knows which rotation is currently
|
|
75
75
|
// live needs to pass it (see SectionedDropdown's own `rotation` prop for the wiring this expects).
|
|
76
76
|
//
|
|
77
77
|
// Also returns `maxHeight`: the actual room available in whichever vertical direction got picked.
|
|
78
|
-
// Flipping above-vs-below only ever picks the *better* side
|
|
78
|
+
// Flipping above-vs-below only ever picks the *better* side - on a screen short enough that neither
|
|
79
79
|
// side has room for the whole thing (the same landscape case, just more cramped), that's still not
|
|
80
80
|
// enough on its own. The caller is expected to cap its content to this and let it scroll instead of
|
|
81
|
-
// silently overflowing past the screen edge
|
|
81
|
+
// silently overflowing past the screen edge - this hook only decides where the popover opens, not
|
|
82
82
|
// how its content copes with running out of room, since that's presentation-specific (a menu scrolls
|
|
83
83
|
// its rows; other content might reasonably do something else).
|
|
84
84
|
//
|
|
85
|
-
// Re-measures on every open (not continuously)
|
|
85
|
+
// Re-measures on every open (not continuously) - the trigger's screen position can't change while
|
|
86
86
|
// its own popover is showing anyway, so there's nothing to gain from tracking it beyond that.
|
|
87
87
|
export function useAutoAlign(open: boolean, contentWidth: number, contentHeight: number, rotation: PopoverRotation = 0) {
|
|
88
88
|
const triggerRef = useRef<View>(null)
|
|
@@ -90,17 +90,17 @@ export function useAutoAlign(open: boolean, contentWidth: number, contentHeight:
|
|
|
90
90
|
const [align, setAlign] = useState<PopoverAlign>('center')
|
|
91
91
|
const [verticalAlign, setVerticalAlign] = useState<PopoverVerticalAlign>('below')
|
|
92
92
|
const [maxHeight, setMaxHeight] = useState<number>(contentHeight)
|
|
93
|
-
// False from the instant `open` goes true until this open's own measurement actually lands
|
|
93
|
+
// False from the instant `open` goes true until this open's own measurement actually lands -
|
|
94
94
|
// measureInWindow resolves via a native callback, not synchronously, so there's a real gap where
|
|
95
95
|
// `align`/`verticalAlign` still hold whatever a *previous* open last measured (or the plain
|
|
96
96
|
// 'center'/'below' guess, the very first time). Left ungated, the popover would mount and paint at
|
|
97
97
|
// that stale/guessed position for one frame, then visibly jump the instant the real measurement
|
|
98
|
-
// lands
|
|
98
|
+
// lands - the caller is expected to hold the popover itself hidden (not just unmeasured content)
|
|
99
99
|
// until this flips true, which is what actually avoids the flash rather than just relocating it.
|
|
100
100
|
const [measured, setMeasured] = useState(false)
|
|
101
101
|
// Detects the false->true edge of `open` *during render* (React's "adjusting state during render"
|
|
102
102
|
// pattern) rather than in the effect below, specifically so `measured` is already false by the
|
|
103
|
-
// time this same render commits
|
|
103
|
+
// time this same render commits - an effect-based reset would still let one frame paint at the
|
|
104
104
|
// stale/guessed alignment before the reset (and the eventual re-measurement) caught up.
|
|
105
105
|
const [prevOpen, setPrevOpen] = useState(open)
|
|
106
106
|
if (open !== prevOpen) {
|
|
@@ -115,12 +115,12 @@ export function useAutoAlign(open: boolean, contentWidth: number, contentHeight:
|
|
|
115
115
|
const overflowsRight = centerX + contentWidth / 2 > windowWidth - EDGE_MARGIN
|
|
116
116
|
const overflowsLeft = centerX - contentWidth / 2 < EDGE_MARGIN
|
|
117
117
|
// Both directions overflowing means the popover is simply wider than the screen has room for
|
|
118
|
-
// either way
|
|
118
|
+
// either way - 'center' is the least-bad choice there (symmetric clipping beats asymmetric).
|
|
119
119
|
if (overflowsRight && !overflowsLeft) setAlign('right')
|
|
120
120
|
else if (overflowsLeft && !overflowsRight) setAlign('left')
|
|
121
121
|
else setAlign('center')
|
|
122
122
|
|
|
123
|
-
// Below is the default/preferred direction
|
|
123
|
+
// Below is the default/preferred direction - only flips to above when there's genuinely more
|
|
124
124
|
// room that way, not merely whenever below is imperfect, so a popover close to fitting either
|
|
125
125
|
// way doesn't flip-flop from a one-pixel margin difference.
|
|
126
126
|
const roomBelow = windowHeight - (y + triggerHeight) - EDGE_MARGIN
|
|
@@ -133,10 +133,10 @@ export function useAutoAlign(open: boolean, contentWidth: number, contentHeight:
|
|
|
133
133
|
}, [contentWidth, contentHeight, windowWidth, windowHeight, rotation])
|
|
134
134
|
|
|
135
135
|
// Re-measures whenever `open` flips true, and also if `measure` itself changes identity (i.e.
|
|
136
|
-
// contentWidth/contentHeight/windowWidth/windowHeight/rotation changed) while already open
|
|
137
|
-
// a live window resize, or the device being physically turned while the popover is showing
|
|
136
|
+
// contentWidth/contentHeight/windowWidth/windowHeight/rotation changed) while already open - e.g.
|
|
137
|
+
// a live window resize, or the device being physically turned while the popover is showing - so it
|
|
138
138
|
// never goes stale while the popover is actually showing. `measured` only resets on a genuine
|
|
139
|
-
// (re)open, via the render-phase check above
|
|
139
|
+
// (re)open, via the render-phase check above - not here on every re-measure, so an already-visible
|
|
140
140
|
// popover never flickers hidden again just because the window resized (or rotated) under it.
|
|
141
141
|
useEffect(() => {
|
|
142
142
|
if (open) measure()
|
package/src/usePopoverHost.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface PopoverHost {
|
|
|
9
9
|
// Shared open/closed state for a small group of sibling popovers (e.g. one player panel's color
|
|
10
10
|
// picker and the shared controls' dropdowns) so opening one closes any other already open in the
|
|
11
11
|
// same group. Give each player's panel its own instance so opening a popover in one player's panel
|
|
12
|
-
// never affects another player's
|
|
12
|
+
// never affects another player's - that independence is what lets two people edit their own setup
|
|
13
13
|
// at once on a split screen.
|
|
14
14
|
export function usePopoverHost(): PopoverHost {
|
|
15
15
|
const [openId, setOpenId] = useState<string | null>(null)
|
|
@@ -5,7 +5,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
|
5
5
|
|
|
6
6
|
import { PopoverRotation, PopoverVerticalAlign, useAutoAlign } from './useAutoAlign'
|
|
7
7
|
|
|
8
|
-
// Matches useAutoAlign's own EDGE_MARGIN convention
|
|
8
|
+
// Matches useAutoAlign's own EDGE_MARGIN convention - not exported from that file, so re-declared
|
|
9
9
|
// here rather than reaching into its internals.
|
|
10
10
|
const EDGE_MARGIN = 12
|
|
11
11
|
|
|
@@ -15,63 +15,63 @@ interface OwnBounds {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// Wraps useAutoAlign with a zone-aware vertical decision for popovers that live inside a
|
|
18
|
-
// @tastic/split-screen zone (see useZoneBounds' own doc)
|
|
18
|
+
// @tastic/split-screen zone (see useZoneBounds' own doc) - a no-op outside one (e.g. a solo,
|
|
19
19
|
// non-split-screen screen), where useAutoAlign's own decision is returned untouched.
|
|
20
20
|
//
|
|
21
21
|
// useAutoAlign picks above-vs-below and its own maxHeight by comparing room against the *full
|
|
22
|
-
// device window*
|
|
22
|
+
// device window* - for a trigger inside a zone, that's wrong in a way a simple clamp on top can't
|
|
23
23
|
// fix: capping its chosen direction after the fact still leaves it capable of *choosing* the
|
|
24
24
|
// direction that points into the shared row (or the opposite player's own zone), which, once
|
|
25
|
-
// correctly capped at that boundary, often has hardly any room at all
|
|
25
|
+
// correctly capped at that boundary, often has hardly any room at all - nearly useless rather than
|
|
26
26
|
// merely mis-sized. The fix has to re-derive the whole vertical decision using the zone's real
|
|
27
27
|
// boundary instead of the window edge, preferring whichever real-world direction is actually safe
|
|
28
|
-
// (away from the shared row
|
|
28
|
+
// (away from the shared row - that direction's room really is bounded by the screen edge, same as
|
|
29
29
|
// useAutoAlign already assumes) and only reaching toward the shared row when the content doesn't
|
|
30
|
-
// fit the safe side and the shared side genuinely has more room
|
|
30
|
+
// fit the safe side and the shared side genuinely has more room - the same "only flips when
|
|
31
31
|
// genuinely better" bias useAutoAlign itself uses, just comparing against the zone's own boundary
|
|
32
32
|
// instead of the opposite screen edge.
|
|
33
33
|
//
|
|
34
34
|
// One more twist specific to p2's 180°-rotated zone: PopoverBody positions itself with plain
|
|
35
35
|
// relative CSS (`top:'100%'`/`bottom:'100%'`) against its trigger, resolved in *local*
|
|
36
|
-
// (pre-rotation) space and then rotated along with everything else in the zone
|
|
36
|
+
// (pre-rotation) space and then rotated along with everything else in the zone - which inverts it,
|
|
37
37
|
// local 'below' the trigger paints as real-world *above* it once the whole zone flips 180°, and
|
|
38
38
|
// vice versa. This hook's result has to be flipped back to the *local* label PopoverBody expects
|
|
39
39
|
// whenever the zone itself is rotated (zone.rotated), or it ends up choosing the real-world
|
|
40
|
-
// direction it meant to avoid
|
|
40
|
+
// direction it meant to avoid - which is exactly the bug an earlier version of this hook had for
|
|
41
41
|
// p2 specifically (it worked correctly for p1, which isn't rotated, and that's what made the bug
|
|
42
42
|
// easy to miss during testing).
|
|
43
43
|
//
|
|
44
44
|
// CORRECTION (2026-09-18): the paragraph above used to also claim "this hook's own direction math
|
|
45
|
-
// is all real-world (from measureInWindow, which
|
|
46
|
-
// post-transform)"
|
|
45
|
+
// is all real-world (from measureInWindow, which - unlike onLayout - already resolves
|
|
46
|
+
// post-transform)" - that's the exact claim useAutoAlign.ts's own doc documents as having already
|
|
47
47
|
// shipped one real, unnoticed bug once (measureInWindow reports the PRE-transform frame, not
|
|
48
48
|
// post-transform; see that file's own comment). This hook still doesn't apply useAutoAlign's own
|
|
49
49
|
// rotateRect(...) correction to its raw measureInWindow read below (awayRoom/towardRoom are
|
|
50
|
-
// computed straight from the uncorrected y/height)
|
|
50
|
+
// computed straight from the uncorrected y/height) - that's a real, currently-unfixed gap, left
|
|
51
51
|
// alone deliberately rather than guessed at: DualZoneLayout's own sharedEdgeY (see
|
|
52
52
|
// React-Native-Split-Screen's DualZoneLayout.tsx, measureShared) has the identical uncorrected-
|
|
53
53
|
// measureInWindow gap, since sharedRef sits under the same live-rotating outer FakeLandscapeView
|
|
54
|
-
// every real caller wraps this in
|
|
54
|
+
// every real caller wraps this in - and zone.rotated alone isn't sufficient to fix just this
|
|
55
55
|
// hook's own half of it (it captures a trigger's own net rotation for p1/p2 specifically, which
|
|
56
56
|
// happens to fully account for the outer rotation there, but sharedEdgeY's own correction needs
|
|
57
57
|
// the outer rotation directly, which differs from zone.rotated for whichever seat zone.rotated is
|
|
58
58
|
// currently true for). Correcting one side without the other risks making the comparison *more*
|
|
59
|
-
// wrong, not less. No current caller in the fleet actually exercises this path today
|
|
59
|
+
// wrong, not less. No current caller in the fleet actually exercises this path today - every real
|
|
60
60
|
// cpuDifficultyAlignOverride call site across the fleet is a confirmed no-op, never actually
|
|
61
|
-
// rendering inside a live DualZoneLayout zone
|
|
61
|
+
// rendering inside a live DualZoneLayout zone - so nothing currently miscalculates in practice, but
|
|
62
62
|
// this needs real rotated-zone test coverage (ideally live device testing, not just a unit test
|
|
63
63
|
// stubbing measureInWindow) before either hook's room-size/direction math under live device
|
|
64
64
|
// rotation is trustworthy.
|
|
65
65
|
//
|
|
66
|
-
// `rotation` IS threaded through below, though
|
|
66
|
+
// `rotation` IS threaded through below, though - that part's unambiguous and low-risk on its own:
|
|
67
67
|
// it only ever reaches useAutoAlign's own (rotation-aware) horizontal align decision, which is
|
|
68
68
|
// exactly what a caller gets back verbatim via the `if (!zone) return auto` fallback whenever this
|
|
69
69
|
// hook is used outside a zone at all (its own documented "safe to reach for even in a component
|
|
70
|
-
// that might or might not end up inside one" case)
|
|
70
|
+
// that might or might not end up inside one" case) - matching every sibling rotation-aware
|
|
71
71
|
// component's own default of an ambient `useRotation()` read when the caller omits it.
|
|
72
72
|
//
|
|
73
73
|
// The "away" direction's own room also has to stop at the physical safe area, not the raw window
|
|
74
|
-
// edge
|
|
74
|
+
// edge - useAutoAlign's own windowHeight is the full device height, notch/status bar and home
|
|
75
75
|
// indicator included, so left alone this hook's fix for the shared-row boundary would just trade
|
|
76
76
|
// one overflow (into the other zone) for another (under the status bar, or behind the home
|
|
77
77
|
// indicator). The toward-shared direction doesn't need this: sharedEdgeY already sits well inside
|
|
@@ -83,11 +83,11 @@ export function useZoneClampedAlign(open: boolean, contentWidth: number, content
|
|
|
83
83
|
const insets = useSafeAreaInsets()
|
|
84
84
|
const shouldMeasure = open && auto.measured && zone !== null
|
|
85
85
|
|
|
86
|
-
// null until this popover's own zone-aware measurement lands (see the effect below)
|
|
86
|
+
// null until this popover's own zone-aware measurement lands (see the effect below) - separate
|
|
87
87
|
// from auto.measured, which only covers useAutoAlign's own (zone-blind) measurement.
|
|
88
88
|
const [ownBounds, setOwnBounds] = useState<OwnBounds | null>(null)
|
|
89
89
|
// Resets synchronously during render, not in the effect below, the instant shouldMeasure flips
|
|
90
|
-
// either direction
|
|
90
|
+
// either direction - same "adjust state during render" pattern useAutoAlign itself uses for its
|
|
91
91
|
// own measured reset (see that hook's own comment), so a stale ownBounds from a previous
|
|
92
92
|
// open/zone never survives into a render it doesn't apply to, not even for the one frame before
|
|
93
93
|
// an effect would otherwise catch up.
|
|
@@ -99,15 +99,15 @@ export function useZoneClampedAlign(open: boolean, contentWidth: number, content
|
|
|
99
99
|
|
|
100
100
|
// Re-measures the same trigger node useAutoAlign itself measures, once *its* measurement has
|
|
101
101
|
// landed (auto.measured, folded into shouldMeasure) so the ref is guaranteed mounted and settled.
|
|
102
|
-
// A second measureInWindow call on the same node, not a shared one with useAutoAlign
|
|
102
|
+
// A second measureInWindow call on the same node, not a shared one with useAutoAlign - that
|
|
103
103
|
// hook's own measurement is fully internal to it, with nothing exposed to piggyback on beyond the
|
|
104
104
|
// ref itself.
|
|
105
105
|
useEffect(() => {
|
|
106
106
|
if (!shouldMeasure) return
|
|
107
107
|
auto.triggerRef.current?.measureInWindow((_x, y, _width, height) => {
|
|
108
108
|
const belowShared = zone!.zoneSide === 'belowShared'
|
|
109
|
-
// Same shape as useAutoAlign's own roomBelow/roomAbove
|
|
110
|
-
// else between the popover and the real screen edge
|
|
109
|
+
// Same shape as useAutoAlign's own roomBelow/roomAbove - this is the direction with nothing
|
|
110
|
+
// else between the popover and the real screen edge - but stopping at the safe area inset
|
|
111
111
|
// instead of the raw window edge (insets.top/insets.bottom are always real physical-device
|
|
112
112
|
// edges, unaffected by any rotation applied further down the tree, so this is correct as-is
|
|
113
113
|
// for p2's rotated zone too, not just p1's).
|
|
@@ -118,7 +118,7 @@ export function useZoneClampedAlign(open: boolean, contentWidth: number, content
|
|
|
118
118
|
// Real-world direction, before any rotation compensation.
|
|
119
119
|
let awaySide: PopoverVerticalAlign = belowShared ? 'below' : 'above'
|
|
120
120
|
let towardSide: PopoverVerticalAlign = belowShared ? 'above' : 'below'
|
|
121
|
-
// See this hook's own doc
|
|
121
|
+
// See this hook's own doc - PopoverBody's local-then-rotate positioning inverts 'above'/
|
|
122
122
|
// 'below' for a rotated zone, so the real-world label chosen above has to flip back to the
|
|
123
123
|
// local one that actually produces it once painted.
|
|
124
124
|
if (zone!.rotated) {
|