@tastic/hud 0.6.0 → 0.8.1
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 +48 -8
- package/dist/SkiaGate.d.mts +13 -0
- package/dist/SkiaGate.d.ts +13 -0
- package/dist/SkiaGate.js +45 -0
- package/dist/SkiaGate.mjs +6 -0
- package/dist/chunk-XEOFR236.mjs +21 -0
- package/dist/index.d.mts +124 -75
- package/dist/index.d.ts +124 -75
- package/dist/index.js +476 -328
- package/dist/index.mjs +440 -310
- package/package.json +13 -6
- package/src/BaseSettingsDialog.tsx +9 -6
- package/src/BaseStatsScreen.tsx +10 -7
- package/src/ConfirmDialog.tsx +104 -0
- package/src/ControlSchemePicker.tsx +53 -0
- package/src/CornerActionButtons.tsx +10 -2
- package/src/CornerStatusBadges.tsx +87 -0
- package/src/InlineColorPicker.tsx +10 -4
- package/src/SectionedDropdown.tsx +17 -3
- package/src/SkiaGate.tsx +45 -0
- package/src/SkiaGate.web.tsx +9 -0
- package/src/TriggerGaugeHost.tsx +12 -20
- package/src/index.ts +11 -7
- package/src/useAutoAlign.ts +58 -16
package/src/index.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export { AchievementRow, LOCKED_BADGE_COLOR } from './AchievementRow'
|
|
2
2
|
export { BaseSettingsDialog, type BaseSettingsDialogProps } from './BaseSettingsDialog'
|
|
3
3
|
export { BaseStatsScreen } from './BaseStatsScreen'
|
|
4
|
+
export { ConfirmDialog, type ConfirmDialogProps } from './ConfirmDialog'
|
|
4
5
|
export { ContentGutter } from './ContentGutter'
|
|
6
|
+
export { ControlSchemePicker, type ControlSchemePickerProps } from './ControlSchemePicker'
|
|
5
7
|
export { CornerActionButtons } from './CornerActionButtons'
|
|
8
|
+
export { CornerStatusBadges, type CornerStatusBadgeSeat, type CornerStatusBadgesProps, type OrientationMode } from './CornerStatusBadges'
|
|
6
9
|
export { MONO_FONT } from './fonts'
|
|
7
10
|
export { InlineColorPicker } from './InlineColorPicker'
|
|
8
11
|
export { getLabeledDropdownContentHeight, LABELED_DROPDOWN_POPOVER_WIDTH, LabeledDropdown, type LabeledDropdownOption } from './LabeledDropdown'
|
|
@@ -12,16 +15,17 @@ export { PressAwayOverlay } from './PressAwayOverlay'
|
|
|
12
15
|
export { ReadyButton } from './ReadyButton'
|
|
13
16
|
export { type MenuOption, type MenuSection, type MultiSelectSection, SectionedDropdown, type SingleSelectSection } from './SectionedDropdown'
|
|
14
17
|
export { SharedActionBand } from './SharedActionBand'
|
|
18
|
+
export { SkiaGate, type SkiaGateProps } from './SkiaGate'
|
|
15
19
|
export { StatRow } from './StatRow'
|
|
16
20
|
export { StatSection } from './StatSection'
|
|
17
21
|
// TriggerGauge itself (the raw Skia-based component) is deliberately NOT exported here, even
|
|
18
|
-
// though a native-only consumer could safely skip TriggerGaugeHost's
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
22
|
+
// though a native-only consumer could safely skip TriggerGaugeHost's SkiaGate indirection. A
|
|
23
|
+
// value re-export forces the bundler to fold TriggerGauge.tsx's own `Skia` import into a chunk
|
|
24
|
+
// that this entry point imports eagerly at the top level — since that chunk is then shared with
|
|
25
|
+
// TriggerGaugeHost's own dynamic import too, it defeats TriggerGaugeHost's deferred loading for
|
|
26
|
+
// every consumer, on every platform, not just the ones reaching for the raw component.
|
|
27
|
+
// TriggerGaugeHost is the only supported way to render this.
|
|
24
28
|
export type { TriggerGaugeProps } from './TriggerGauge'
|
|
25
29
|
export { default as TriggerGaugeHost } from './TriggerGaugeHost'
|
|
26
|
-
export { type PopoverAlign, type PopoverVerticalAlign, useAutoAlign } from './useAutoAlign'
|
|
30
|
+
export { type PopoverAlign, type PopoverRotation, type PopoverVerticalAlign, useAutoAlign } from './useAutoAlign'
|
|
27
31
|
export { type PopoverHost, usePopoverHost } from './usePopoverHost'
|
package/src/useAutoAlign.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ViewRotation } from '@tastic/core'
|
|
1
2
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
3
|
import { useWindowDimensions, View } from 'react-native'
|
|
3
4
|
|
|
@@ -7,15 +8,55 @@ const EDGE_MARGIN = 12
|
|
|
7
8
|
|
|
8
9
|
export type PopoverAlign = 'left' | 'right' | 'center'
|
|
9
10
|
export type PopoverVerticalAlign = 'below' | 'above'
|
|
11
|
+
// Same set of values as @tastic/core's own ViewRotation — re-exported under this package's existing
|
|
12
|
+
// name rather than switching every call site to import ViewRotation directly, since "popover
|
|
13
|
+
// rotation" reads clearer at these call sites than the more general core name.
|
|
14
|
+
export type PopoverRotation = ViewRotation
|
|
10
15
|
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
16
|
+
// Reflects a rect from the PRE-rotation layout frame measureInWindow actually reports (see
|
|
17
|
+
// useAutoAlign's own comment below for why that's what it reports, not the post-rotation visual
|
|
18
|
+
// position) into its true, post-rotation position — needed for a trigger sitting inside a
|
|
19
|
+
// @tastic/split-screen-style FakeLandscapeView, or any other ancestor rotated the same way (a bare
|
|
20
|
+
// CSS `transform: rotate()`, not an actual OS-level orientation change).
|
|
21
|
+
//
|
|
22
|
+
// The pivot is always the window's own center, for every rotation amount: FakeLandscapeView (and
|
|
23
|
+
// this package's own doc-recommended equivalent for a caller that rolls its own) always centers its
|
|
24
|
+
// rotated container — width/height swapped first for ±90°, unchanged for 180° — within the original,
|
|
25
|
+
// un-rotated screen bounds before applying the CSS rotation, so the transform's own effective pivot
|
|
26
|
+
// lands at that shared center regardless of which of the three non-zero rotations it is.
|
|
27
|
+
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 } {
|
|
28
|
+
if (rotation === 0) return { x, y, width, height }
|
|
29
|
+
const cx = windowWidth / 2
|
|
30
|
+
const cy = windowHeight / 2
|
|
31
|
+
const dx = x + width / 2 - cx
|
|
32
|
+
const dy = y + height / 2 - cy
|
|
33
|
+
// Clockwise-positive about (cx, cy) — matches CSS's own rotate(deg) sign convention, which is
|
|
34
|
+
// where every rotation value this ever receives ultimately comes from (getViewRotation and
|
|
35
|
+
// FakeLandscapeView both already commit to that same convention).
|
|
36
|
+
const [newDx, newDy, newWidth, newHeight] = rotation === 180 ? [-dx, -dy, width, height] : rotation === 90 ? [-dy, dx, height, width] : [dy, -dx, height, width]
|
|
37
|
+
return { x: cx + newDx - newWidth / 2, y: cy + newDy - newHeight / 2, width: newWidth, height: newHeight }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Measures a trigger's on-screen position (via measureInWindow) and picks whichever alignment keeps
|
|
41
|
+
// a popover of `contentWidth`×`contentHeight` from overflowing a screen edge — horizontally
|
|
42
|
+
// (left/right/center, preferring center) and vertically (below/above, preferring below — the usual
|
|
43
|
+
// case — but flipping to above when the trigger doesn't have enough room underneath it, which is
|
|
44
|
+
// what a short landscape screen with the trigger row near the top actually runs into).
|
|
45
|
+
//
|
|
46
|
+
// measureInWindow resolves the trigger's PRE-transform layout frame — confirmed against a real
|
|
47
|
+
// rotated screen, not just reasoned about: an earlier version of this comment claimed the opposite
|
|
48
|
+
// (that measureInWindow "resolves post-transform"), which is what let a real bug ship unnoticed,
|
|
49
|
+
// since nothing here ever actually exercised a rotated case to catch that claim being wrong (see
|
|
50
|
+
// this file's own test suite, which now does). A plain `onLayout` reports the same pre-transform
|
|
51
|
+
// frame — there is no plain RN layout API that reports a transformed descendant's true visual
|
|
52
|
+
// position, which is exactly why `rotation` below exists: it's the one piece of information this
|
|
53
|
+
// hook cannot recover from measurement alone, since Yoga's layout pass and a CSS `transform` are
|
|
54
|
+
// deliberately independent stages, so the caller has to supply it.
|
|
55
|
+
//
|
|
56
|
+
// `rotation` defaults to 0 (measureInWindow's raw result used as-is) — every existing caller that
|
|
57
|
+
// never sits inside a rotated ancestor keeps behaving identically; only a caller that both (a) can
|
|
58
|
+
// end up inside a @tastic/split-screen-style rotated zone and (b) knows which rotation is currently
|
|
59
|
+
// live needs to pass it (see SectionedDropdown's own `rotation` prop for the wiring this expects).
|
|
19
60
|
//
|
|
20
61
|
// Also returns `maxHeight`: the actual room available in whichever vertical direction got picked.
|
|
21
62
|
// Flipping above-vs-below only ever picks the *better* side — on a screen short enough that neither
|
|
@@ -27,7 +68,7 @@ export type PopoverVerticalAlign = 'below' | 'above'
|
|
|
27
68
|
//
|
|
28
69
|
// Re-measures on every open (not continuously) — the trigger's screen position can't change while
|
|
29
70
|
// its own popover is showing anyway, so there's nothing to gain from tracking it beyond that.
|
|
30
|
-
export function useAutoAlign(open: boolean, contentWidth: number, contentHeight: number) {
|
|
71
|
+
export function useAutoAlign(open: boolean, contentWidth: number, contentHeight: number, rotation: PopoverRotation = 0) {
|
|
31
72
|
const triggerRef = useRef<View>(null)
|
|
32
73
|
const { width: windowWidth, height: windowHeight } = useWindowDimensions()
|
|
33
74
|
const [align, setAlign] = useState<PopoverAlign>('center')
|
|
@@ -52,7 +93,8 @@ export function useAutoAlign(open: boolean, contentWidth: number, contentHeight:
|
|
|
52
93
|
}
|
|
53
94
|
|
|
54
95
|
const measure = useCallback(() => {
|
|
55
|
-
triggerRef.current?.measureInWindow((
|
|
96
|
+
triggerRef.current?.measureInWindow((rawX, rawY, rawWidth, rawHeight) => {
|
|
97
|
+
const { x, y, width: triggerWidth, height: triggerHeight } = rotateRect(rawX, rawY, rawWidth, rawHeight, rotation, windowWidth, windowHeight)
|
|
56
98
|
const centerX = x + triggerWidth / 2
|
|
57
99
|
const overflowsRight = centerX + contentWidth / 2 > windowWidth - EDGE_MARGIN
|
|
58
100
|
const overflowsLeft = centerX - contentWidth / 2 < EDGE_MARGIN
|
|
@@ -72,14 +114,14 @@ export function useAutoAlign(open: boolean, contentWidth: number, contentHeight:
|
|
|
72
114
|
setMaxHeight(Math.max(above ? roomAbove : roomBelow, 0))
|
|
73
115
|
setMeasured(true)
|
|
74
116
|
})
|
|
75
|
-
}, [contentWidth, contentHeight, windowWidth, windowHeight])
|
|
117
|
+
}, [contentWidth, contentHeight, windowWidth, windowHeight, rotation])
|
|
76
118
|
|
|
77
119
|
// Re-measures whenever `open` flips true, and also if `measure` itself changes identity (i.e.
|
|
78
|
-
// contentWidth/contentHeight/windowWidth/windowHeight changed) while already open — e.g.
|
|
79
|
-
// window resize
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
// under it.
|
|
120
|
+
// contentWidth/contentHeight/windowWidth/windowHeight/rotation changed) while already open — e.g.
|
|
121
|
+
// a live window resize, or the device being physically turned while the popover is showing — so it
|
|
122
|
+
// never goes stale while the popover is actually showing. `measured` only resets on a genuine
|
|
123
|
+
// (re)open, via the render-phase check above — not here on every re-measure, so an already-visible
|
|
124
|
+
// popover never flickers hidden again just because the window resized (or rotated) under it.
|
|
83
125
|
useEffect(() => {
|
|
84
126
|
if (open) measure()
|
|
85
127
|
}, [open, measure])
|