@tastic/hud 0.8.3 → 0.8.5

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.
@@ -1,6 +1,7 @@
1
1
  import { useAutoPaperTheme } from '@rific/auto-paper'
2
2
  import { Button } from '@rific/feedback-press'
3
3
  import { toRotationStyle, type ViewRotation } from '@tastic/core'
4
+ import { type ReactNode } from 'react'
4
5
  import { StyleSheet, View } from 'react-native'
5
6
  import { Icon, Portal, Text } from 'react-native-paper'
6
7
 
@@ -9,11 +10,11 @@ import { getOverlayCardColors, overlayStyles } from './overlayCard'
9
10
  export interface ConfirmDialogProps {
10
11
  visible: boolean
11
12
  title: string
12
- message: string
13
+ message: ReactNode
13
14
  confirmLabel: string
14
- cancelLabel: string
15
+ cancelLabel?: string
15
16
  onConfirm: () => void
16
- onCancel: () => void
17
+ onCancel?: () => void
17
18
  // Live physical-hold rotation (see @tastic/core's getViewRotation) - same convention as
18
19
  // BaseSettingsDialog's own rotation prop; defaults to 0 for a caller with no per-player zone to
19
20
  // match (a portrait-only or single-player game has nothing to stay consistent with).
@@ -41,6 +42,7 @@ export function ConfirmDialog({ visible, title, message, confirmLabel, cancelLab
41
42
  const { fg, cardBg, cardBorder } = getOverlayCardColors(dark)
42
43
  const accent = destructive ? colors.danger : colors.primary
43
44
  const onAccent = destructive ? colors.onDanger : colors.onPrimary
45
+ const showCancel = cancelLabel !== undefined && onCancel !== undefined
44
46
 
45
47
  return (
46
48
  <Portal>
@@ -54,10 +56,12 @@ export function ConfirmDialog({ visible, title, message, confirmLabel, cancelLab
54
56
  {message}
55
57
  </Text>
56
58
  <View style={styles.actions}>
57
- <Button mode='outlined' onPress={onCancel} style={styles.actionButton}>
58
- {cancelLabel}
59
- </Button>
60
- <Button mode='contained' onPress={onConfirm} style={styles.actionButton} buttonColor={accent} textColor={onAccent}>
59
+ {showCancel && (
60
+ <Button mode='outlined' onPress={onCancel} style={styles.actionButton}>
61
+ {cancelLabel}
62
+ </Button>
63
+ )}
64
+ <Button mode='contained' onPress={onConfirm} style={showCancel ? styles.actionButton : styles.actionButtonSingle} buttonColor={accent} textColor={onAccent}>
61
65
  {confirmLabel}
62
66
  </Button>
63
67
  </View>
@@ -68,25 +72,43 @@ export function ConfirmDialog({ visible, title, message, confirmLabel, cancelLab
68
72
  }
69
73
 
70
74
  const styles = StyleSheet.create({
71
- // flexBasis (not flex: 1's own 0%) is each button's real floor: whenever both fit side by side,
72
- // flexGrow still splits any leftover width evenly beyond that floor, same visual result as the
73
- // old flex: 1 on a roomy screen - but a button can never be squeezed narrower than 128, only
74
- // wrap to its own row instead (see actions' flexWrap below). react-native-paper's Button
75
+ // minWidth (not flexBasis) is each button's real floor - whenever both fit side by side, flexGrow
76
+ // still splits any leftover width evenly beyond that floor, same visual result as a plain flex: 1
77
+ // on a roomy screen, but a button can never be squeezed narrower than 128, only wrap to its own
78
+ // row instead (see actions' flexWrap below). This used to be flexBasis: 128, which measured
79
+ // correctly on web but rendered every button roughly SQUARE on iOS - confirmed live (see the
80
+ // "Quit Match?" Cancel/Quit buttons and the single-button "OK" dialogs both coming out ~128pt
81
+ // tall instead of wide). Root cause: react-native-paper's Button renders its Surface as two
82
+ // nested views on iOS only (Surface.tsx's SurfaceIOS, for shadow rendering) - only a curated
83
+ // allowlist of style keys (flex/flexGrow/flexShrink/width/height/position/... - notably NOT
84
+ // flexBasis) reaches the outer view, which is the real flex item inside this row; every other
85
+ // key, flexBasis included, falls through to an inner view whose own unspecified flexDirection
86
+ // defaults to column. flexGrow: 1 landed on the (row-context) outer view and grew width
87
+ // correctly; flexBasis: 128 landed on the (column-context) inner view instead, where a flexBasis
88
+ // sets HEIGHT, not width. minWidth isn't axis-relative like flexBasis, so it constrains width
89
+ // correctly regardless of which of the two views it ends up on. react-native-paper's Button also
75
90
  // hardcodes numberOfLines={1} on its label with no override, so a button squeezed under its
76
91
  // label's natural width silently ellipsizes ("Cancel" -> "Can...") instead of wrapping - assumed
77
92
  // fixable by just giving the row more width, until confirmed live that these two-word labels
78
93
  // (Cancel/Reset/New Game) can still lose the truncation fight even with the extra room card's own
79
- // width fix below provides, on a narrow enough screen. flexBasis is the real fix: past a certain
80
- // width there's no side-by-side arrangement that fits both buttons at their natural label size,
81
- // so the layout has to give (stack) rather than the label (truncate).
94
+ // width fix below provides, on a narrow enough screen.
82
95
  actionButton: {
83
- flexBasis: 128,
84
- flexGrow: 1
96
+ flexGrow: 1,
97
+ minWidth: 128
98
+ },
99
+ // A lone confirm button (no cancel - BaseSettingsDialog's info-only "OK" dialogs, e.g. the
100
+ // update-check "no update found" message) has nothing left to share actions' width with, so
101
+ // actionButton's own flexGrow: 1 would stretch it across the entire row instead of reading as a
102
+ // normal button. Same 128 floor, no flexGrow, so it stays at that natural size instead of filling
103
+ // the card - centered by actions' own justifyContent below.
104
+ actionButtonSingle: {
105
+ minWidth: 128
85
106
  },
86
107
  actions: {
87
108
  flexDirection: 'row',
88
109
  flexWrap: 'wrap',
89
110
  gap: 12,
111
+ justifyContent: 'center',
90
112
  width: '100%'
91
113
  },
92
114
  body: {
@@ -0,0 +1,28 @@
1
+ import { useUpdater } from '@rific/updater'
2
+ import { type ViewRotation } from '@tastic/core'
3
+
4
+ import { ConfirmDialog } from './ConfirmDialog'
5
+ import { useUpdateConfirmBridge } from './useUpdateConfirmBridge'
6
+
7
+ export interface UpdateDialogProps {
8
+ // Same live physical-hold rotation convention as ConfirmDialog/BaseSettingsDialog's own rotation
9
+ // prop - defaults to 0 for a caller with no per-player zone to match.
10
+ rotation?: ViewRotation
11
+ autoCheck?: boolean
12
+ autoPrompt?: boolean
13
+ onError?: (message: string) => void
14
+ }
15
+
16
+ // Drop-in background updater: mount once (e.g. an app's root layout) and it owns its own
17
+ // useUpdater() instance end to end, rendering a themed ConfirmDialog instead of useUpdater's
18
+ // default Alert-based prompt whenever an update is found. BaseSettingsDialog's own manual "Check
19
+ // for Updates" button deliberately does NOT render this component - see its own comment on why
20
+ // that flow keeps a second, separate useUpdater() instance instead of reusing this one.
21
+ export function UpdateDialog({ rotation = 0, autoCheck = true, autoPrompt = true, onError }: UpdateDialogProps) {
22
+ const { manifest, onConfirm, respond } = useUpdateConfirmBridge()
23
+ useUpdater({ autoCheck, autoPrompt, onConfirm, onError })
24
+
25
+ const date = manifest ? new Date(manifest.createdAt) : null
26
+
27
+ return <ConfirmDialog visible={!!manifest} title='Update Available' message={date ? `Released ${date.toLocaleDateString()} at ${date.toLocaleTimeString()}` : ''} confirmLabel='Restart' cancelLabel='Later' icon='update' rotation={rotation} onConfirm={() => respond(true)} onCancel={() => respond(false)} />
28
+ }
package/src/index.ts CHANGED
@@ -28,5 +28,6 @@ export { StatSection } from './StatSection'
28
28
  // TriggerGaugeHost is the only supported way to render this.
29
29
  export type { TriggerGaugeProps } from './TriggerGauge'
30
30
  export { default as TriggerGaugeHost } from './TriggerGaugeHost'
31
+ export { UpdateDialog, type UpdateDialogProps } from './UpdateDialog'
31
32
  export { type PopoverAlign, type PopoverRotation, type PopoverVerticalAlign, useAutoAlign } from './useAutoAlign'
32
33
  export { type PopoverHost, usePopoverHost } from './usePopoverHost'
@@ -0,0 +1,30 @@
1
+ import type { UpdateManifest } from '@rific/updater'
2
+ import { useCallback, useRef, useState } from 'react'
3
+
4
+ // Bridges @rific/updater's async onConfirm: (manifest) => Promise<boolean> into plain render
5
+ // state a ConfirmDialog can drive: `manifest` is the pending update to show (or null when there's
6
+ // nothing to confirm), and `respond` both resolves the promise onConfirm handed back to useUpdater
7
+ // and clears `manifest` so the dialog closes. Internal - not exported from index.ts. Shared by
8
+ // UpdateDialog and BaseSettingsDialog's own manual-check flow, so each only owns its own
9
+ // useUpdater() instance (see BaseSettingsDialog's comment on why that instance can't be shared).
10
+ export function useUpdateConfirmBridge() {
11
+ const [manifest, setManifest] = useState<UpdateManifest | null>(null)
12
+ const resolveRef = useRef<((confirmed: boolean) => void) | null>(null)
13
+
14
+ const onConfirm = useCallback(
15
+ (next: UpdateManifest) =>
16
+ new Promise<boolean>((resolve) => {
17
+ resolveRef.current = resolve
18
+ setManifest(next)
19
+ }),
20
+ []
21
+ )
22
+
23
+ const respond = useCallback((confirmed: boolean) => {
24
+ setManifest(null)
25
+ resolveRef.current?.(confirmed)
26
+ resolveRef.current = null
27
+ }, [])
28
+
29
+ return { manifest, onConfirm, respond }
30
+ }