@tastic/hud 0.17.0 → 0.19.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 +31 -16
- package/dist/{TriggerGauge-2OJOOF6I.mjs → TriggerGauge-GIHKGAZE.mjs} +5 -5
- package/dist/chrome/index.d.mts +23 -17
- package/dist/chrome/index.d.ts +23 -17
- package/dist/chrome/index.js +298 -211
- package/dist/chrome/index.mjs +11 -5
- package/dist/{chunk-27FIA26F.mjs → chunk-HGJKNLMD.mjs} +165 -114
- package/dist/{chunk-AX4GC5KU.mjs → chunk-PUF6TABL.mjs} +60 -24
- package/dist/guide/index.d.mts +3 -0
- package/dist/guide/index.d.ts +3 -0
- package/dist/guide/index.js +122 -74
- package/dist/guide/index.mjs +16 -8
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +471 -384
- package/dist/index.mjs +12 -6
- package/package.json +12 -12
- package/src/BaseSettingsDialog.tsx +224 -146
- package/src/ConfirmDialog.tsx +8 -15
- package/src/DialogHeader.tsx +72 -0
- package/src/OverlaySurface.tsx +21 -13
- package/src/TriggerGauge.tsx +9 -9
- package/src/chrome/index.ts +3 -1
- package/src/guide/HowToPlayDialog.tsx +23 -9
- package/src/index.ts +3 -1
- package/src/overlayCard.ts +11 -34
- package/src/useCompactDialog.ts +23 -0
package/src/ConfirmDialog.tsx
CHANGED
|
@@ -3,10 +3,12 @@ import { Button } from '@rific/feedback-press'
|
|
|
3
3
|
import { toRotationStyle, type ViewRotation } from '@tastic/core'
|
|
4
4
|
import { type ReactNode } from 'react'
|
|
5
5
|
import { StyleSheet, View } from 'react-native'
|
|
6
|
-
import {
|
|
6
|
+
import { Portal, Text } from 'react-native-paper'
|
|
7
7
|
|
|
8
|
+
import { DialogHeader } from './DialogHeader'
|
|
8
9
|
import { overlayActionStyles } from './overlayCard'
|
|
9
10
|
import { OverlayBackdrop, OverlayCard, useOverlayCardColors } from './OverlaySurface'
|
|
11
|
+
import { useCompactDialog } from './useCompactDialog'
|
|
10
12
|
|
|
11
13
|
export interface ConfirmDialogProps {
|
|
12
14
|
visible: boolean
|
|
@@ -20,8 +22,8 @@ export interface ConfirmDialogProps {
|
|
|
20
22
|
// BaseSettingsDialog's own rotation prop; defaults to 0 for a caller with no per-player zone to
|
|
21
23
|
// match (a portrait-only or single-player game has nothing to stay consistent with).
|
|
22
24
|
rotation?: ViewRotation
|
|
23
|
-
// Any react-native-paper Icon `source` name, shown
|
|
24
|
-
// question mark, since this component has no way to know why a caller is asking.
|
|
25
|
+
// Any react-native-paper Icon `source` name, shown in the header (DialogHeader) - defaults to a
|
|
26
|
+
// plain question mark, since this component has no way to know why a caller is asking.
|
|
25
27
|
icon?: string
|
|
26
28
|
// Whether the confirm button reads as destructive (the fleet's shared `danger` semantic role)
|
|
27
29
|
// instead of the default primary-colored affirmative action.
|
|
@@ -40,6 +42,7 @@ export interface ConfirmDialogProps {
|
|
|
40
42
|
export function ConfirmDialog({ visible, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, rotation = 0, icon = 'help-circle-outline', destructive = false }: ConfirmDialogProps) {
|
|
41
43
|
const { colors } = useAutoPaperTheme()
|
|
42
44
|
const { fg } = useOverlayCardColors()
|
|
45
|
+
const { compact, onLayout } = useCompactDialog(rotation)
|
|
43
46
|
if (!visible) return null
|
|
44
47
|
|
|
45
48
|
const accent = destructive ? colors.danger : colors.primary
|
|
@@ -48,12 +51,9 @@ export function ConfirmDialog({ visible, title, message, confirmLabel, cancelLab
|
|
|
48
51
|
|
|
49
52
|
return (
|
|
50
53
|
<Portal>
|
|
51
|
-
<OverlayBackdrop style={styles.overlay}>
|
|
54
|
+
<OverlayBackdrop style={styles.overlay} onLayout={onLayout}>
|
|
52
55
|
<OverlayCard style={[styles.card, toRotationStyle(rotation)]}>
|
|
53
|
-
<
|
|
54
|
-
<Text variant='headlineLarge' style={[styles.title, { color: accent }]}>
|
|
55
|
-
{title}
|
|
56
|
-
</Text>
|
|
56
|
+
<DialogHeader icon={icon} title={title} color={accent} compact={compact} />
|
|
57
57
|
<Text variant='bodyLarge' style={[styles.body, { color: fg }]}>
|
|
58
58
|
{message}
|
|
59
59
|
</Text>
|
|
@@ -91,12 +91,5 @@ const styles = StyleSheet.create({
|
|
|
91
91
|
// screen narrower than maxWidth's own 360.
|
|
92
92
|
overlay: {
|
|
93
93
|
paddingHorizontal: 24
|
|
94
|
-
},
|
|
95
|
-
// textAlign is load-bearing, not decorative, once `title` wraps to more than one line - centered
|
|
96
|
-
// within card's own alignItems: 'center' only centers the Text's block as a whole; each wrapped
|
|
97
|
-
// line inside that block still defaults to left-aligned without this.
|
|
98
|
-
title: {
|
|
99
|
-
fontWeight: 'bold',
|
|
100
|
-
textAlign: 'center'
|
|
101
94
|
}
|
|
102
95
|
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useAutoPaperTheme } from '@rific/auto-paper'
|
|
2
|
+
import { type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'
|
|
3
|
+
import { Icon, Text } from 'react-native-paper'
|
|
4
|
+
|
|
5
|
+
// Below this much height every dialog uses the compact header (and the guide its smaller art). It is
|
|
6
|
+
// the height of the space the dialog lays out in, counted along the CARD's own vertical axis: the
|
|
7
|
+
// frame's height for an upright card, the frame's width for a card turned a quarter (see
|
|
8
|
+
// useCompactDialog.ts). The how-to-play guide borrows this file, so it imports nothing heavier than
|
|
9
|
+
// paper and auto-paper (see guideImports.test.ts); the hook, which needs @tastic/core, lives apart. 528 is the guide's old rule (480 of usable height inside its 24pt gutters): a
|
|
10
|
+
// phone held sideways (320-430) is compact, a small phone upright (568+) and an iPad either way are not.
|
|
11
|
+
export const COMPACT_DIALOG_HEIGHT = 528
|
|
12
|
+
|
|
13
|
+
export function isCompactDialogHeight(height: number): boolean {
|
|
14
|
+
return height < COMPACT_DIALOG_HEIGHT
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DialogHeaderProps {
|
|
18
|
+
// Any react-native-paper Icon `source` name.
|
|
19
|
+
icon: string
|
|
20
|
+
title: string
|
|
21
|
+
// The icon and title color. Defaults to the theme's primary; ConfirmDialog passes its danger role
|
|
22
|
+
// when destructive.
|
|
23
|
+
color?: string
|
|
24
|
+
// A short space (see useCompactDialog.ts): the icon moves beside the title, at half size, so a sheet
|
|
25
|
+
// keeps its reading room.
|
|
26
|
+
compact?: boolean
|
|
27
|
+
style?: StyleProp<ViewStyle>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The one header every dialog in the fleet opens with (Jay, 2026-10-15: "they should all have the same
|
|
31
|
+
// title and maybe an icon"): a 64pt icon over a large bold title, both in the accent color, centered.
|
|
32
|
+
// ConfirmDialog, the How to Play guide, BaseSettingsDialog and every app's own result cards use it,
|
|
33
|
+
// so a dialog never hand-rolls its own title again.
|
|
34
|
+
export function DialogHeader({ icon, title, color, compact = false, style }: DialogHeaderProps) {
|
|
35
|
+
const { colors } = useAutoPaperTheme()
|
|
36
|
+
const accent = color ?? colors.primary
|
|
37
|
+
return (
|
|
38
|
+
<View style={[compact ? styles.compact : styles.full, style]}>
|
|
39
|
+
<Icon source={icon} size={compact ? COMPACT_ICON_SIZE : ICON_SIZE} color={accent} />
|
|
40
|
+
<Text accessibilityRole='header' variant={compact ? 'headlineSmall' : 'headlineLarge'} style={[styles.title, compact && styles.titleCompact, { color: accent }]}>
|
|
41
|
+
{title}
|
|
42
|
+
</Text>
|
|
43
|
+
</View>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const ICON_SIZE = 64
|
|
48
|
+
const COMPACT_ICON_SIZE = 32
|
|
49
|
+
|
|
50
|
+
const styles = StyleSheet.create({
|
|
51
|
+
compact: {
|
|
52
|
+
alignItems: 'center',
|
|
53
|
+
alignSelf: 'stretch',
|
|
54
|
+
flexDirection: 'row',
|
|
55
|
+
gap: 12,
|
|
56
|
+
justifyContent: 'center'
|
|
57
|
+
},
|
|
58
|
+
full: {
|
|
59
|
+
alignItems: 'center',
|
|
60
|
+
alignSelf: 'stretch',
|
|
61
|
+
gap: 16
|
|
62
|
+
},
|
|
63
|
+
// textAlign matters once a title wraps: the card centers the Text's block, but each wrapped line
|
|
64
|
+
// still defaults to left-aligned without it.
|
|
65
|
+
title: {
|
|
66
|
+
fontWeight: 'bold',
|
|
67
|
+
textAlign: 'center'
|
|
68
|
+
},
|
|
69
|
+
titleCompact: {
|
|
70
|
+
flexShrink: 1
|
|
71
|
+
}
|
|
72
|
+
})
|
package/src/OverlaySurface.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BlurView, useAutoPaperTheme, useBlur } from '@rific/auto-paper'
|
|
2
2
|
import { type ReactNode } from 'react'
|
|
3
|
-
import { type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'
|
|
3
|
+
import { type LayoutChangeEvent, type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'
|
|
4
4
|
|
|
5
5
|
import { overlayStyles } from './overlayCard'
|
|
6
6
|
|
|
@@ -12,8 +12,7 @@ export interface OverlayCardThemeColors {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// The colors for text and edges on an OverlayCard, taken from the app's own theme (Jay, 2026-10-15:
|
|
15
|
-
// the cards follow each game's palette instead of the old literal black/white "retro"
|
|
16
|
-
// getOverlayCardColors still returns for apps that haven't moved over yet).
|
|
15
|
+
// the cards follow each game's palette instead of the old literal black/white "retro" card).
|
|
17
16
|
export function useOverlayCardColors(): OverlayCardThemeColors {
|
|
18
17
|
const { colors } = useAutoPaperTheme()
|
|
19
18
|
return { fg: colors.onSurface, fgMuted: colors.onSurfaceVariant, cardBorder: colors.outlineVariant, backdrop: colors.backdrop }
|
|
@@ -22,13 +21,19 @@ export function useOverlayCardColors(): OverlayCardThemeColors {
|
|
|
22
21
|
export interface OverlayBackdropProps {
|
|
23
22
|
children?: ReactNode
|
|
24
23
|
style?: StyleProp<ViewStyle>
|
|
24
|
+
// useCompactDialog's onLayout (DialogHeader.tsx): the backdrop fills the space the dialog lays out in.
|
|
25
|
+
onLayout?: (e: LayoutChangeEvent) => void
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
// The full-screen dim behind a card: overlayStyles.overlay's geometry in the theme's own backdrop
|
|
28
29
|
// color (the same scrim auto-paper's Dialog uses), lighter than the old solid 72% black.
|
|
29
|
-
export function OverlayBackdrop({ children, style }: OverlayBackdropProps) {
|
|
30
|
+
export function OverlayBackdrop({ children, style, onLayout }: OverlayBackdropProps) {
|
|
30
31
|
const { backdrop } = useOverlayCardColors()
|
|
31
|
-
return
|
|
32
|
+
return (
|
|
33
|
+
<View onLayout={onLayout} style={[overlayStyles.overlay, { backgroundColor: backdrop }, style]}>
|
|
34
|
+
{children}
|
|
35
|
+
</View>
|
|
36
|
+
)
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
export interface OverlayCardProps {
|
|
@@ -38,20 +43,23 @@ export interface OverlayCardProps {
|
|
|
38
43
|
testID?: string
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
// The card itself
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
46
|
+
// The card itself: the same surface as auto-paper's Dialog, which BaseSettingsDialog's sheet is drawn
|
|
47
|
+
// on, so every dialog in the fleet looks like one family (Jay, 2026-10-15: the card used to sit at
|
|
48
|
+
// elevation 3, MD3's wash of the theme's primary, and next to the plain Settings sheet it read as a
|
|
49
|
+
// different shade; he preferred the sheet). So: auto-paper's BlurView with no elevation (the theme's
|
|
50
|
+
// plain surface), no border, and the Dialog's own corner radius (7 x the theme's roundness, 28 at MD3's
|
|
51
|
+
// default). It blurs whatever is behind it when the player's Blur setting is on and the app has handed
|
|
52
|
+
// expo-blur to auto-paper's Provider, and otherwise paints that surface on its own: on Android (no blur
|
|
53
|
+
// target here), in an app without expo-blur, or with Blur off. Beyond passing expoBlur to auto-paper's
|
|
54
|
+
// Provider, an app wires nothing for the blur.
|
|
47
55
|
// BlurView itself only follows its `blur` prop (on by default), so the setting is read here with
|
|
48
56
|
// useBlur(), the same way auto-paper's own Dialog and Menu read it.
|
|
49
57
|
// overflow: hidden keeps a native blur inside the card's rounded corners.
|
|
50
58
|
export function OverlayCard({ children, style, ...rest }: OverlayCardProps) {
|
|
51
|
-
const {
|
|
59
|
+
const { isV3, roundness } = useAutoPaperTheme()
|
|
52
60
|
const blur = useBlur()
|
|
53
61
|
return (
|
|
54
|
-
<BlurView blur={blur}
|
|
62
|
+
<BlurView blur={blur} style={[overlayStyles.card, styles.card, { borderRadius: (isV3 ? 7 : 1) * roundness }, style]} {...rest}>
|
|
55
63
|
{children}
|
|
56
64
|
</BlurView>
|
|
57
65
|
)
|
package/src/TriggerGauge.tsx
CHANGED
|
@@ -77,10 +77,10 @@ const SELECTION_TRANSITION_DURATION_MS = 260
|
|
|
77
77
|
// Arc of dashes drawn around a SectionedDropdown trigger, purely decorative-informational - the
|
|
78
78
|
// trigger's own icon stays exactly as it is (see SectionedDropdown's triggerIcon), this just frames
|
|
79
79
|
// it with an at-a-glance read of *which* option is active without opening the popover. Each dash is
|
|
80
|
-
// a real Skia arc segment (
|
|
81
|
-
// enough to look bold started visibly crossing its neighbors on a sparse (e.g. 3-option)
|
|
82
|
-
// since a chord that long diverges a lot from the circle it's meant to trace; an actual arc
|
|
83
|
-
// the circle at any width. Skia measures angles clockwise from 3 o'clock, so a center angle here
|
|
80
|
+
// a real Skia arc segment (PathBuilder.addArc), not a straight bar approximating one - a straight
|
|
81
|
+
// bar wide enough to look bold started visibly crossing its neighbors on a sparse (e.g. 3-option)
|
|
82
|
+
// gauge, since a chord that long diverges a lot from the circle it's meant to trace; an actual arc
|
|
83
|
+
// stays on the circle at any width. Skia measures angles clockwise from 3 o'clock, so a center angle here
|
|
84
84
|
// (measured clockwise from 12, like startDeg/endDeg) is shifted by -90° before being handed to
|
|
85
85
|
// addArc.
|
|
86
86
|
export function TriggerGauge({ segments, litIndices, size, accentColor, mutedColor, dashWidth, startDeg = DEFAULT_START_DEG, endDeg = startDeg }: TriggerGaugeProps) {
|
|
@@ -201,7 +201,7 @@ export function TriggerGauge({ segments, litIndices, size, accentColor, mutedCol
|
|
|
201
201
|
// there the instant a lit dash starts shrinking off of it, rather than popping in only once the
|
|
202
202
|
// shrink finishes.
|
|
203
203
|
const unlitPath = useDerivedValue(() => {
|
|
204
|
-
const path = Skia.
|
|
204
|
+
const path = Skia.PathBuilder.Make()
|
|
205
205
|
for (let i = 0; i < segments; i++) {
|
|
206
206
|
if (litIndices.includes(i)) continue
|
|
207
207
|
let local = 1
|
|
@@ -217,7 +217,7 @@ export function TriggerGauge({ segments, litIndices, size, accentColor, mutedCol
|
|
|
217
217
|
const dashCenterDeg = startDeg + (i + 0.5) * step
|
|
218
218
|
path.addArc(oval, dashCenterDeg - 90 - sweep / 2, sweep)
|
|
219
219
|
}
|
|
220
|
-
return path
|
|
220
|
+
return path.detach()
|
|
221
221
|
// litIndicesKey, not litIndices - the array itself is a brand-new reference on every render
|
|
222
222
|
// (including ones triggered by a totally unrelated sibling gauge or settings field), which would
|
|
223
223
|
// otherwise force this worklet to rebuild on every such render. The stable string key only
|
|
@@ -226,7 +226,7 @@ export function TriggerGauge({ segments, litIndices, size, accentColor, mutedCol
|
|
|
226
226
|
}, [segments, litIndicesKey, step, startDeg, dashAngleDeg, totalDurationMs, oval.x, oval.y, oval.width, oval.height])
|
|
227
227
|
|
|
228
228
|
const litPath = useDerivedValue(() => {
|
|
229
|
-
const path = Skia.
|
|
229
|
+
const path = Skia.PathBuilder.Make()
|
|
230
230
|
// The common case - a single-select trigger swapping from one tier to another - is just one
|
|
231
231
|
// full-width arc sitting at angleDeg's current value, which useLayoutEffect above retargets
|
|
232
232
|
// (never resets) on every change; this alone gives a real rotating handoff, including smoothly
|
|
@@ -234,7 +234,7 @@ export function TriggerGauge({ segments, litIndices, size, accentColor, mutedCol
|
|
|
234
234
|
// is done - before that, the sweep-width stagger below owns the reveal instead.
|
|
235
235
|
if (mountProgress.value >= 1 && singleSelect) {
|
|
236
236
|
path.addArc(oval, angleDeg.value - 90 - dashAngleDeg / 2, dashAngleDeg)
|
|
237
|
-
return path
|
|
237
|
+
return path.detach()
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
for (let i = 0; i < segments; i++) {
|
|
@@ -261,7 +261,7 @@ export function TriggerGauge({ segments, litIndices, size, accentColor, mutedCol
|
|
|
261
261
|
const dashCenterDeg = startDeg + (i + 0.5) * step
|
|
262
262
|
path.addArc(oval, dashCenterDeg - 90 - sweep / 2, sweep)
|
|
263
263
|
}
|
|
264
|
-
return path
|
|
264
|
+
return path.detach()
|
|
265
265
|
// litIndicesKey/transitionFromKey, not the arrays themselves - see unlitPath's identical comment.
|
|
266
266
|
}, [segments, litIndicesKey, transitionFromKey, singleSelect, step, startDeg, dashAngleDeg, totalDurationMs, oval.x, oval.y, oval.width, oval.height])
|
|
267
267
|
|
package/src/chrome/index.ts
CHANGED
|
@@ -13,11 +13,13 @@ export { BaseStatsScreen } from '../BaseStatsScreen'
|
|
|
13
13
|
export { ConfirmDialog, type ConfirmDialogProps } from '../ConfirmDialog'
|
|
14
14
|
export { ContentGutter } from '../ContentGutter'
|
|
15
15
|
export { CornerActionButtons } from '../CornerActionButtons'
|
|
16
|
+
export { COMPACT_DIALOG_HEIGHT, DialogHeader, type DialogHeaderProps, isCompactDialogHeight } from '../DialogHeader'
|
|
16
17
|
export { MONO_FONT } from '../fonts'
|
|
17
|
-
export {
|
|
18
|
+
export { overlayActionStyles, overlayStyles } from '../overlayCard'
|
|
18
19
|
export { OverlayBackdrop, type OverlayBackdropProps, OverlayCard, type OverlayCardProps, type OverlayCardThemeColors, useOverlayCardColors } from '../OverlaySurface'
|
|
19
20
|
export { RotatedFrame, type RotatedFrameProps } from '../RotatedFrame'
|
|
20
21
|
export { getStaggeredWordDuration, StaggeredWord, type StaggeredWordProps } from '../StaggeredWord'
|
|
21
22
|
export { StatRow } from '../StatRow'
|
|
22
23
|
export { StatSection } from '../StatSection'
|
|
23
24
|
export { UpdateDialog, type UpdateDialogProps } from '../UpdateDialog'
|
|
25
|
+
export { useCompactDialog } from '../useCompactDialog'
|
|
@@ -5,6 +5,7 @@ import { BackHandler, type NativeScrollEvent, type NativeSyntheticEvent, ScrollV
|
|
|
5
5
|
import { Portal, Text } from 'react-native-paper'
|
|
6
6
|
import { useReducedMotion } from 'react-native-reanimated'
|
|
7
7
|
|
|
8
|
+
import { DialogHeader, isCompactDialogHeight } from '../DialogHeader'
|
|
8
9
|
import { overlayActionStyles } from '../overlayCard'
|
|
9
10
|
import { OverlayBackdrop, OverlayCard, useOverlayCardColors } from '../OverlaySurface'
|
|
10
11
|
import type { GuideRotation, GuideStep } from './types'
|
|
@@ -19,9 +20,11 @@ const MAX_CARD_WIDTH = 360
|
|
|
19
20
|
// lines mean fewer of them, which is what actually has to fit (see sizing below).
|
|
20
21
|
const MAX_CARD_WIDTH_TURNED = 520
|
|
21
22
|
const ART_HEIGHT = 132
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
23
|
+
// On a short screen (isCompactDialogHeight, the rule every dialog's header follows) the illustration
|
|
24
|
+
// box shrinks. Before the dialog header, that fit the whole card (padding, art, title, two lines of
|
|
25
|
+
// body, dots, buttons) on a 360pt-wide phone held sideways; the compact header adds 48pt, so a page
|
|
26
|
+
// there now scrolls a little (see the pager below). The art can't shrink further: the apps' own art
|
|
27
|
+
// is drawn at fixed sizes (Solitaire's is 72-98pt tall). The page's own gaps tighten instead (pageCompact).
|
|
25
28
|
const ART_HEIGHT_COMPACT = 88
|
|
26
29
|
|
|
27
30
|
// Where the card has to fit. The fleet's rotation is FAKE: the OS window stays portrait and the card
|
|
@@ -34,11 +37,13 @@ export function getGuideCardSize(windowWidth: number, windowHeight: number, rota
|
|
|
34
37
|
const availableWidth = (turned ? windowHeight : windowWidth) - OVERLAY_GUTTER * 2
|
|
35
38
|
const availableHeight = (turned ? windowWidth : windowHeight) - OVERLAY_GUTTER * 2
|
|
36
39
|
const width = Math.min(turned ? MAX_CARD_WIDTH_TURNED : MAX_CARD_WIDTH, availableWidth)
|
|
40
|
+
const compact = isCompactDialogHeight(turned ? windowWidth : windowHeight)
|
|
37
41
|
return {
|
|
38
42
|
width,
|
|
39
43
|
pageWidth: width - CARD_PADDING * 2,
|
|
40
44
|
maxHeight: availableHeight,
|
|
41
|
-
artHeight:
|
|
45
|
+
artHeight: compact ? ART_HEIGHT_COMPACT : ART_HEIGHT,
|
|
46
|
+
compact
|
|
42
47
|
}
|
|
43
48
|
}
|
|
44
49
|
|
|
@@ -53,11 +58,15 @@ export interface HowToPlayDialogProps {
|
|
|
53
58
|
// The app's live fake-landscape rotation (see GuideRotation). Portal content escapes any rotated
|
|
54
59
|
// ancestor, so the card rotates itself, and sizes itself for the turn (see getGuideCardSize).
|
|
55
60
|
rotation?: GuideRotation
|
|
61
|
+
// The dialog's own header (DialogHeader), above the pages: the guide opens unprompted on a first
|
|
62
|
+
// launch, so it says what it is before the first card's own title (Jay, 2026-10-15).
|
|
63
|
+
title?: string
|
|
64
|
+
icon?: string
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
type CardProps = Omit<HowToPlayDialogProps, 'visible'>
|
|
59
68
|
|
|
60
|
-
function HowToPlayCard({ steps, onFinish, onSkip, rotation = 0 }: CardProps) {
|
|
69
|
+
function HowToPlayCard({ steps, onFinish, onSkip, rotation = 0, title = 'How to Play', icon = 'help-circle-outline' }: CardProps) {
|
|
61
70
|
const { dark, colors } = useAutoPaperTheme()
|
|
62
71
|
const { width: windowWidth, height: windowHeight } = useWindowDimensions()
|
|
63
72
|
const reducedMotion = useReducedMotion()
|
|
@@ -75,7 +84,7 @@ function HowToPlayCard({ steps, onFinish, onSkip, rotation = 0 }: CardProps) {
|
|
|
75
84
|
}, [onSkip])
|
|
76
85
|
|
|
77
86
|
const { fg, fgMuted } = useOverlayCardColors()
|
|
78
|
-
const { width: cardWidth, pageWidth, maxHeight, artHeight } = getGuideCardSize(windowWidth, windowHeight, rotation)
|
|
87
|
+
const { width: cardWidth, pageWidth, maxHeight, artHeight, compact } = getGuideCardSize(windowWidth, windowHeight, rotation)
|
|
79
88
|
const rotationStyle = rotation === 0 ? undefined : { transform: [{ rotate: `${rotation}deg` }] }
|
|
80
89
|
const isFirst = index === 0
|
|
81
90
|
const isLast = index === steps.length - 1
|
|
@@ -122,12 +131,13 @@ function HowToPlayCard({ steps, onFinish, onSkip, rotation = 0 }: CardProps) {
|
|
|
122
131
|
<Portal>
|
|
123
132
|
<OverlayBackdrop style={styles.overlay}>
|
|
124
133
|
<OverlayCard accessibilityViewIsModal style={[styles.card, { width: cardWidth, maxHeight }, rotationStyle]}>
|
|
134
|
+
<DialogHeader icon={icon} title={title} compact={compact} />
|
|
125
135
|
{/* The pager shrinks (flexShrink) when the card hits maxHeight, and each page is its own
|
|
126
136
|
vertical ScrollView, so a page that still doesn't fit (a long translation, large text)
|
|
127
137
|
scrolls instead of being clipped. In the normal case nothing scrolls. */}
|
|
128
138
|
<ScrollView ref={scrollRef} horizontal pagingEnabled showsHorizontalScrollIndicator={false} onScroll={handleScroll} scrollEventThrottle={16} onMomentumScrollEnd={handleMomentumEnd} style={[styles.pager, { width: pageWidth }]}>
|
|
129
139
|
{steps.map((step, i) => (
|
|
130
|
-
<ScrollView key={i} nestedScrollEnabled showsVerticalScrollIndicator={false} style={{ width: pageWidth }} contentContainerStyle={styles.page}>
|
|
140
|
+
<ScrollView key={i} nestedScrollEnabled showsVerticalScrollIndicator={false} style={{ width: pageWidth }} contentContainerStyle={[styles.page, compact && styles.pageCompact]}>
|
|
131
141
|
{step.art !== undefined && <View style={[styles.art, { height: artHeight, backgroundColor: artBackground }]}>{step.art}</View>}
|
|
132
142
|
<Text variant='headlineSmall' style={[styles.title, { color: fg }]}>
|
|
133
143
|
{step.title}
|
|
@@ -196,8 +206,7 @@ const styles = StyleSheet.create({
|
|
|
196
206
|
body: {
|
|
197
207
|
textAlign: 'center'
|
|
198
208
|
},
|
|
199
|
-
//
|
|
200
|
-
// here (a paged card wants more width for its art) and the shared 16px gap is right.
|
|
209
|
+
// overlayCard's shared `card` padding, restated: the page width above is computed from it.
|
|
201
210
|
card: {
|
|
202
211
|
padding: CARD_PADDING
|
|
203
212
|
},
|
|
@@ -220,6 +229,11 @@ const styles = StyleSheet.create({
|
|
|
220
229
|
alignItems: 'center',
|
|
221
230
|
gap: 12
|
|
222
231
|
},
|
|
232
|
+
// The 8pt this saves is what the compact dialog header cost a page on a 402pt-tall window (a
|
|
233
|
+
// Pro-size iPhone held sideways, measured on web): without it the body's second line scrolled.
|
|
234
|
+
pageCompact: {
|
|
235
|
+
gap: 8
|
|
236
|
+
},
|
|
223
237
|
pager: {
|
|
224
238
|
flexShrink: 1
|
|
225
239
|
},
|
package/src/index.ts
CHANGED
|
@@ -9,11 +9,12 @@ export { ContentGutter } from './ContentGutter'
|
|
|
9
9
|
export { ControlSchemePicker, type ControlSchemePickerProps } from './ControlSchemePicker'
|
|
10
10
|
export { CornerActionButtons } from './CornerActionButtons'
|
|
11
11
|
export { CornerStatusBadges, type CornerStatusBadgeSeat, type CornerStatusBadgesProps, type OrientationMode } from './CornerStatusBadges'
|
|
12
|
+
export { COMPACT_DIALOG_HEIGHT, DialogHeader, type DialogHeaderProps, isCompactDialogHeight } from './DialogHeader'
|
|
12
13
|
export { MONO_FONT } from './fonts'
|
|
13
14
|
export { getInlineColorPickerContentSize, InlineColorPicker } from './InlineColorPicker'
|
|
14
15
|
export { getLabeledDropdownContentHeight, LABELED_DROPDOWN_POPOVER_WIDTH, LabeledDropdown, type LabeledDropdownOption } from './LabeledDropdown'
|
|
15
16
|
export { loadSkiaWeb } from './loadSkiaWeb'
|
|
16
|
-
export {
|
|
17
|
+
export { overlayActionStyles, overlayStyles } from './overlayCard'
|
|
17
18
|
export { OverlayBackdrop, type OverlayBackdropProps, OverlayCard, type OverlayCardProps, type OverlayCardThemeColors, useOverlayCardColors } from './OverlaySurface'
|
|
18
19
|
export { getColorPopoverId, getCpuDifficultyPopoverId, PlayerSetupPanel, type PlayerSetupPanelProps } from './PlayerSetupPanel'
|
|
19
20
|
export { PopoverBody } from './PopoverBody'
|
|
@@ -37,6 +38,7 @@ export type { TriggerGaugeProps } from './TriggerGauge'
|
|
|
37
38
|
export { default as TriggerGaugeHost } from './TriggerGaugeHost'
|
|
38
39
|
export { UpdateDialog, type UpdateDialogProps } from './UpdateDialog'
|
|
39
40
|
export { type AlignResult, type PopoverAlign, type PopoverRotation, type PopoverVerticalAlign, useAutoAlign } from './useAutoAlign'
|
|
41
|
+
export { useCompactDialog } from './useCompactDialog'
|
|
40
42
|
export { type PopoverHost, usePopoverHost } from './usePopoverHost'
|
|
41
43
|
export { type QuitConfirmation, useQuitConfirmation } from './useQuitConfirmation'
|
|
42
44
|
export { useZoneClampedAlign } from './useZoneClampedAlign'
|
package/src/overlayCard.ts
CHANGED
|
@@ -1,44 +1,21 @@
|
|
|
1
1
|
import { StyleSheet } from 'react-native'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// longer use it.
|
|
13
|
-
/**
|
|
14
|
-
* @deprecated The literal black/white card colors. hud's own cards moved to the theme-driven, blur-aware
|
|
15
|
-
* OverlayCard / OverlayBackdrop / useOverlayCardColors (OverlaySurface.tsx) on 2026-10-15; this stays
|
|
16
|
-
* only until every app's own result cards have moved over too.
|
|
17
|
-
*/
|
|
18
|
-
export function getOverlayCardColors(dark: boolean): OverlayCardColors {
|
|
19
|
-
return {
|
|
20
|
-
fg: dark ? '#FFFFFF' : '#000000',
|
|
21
|
-
fgMuted: dark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.4)',
|
|
22
|
-
cardBg: dark ? '#111111' : '#F2F2F2',
|
|
23
|
-
cardBorder: dark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Only the shell shape every overlay card shares (backdrop + card geometry) - deliberately not the
|
|
28
|
-
// title/body styling, which already legitimately varies by consumer (each app's own score/badge/zone
|
|
29
|
-
// content). New cards should use OverlayBackdrop and OverlayCard (OverlaySurface.tsx), which apply
|
|
30
|
-
// `overlay` and `card` underneath and set the theme's backdrop and border colors themselves (the
|
|
31
|
-
// backdrop replaces `overlay`'s rgba(0,0,0,0.72)). Only an app still on the deprecated
|
|
32
|
-
// getOverlayCardColors puts its cardBg and cardBorder on `card` by hand. The one button row every
|
|
33
|
-
// overlay card with actions shares lives in overlayActionStyles below.
|
|
3
|
+
// Only the shell shape every overlay card shares (backdrop + card geometry). The title is
|
|
4
|
+
// DialogHeader's; the body still legitimately varies by consumer (each app's own score/badge/zone
|
|
5
|
+
// content). Cards should use OverlayBackdrop and OverlayCard (OverlaySurface.tsx), which apply
|
|
6
|
+
// `overlay` and `card` underneath and set the theme's backdrop color and corner radius themselves
|
|
7
|
+
// (the backdrop replaces `overlay`'s rgba(0,0,0,0.72)). The one button row every overlay card with
|
|
8
|
+
// actions shares lives in overlayActionStyles below.
|
|
9
|
+
// No border and 24pt of padding, like the Settings sheet (auto-paper's Dialog, 24pt insets on its
|
|
10
|
+
// title and scroll area), so the two families match (Jay, 2026-10-15). borderRadius is that Dialog's
|
|
11
|
+
// at MD3's default roundness; OverlayCard derives it from the live theme.
|
|
34
12
|
export const overlayStyles = StyleSheet.create({
|
|
35
13
|
card: {
|
|
36
14
|
alignItems: 'center',
|
|
37
|
-
borderRadius:
|
|
38
|
-
borderWidth: 1,
|
|
15
|
+
borderRadius: 28,
|
|
39
16
|
gap: 16,
|
|
40
17
|
maxWidth: 360,
|
|
41
|
-
padding:
|
|
18
|
+
padding: 24
|
|
42
19
|
},
|
|
43
20
|
overlay: {
|
|
44
21
|
alignItems: 'center',
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useRotatedWindowDimensions, type ViewRotation } from '@tastic/core'
|
|
2
|
+
import { useCallback, useState } from 'react'
|
|
3
|
+
import { type LayoutChangeEvent, useWindowDimensions } from 'react-native'
|
|
4
|
+
|
|
5
|
+
import { isCompactDialogHeight } from './DialogHeader'
|
|
6
|
+
|
|
7
|
+
// Whether a dialog should use the compact header, and the onLayout to put on its OverlayBackdrop. The
|
|
8
|
+
// backdrop fills whatever the dialog lays out in, so its measured size is the real answer: a turned
|
|
9
|
+
// game frame, an upright root, either one with a card that turns itself. Before that first layout it
|
|
10
|
+
// guesses: a dialog that turns itself (rotation ±90 or 180) sits at the root, in the upright window; one
|
|
11
|
+
// that doesn't sits in a frame the app has already turned by the live rotation.
|
|
12
|
+
export function useCompactDialog(rotation: ViewRotation = 0): { compact: boolean; onLayout: (e: LayoutChangeEvent) => void } {
|
|
13
|
+
const window = useWindowDimensions()
|
|
14
|
+
const framed = useRotatedWindowDimensions()
|
|
15
|
+
const [measured, setMeasured] = useState<{ width: number; height: number } | null>(null)
|
|
16
|
+
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
|
17
|
+
const { width, height } = e.nativeEvent.layout
|
|
18
|
+
setMeasured((prev) => (prev && prev.width === width && prev.height === height ? prev : { width, height }))
|
|
19
|
+
}, [])
|
|
20
|
+
const box = measured ?? (rotation === 0 ? framed : window)
|
|
21
|
+
const turned = rotation === 90 || rotation === -90
|
|
22
|
+
return { compact: isCompactDialogHeight(turned ? box.width : box.height), onLayout }
|
|
23
|
+
}
|