@tastic/hud 0.21.0 → 0.23.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/hud",
3
- "version": "0.21.0",
3
+ "version": "0.23.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",
@@ -88,7 +88,7 @@
88
88
  "@infinitetoken/jest-config": "^0.5.2",
89
89
  "@infinitetoken/tsconfig": "^0.6.0",
90
90
  "@rific/auto-paper": "^0.13.3",
91
- "@rific/core": "^0.3.1",
91
+ "@rific/core": "^0.4.0",
92
92
  "@rific/feedback-press": "^0.13.0",
93
93
  "@rific/updater": "^0.7.1",
94
94
  "@shopify/react-native-skia": "^2.14.0",
@@ -117,6 +117,7 @@
117
117
  },
118
118
  "peerDependencies": {
119
119
  "@rific/auto-paper": "^0.13.0",
120
+ "@rific/core": ">=0.4.0",
120
121
  "@rific/feedback-press": ">=0.10.2",
121
122
  "@rific/updater": ">=0.4.0",
122
123
  "@shopify/react-native-skia": ">=2.6.0",
@@ -15,12 +15,13 @@ import { MONO_FONT } from './fonts'
15
15
  interface Props {
16
16
  rows: AchievementCatalogRow[]
17
17
  label?: string
18
- // Independently optional overrides for this section's own heading/row colors - default to the
19
- // same useAutoPaperTheme()-derived formula as AchievementRow/StatSection/StatRow/BaseStatsScreen
20
- // (see BaseStatsScreen's own doc for why) when omitted, so every existing caller renders
21
- // identically to before. Passed straight through to each <AchievementRow>.
18
+ // Independently optional overrides for the heading's and rows' colors. fg and sectionBg go to each
19
+ // <AchievementRow> (whose own defaults are the well's, see wellSurface.ts). fgMuted goes to the heading and
20
+ // the rows; omitted, the heading (drawn on the screen, not on a well) keeps its own screen-derived default
21
+ // and the rows theirs.
22
22
  fg?: string
23
23
  fgMuted?: string
24
+ sectionBg?: string
24
25
  }
25
26
 
26
27
  // The "ALL ACHIEVEMENTS" heading + one <AchievementRow> per row - extracted verbatim from every
@@ -42,7 +43,7 @@ interface Props {
42
43
  // about 1.4:1 and 1.8:1 against the light screen's white, so a bar or check in either barely shows;
43
44
  // mixed, they're about 5:1 and 6:1 (bronze goes from about 3:1 to 9:1). The badge keeps the true tier
44
45
  // color: its glyph is black, which reads on all three.
45
- export function AchievementCatalogSection({ rows, label = 'ALL ACHIEVEMENTS', fg, fgMuted: fgMutedOverride }: Props) {
46
+ export function AchievementCatalogSection({ rows, label = 'ALL ACHIEVEMENTS', fg, fgMuted: fgMutedOverride, sectionBg }: Props) {
46
47
  const { dark } = useAutoPaperTheme()
47
48
  const fgMuted = fgMutedOverride ?? (dark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)')
48
49
  const tierAccent = (tierColor: string) => (dark ? tierColor : getBlendedColor('#000000', tierColor, 0.5))
@@ -53,7 +54,7 @@ export function AchievementCatalogSection({ rows, label = 'ALL ACHIEVEMENTS', fg
53
54
  {label}
54
55
  </Text>
55
56
  {rows.map((row) => (
56
- <AchievementRow key={row.id} icon={row.icon} title={row.title} description={row.description} badgeColor={row.unlockedAt !== undefined ? row.tierColor : LOCKED_BADGE_COLOR} checkColor={tierAccent(row.tierColor)} unlockedLabel={row.unlockedLabel} progress={row.progress} deviceMarker={row.deviceMarker} fg={fg} fgMuted={fgMutedOverride} />
57
+ <AchievementRow key={row.id} icon={row.icon} title={row.title} description={row.description} badgeColor={row.unlockedAt !== undefined ? row.tierColor : LOCKED_BADGE_COLOR} checkColor={tierAccent(row.tierColor)} unlockedLabel={row.unlockedLabel} progress={row.progress} deviceMarker={row.deviceMarker} fg={fg} fgMuted={fgMutedOverride} sectionBg={sectionBg} />
57
58
  ))}
58
59
  </Fragment>
59
60
  )
@@ -1,7 +1,8 @@
1
- import { useAutoPaperTheme } from '@rific/auto-paper'
2
1
  import { StyleSheet, View } from 'react-native'
3
2
  import { Icon, ProgressBar, Text } from 'react-native-paper'
4
3
 
4
+ import { useWellColors, WELL_SHADOW } from './wellSurface'
5
+
5
6
  // A neutral gray for a locked badge's own background - exported so a caller computing its own
6
7
  // badge color (unlocked ? tierColor : LOCKED_BADGE_COLOR, matching LightCycles' own achievements.tsx)
7
8
  // doesn't need to invent or duplicate this exact tone.
@@ -32,10 +33,9 @@ interface Props {
32
33
  // from a specific profile's own tab - optional since only an app with per-profile stat views has
33
34
  // this concept at all.
34
35
  deviceMarker?: boolean
35
- // Independently optional overrides for this row's own text/background colors - each defaults to
36
- // the same dark-mode-derived formula as StatRow/StatSection/BaseStatsScreen (see that component's
37
- // own doc for why) when omitted, so every existing caller renders identically to before. A caller
38
- // with its own app-wide chrome palette passes whichever of the three it needs.
36
+ // Independently optional overrides for this row's own text/background colors - each defaults to the well's
37
+ // (wellSurface.ts: the theme's surface and its onSurface/onSurfaceVariant text), like StatSection/StatRow. A
38
+ // caller with its own app-wide chrome palette passes whichever of the three it needs.
39
39
  fg?: string
40
40
  fgMuted?: string
41
41
  sectionBg?: string
@@ -44,16 +44,15 @@ interface Props {
44
44
  // One row per achievement - a tier-colored (or locked-gray) badge, title/description, an optional
45
45
  // progress bar while locked, and either an unlocked check-mark+label or a lock icon. Extracted
46
46
  // verbatim from LightCycles' own achievements.tsx, where this exact row shape backed its entire
47
- // "ALL ACHIEVEMENTS" list. fg/fgMuted/its own section background default to the same
48
- // useAutoPaperTheme()-derived formula as StatRow/StatSection/BaseStatsScreen (see BaseStatsScreen's
49
- // own doc for why), each independently overridable via the matching prop for a caller with its own
50
- // chrome palette. Has no opinion on what an achievement actually IS (id, unlock predicate, catalog)
47
+ // "ALL ACHIEVEMENTS" list. Each row is its own well (wellSurface.ts: the theme's surface on MD3's level-1 shadow);
48
+ // fg/fgMuted/its own background default to the well's colors, each independently overridable via the matching
49
+ // prop for a caller with its own chrome palette. Has no opinion on what an achievement actually IS (id, unlock predicate, catalog)
51
50
  // - that stays entirely app-local; this is purely the presentational row.
52
51
  export function AchievementRow({ icon, title, description, badgeColor, unlockedLabel, checkColor, progress, deviceMarker, fg: fgOverride, fgMuted: fgMutedOverride, sectionBg: sectionBgOverride }: Props) {
53
- const { dark } = useAutoPaperTheme()
54
- const fg = fgOverride ?? (dark ? '#FFFFFF' : '#000000')
55
- const fgMuted = fgMutedOverride ?? (dark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)')
56
- const sectionBg = sectionBgOverride ?? (dark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)')
52
+ const well = useWellColors()
53
+ const fg = fgOverride ?? well.fg
54
+ const fgMuted = fgMutedOverride ?? well.fgMuted
55
+ const sectionBg = sectionBgOverride ?? well.bg
57
56
  const unlocked = unlockedLabel !== undefined
58
57
 
59
58
  return (
@@ -116,6 +115,7 @@ const styles = StyleSheet.create({
116
115
  row: {
117
116
  alignItems: 'center',
118
117
  borderRadius: 12,
118
+ boxShadow: WELL_SHADOW,
119
119
  flexDirection: 'row',
120
120
  gap: 12,
121
121
  padding: 12
@@ -9,6 +9,11 @@ interface ActivityStats {
9
9
 
10
10
  interface Props {
11
11
  stats: ActivityStats
12
+ // Optional overrides for the section's and its rows' colors, passed straight through to StatSection and each
13
+ // StatRow (whose own defaults are the well's, see wellSurface.ts) - for a caller with its own chrome palette.
14
+ fg?: string
15
+ fgMuted?: string
16
+ sectionBg?: string
12
17
  }
13
18
 
14
19
  // The "ACTIVITY" StatSection (Days Played / Day Streak / Best Day Streak) - extracted verbatim
@@ -18,12 +23,12 @@ interface Props {
18
23
  // currentDayStreak/bestDayStreak) rather than importing DayStreakState itself, so this component
19
24
  // has no dependency on @tastic/achievements at all: any caller's own stats/statsView object that
20
25
  // happens to carry these three fields already satisfies it structurally.
21
- export function ActivityStatSection({ stats }: Props) {
26
+ export function ActivityStatSection({ stats, fg, fgMuted, sectionBg }: Props) {
22
27
  return (
23
- <StatSection label='ACTIVITY'>
24
- <StatRow label='Days Played' value={String(stats.distinctDaysPlayed)} />
25
- <StatRow label='Day Streak' value={String(stats.currentDayStreak)} />
26
- <StatRow label='Best Day Streak' value={String(stats.bestDayStreak)} />
28
+ <StatSection label='ACTIVITY' fg={fg} sectionBg={sectionBg}>
29
+ <StatRow label='Days Played' value={String(stats.distinctDaysPlayed)} fg={fg} fgMuted={fgMuted} />
30
+ <StatRow label='Day Streak' value={String(stats.currentDayStreak)} fg={fg} fgMuted={fgMuted} />
31
+ <StatRow label='Best Day Streak' value={String(stats.bestDayStreak)} fg={fg} fgMuted={fgMuted} />
27
32
  </StatSection>
28
33
  )
29
34
  }
@@ -1,4 +1,5 @@
1
1
  import { AutoAppearancePicker, Dialog, FadingScrollView, useAutoPaperTheme, useBlurModule, useThemeSettings } from '@rific/auto-paper'
2
+ import { useExpoKeepAwake, useKeepAwakeSettings } from '@rific/core'
2
3
  import { Button, SoundContext, TouchableRipple, useHapticSettings, useSoundSettings, useVibration } from '@rific/feedback-press'
3
4
  import { useUpdater } from '@rific/updater'
4
5
  import { rotateInsets, useIsTouchPrimaryDevice, useRotation, type ViewRotation } from '@tastic/core'
@@ -56,13 +57,7 @@ export interface BaseSettingsDialogProps {
56
57
  // @tastic/edge-guard's own swizzle is a no-op on every other platform.
57
58
  deferBottomEdgeGestures?: boolean
58
59
  onDeferBottomEdgeGestures?: (value: boolean) => void
59
- // Optional, and always passed as a pair - omit both for an app that doesn't keep the screen awake.
60
- // This dialog only shows and reports the toggle; the app itself keeps the screen on while it's set
61
- // (expo-keep-awake on its match screen), so this package needs no native dependency for it. Shown
62
- // on the same devices as Lock Orientation (see isTouchPrimary below): a desktop/laptop browser
63
- // stays awake on its own while someone is playing with its keyboard or mouse.
64
- keepAwake?: boolean
65
- onKeepAwakeChange?: (value: boolean) => void
60
+ // No Keep Awake props: that row reads @rific/core's Keep Awake setting itself (see showKeepAwake).
66
61
  // Explicit opt-outs for the rows that pull their own state straight from @rific/feedback-press,
67
62
  // @rific/auto-paper, and @rific/updater's own shared contexts/hooks rather than from anything the
68
63
  // caller passes in - unlike Lock Orientation/Edge Guard above, there's no "value" to omit for
@@ -97,7 +92,7 @@ export interface BaseSettingsDialogProps {
97
92
  // is the seam for whatever isn't shared: a game with its own board/difficulty settings (or a stats
98
93
  // backup flow, or anything else genuinely specific to it) renders those below Appearance, not by
99
94
  // forking this component.
100
- export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, keepAwake, onKeepAwakeChange, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }: BaseSettingsDialogProps) {
95
+ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }: BaseSettingsDialogProps) {
101
96
  const { colors } = useAutoPaperTheme()
102
97
  const ambientRotation = useRotation()
103
98
  const rotation = rotationOverride ?? ambientRotation
@@ -113,7 +108,7 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
113
108
  // Dialog's own 32pt padding is simply overhung.
114
109
  const insets = rotateInsets(useSafeAreaInsets(), rotation)
115
110
  const sheetWidth = Math.min(overlayStyles.card.maxWidth, (turned ? window.height : window.width) - 2 * Math.max(insets.left, insets.right, SHEET_GUTTER))
116
- // Also gates the Lock Orientation and Keep Awake rows below (alongside their own props) - true
111
+ // Also gates the Lock Orientation and Keep Awake rows below (alongside their own conditions) - true
117
112
  // unconditionally on native (see the hook's own doc), so this only actually excludes a
118
113
  // desktop/laptop browser, where a mouse-driven window has no physical orientation to lock
119
114
  // against; a resize there would just make the setting silently stop reflowing the layout instead
@@ -121,6 +116,8 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
121
116
  const isTouchPrimary = useIsTouchPrimaryDevice()
122
117
  const { settings: hapticSettings, set: setHapticSettings } = useHapticSettings()
123
118
  const { settings: soundSettings, set: setSoundSettings } = useSoundSettings()
119
+ const { settings: keepAwakeSettings, set: setKeepAwakeSettings } = useKeepAwakeSettings()
120
+ const expoKeepAwake = useExpoKeepAwake()
124
121
  // Sound/Haptics toggle themselves: the ripple's automatic press feedback fires on onPressIn,
125
122
  // before onPress applies the toggle, so it reflects the OLD enabled value - backwards from what
126
123
  // a settings toggle should confirm (turning off would click/buzz, turning on would go silent).
@@ -160,7 +157,10 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
160
157
 
161
158
  const showLockOrientation = isTouchPrimary && lockOrientation !== undefined && onLockOrientationChange !== undefined
162
159
  const showEdgeGuard = Platform.OS === 'ios' && deferBottomEdgeGestures !== undefined && onDeferBottomEdgeGestures !== undefined
163
- const showKeepAwake = isTouchPrimary && keepAwake !== undefined && onKeepAwakeChange !== undefined
160
+ // Only in an app that opted in by handing expo-keep-awake to @rific/core's KeepAwakeProvider:
161
+ // without the module the setting would do nothing. On the same devices as Lock Orientation, since a
162
+ // desktop/laptop browser stays awake on its own while someone plays with its keyboard or mouse.
163
+ const showKeepAwake = isTouchPrimary && expoKeepAwake !== undefined
164
164
  const showHaptics = Platform.OS !== 'web' && !hideHaptics
165
165
  const showSound = !hideSound
166
166
  const showAppearance = !hideAppearance
@@ -243,11 +243,11 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
243
243
  </TouchableRipple>
244
244
  )}
245
245
 
246
- {/* See the keepAwake prop's own doc: the app does the keeping awake, this only toggles it. */}
246
+ {/* The app's own useKeepAwake('game') does the keeping awake; this only flips the shared setting. */}
247
247
  {showKeepAwake && (
248
- <TouchableRipple onPress={() => onKeepAwakeChange!(!keepAwake)} style={styles.toggleButton} accessibilityLabel={`Keep awake ${keepAwake ? 'on' : 'off'}`}>
248
+ <TouchableRipple onPress={() => setKeepAwakeSettings({ enabled: !keepAwakeSettings.enabled })} style={styles.toggleButton} accessibilityLabel={`Keep awake ${keepAwakeSettings.enabled ? 'on' : 'off'}`}>
249
249
  <View style={styles.toggleContent}>
250
- <SettingIcon source={keepAwake ? 'eye' : 'eye-closed'} color={keepAwake ? colors.tertiary : colors.onSurfaceVariant} containerColor={keepAwake ? colors.tertiaryContainer : colors.surfaceVariant} />
250
+ <SettingIcon source={keepAwakeSettings.enabled ? 'eye' : 'eye-closed'} color={keepAwakeSettings.enabled ? colors.tertiary : colors.onSurfaceVariant} containerColor={keepAwakeSettings.enabled ? colors.tertiaryContainer : colors.surfaceVariant} />
251
251
  <View style={styles.flexShrink}>
252
252
  <Text variant='bodyLarge' style={{ color: colors.onSurface }}>
253
253
  Keep Awake
package/src/StatRow.tsx CHANGED
@@ -1,29 +1,28 @@
1
- import { useAutoPaperTheme } from '@rific/auto-paper'
2
1
  import { StyleSheet, View } from 'react-native'
3
2
  import { Text } from 'react-native-paper'
4
3
 
4
+ import { useWellColors } from './wellSurface'
5
+
5
6
  interface Props {
6
7
  label: string
7
8
  value: string
8
- // Independently optional overrides for this row's own text colors - default to the same
9
- // useAutoPaperTheme()-derived formula as BaseStatsScreen (see that component's own doc for why)
10
- // when omitted, so every existing caller renders identically to before.
9
+ // Independently optional overrides for this row's own text colors - default to the text colors of the well it
10
+ // sits in (wellSurface.ts: onSurface, onSurfaceVariant).
11
11
  fg?: string
12
12
  fgMuted?: string
13
13
  }
14
14
 
15
15
  // A single labeled stat - label on the left (muted), value on the right (bold, full-contrast).
16
16
  // Extracted from LightCycles' own achievements.tsx, where this exact row shape backed every
17
- // section (Overall/Vs CPU/Two Player/Activity). fg/fgMuted default to the same
18
- // useAutoPaperTheme()-derived formula as BaseStatsScreen (see that component's own doc for why),
19
- // each independently overridable via the matching prop for a caller with its own chrome palette.
17
+ // section (Overall/Vs CPU/Two Player/Activity). fg/fgMuted default to the text colors of the StatSection well it
18
+ // sits in, each independently overridable via the matching prop for a caller with its own chrome palette.
20
19
  // Deliberately just label+value text, not a color swatch or profile chip - an app needing a row
21
- // with a leading visual builds its own on top of this same dark-mode formula (see LightCycles' own
20
+ // with a leading visual builds its own on top of the same useWellColors() (see LightCycles' own
22
21
  // ColorStatRow/ProfileRankingRow for an example), since what that visual even is varies per game.
23
22
  export function StatRow({ label, value, fg: fgOverride, fgMuted: fgMutedOverride }: Props) {
24
- const { dark } = useAutoPaperTheme()
25
- const fg = fgOverride ?? (dark ? '#FFFFFF' : '#000000')
26
- const fgMuted = fgMutedOverride ?? (dark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)')
23
+ const well = useWellColors()
24
+ const fg = fgOverride ?? well.fg
25
+ const fgMuted = fgMutedOverride ?? well.fgMuted
27
26
 
28
27
  return (
29
28
  <View style={styles.row}>
@@ -1,29 +1,28 @@
1
- import { useAutoPaperTheme } from '@rific/auto-paper'
2
1
  import { ReactNode } from 'react'
3
2
  import { StyleSheet, View } from 'react-native'
4
3
  import { Text } from 'react-native-paper'
5
4
 
5
+ import { useWellColors, WELL_SHADOW } from './wellSurface'
6
+
6
7
  interface Props {
7
8
  label: string
8
- // Independently optional overrides for this section's own label color/background - default to
9
- // the same useAutoPaperTheme()-derived formula as StatRow/BaseStatsScreen (see BaseStatsScreen's
10
- // own doc for why) when omitted, so every existing caller renders identically to before.
9
+ // Independently optional overrides for this section's own label color/background - default to the well's
10
+ // (wellSurface.ts: the theme's surface and its onSurface text).
11
11
  fg?: string
12
12
  sectionBg?: string
13
13
  children: ReactNode
14
14
  }
15
15
 
16
- // A labeled, tinted-background grouping box - the "OVERALL"/"VS CPU"/"ACTIVITY"-style section
17
- // shape from LightCycles' own achievements.tsx, generalized to any content (typically a stack of
18
- // StatRow). fg/sectionBg default to the same useAutoPaperTheme()-derived formula as
19
- // StatRow/BaseStatsScreen (see BaseStatsScreen's own doc for why), each independently overridable
20
- // via the matching prop for a caller with its own chrome palette. The label itself carries the
21
- // section's own meaning (what it's a section OF), so this component has no opinion on that beyond
22
- // rendering the string it's given.
16
+ // A labeled grouping box, a well (wellSurface.ts: the theme's surface on MD3's level-1 shadow) - the
17
+ // "OVERALL"/"VS CPU"/"ACTIVITY"-style section shape from LightCycles' own achievements.tsx, generalized to any
18
+ // content (typically a stack of StatRow). fg/sectionBg default to the well's colors, each independently
19
+ // overridable via the matching prop for a caller with its own chrome palette. It was a faint tint until
20
+ // 2026-10-17 (6% white in dark mode, 4% black in light). The label itself carries the section's own meaning
21
+ // (what it's a section OF), so this component has no opinion on that beyond rendering the string it's given.
23
22
  export function StatSection({ label, fg: fgOverride, sectionBg: sectionBgOverride, children }: Props) {
24
- const { dark } = useAutoPaperTheme()
25
- const fg = fgOverride ?? (dark ? '#FFFFFF' : '#000000')
26
- const sectionBg = sectionBgOverride ?? (dark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)')
23
+ const well = useWellColors()
24
+ const fg = fgOverride ?? well.fg
25
+ const sectionBg = sectionBgOverride ?? well.bg
27
26
 
28
27
  return (
29
28
  <View style={[styles.section, { backgroundColor: sectionBg }]}>
@@ -46,6 +45,7 @@ const styles = StyleSheet.create({
46
45
  },
47
46
  section: {
48
47
  borderRadius: 12,
48
+ boxShadow: WELL_SHADOW,
49
49
  gap: 8,
50
50
  padding: 16
51
51
  }
@@ -1,8 +1,11 @@
1
1
  // The fleet-chrome entry point (`@tastic/hud/chrome`): the shared dialogs, stats screen pieces, corner
2
- // buttons and style tokens, reachable without the main barrel, for an app that has none of its heavy
3
- // peers. Hangman is the reason it exists: it has @tastic/core, @rific/updater and the Paper stack, but
4
- // no Skia, no @tastic/profile and no @tastic/split-screen. Everything reachable from this file must stay
5
- // free of those three and of the main barrel; @tastic/achievements may appear only as `import type`.
2
+ // buttons, popover primitives, color picker and style tokens, reachable without the main barrel, for an
3
+ // app that has none of its heavy peers. Hangman is the reason it exists: it has @tastic/core,
4
+ // @rific/updater and the Paper stack, but no Skia, no @tastic/profile and no @tastic/split-screen. The
5
+ // popover pieces (PopoverBody, PressAwayOverlay, usePopoverHost, useAutoAlign) and InlineColorPicker are
6
+ // here for @tastic/profile 0.16+, which imports them from this entry instead of the main barrel so its
7
+ // ProfilePicker and ProfilesScreen work in Hangman. Everything reachable from this file must stay free of
8
+ // those three and of the main barrel; @tastic/achievements may appear only as `import type`.
6
9
  // AchievementUnlockList is left out on purpose because it value-imports @tastic/profile's ProfileChip.
7
10
  // src/__tests__/chromeImports.test.ts walks the real import graph from here and enforces all of it.
8
11
  export { AchievementCatalogSection } from '../AchievementCatalogSection'
@@ -15,11 +18,17 @@ export { ContentGutter } from '../ContentGutter'
15
18
  export { CornerActionButtons } from '../CornerActionButtons'
16
19
  export { COMPACT_DIALOG_HEIGHT, DialogHeader, type DialogHeaderProps, isCompactDialogHeight } from '../DialogHeader'
17
20
  export { MONO_FONT } from '../fonts'
21
+ export { getInlineColorPickerContentSize, InlineColorPicker } from '../InlineColorPicker'
18
22
  export { overlayActionStyles, overlayStyles } from '../overlayCard'
19
23
  export { OverlayBackdrop, type OverlayBackdropProps, OverlayCard, type OverlayCardProps, type OverlayCardThemeColors, useOverlayCardColors } from '../OverlaySurface'
24
+ export { PopoverBody } from '../PopoverBody'
25
+ export { PressAwayOverlay } from '../PressAwayOverlay'
20
26
  export { RotatedFrame, type RotatedFrameProps } from '../RotatedFrame'
21
27
  export { getStaggeredWordDuration, StaggeredWord, type StaggeredWordProps } from '../StaggeredWord'
22
28
  export { StatRow } from '../StatRow'
23
29
  export { StatSection } from '../StatSection'
24
30
  export { UpdateDialog, type UpdateDialogProps } from '../UpdateDialog'
31
+ export { type AlignResult, type PopoverAlign, type PopoverRotation, type PopoverVerticalAlign, useAutoAlign } from '../useAutoAlign'
25
32
  export { useCompactDialog } from '../useCompactDialog'
33
+ export { type PopoverHost, usePopoverHost } from '../usePopoverHost'
34
+ export { useWellColors, WELL_SHADOW, type WellColors } from '../wellSurface'
package/src/index.ts CHANGED
@@ -42,3 +42,4 @@ export { useCompactDialog } from './useCompactDialog'
42
42
  export { type PopoverHost, usePopoverHost } from './usePopoverHost'
43
43
  export { type QuitConfirmation, useQuitConfirmation } from './useQuitConfirmation'
44
44
  export { useZoneClampedAlign } from './useZoneClampedAlign'
45
+ export { useWellColors, WELL_SHADOW, type WellColors } from './wellSurface'
@@ -0,0 +1,30 @@
1
+ import { useAutoPaperTheme } from '@rific/auto-paper'
2
+
3
+ // A well is a resting card on a screen: a stats section, an achievement row, a game's menu tile. Every well in the
4
+ // fleet is the theme's plain surface (the same surface as the dialogs, OverlayCard and auto-paper's Dialog) lifted off
5
+ // the screen by MD3's level-1 shadow, with no elevation tint (Jay, 2026-10-17: Solitaire's Achievements wells read as
6
+ // a different shade from its Home tiles, and he picked "shadow, no tint" over MD3's tonal elevation, which washes the
7
+ // surface with the primary like the dialogs no longer are).
8
+ //
9
+ // The shadow is what makes a well show on a screen the color of the surface: MD3 (as react-native-paper implements it)
10
+ // gives `background` and `surface` the same tone, and BaseStatsScreen's light-mode #FFFFFF is the same as the light
11
+ // surface to the eye. In dark mode the surface is already lighter than a black screen, and the shadow barely shows.
12
+
13
+ // MD3's level-1 elevation shadow (its web spec's two layers: a tight key shadow and a soft ambient one). boxShadow is
14
+ // drawn outside the view on iOS, Android and web alike, so a well's own background is never darkened by it.
15
+ export const WELL_SHADOW = '0px 1px 2px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15)'
16
+
17
+ export interface WellColors {
18
+ // The well's own background.
19
+ bg: string
20
+ // Text on the well: titles and values, then labels and descriptions.
21
+ fg: string
22
+ fgMuted: string
23
+ }
24
+
25
+ // The colors of a well and the text on it, from the app's own theme: the same roles the dialogs' text uses
26
+ // (useOverlayCardColors).
27
+ export function useWellColors(): WellColors {
28
+ const { colors } = useAutoPaperTheme()
29
+ return { bg: colors.surface, fg: colors.onSurface, fgMuted: colors.onSurfaceVariant }
30
+ }