@tastic/hud 0.16.0 → 0.18.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 +57 -3
- package/dist/{TriggerGauge-2OJOOF6I.mjs → TriggerGauge-GIHKGAZE.mjs} +5 -5
- package/dist/chrome/index.d.mts +39 -10
- package/dist/chrome/index.d.ts +39 -10
- package/dist/chrome/index.js +309 -196
- package/dist/chrome/index.mjs +19 -7
- package/dist/{chunk-32U2OIRW.mjs → chunk-PUF6TABL.mjs} +76 -16
- package/dist/{chunk-JXRBXQLQ.mjs → chunk-Z2WUUYTE.mjs} +164 -113
- package/dist/guide/index.d.mts +3 -0
- package/dist/guide/index.d.ts +3 -0
- package/dist/guide/index.js +136 -72
- package/dist/guide/index.mjs +25 -13
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +480 -367
- package/dist/index.mjs +19 -7
- package/package.json +10 -10
- package/src/BaseSettingsDialog.tsx +203 -129
- package/src/ConfirmDialog.tsx +21 -26
- package/src/DialogHeader.tsx +72 -0
- package/src/OverlaySurface.tsx +70 -0
- package/src/TriggerGauge.tsx +9 -9
- package/src/chrome/index.ts +4 -1
- package/src/guide/HowToPlayDialog.tsx +37 -17
- package/src/index.ts +4 -1
- package/src/overlayCard.ts +11 -29
- package/src/useCompactDialog.ts +23 -0
package/src/ConfirmDialog.tsx
CHANGED
|
@@ -3,9 +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 {
|
|
8
|
+
import { DialogHeader } from './DialogHeader'
|
|
9
|
+
import { overlayActionStyles } from './overlayCard'
|
|
10
|
+
import { OverlayBackdrop, OverlayCard, useOverlayCardColors } from './OverlaySurface'
|
|
11
|
+
import { useCompactDialog } from './useCompactDialog'
|
|
9
12
|
|
|
10
13
|
export interface ConfirmDialogProps {
|
|
11
14
|
visible: boolean
|
|
@@ -19,39 +22,38 @@ export interface ConfirmDialogProps {
|
|
|
19
22
|
// BaseSettingsDialog's own rotation prop; defaults to 0 for a caller with no per-player zone to
|
|
20
23
|
// match (a portrait-only or single-player game has nothing to stay consistent with).
|
|
21
24
|
rotation?: ViewRotation
|
|
22
|
-
// Any react-native-paper Icon `source` name, shown
|
|
23
|
-
// 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.
|
|
24
27
|
icon?: string
|
|
25
28
|
// Whether the confirm button reads as destructive (the fleet's shared `danger` semantic role)
|
|
26
29
|
// instead of the default primary-colored affirmative action.
|
|
27
30
|
destructive?: boolean
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
// A themed replacement for the native Alert.alert/window.confirm two-button confirm pattern
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
33
|
+
// A themed replacement for the native Alert.alert/window.confirm two-button confirm pattern, the same
|
|
34
|
+
// card BaseSettingsDialog's own update-check info messages use (see its onInfo/infoMessage),
|
|
35
|
+
// generalized into its own reusable, exported component for any caller that needs a real yes/no
|
|
36
|
+
// decision instead of a single OK dismissal. The card and backdrop come from OverlaySurface.tsx: the
|
|
37
|
+
// theme's raised surface, blurred when the player's Blur setting allows it. A backdrop + centered
|
|
38
|
+
// card, not react-native-paper's own Dialog (which BaseSettingsDialog uses instead) -
|
|
35
39
|
// this renders identically on every platform, including web, where react-native-web's Alert.alert
|
|
36
40
|
// is a hard no-op (confirmed: its entire implementation is `static alert() {}`) and Dialog's own
|
|
37
41
|
// Portal-based positioning has its own web quirks BaseSettingsDialog already works around.
|
|
38
42
|
export function ConfirmDialog({ visible, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, rotation = 0, icon = 'help-circle-outline', destructive = false }: ConfirmDialogProps) {
|
|
39
|
-
const {
|
|
43
|
+
const { colors } = useAutoPaperTheme()
|
|
44
|
+
const { fg } = useOverlayCardColors()
|
|
45
|
+
const { compact, onLayout } = useCompactDialog(rotation)
|
|
40
46
|
if (!visible) return null
|
|
41
47
|
|
|
42
|
-
const { fg, cardBg, cardBorder } = getOverlayCardColors(dark)
|
|
43
48
|
const accent = destructive ? colors.danger : colors.primary
|
|
44
49
|
const onAccent = destructive ? colors.onDanger : colors.onPrimary
|
|
45
50
|
const showCancel = cancelLabel !== undefined && onCancel !== undefined
|
|
46
51
|
|
|
47
52
|
return (
|
|
48
53
|
<Portal>
|
|
49
|
-
<
|
|
50
|
-
<
|
|
51
|
-
<
|
|
52
|
-
<Text variant='headlineLarge' style={[styles.title, { color: accent }]}>
|
|
53
|
-
{title}
|
|
54
|
-
</Text>
|
|
54
|
+
<OverlayBackdrop style={styles.overlay} onLayout={onLayout}>
|
|
55
|
+
<OverlayCard style={[styles.card, toRotationStyle(rotation)]}>
|
|
56
|
+
<DialogHeader icon={icon} title={title} color={accent} compact={compact} />
|
|
55
57
|
<Text variant='bodyLarge' style={[styles.body, { color: fg }]}>
|
|
56
58
|
{message}
|
|
57
59
|
</Text>
|
|
@@ -65,8 +67,8 @@ export function ConfirmDialog({ visible, title, message, confirmLabel, cancelLab
|
|
|
65
67
|
{confirmLabel}
|
|
66
68
|
</Button>
|
|
67
69
|
</View>
|
|
68
|
-
</
|
|
69
|
-
</
|
|
70
|
+
</OverlayCard>
|
|
71
|
+
</OverlayBackdrop>
|
|
70
72
|
</Portal>
|
|
71
73
|
)
|
|
72
74
|
}
|
|
@@ -89,12 +91,5 @@ const styles = StyleSheet.create({
|
|
|
89
91
|
// screen narrower than maxWidth's own 360.
|
|
90
92
|
overlay: {
|
|
91
93
|
paddingHorizontal: 24
|
|
92
|
-
},
|
|
93
|
-
// textAlign is load-bearing, not decorative, once `title` wraps to more than one line - centered
|
|
94
|
-
// within card's own alignItems: 'center' only centers the Text's block as a whole; each wrapped
|
|
95
|
-
// line inside that block still defaults to left-aligned without this.
|
|
96
|
-
title: {
|
|
97
|
-
fontWeight: 'bold',
|
|
98
|
-
textAlign: 'center'
|
|
99
94
|
}
|
|
100
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
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { BlurView, useAutoPaperTheme, useBlur } from '@rific/auto-paper'
|
|
2
|
+
import { type ReactNode } from 'react'
|
|
3
|
+
import { type LayoutChangeEvent, type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'
|
|
4
|
+
|
|
5
|
+
import { overlayStyles } from './overlayCard'
|
|
6
|
+
|
|
7
|
+
export interface OverlayCardThemeColors {
|
|
8
|
+
fg: string
|
|
9
|
+
fgMuted: string
|
|
10
|
+
cardBorder: string
|
|
11
|
+
backdrop: string
|
|
12
|
+
}
|
|
13
|
+
|
|
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" card).
|
|
16
|
+
export function useOverlayCardColors(): OverlayCardThemeColors {
|
|
17
|
+
const { colors } = useAutoPaperTheme()
|
|
18
|
+
return { fg: colors.onSurface, fgMuted: colors.onSurfaceVariant, cardBorder: colors.outlineVariant, backdrop: colors.backdrop }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface OverlayBackdropProps {
|
|
22
|
+
children?: ReactNode
|
|
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
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The full-screen dim behind a card: overlayStyles.overlay's geometry in the theme's own backdrop
|
|
29
|
+
// color (the same scrim auto-paper's Dialog uses), lighter than the old solid 72% black.
|
|
30
|
+
export function OverlayBackdrop({ children, style, onLayout }: OverlayBackdropProps) {
|
|
31
|
+
const { backdrop } = useOverlayCardColors()
|
|
32
|
+
return (
|
|
33
|
+
<View onLayout={onLayout} style={[overlayStyles.overlay, { backgroundColor: backdrop }, style]}>
|
|
34
|
+
{children}
|
|
35
|
+
</View>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface OverlayCardProps {
|
|
40
|
+
children?: ReactNode
|
|
41
|
+
style?: StyleProp<ViewStyle>
|
|
42
|
+
accessibilityViewIsModal?: boolean
|
|
43
|
+
testID?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
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.
|
|
55
|
+
// BlurView itself only follows its `blur` prop (on by default), so the setting is read here with
|
|
56
|
+
// useBlur(), the same way auto-paper's own Dialog and Menu read it.
|
|
57
|
+
// overflow: hidden keeps a native blur inside the card's rounded corners.
|
|
58
|
+
export function OverlayCard({ children, style, ...rest }: OverlayCardProps) {
|
|
59
|
+
const { isV3, roundness } = useAutoPaperTheme()
|
|
60
|
+
const blur = useBlur()
|
|
61
|
+
return (
|
|
62
|
+
<BlurView blur={blur} style={[overlayStyles.card, styles.card, { borderRadius: (isV3 ? 7 : 1) * roundness }, style]} {...rest}>
|
|
63
|
+
{children}
|
|
64
|
+
</BlurView>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const styles = StyleSheet.create({
|
|
69
|
+
card: { overflow: 'hidden' }
|
|
70
|
+
})
|
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,10 +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'
|
|
19
|
+
export { OverlayBackdrop, type OverlayBackdropProps, OverlayCard, type OverlayCardProps, type OverlayCardThemeColors, useOverlayCardColors } from '../OverlaySurface'
|
|
18
20
|
export { RotatedFrame, type RotatedFrameProps } from '../RotatedFrame'
|
|
19
21
|
export { getStaggeredWordDuration, StaggeredWord, type StaggeredWordProps } from '../StaggeredWord'
|
|
20
22
|
export { StatRow } from '../StatRow'
|
|
21
23
|
export { StatSection } from '../StatSection'
|
|
22
24
|
export { UpdateDialog, type UpdateDialogProps } from '../UpdateDialog'
|
|
25
|
+
export { useCompactDialog } from '../useCompactDialog'
|
|
@@ -5,7 +5,9 @@ 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 {
|
|
8
|
+
import { DialogHeader, isCompactDialogHeight } from '../DialogHeader'
|
|
9
|
+
import { overlayActionStyles } from '../overlayCard'
|
|
10
|
+
import { OverlayBackdrop, OverlayCard, useOverlayCardColors } from '../OverlaySurface'
|
|
9
11
|
import type { GuideRotation, GuideStep } from './types'
|
|
10
12
|
|
|
11
13
|
// The overlay's own breathing room and the card's own inner padding. Together with the card's max
|
|
@@ -18,9 +20,11 @@ const MAX_CARD_WIDTH = 360
|
|
|
18
20
|
// lines mean fewer of them, which is what actually has to fit (see sizing below).
|
|
19
21
|
const MAX_CARD_WIDTH_TURNED = 520
|
|
20
22
|
const ART_HEIGHT = 132
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
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).
|
|
24
28
|
const ART_HEIGHT_COMPACT = 88
|
|
25
29
|
|
|
26
30
|
// Where the card has to fit. The fleet's rotation is FAKE: the OS window stays portrait and the card
|
|
@@ -33,11 +37,13 @@ export function getGuideCardSize(windowWidth: number, windowHeight: number, rota
|
|
|
33
37
|
const availableWidth = (turned ? windowHeight : windowWidth) - OVERLAY_GUTTER * 2
|
|
34
38
|
const availableHeight = (turned ? windowWidth : windowHeight) - OVERLAY_GUTTER * 2
|
|
35
39
|
const width = Math.min(turned ? MAX_CARD_WIDTH_TURNED : MAX_CARD_WIDTH, availableWidth)
|
|
40
|
+
const compact = isCompactDialogHeight(turned ? windowWidth : windowHeight)
|
|
36
41
|
return {
|
|
37
42
|
width,
|
|
38
43
|
pageWidth: width - CARD_PADDING * 2,
|
|
39
44
|
maxHeight: availableHeight,
|
|
40
|
-
artHeight:
|
|
45
|
+
artHeight: compact ? ART_HEIGHT_COMPACT : ART_HEIGHT,
|
|
46
|
+
compact
|
|
41
47
|
}
|
|
42
48
|
}
|
|
43
49
|
|
|
@@ -52,11 +58,15 @@ export interface HowToPlayDialogProps {
|
|
|
52
58
|
// The app's live fake-landscape rotation (see GuideRotation). Portal content escapes any rotated
|
|
53
59
|
// ancestor, so the card rotates itself, and sizes itself for the turn (see getGuideCardSize).
|
|
54
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
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
type CardProps = Omit<HowToPlayDialogProps, 'visible'>
|
|
58
68
|
|
|
59
|
-
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) {
|
|
60
70
|
const { dark, colors } = useAutoPaperTheme()
|
|
61
71
|
const { width: windowWidth, height: windowHeight } = useWindowDimensions()
|
|
62
72
|
const reducedMotion = useReducedMotion()
|
|
@@ -73,8 +83,8 @@ function HowToPlayCard({ steps, onFinish, onSkip, rotation = 0 }: CardProps) {
|
|
|
73
83
|
return () => subscription.remove()
|
|
74
84
|
}, [onSkip])
|
|
75
85
|
|
|
76
|
-
const { fg,
|
|
77
|
-
const { width: cardWidth, pageWidth, maxHeight, artHeight } = getGuideCardSize(windowWidth, windowHeight, rotation)
|
|
86
|
+
const { fg, fgMuted } = useOverlayCardColors()
|
|
87
|
+
const { width: cardWidth, pageWidth, maxHeight, artHeight, compact } = getGuideCardSize(windowWidth, windowHeight, rotation)
|
|
78
88
|
const rotationStyle = rotation === 0 ? undefined : { transform: [{ rotate: `${rotation}deg` }] }
|
|
79
89
|
const isFirst = index === 0
|
|
80
90
|
const isLast = index === steps.length - 1
|
|
@@ -119,14 +129,15 @@ function HowToPlayCard({ steps, onFinish, onSkip, rotation = 0 }: CardProps) {
|
|
|
119
129
|
|
|
120
130
|
return (
|
|
121
131
|
<Portal>
|
|
122
|
-
<
|
|
123
|
-
<
|
|
132
|
+
<OverlayBackdrop style={styles.overlay}>
|
|
133
|
+
<OverlayCard accessibilityViewIsModal style={[styles.card, { width: cardWidth, maxHeight }, rotationStyle]}>
|
|
134
|
+
<DialogHeader icon={icon} title={title} compact={compact} />
|
|
124
135
|
{/* The pager shrinks (flexShrink) when the card hits maxHeight, and each page is its own
|
|
125
136
|
vertical ScrollView, so a page that still doesn't fit (a long translation, large text)
|
|
126
137
|
scrolls instead of being clipped. In the normal case nothing scrolls. */}
|
|
127
138
|
<ScrollView ref={scrollRef} horizontal pagingEnabled showsHorizontalScrollIndicator={false} onScroll={handleScroll} scrollEventThrottle={16} onMomentumScrollEnd={handleMomentumEnd} style={[styles.pager, { width: pageWidth }]}>
|
|
128
139
|
{steps.map((step, i) => (
|
|
129
|
-
<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]}>
|
|
130
141
|
{step.art !== undefined && <View style={[styles.art, { height: artHeight, backgroundColor: artBackground }]}>{step.art}</View>}
|
|
131
142
|
<Text variant='headlineSmall' style={[styles.title, { color: fg }]}>
|
|
132
143
|
{step.title}
|
|
@@ -140,8 +151,10 @@ function HowToPlayCard({ steps, onFinish, onSkip, rotation = 0 }: CardProps) {
|
|
|
140
151
|
|
|
141
152
|
{steps.length > 1 && (
|
|
142
153
|
<View accessible accessibilityLabel={`Step ${index + 1} of ${steps.length}`} style={styles.dots}>
|
|
154
|
+
{/* fgMuted is the full-strength onSurfaceVariant, so the inactive dots are dimmed to half or
|
|
155
|
+
they'd stand out more than the active primary one. */}
|
|
143
156
|
{steps.map((_, i) => (
|
|
144
|
-
<View key={i} style={[styles.dot, { backgroundColor:
|
|
157
|
+
<View key={i} style={i === index ? [styles.dot, { backgroundColor: colors.primary }] : [styles.dot, styles.dotInactive, { backgroundColor: fgMuted }]} />
|
|
145
158
|
))}
|
|
146
159
|
</View>
|
|
147
160
|
)}
|
|
@@ -161,14 +174,14 @@ function HowToPlayCard({ steps, onFinish, onSkip, rotation = 0 }: CardProps) {
|
|
|
161
174
|
{isLast ? 'Ready' : 'Next'}
|
|
162
175
|
</Button>
|
|
163
176
|
</View>
|
|
164
|
-
</
|
|
165
|
-
</
|
|
177
|
+
</OverlayCard>
|
|
178
|
+
</OverlayBackdrop>
|
|
166
179
|
</Portal>
|
|
167
180
|
)
|
|
168
181
|
}
|
|
169
182
|
|
|
170
183
|
// A short, skippable, paged "how to play" card - the one shared shape every fleet game's intro takes,
|
|
171
|
-
// built on the same
|
|
184
|
+
// built on the same overlay card as ConfirmDialog (see OverlaySurface.tsx) so it reads as
|
|
172
185
|
// part of the same family. Controlled and presentational: whether it's visible, and what happens on
|
|
173
186
|
// finish/skip (persisting "seen", mostly), is the caller's - see GuideProvider for the wiring nearly
|
|
174
187
|
// every app wants. Buttons are the primary way through; the paging ScrollView underneath is what
|
|
@@ -193,8 +206,7 @@ const styles = StyleSheet.create({
|
|
|
193
206
|
body: {
|
|
194
207
|
textAlign: 'center'
|
|
195
208
|
},
|
|
196
|
-
//
|
|
197
|
-
// 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.
|
|
198
210
|
card: {
|
|
199
211
|
padding: CARD_PADDING
|
|
200
212
|
},
|
|
@@ -203,6 +215,9 @@ const styles = StyleSheet.create({
|
|
|
203
215
|
height: 8,
|
|
204
216
|
width: 8
|
|
205
217
|
},
|
|
218
|
+
dotInactive: {
|
|
219
|
+
opacity: 0.5
|
|
220
|
+
},
|
|
206
221
|
dots: {
|
|
207
222
|
flexDirection: 'row',
|
|
208
223
|
gap: 8
|
|
@@ -214,6 +229,11 @@ const styles = StyleSheet.create({
|
|
|
214
229
|
alignItems: 'center',
|
|
215
230
|
gap: 12
|
|
216
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
|
+
},
|
|
217
237
|
pager: {
|
|
218
238
|
flexShrink: 1
|
|
219
239
|
},
|
package/src/index.ts
CHANGED
|
@@ -9,11 +9,13 @@ 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'
|
|
18
|
+
export { OverlayBackdrop, type OverlayBackdropProps, OverlayCard, type OverlayCardProps, type OverlayCardThemeColors, useOverlayCardColors } from './OverlaySurface'
|
|
17
19
|
export { getColorPopoverId, getCpuDifficultyPopoverId, PlayerSetupPanel, type PlayerSetupPanelProps } from './PlayerSetupPanel'
|
|
18
20
|
export { PopoverBody } from './PopoverBody'
|
|
19
21
|
export { PressAwayOverlay } from './PressAwayOverlay'
|
|
@@ -36,6 +38,7 @@ export type { TriggerGaugeProps } from './TriggerGauge'
|
|
|
36
38
|
export { default as TriggerGaugeHost } from './TriggerGaugeHost'
|
|
37
39
|
export { UpdateDialog, type UpdateDialogProps } from './UpdateDialog'
|
|
38
40
|
export { type AlignResult, type PopoverAlign, type PopoverRotation, type PopoverVerticalAlign, useAutoAlign } from './useAutoAlign'
|
|
41
|
+
export { useCompactDialog } from './useCompactDialog'
|
|
39
42
|
export { type PopoverHost, usePopoverHost } from './usePopoverHost'
|
|
40
43
|
export { type QuitConfirmation, useQuitConfirmation } from './useQuitConfirmation'
|
|
41
44
|
export { useZoneClampedAlign } from './useZoneClampedAlign'
|
package/src/overlayCard.ts
CHANGED
|
@@ -1,39 +1,21 @@
|
|
|
1
1
|
import { StyleSheet } from 'react-native'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// background role, see either component's own "High-contrast retro look" comment for why. Every
|
|
13
|
-
// fleet app's own Game-Over/Round-Over/Match-Over/Quit-confirmation overlay re-derives this same
|
|
14
|
-
// three-or-four-line formula again, which is what this export exists to replace.
|
|
15
|
-
export function getOverlayCardColors(dark: boolean): OverlayCardColors {
|
|
16
|
-
return {
|
|
17
|
-
fg: dark ? '#FFFFFF' : '#000000',
|
|
18
|
-
fgMuted: dark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.4)',
|
|
19
|
-
cardBg: dark ? '#111111' : '#F2F2F2',
|
|
20
|
-
cardBorder: dark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Only the shell shape every overlay card shares (backdrop + card geometry) - deliberately not the
|
|
25
|
-
// title/body styling, which already legitimately varies by consumer (each app's own score/badge/zone
|
|
26
|
-
// content). A consumer applies `{backgroundColor: cardBg, borderColor: cardBorder}` from
|
|
27
|
-
// getOverlayCardColors on top of `card` itself, same as every existing call site already does. The
|
|
28
|
-
// one button row every 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.
|
|
29
12
|
export const overlayStyles = StyleSheet.create({
|
|
30
13
|
card: {
|
|
31
14
|
alignItems: 'center',
|
|
32
|
-
borderRadius:
|
|
33
|
-
borderWidth: 1,
|
|
15
|
+
borderRadius: 28,
|
|
34
16
|
gap: 16,
|
|
35
17
|
maxWidth: 360,
|
|
36
|
-
padding:
|
|
18
|
+
padding: 24
|
|
37
19
|
},
|
|
38
20
|
overlay: {
|
|
39
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
|
+
}
|