@tastic/hud 0.8.1 → 0.8.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/hud",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
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",
@@ -77,7 +77,7 @@
77
77
  "@rific/feedback-press": "^0.10.8",
78
78
  "@rific/updater": "^0.6.0",
79
79
  "@shopify/react-native-skia": "^2.6.2",
80
- "@tastic/core": "^0.3.0",
80
+ "@tastic/core": "^0.4.1",
81
81
  "@testing-library/dom": "^10.4.1",
82
82
  "@testing-library/react": "^16.3.2",
83
83
  "@types/jest": "^30.0.0",
@@ -1,11 +1,13 @@
1
1
  import { AutoAppearancePicker, Dialog, useAutoPaperTheme } 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
- import { useIsTouchPrimaryDevice, useRotation } from '@tastic/core'
4
+ import { toRotationStyle, useIsTouchPrimaryDevice, useRotation, type ViewRotation } from '@tastic/core'
5
5
  import { ReactNode, useContext, useState } from 'react'
6
6
  import { Platform, ScrollView, StyleSheet, View } from 'react-native'
7
7
  import { Icon, Portal, Text } from 'react-native-paper'
8
8
 
9
+ import { getOverlayCardColors, overlayStyles } from './overlayCard'
10
+
9
11
  interface SettingIconProps {
10
12
  source: string
11
13
  color: string
@@ -31,7 +33,7 @@ export interface BaseSettingsDialogProps {
31
33
  // dialog), so it just rotates its own content in place. Defaults to a live ambient read via
32
34
  // useRotation() when omitted; pass this only when a caller's own reading needs to differ from the
33
35
  // ambient one (rare — most callers should simply omit it).
34
- rotation?: number
36
+ rotation?: ViewRotation
35
37
  // Each app's own OTA version (e.g. release.otaVersion) — not something this package can read
36
38
  // itself, since every consuming app tracks its own release.ts independently. Accepts a number too
37
39
  // since that's what release.otaVersion actually is in most apps (a bare incrementing counter, not
@@ -118,15 +120,11 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
118
120
  const showAppearance = !hideAppearance
119
121
  const showUpdateCheck = Platform.OS !== 'web' && !hideUpdateCheck
120
122
 
121
- // High-contrast retro look: literal black/white by appearance, not auto-paper's own (slightly
122
- // tinted) background role.
123
- const fg = dark ? '#FFFFFF' : '#000000'
124
- const cardBg = dark ? '#111111' : '#F2F2F2'
125
- const cardBorder = dark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'
123
+ const { fg, cardBg, cardBorder } = getOverlayCardColors(dark)
126
124
 
127
125
  return (
128
126
  <>
129
- <Dialog visible={visible} onDismiss={onDismiss} style={[styles.dialog, rotation % 360 !== 0 && { transform: [{ rotate: `${rotation}deg` }] }]}>
127
+ <Dialog visible={visible} onDismiss={onDismiss} style={[styles.dialog, toRotationStyle(rotation)]}>
130
128
  <Dialog.Title>Settings</Dialog.Title>
131
129
  {/* react-native-paper's Dialog.ScrollArea has a fixed 24px marginBottom baked in (meant to
132
130
  reserve room for a Dialog.Actions row below it) — overridden to 0 since this dialog has
@@ -265,8 +263,8 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
265
263
  </Dialog>
266
264
  {infoMessage && (
267
265
  <Portal>
268
- <View style={styles.overlay}>
269
- <View style={[styles.overlayCard, { backgroundColor: cardBg, borderColor: cardBorder }, rotation % 360 !== 0 && { transform: [{ rotate: `${rotation}deg` }] }]}>
266
+ <View style={overlayStyles.overlay}>
267
+ <View style={[overlayStyles.card, { backgroundColor: cardBg, borderColor: cardBorder }, toRotationStyle(rotation)]}>
270
268
  <Icon source='information-outline' size={64} color={colors.secondary} />
271
269
  <Text variant='headlineLarge' style={[styles.overlayTitle, { color: colors.secondary }]}>
272
270
  {infoMessage.title}
@@ -307,26 +305,8 @@ const styles = StyleSheet.create({
307
305
  justifyContent: 'center',
308
306
  width: 36
309
307
  },
310
- overlay: {
311
- alignItems: 'center',
312
- backgroundColor: 'rgba(0,0,0,0.72)',
313
- bottom: 0,
314
- justifyContent: 'center',
315
- left: 0,
316
- position: 'absolute',
317
- right: 0,
318
- top: 0
319
- },
320
308
  overlayBody: { textAlign: 'center' },
321
309
  overlayButton: { width: 160 },
322
- overlayCard: {
323
- alignItems: 'center',
324
- borderRadius: 20,
325
- borderWidth: 1,
326
- gap: 16,
327
- maxWidth: 360,
328
- padding: 32
329
- },
330
310
  overlayTitle: { fontWeight: 'bold' },
331
311
  scrollArea: {
332
312
  marginBottom: 0
@@ -1,9 +1,11 @@
1
1
  import { useAutoPaperTheme } from '@rific/auto-paper'
2
2
  import { Button, IconButton } from '@rific/feedback-press'
3
- import { useRotation } from '@tastic/core'
3
+ import { useRotation, type ViewRotation } from '@tastic/core'
4
4
  import { ReactNode, useState } from 'react'
5
5
  import { ScrollView, StyleSheet, View } from 'react-native'
6
- import { Icon, Portal, Text } from 'react-native-paper'
6
+ import { Text } from 'react-native-paper'
7
+
8
+ import { ConfirmDialog } from './ConfirmDialog'
7
9
 
8
10
  // react-native-paper's own MD3 Text variant names — mirrored locally rather than importing its
9
11
  // internal VariantProp/MD3TypescaleKey (not exported from the package's public entry point) just
@@ -12,12 +14,13 @@ type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headl
12
14
 
13
15
  interface Props {
14
16
  // Explicit override for the live physical-hold rotation (see @tastic/core's useRotation) — only
15
- // affects this component's own Portal-rendered reset-confirm overlay, which (like any Portal
16
- // content) is unaffected by whichever screen's own FakeLandscapeView wraps the trigger and so has
17
- // to rotate itself. Defaults to a live ambient read via useRotation() when omitted — note this
18
- // does NOT make the header/scrollable content below rotate; the caller's own screen still needs
19
- // its own FakeLandscapeView wrap for that (this component has no transform on its own main body).
20
- rotation?: number
17
+ // affects this component's own Portal-rendered reset-confirm dialog (via ConfirmDialog's own
18
+ // rotation prop), which (like any Portal content) is unaffected by whichever screen's own
19
+ // FakeLandscapeView wraps the trigger and so has to rotate itself. Defaults to a live ambient
20
+ // read via useRotation() when omitted — note this does NOT make the header/scrollable content
21
+ // below rotate; the caller's own screen still needs its own FakeLandscapeView wrap for that
22
+ // (this component has no transform on its own main body).
23
+ rotation?: ViewRotation
21
24
  title?: string
22
25
  // 'headlineSmall' (this screen's own default, below) — was originally 'displaySmall' (the full
23
26
  // display tier's own smallest step), then 'headlineLarge', both still read as oversized for a
@@ -36,39 +39,38 @@ interface Props {
36
39
  resetLabel?: string
37
40
  resetConfirmTitle?: string
38
41
  resetConfirmBody?: string
39
- // Independently optional overrides for this screen's own background/text colors — each defaults
40
- // to the literal-black/white-by-appearance formula described below when omitted, so every
41
- // existing caller (none of which pass these) renders identically to before. A caller with its own
42
- // app-wide chrome palette (e.g. a tinted background rather than neutral black/white) passes
43
- // whichever of the three it needs; cardBorder on the reset-confirm overlay stays fixed regardless,
44
- // since its low alpha already reads fine against any cardBg.
42
+ // Independently optional overrides for this screen's own header/background colors — each
43
+ // defaults to the literal-black/white-by-appearance formula described below when omitted, so
44
+ // every existing caller (none of which pass these) renders identically to before. A caller with
45
+ // its own app-wide chrome palette (e.g. a tinted background rather than neutral black/white)
46
+ // passes whichever of the two it needs.
47
+ //
48
+ // There used to be a third override here, cardBg, plus a separate accentColor override for the
49
+ // reset-confirm dialog's own icon/title/card. Both are gone: the reset-confirm step used to be a
50
+ // hand-rolled lookalike of ConfirmDialog with its own copy of the overlay/card/button styling,
51
+ // which is exactly how it drifted from ConfirmDialog's real look in the first place (different
52
+ // button layout, different Cancel styling, and — via cardBg — a different, caller-tintable card
53
+ // background no other ConfirmDialog in the fleet has). It now renders a real ConfirmDialog
54
+ // instance below instead of its own copy, which by construction can never drift from every other
55
+ // confirm prompt in the app again — at the cost of losing that per-caller tinting, which is the
56
+ // right trade: a "you're about to erase everything" prompt should look and read identically
57
+ // everywhere, not take on whichever screen happened to trigger it.
45
58
  fg?: string
46
59
  bg?: string
47
- cardBg?: string
48
- // Overrides the confirm-overlay's alert icon/title, which otherwise default to auto-paper's
49
- // colors.secondary. That default is tuned to read against auto-paper's OWN background role, not
50
- // necessarily against a cardBg override above — a caller passing cardBg should generally pass a
51
- // matching accentColor too, rather than risk colors.secondary landing close in hue/lightness to
52
- // its own custom card background (this is what actually happened with a tinted felt cardBg in the
53
- // app this was first overridden for). Every button on this screen (the reset trigger included)
54
- // fills with colors.secondary/onSecondary as a self-contained, always-legible pair instead of
55
- // relying on a text-only color read against the surrounding chrome, so none of them need this —
56
- // only the bare icon/title do.
57
- accentColor?: string
58
60
  children: ReactNode
59
61
  }
60
62
 
61
63
  // The stats/achievements screen shell every @tastic game shares — back button + title header,
62
64
  // scrollable content area, and an optional reset-everything action with its own confirm-before-
63
- // destroying overlay — extracted after the same header/scroll/reset-confirm shape was independently
65
+ // destroying dialog — extracted after the same header/scroll/reset-confirm shape was independently
64
66
  // built into LightCycles' own achievements.tsx. Colors default to the same literal-black/white-by-
65
67
  // appearance formula BaseSettingsDialog hardcodes internally (same as this package's own
66
- // BaseSettingsDialog) — zero-config for every existing caller — but fg/bg/cardBg are each
67
- // independently overridable for a caller whose own app-wide chrome palette isn't that literal
68
- // black/white convention. `children` is the seam for whatever isn't shared: this component has no
69
- // opinion on what a "stat" is or how achievements are catalogued — see StatRow/StatSection/
70
- // AchievementRow for the smaller presentational pieces built to go inside it.
71
- export function BaseStatsScreen({ rotation: rotationOverride, title = 'Achievements', titleVariant = 'headlineSmall', onBack, insets, onReset, resetLabel = 'Reset All Stats', resetConfirmTitle = 'Reset Everything?', resetConfirmBody = 'This permanently erases all stats and achievements. This cannot be undone.', fg: fgOverride, bg: bgOverride, cardBg: cardBgOverride, accentColor: accentColorOverride, children }: Props) {
68
+ // BaseSettingsDialog) — zero-config for every existing caller — but fg/bg are each independently
69
+ // overridable for a caller whose own app-wide chrome palette isn't that literal black/white
70
+ // convention. `children` is the seam for whatever isn't shared: this component has no opinion on
71
+ // what a "stat" is or how achievements are catalogued — see StatRow/StatSection/AchievementRow for
72
+ // the smaller presentational pieces built to go inside it.
73
+ export function BaseStatsScreen({ rotation: rotationOverride, title = 'Achievements', titleVariant = 'headlineSmall', onBack, insets, onReset, resetLabel = 'Reset All Stats', resetConfirmTitle = 'Reset Everything?', resetConfirmBody = 'This permanently erases all stats and achievements. This cannot be undone.', fg: fgOverride, bg: bgOverride, children }: Props) {
72
74
  const { dark, colors } = useAutoPaperTheme()
73
75
  const ambientRotation = useRotation()
74
76
  const rotation = rotationOverride ?? ambientRotation
@@ -77,15 +79,10 @@ export function BaseStatsScreen({ rotation: rotationOverride, title = 'Achieveme
77
79
 
78
80
  // High-contrast retro look by default: literal black/white by appearance, matching every other
79
81
  // screen in this ecosystem (title/loadout/settings all use this identical formula) rather than
80
- // auto-paper's own (slightly tinted) background role — same convention BaseSettingsDialog's own
81
- // fg/cardBg/cardBorder use internally. Overridable per-instance via the props above for a caller
82
- // whose own chrome isn't that literal black/white convention; cardBorder has no override (see the
83
- // Props doc above for why).
82
+ // auto-paper's own (slightly tinted) background role. Overridable per-instance via the props
83
+ // above for a caller whose own chrome isn't that literal black/white convention.
84
84
  const fg = fgOverride ?? (dark ? '#FFFFFF' : '#000000')
85
85
  const bg = bgOverride ?? (dark ? '#000000' : '#FFFFFF')
86
- const cardBg = cardBgOverride ?? (dark ? '#111111' : '#F2F2F2')
87
- const cardBorder = dark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'
88
- const accentColor = accentColorOverride ?? colors.secondary
89
86
 
90
87
  return (
91
88
  <View style={[styles.container, { backgroundColor: bg }]}>
@@ -100,42 +97,27 @@ export function BaseStatsScreen({ rotation: rotationOverride, title = 'Achieveme
100
97
  {children}
101
98
 
102
99
  {showReset && (
103
- <Button mode='contained' onPress={() => setConfirmResetVisible(true)} buttonColor={colors.secondary} textColor={colors.onSecondary} style={styles.resetButton}>
100
+ <Button mode='contained' onPress={() => setConfirmResetVisible(true)} buttonColor={colors.danger} textColor={colors.onDanger} style={styles.resetButton}>
104
101
  {resetLabel}
105
102
  </Button>
106
103
  )}
107
104
  </ScrollView>
108
105
 
109
- {confirmResetVisible && (
110
- <Portal>
111
- <View style={[styles.overlay, rotation % 360 !== 0 && { transform: [{ rotate: `${rotation}deg` }] }]}>
112
- <View style={[styles.overlayCard, { backgroundColor: cardBg, borderColor: cardBorder }]}>
113
- <Icon source='alert-outline' size={64} color={accentColor} />
114
- <Text variant='headlineLarge' style={[styles.overlayTitle, { color: accentColor }]}>
115
- {resetConfirmTitle}
116
- </Text>
117
- <Text variant='bodyLarge' style={[styles.overlayBody, { color: fg }]}>
118
- {resetConfirmBody}
119
- </Text>
120
- <Button mode='contained' onPress={() => setConfirmResetVisible(false)} style={styles.overlayButton}>
121
- Cancel
122
- </Button>
123
- <Button
124
- mode='contained'
125
- onPress={() => {
126
- onReset?.()
127
- setConfirmResetVisible(false)
128
- }}
129
- style={styles.overlayButton}
130
- buttonColor={colors.secondary}
131
- textColor={colors.onSecondary}
132
- >
133
- Reset
134
- </Button>
135
- </View>
136
- </View>
137
- </Portal>
138
- )}
106
+ <ConfirmDialog
107
+ visible={confirmResetVisible}
108
+ title={resetConfirmTitle}
109
+ message={resetConfirmBody}
110
+ confirmLabel='Reset'
111
+ cancelLabel='Cancel'
112
+ icon='alert-outline'
113
+ destructive
114
+ rotation={rotation}
115
+ onCancel={() => setConfirmResetVisible(false)}
116
+ onConfirm={() => {
117
+ onReset?.()
118
+ setConfirmResetVisible(false)
119
+ }}
120
+ />
139
121
  </View>
140
122
  )
141
123
  }
@@ -154,30 +136,6 @@ const styles = StyleSheet.create({
154
136
  gap: 4,
155
137
  paddingBottom: 8
156
138
  },
157
- // Same overlay shape BaseSettingsDialog's own info overlay uses (backdrop + centered card),
158
- // rotated in place the same way rather than via Portal-root transform for the same reason — see
159
- // that component's own identical styles for the full rationale.
160
- overlay: {
161
- alignItems: 'center',
162
- backgroundColor: 'rgba(0,0,0,0.72)',
163
- bottom: 0,
164
- justifyContent: 'center',
165
- left: 0,
166
- position: 'absolute',
167
- right: 0,
168
- top: 0
169
- },
170
- overlayBody: { textAlign: 'center' },
171
- overlayButton: { width: 160 },
172
- overlayCard: {
173
- alignItems: 'center',
174
- borderRadius: 20,
175
- borderWidth: 1,
176
- gap: 16,
177
- maxWidth: 360,
178
- padding: 32
179
- },
180
- overlayTitle: { fontWeight: 'bold', marginBottom: -8 },
181
139
  resetButton: {
182
140
  marginTop: 16
183
141
  },
@@ -1,8 +1,11 @@
1
1
  import { useAutoPaperTheme } from '@rific/auto-paper'
2
2
  import { Button } from '@rific/feedback-press'
3
+ import { toRotationStyle, type ViewRotation } from '@tastic/core'
3
4
  import { StyleSheet, View } from 'react-native'
4
5
  import { Icon, Portal, Text } from 'react-native-paper'
5
6
 
7
+ import { getOverlayCardColors, overlayStyles } from './overlayCard'
8
+
6
9
  export interface ConfirmDialogProps {
7
10
  visible: boolean
8
11
  title: string
@@ -11,10 +14,10 @@ export interface ConfirmDialogProps {
11
14
  cancelLabel: string
12
15
  onConfirm: () => void
13
16
  onCancel: () => void
14
- // Live physical-hold rotation (see @tastic/split-screen's getViewRotation) - same convention as
17
+ // Live physical-hold rotation (see @tastic/core's getViewRotation) - same convention as
15
18
  // BaseSettingsDialog's own rotation prop; defaults to 0 for a caller with no per-player zone to
16
19
  // match (a portrait-only or single-player game has nothing to stay consistent with).
17
- rotation?: number
20
+ rotation?: ViewRotation
18
21
  // Any react-native-paper Icon `source` name, shown above the title - defaults to a plain
19
22
  // question mark, since this component has no way to know why a caller is asking.
20
23
  icon?: string
@@ -35,18 +38,14 @@ export function ConfirmDialog({ visible, title, message, confirmLabel, cancelLab
35
38
  const { dark, colors } = useAutoPaperTheme()
36
39
  if (!visible) return null
37
40
 
38
- // High-contrast retro look: literal black/white by appearance, not auto-paper's own (slightly
39
- // tinted) background role - same convention BaseSettingsDialog's own overlay card uses.
40
- const fg = dark ? '#FFFFFF' : '#000000'
41
- const cardBg = dark ? '#111111' : '#F2F2F2'
42
- const cardBorder = dark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'
41
+ const { fg, cardBg, cardBorder } = getOverlayCardColors(dark)
43
42
  const accent = destructive ? colors.danger : colors.primary
44
43
  const onAccent = destructive ? colors.onDanger : colors.onPrimary
45
44
 
46
45
  return (
47
46
  <Portal>
48
- <View style={styles.overlay}>
49
- <View style={[styles.card, { backgroundColor: cardBg, borderColor: cardBorder }, rotation % 360 !== 0 && { transform: [{ rotate: `${rotation}deg` }] }]}>
47
+ <View style={[overlayStyles.overlay, styles.overlay]}>
48
+ <View style={[overlayStyles.card, styles.card, { backgroundColor: cardBg, borderColor: cardBorder }, toRotationStyle(rotation)]}>
50
49
  <Icon source={icon} size={64} color={accent} />
51
50
  <Text variant='headlineLarge' style={[styles.title, { color: accent }]}>
52
51
  {title}
@@ -69,36 +68,50 @@ export function ConfirmDialog({ visible, title, message, confirmLabel, cancelLab
69
68
  }
70
69
 
71
70
  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
+ // hardcodes numberOfLines={1} on its label with no override, so a button squeezed under its
76
+ // label's natural width silently ellipsizes ("Cancel" -> "Can...") instead of wrapping - assumed
77
+ // fixable by just giving the row more width, until confirmed live that these two-word labels
78
+ // (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).
72
82
  actionButton: {
73
- flex: 1
83
+ flexBasis: 128,
84
+ flexGrow: 1
74
85
  },
75
86
  actions: {
76
87
  flexDirection: 'row',
88
+ flexWrap: 'wrap',
77
89
  gap: 12,
78
90
  width: '100%'
79
91
  },
80
92
  body: {
81
93
  textAlign: 'center'
82
94
  },
95
+ // Additive on top of overlayCard's own shared `card` shape (borderRadius/borderWidth/gap/maxWidth/
96
+ // padding) — claims its full allowance instead of just maxWidth alone, which only caps how wide the
97
+ // card is allowed to get - without an explicit width, a flex-centered child sizes to its own content
98
+ // instead of stretching, so the card (and therefore actions below) can end up narrower than the
99
+ // screen actually has room for, needlessly forcing the action row to wrap sooner than it has to.
83
100
  card: {
84
- alignItems: 'center',
85
- borderRadius: 20,
86
- borderWidth: 1,
87
- gap: 16,
88
- maxWidth: 360,
89
- padding: 32
101
+ width: '100%'
90
102
  },
103
+ // Additive on top of overlayCard's own shared `overlay` backdrop — gives the card room to breathe
104
+ // from the screen edges now that it reliably stretches to fill its full width allowance (see
105
+ // card's own width comment) - without this, width: '100%' would run the card edge-to-edge on a
106
+ // screen narrower than maxWidth's own 360.
91
107
  overlay: {
92
- alignItems: 'center',
93
- backgroundColor: 'rgba(0,0,0,0.72)',
94
- bottom: 0,
95
- justifyContent: 'center',
96
- left: 0,
97
- position: 'absolute',
98
- right: 0,
99
- top: 0
108
+ paddingHorizontal: 24
100
109
  },
110
+ // textAlign is load-bearing, not decorative, once `title` wraps to more than one line - centered
111
+ // within card's own alignItems: 'center' only centers the Text's block as a whole; each wrapped
112
+ // line inside that block still defaults to left-aligned without this.
101
113
  title: {
102
- fontWeight: 'bold'
114
+ fontWeight: 'bold',
115
+ textAlign: 'center'
103
116
  }
104
117
  })
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export { MONO_FONT } from './fonts'
10
10
  export { InlineColorPicker } from './InlineColorPicker'
11
11
  export { getLabeledDropdownContentHeight, LABELED_DROPDOWN_POPOVER_WIDTH, LabeledDropdown, type LabeledDropdownOption } from './LabeledDropdown'
12
12
  export { loadSkiaWeb } from './loadSkiaWeb'
13
+ export { getOverlayCardColors, type OverlayCardColors, overlayStyles } from './overlayCard'
13
14
  export { PopoverBody } from './PopoverBody'
14
15
  export { PressAwayOverlay } from './PressAwayOverlay'
15
16
  export { ReadyButton } from './ReadyButton'
@@ -0,0 +1,48 @@
1
+ import { StyleSheet } from 'react-native'
2
+
3
+ export interface OverlayCardColors {
4
+ fg: string
5
+ fgMuted: string
6
+ cardBg: string
7
+ cardBorder: string
8
+ }
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.
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
+ // button/title/body styling, which already legitimately varies by consumer (BaseSettingsDialog's
26
+ // single centered OK button vs. ConfirmDialog's two-button flexBasis-wrap row vs. each app's own
27
+ // score/badge/zone content). A consumer applies `{backgroundColor: cardBg, borderColor: cardBorder}`
28
+ // from getOverlayCardColors on top of `card` itself, same as every existing call site already does.
29
+ export const overlayStyles = StyleSheet.create({
30
+ card: {
31
+ alignItems: 'center',
32
+ borderRadius: 20,
33
+ borderWidth: 1,
34
+ gap: 16,
35
+ maxWidth: 360,
36
+ padding: 32
37
+ },
38
+ overlay: {
39
+ alignItems: 'center',
40
+ backgroundColor: 'rgba(0,0,0,0.72)',
41
+ bottom: 0,
42
+ justifyContent: 'center',
43
+ left: 0,
44
+ position: 'absolute',
45
+ right: 0,
46
+ top: 0
47
+ }
48
+ })