@tastic/hud 0.16.0 → 0.17.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/dist/index.mjs CHANGED
@@ -18,12 +18,15 @@ import {
18
18
  StatSection,
19
19
  UpdateDialog,
20
20
  getStaggeredWordDuration
21
- } from "./chunk-JXRBXQLQ.mjs";
21
+ } from "./chunk-27FIA26F.mjs";
22
22
  import {
23
+ OverlayBackdrop,
24
+ OverlayCard,
23
25
  getOverlayCardColors,
24
26
  overlayActionStyles,
25
- overlayStyles
26
- } from "./chunk-32U2OIRW.mjs";
27
+ overlayStyles,
28
+ useOverlayCardColors
29
+ } from "./chunk-AX4GC5KU.mjs";
27
30
 
28
31
  // src/AchievementUnlockList.tsx
29
32
  import { getContrastColor, useAutoPaperTheme } from "@rific/auto-paper";
@@ -884,6 +887,8 @@ export {
884
887
  LOCKED_BADGE_COLOR,
885
888
  LabeledDropdown,
886
889
  MONO_FONT,
890
+ OverlayBackdrop,
891
+ OverlayCard,
887
892
  PlayerSetupPanel,
888
893
  PopoverBody,
889
894
  PressAwayOverlay,
@@ -907,6 +912,7 @@ export {
907
912
  overlayActionStyles,
908
913
  overlayStyles,
909
914
  useAutoAlign,
915
+ useOverlayCardColors,
910
916
  usePopoverHost,
911
917
  useQuitConfirmation,
912
918
  useZoneClampedAlign
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/hud",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Visual component kit for local-multiplayer React Native games: inline (non-modal) popovers, dropdowns, color pickers, gauges, ready buttons, and dialogs, built to stay scoped to one player's own zone on a shared screen",
5
5
  "keywords": [
6
6
  "react-native",
@@ -1,4 +1,4 @@
1
- import { AutoAppearancePicker, Dialog, useAutoPaperTheme } from '@rific/auto-paper'
1
+ import { AutoAppearancePicker, Dialog, useAutoPaperTheme, useBlurModule, useThemeSettings } from '@rific/auto-paper'
2
2
  import { Button, SoundContext, TouchableRipple, useHapticSettings, useSoundSettings, useVibration } from '@rific/feedback-press'
3
3
  import { useUpdater } from '@rific/updater'
4
4
  import { useIsTouchPrimaryDevice, useRotation, type ViewRotation } from '@tastic/core'
@@ -106,6 +106,8 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
106
106
  // manually here instead, gated on the NEW value so it only plays when switching that channel on.
107
107
  const sound = useContext(SoundContext)
108
108
  const { forceShort: forceHapticFeedback } = useVibration()
109
+ const { settings: themeSettings, set: setThemeSettings } = useThemeSettings()
110
+ const expoBlur = useBlurModule()
109
111
  // Covers check()'s three purely-informational cases (dev-mode disabled, web unsupported, no
110
112
  // update found) via onInfo below - a retro card overlay instead of the native Alert.alert those
111
113
  // cases fall back to by default.
@@ -139,6 +141,8 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
139
141
  const showHaptics = Platform.OS !== 'web' && !hideHaptics
140
142
  const showSound = !hideSound
141
143
  const showAppearance = !hideAppearance
144
+ // See the Blur row's own comment below for why it hides on Android and without expo-blur.
145
+ const showBlur = showAppearance && Platform.OS !== 'android' && expoBlur !== undefined && expoBlur !== null
142
146
  const showUpdateCheck = Platform.OS !== 'web' && !hideUpdateCheck
143
147
 
144
148
  // Laid out in its own turned frame rather than rotated in place. Paper's Portal mounts at the app
@@ -297,6 +301,23 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
297
301
  APPEARANCE
298
302
  </Text>
299
303
  <AutoAppearancePicker showLabels={false} />
304
+ {/* Blur (Jay, 2026-10-15): one fleet-wide switch for auto-paper's own `blur` setting,
305
+ which every blur-capable surface follows on its own (this sheet, and the
306
+ OverlayCard behind ConfirmDialog, UpdateDialog and the guide). Off, or wherever
307
+ blur can't happen, those surfaces paint the theme's raised surface instead. So the
308
+ row only shows where turning it on would change something: not on Android
309
+ (auto-paper's BlurView needs a blur target there, which these cards don't have) and
310
+ not in an app that never handed expo-blur to auto-paper's Provider. */}
311
+ {showBlur && (
312
+ <TouchableRipple onPress={() => setThemeSettings({ blur: !themeSettings.blur })} style={styles.toggleButton} accessibilityLabel={`Blur ${themeSettings.blur ? 'on' : 'off'}`}>
313
+ <View style={styles.toggleContent}>
314
+ <SettingIcon source={themeSettings.blur ? 'blur' : 'blur-off'} color={themeSettings.blur ? colors.tertiary : colors.onSurfaceVariant} containerColor={themeSettings.blur ? colors.tertiaryContainer : colors.surfaceVariant} />
315
+ <Text variant='bodyLarge' style={{ color: colors.onSurface }}>
316
+ Blur
317
+ </Text>
318
+ </View>
319
+ </TouchableRipple>
320
+ )}
300
321
  </View>
301
322
  )}
302
323
 
@@ -5,7 +5,8 @@ import { type ReactNode } from 'react'
5
5
  import { StyleSheet, View } from 'react-native'
6
6
  import { Icon, Portal, Text } from 'react-native-paper'
7
7
 
8
- import { getOverlayCardColors, overlayActionStyles, overlayStyles } from './overlayCard'
8
+ import { overlayActionStyles } from './overlayCard'
9
+ import { OverlayBackdrop, OverlayCard, useOverlayCardColors } from './OverlaySurface'
9
10
 
10
11
  export interface ConfirmDialogProps {
11
12
  visible: boolean
@@ -27,27 +28,28 @@ export interface ConfirmDialogProps {
27
28
  destructive?: boolean
28
29
  }
29
30
 
30
- // A themed replacement for the native Alert.alert/window.confirm two-button confirm pattern - the
31
- // same "retro card overlay" look BaseSettingsDialog's own update-check info messages already use
32
- // (see its onInfo/infoMessage), generalized here into its own reusable, exported component for any
33
- // caller that needs a real yes/no decision instead of a single OK dismissal. A solid backdrop +
34
- // centered card, not react-native-paper's own Dialog (which BaseSettingsDialog uses instead) -
31
+ // A themed replacement for the native Alert.alert/window.confirm two-button confirm pattern, the same
32
+ // card BaseSettingsDialog's own update-check info messages use (see its onInfo/infoMessage),
33
+ // generalized into its own reusable, exported component for any caller that needs a real yes/no
34
+ // decision instead of a single OK dismissal. The card and backdrop come from OverlaySurface.tsx: the
35
+ // theme's raised surface, blurred when the player's Blur setting allows it. A backdrop + centered
36
+ // card, not react-native-paper's own Dialog (which BaseSettingsDialog uses instead) -
35
37
  // this renders identically on every platform, including web, where react-native-web's Alert.alert
36
38
  // is a hard no-op (confirmed: its entire implementation is `static alert() {}`) and Dialog's own
37
39
  // Portal-based positioning has its own web quirks BaseSettingsDialog already works around.
38
40
  export function ConfirmDialog({ visible, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, rotation = 0, icon = 'help-circle-outline', destructive = false }: ConfirmDialogProps) {
39
- const { dark, colors } = useAutoPaperTheme()
41
+ const { colors } = useAutoPaperTheme()
42
+ const { fg } = useOverlayCardColors()
40
43
  if (!visible) return null
41
44
 
42
- const { fg, cardBg, cardBorder } = getOverlayCardColors(dark)
43
45
  const accent = destructive ? colors.danger : colors.primary
44
46
  const onAccent = destructive ? colors.onDanger : colors.onPrimary
45
47
  const showCancel = cancelLabel !== undefined && onCancel !== undefined
46
48
 
47
49
  return (
48
50
  <Portal>
49
- <View style={[overlayStyles.overlay, styles.overlay]}>
50
- <View style={[overlayStyles.card, styles.card, { backgroundColor: cardBg, borderColor: cardBorder }, toRotationStyle(rotation)]}>
51
+ <OverlayBackdrop style={styles.overlay}>
52
+ <OverlayCard style={[styles.card, toRotationStyle(rotation)]}>
51
53
  <Icon source={icon} size={64} color={accent} />
52
54
  <Text variant='headlineLarge' style={[styles.title, { color: accent }]}>
53
55
  {title}
@@ -65,8 +67,8 @@ export function ConfirmDialog({ visible, title, message, confirmLabel, cancelLab
65
67
  {confirmLabel}
66
68
  </Button>
67
69
  </View>
68
- </View>
69
- </View>
70
+ </OverlayCard>
71
+ </OverlayBackdrop>
70
72
  </Portal>
71
73
  )
72
74
  }
@@ -0,0 +1,62 @@
1
+ import { BlurView, useAutoPaperTheme, useBlur } from '@rific/auto-paper'
2
+ import { type ReactNode } from 'react'
3
+ import { 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" formula that
16
+ // getOverlayCardColors still returns for apps that haven't moved over yet).
17
+ export function useOverlayCardColors(): OverlayCardThemeColors {
18
+ const { colors } = useAutoPaperTheme()
19
+ return { fg: colors.onSurface, fgMuted: colors.onSurfaceVariant, cardBorder: colors.outlineVariant, backdrop: colors.backdrop }
20
+ }
21
+
22
+ export interface OverlayBackdropProps {
23
+ children?: ReactNode
24
+ style?: StyleProp<ViewStyle>
25
+ }
26
+
27
+ // The full-screen dim behind a card: overlayStyles.overlay's geometry in the theme's own backdrop
28
+ // color (the same scrim auto-paper's Dialog uses), lighter than the old solid 72% black.
29
+ export function OverlayBackdrop({ children, style }: OverlayBackdropProps) {
30
+ const { backdrop } = useOverlayCardColors()
31
+ return <View style={[overlayStyles.overlay, { backgroundColor: backdrop }, style]}>{children}</View>
32
+ }
33
+
34
+ export interface OverlayCardProps {
35
+ children?: ReactNode
36
+ style?: StyleProp<ViewStyle>
37
+ accessibilityViewIsModal?: boolean
38
+ testID?: string
39
+ }
40
+
41
+ // The card itself, on auto-paper's BlurView at elevation 3 (MD3's dialog surface: the theme's surface
42
+ // with a light wash of its primary). It blurs whatever is behind it when the player's Blur setting is
43
+ // on and the app has handed expo-blur to auto-paper's Provider, and otherwise falls back to that
44
+ // raised surface color on its own: on Android (no blur target here), in an app without expo-blur, or
45
+ // with Blur off. So every game gets the same card, and beyond passing expoBlur to auto-paper's
46
+ // Provider, it wires nothing for the blur.
47
+ // BlurView itself only follows its `blur` prop (on by default), so the setting is read here with
48
+ // useBlur(), the same way auto-paper's own Dialog and Menu read it.
49
+ // overflow: hidden keeps a native blur inside the card's rounded corners.
50
+ export function OverlayCard({ children, style, ...rest }: OverlayCardProps) {
51
+ const { cardBorder } = useOverlayCardColors()
52
+ const blur = useBlur()
53
+ return (
54
+ <BlurView blur={blur} elevation={3} style={[overlayStyles.card, styles.card, { borderColor: cardBorder }, style]} {...rest}>
55
+ {children}
56
+ </BlurView>
57
+ )
58
+ }
59
+
60
+ const styles = StyleSheet.create({
61
+ card: { overflow: 'hidden' }
62
+ })
@@ -15,6 +15,7 @@ export { ContentGutter } from '../ContentGutter'
15
15
  export { CornerActionButtons } from '../CornerActionButtons'
16
16
  export { MONO_FONT } from '../fonts'
17
17
  export { getOverlayCardColors, overlayActionStyles, type OverlayCardColors, overlayStyles } from '../overlayCard'
18
+ export { OverlayBackdrop, type OverlayBackdropProps, OverlayCard, type OverlayCardProps, type OverlayCardThemeColors, useOverlayCardColors } from '../OverlaySurface'
18
19
  export { RotatedFrame, type RotatedFrameProps } from '../RotatedFrame'
19
20
  export { getStaggeredWordDuration, StaggeredWord, type StaggeredWordProps } from '../StaggeredWord'
20
21
  export { StatRow } from '../StatRow'
@@ -5,7 +5,8 @@ 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 { getOverlayCardColors, overlayActionStyles, overlayStyles } from '../overlayCard'
8
+ import { overlayActionStyles } from '../overlayCard'
9
+ import { OverlayBackdrop, OverlayCard, useOverlayCardColors } from '../OverlaySurface'
9
10
  import type { GuideRotation, GuideStep } from './types'
10
11
 
11
12
  // The overlay's own breathing room and the card's own inner padding. Together with the card's max
@@ -73,7 +74,7 @@ function HowToPlayCard({ steps, onFinish, onSkip, rotation = 0 }: CardProps) {
73
74
  return () => subscription.remove()
74
75
  }, [onSkip])
75
76
 
76
- const { fg, cardBg, cardBorder, fgMuted } = getOverlayCardColors(dark)
77
+ const { fg, fgMuted } = useOverlayCardColors()
77
78
  const { width: cardWidth, pageWidth, maxHeight, artHeight } = getGuideCardSize(windowWidth, windowHeight, rotation)
78
79
  const rotationStyle = rotation === 0 ? undefined : { transform: [{ rotate: `${rotation}deg` }] }
79
80
  const isFirst = index === 0
@@ -119,8 +120,8 @@ function HowToPlayCard({ steps, onFinish, onSkip, rotation = 0 }: CardProps) {
119
120
 
120
121
  return (
121
122
  <Portal>
122
- <View style={[overlayStyles.overlay, styles.overlay]}>
123
- <View accessibilityViewIsModal style={[overlayStyles.card, styles.card, { width: cardWidth, maxHeight, backgroundColor: cardBg, borderColor: cardBorder }, rotationStyle]}>
123
+ <OverlayBackdrop style={styles.overlay}>
124
+ <OverlayCard accessibilityViewIsModal style={[styles.card, { width: cardWidth, maxHeight }, rotationStyle]}>
124
125
  {/* The pager shrinks (flexShrink) when the card hits maxHeight, and each page is its own
125
126
  vertical ScrollView, so a page that still doesn't fit (a long translation, large text)
126
127
  scrolls instead of being clipped. In the normal case nothing scrolls. */}
@@ -140,8 +141,10 @@ function HowToPlayCard({ steps, onFinish, onSkip, rotation = 0 }: CardProps) {
140
141
 
141
142
  {steps.length > 1 && (
142
143
  <View accessible accessibilityLabel={`Step ${index + 1} of ${steps.length}`} style={styles.dots}>
144
+ {/* fgMuted is the full-strength onSurfaceVariant, so the inactive dots are dimmed to half or
145
+ they'd stand out more than the active primary one. */}
143
146
  {steps.map((_, i) => (
144
- <View key={i} style={[styles.dot, { backgroundColor: i === index ? colors.primary : fgMuted }]} />
147
+ <View key={i} style={i === index ? [styles.dot, { backgroundColor: colors.primary }] : [styles.dot, styles.dotInactive, { backgroundColor: fgMuted }]} />
145
148
  ))}
146
149
  </View>
147
150
  )}
@@ -161,14 +164,14 @@ function HowToPlayCard({ steps, onFinish, onSkip, rotation = 0 }: CardProps) {
161
164
  {isLast ? 'Ready' : 'Next'}
162
165
  </Button>
163
166
  </View>
164
- </View>
165
- </View>
167
+ </OverlayCard>
168
+ </OverlayBackdrop>
166
169
  </Portal>
167
170
  )
168
171
  }
169
172
 
170
173
  // A short, skippable, paged "how to play" card - the one shared shape every fleet game's intro takes,
171
- // built on the same retro overlay-card look as ConfirmDialog (see overlayCard.ts) so it reads as
174
+ // built on the same overlay card as ConfirmDialog (see OverlaySurface.tsx) so it reads as
172
175
  // part of the same family. Controlled and presentational: whether it's visible, and what happens on
173
176
  // finish/skip (persisting "seen", mostly), is the caller's - see GuideProvider for the wiring nearly
174
177
  // every app wants. Buttons are the primary way through; the paging ScrollView underneath is what
@@ -203,6 +206,9 @@ const styles = StyleSheet.create({
203
206
  height: 8,
204
207
  width: 8
205
208
  },
209
+ dotInactive: {
210
+ opacity: 0.5
211
+ },
206
212
  dots: {
207
213
  flexDirection: 'row',
208
214
  gap: 8
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ export { getInlineColorPickerContentSize, InlineColorPicker } from './InlineColo
14
14
  export { getLabeledDropdownContentHeight, LABELED_DROPDOWN_POPOVER_WIDTH, LabeledDropdown, type LabeledDropdownOption } from './LabeledDropdown'
15
15
  export { loadSkiaWeb } from './loadSkiaWeb'
16
16
  export { getOverlayCardColors, overlayActionStyles, type OverlayCardColors, overlayStyles } from './overlayCard'
17
+ export { OverlayBackdrop, type OverlayBackdropProps, OverlayCard, type OverlayCardProps, type OverlayCardThemeColors, useOverlayCardColors } from './OverlaySurface'
17
18
  export { getColorPopoverId, getCpuDifficultyPopoverId, PlayerSetupPanel, type PlayerSetupPanelProps } from './PlayerSetupPanel'
18
19
  export { PopoverBody } from './PopoverBody'
19
20
  export { PressAwayOverlay } from './PressAwayOverlay'
@@ -7,11 +7,14 @@ export interface OverlayCardColors {
7
7
  cardBorder: string
8
8
  }
9
9
 
10
- // The literal-black/white-by-appearance "retro" formula this package's own BaseSettingsDialog and
11
- // ConfirmDialog each independently re-derived - deliberately NOT auto-paper's own (seed-color-tinted)
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.
10
+ // The old literal black/white "retro" card, the formula the fleet apps' own Game-Over/Round-Over/
11
+ // Match-Over overlays each re-derived and that this export was made to replace. hud's own cards no
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
+ */
15
18
  export function getOverlayCardColors(dark: boolean): OverlayCardColors {
16
19
  return {
17
20
  fg: dark ? '#FFFFFF' : '#000000',
@@ -23,9 +26,11 @@ export function getOverlayCardColors(dark: boolean): OverlayCardColors {
23
26
 
24
27
  // Only the shell shape every overlay card shares (backdrop + card geometry) - deliberately not the
25
28
  // 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.
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.
29
34
  export const overlayStyles = StyleSheet.create({
30
35
  card: {
31
36
  alignItems: 'center',