@tastic/hud 0.20.0 → 0.22.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 +10 -1
- package/dist/chrome/index.d.mts +15 -3
- package/dist/chrome/index.d.ts +15 -3
- package/dist/chrome/index.js +55 -42
- package/dist/chrome/index.mjs +7 -3
- package/dist/{chunk-DK3HOZC5.mjs → chunk-N7A7W72S.mjs} +38 -29
- package/dist/index.d.mts +6 -4
- package/dist/index.d.ts +6 -4
- package/dist/index.js +140 -117
- package/dist/index.mjs +66 -53
- package/package.json +10 -10
- package/src/AchievementCatalogSection.tsx +7 -6
- package/src/AchievementRow.tsx +13 -13
- package/src/ActivityStatSection.tsx +10 -5
- package/src/BaseStatsScreen.tsx +7 -5
- package/src/PlayerSetupPanel.tsx +77 -56
- package/src/StatRow.tsx +10 -11
- package/src/StatSection.tsx +14 -14
- package/src/chrome/index.ts +1 -0
- package/src/index.ts +2 -1
- package/src/wellSurface.ts +30 -0
package/src/PlayerSetupPanel.tsx
CHANGED
|
@@ -1,31 +1,35 @@
|
|
|
1
1
|
import { SeedColor } from '@rific/auto-paper'
|
|
2
|
-
import { Profile, ProfilePicker } from '@tastic/profile'
|
|
2
|
+
import { getOptionPickerContentSize, getProfilePickerContentSize, OptionPicker, Profile, ProfilePicker, type ProfilePickerColorSection } from '@tastic/profile'
|
|
3
3
|
import { ReactNode } from 'react'
|
|
4
4
|
import { StyleSheet, View } from 'react-native'
|
|
5
5
|
import { Text } from 'react-native-paper'
|
|
6
6
|
|
|
7
7
|
import { InlineColorPicker } from './InlineColorPicker'
|
|
8
|
-
import {
|
|
8
|
+
import { LabeledDropdownOption } from './LabeledDropdown'
|
|
9
9
|
import { ReadyButton } from './ReadyButton'
|
|
10
10
|
import { AlignResult, PopoverRotation } from './useAutoAlign'
|
|
11
11
|
import { PopoverHost, usePopoverHost } from './usePopoverHost'
|
|
12
|
+
import { useZoneClampedAlign } from './useZoneClampedAlign'
|
|
12
13
|
|
|
13
|
-
// This panel's own internal
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// colorAlignOverride/cpuDifficultyAlignOverride below) doesn't have to hardcode or guess this
|
|
17
|
-
// panel's own internal naming convention. Matches secondPickerId's own job for a caller's own
|
|
14
|
+
// This panel's own internal popover ids, derived from idPrefix - exported so a caller that needs to
|
|
15
|
+
// know whether one of them is currently open (e.g. to elevate the zone or row holding the panel)
|
|
16
|
+
// doesn't have to hardcode or guess this panel's own internal naming convention. Matches secondPickerId's own job for a caller's own
|
|
18
17
|
// externally-constructed picker - this is the equivalent for the two pickers this panel owns
|
|
19
18
|
// itself, which have no prop of their own to name their id with.
|
|
20
19
|
export function getColorPopoverId(idPrefix: string): string {
|
|
21
20
|
return `${idPrefix}-color`
|
|
22
21
|
}
|
|
22
|
+
// The seat button's dropdown on a human seat (@tastic/profile's ProfilePicker opens under
|
|
23
|
+
// `${idPrefix}-profile`). A CPU seat's opens under getCpuDifficultyPopoverId.
|
|
24
|
+
export function getProfilePopoverId(idPrefix: string): string {
|
|
25
|
+
return `${idPrefix}-profile`
|
|
26
|
+
}
|
|
23
27
|
export function getCpuDifficultyPopoverId(idPrefix: string): string {
|
|
24
28
|
return `${idPrefix}-difficulty`
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
export interface PlayerSetupPanelProps<C extends string = string> {
|
|
28
|
-
// Namespaces this panel's popover ids ('color', whatever secondPickerId names
|
|
32
|
+
// Namespaces this panel's popover ids ('profile', 'difficulty', 'color', whatever secondPickerId names) so
|
|
29
33
|
// two panels can safely share one host (see `host` below) without their ids colliding.
|
|
30
34
|
idPrefix: string
|
|
31
35
|
// Shared popover host, when this panel's own popovers should be mutually exclusive with another
|
|
@@ -35,7 +39,7 @@ export interface PlayerSetupPanelProps<C extends string = string> {
|
|
|
35
39
|
// real people editing at once is the point.
|
|
36
40
|
host?: PopoverHost
|
|
37
41
|
label?: string
|
|
38
|
-
// Applied only to the plain `label` fallback text below (ProfilePicker/
|
|
42
|
+
// Applied only to the plain `label` fallback text below (ProfilePicker/OptionPicker render
|
|
39
43
|
// their own fixed chrome regardless of this prop) - omit to inherit whichever font the calling
|
|
40
44
|
// screen's own theme resolves for a labelSmall Text, rather than this package forcing its own
|
|
41
45
|
// MONO_FONT onto what's really the host app's own copy.
|
|
@@ -44,21 +48,24 @@ export interface PlayerSetupPanelProps<C extends string = string> {
|
|
|
44
48
|
onColorChange: (hex: string) => void
|
|
45
49
|
swatches: SeedColor[]
|
|
46
50
|
// The other player's current color - stays visible in the swatch grid but disabled, rather than
|
|
47
|
-
// removed from it entirely. Unless allowSwapTaken is set
|
|
51
|
+
// removed from it entirely. Unless allowSwapTaken is set (the other seat is the CPU), which lets
|
|
52
|
+
// it be picked so the caller can swap the two - see InlineColorPicker's own doc.
|
|
48
53
|
takenColor?: string
|
|
49
54
|
allowSwapTaken?: boolean
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
55
|
+
// A short line under the seat button's Color row while a profile is selected, saying where the
|
|
56
|
+
// color applies (e.g. 'Snake only': the pick is kept in this game, not on the profile). Left out
|
|
57
|
+
// for a guest or the CPU, who have no color anywhere else.
|
|
58
|
+
colorCaption?: string
|
|
59
|
+
// False leaves the Color row out of the seat button's dropdown (e.g. a CPU seat set to "None",
|
|
60
|
+
// with no second player to color). Defaults to true.
|
|
61
|
+
showColor?: boolean
|
|
62
|
+
// The fallback color swatch's placement and rotation, passed straight through to its
|
|
63
|
+
// InlineColorPicker. That swatch is only drawn on a seat with neither a profile picker nor a CPU
|
|
64
|
+
// difficulty picker (see the header below); the seat button places its own dropdown.
|
|
54
65
|
colorAlignOverride?: AlignResult
|
|
55
|
-
// Passed straight through to the internal InlineColorPicker's own `rotation` - same reasoning as
|
|
56
|
-
// colorAlignOverride above (no other way for a caller to reach that picker directly), for a
|
|
57
|
-
// caller whose own rotation reading needs to differ from InlineColorPicker's default ambient
|
|
58
|
-
// useRotation() read (see that component's own `rotation` doc for when that's worth doing).
|
|
59
66
|
colorRotation?: PopoverRotation
|
|
60
67
|
isHuman: boolean
|
|
61
|
-
// Whatever sits
|
|
68
|
+
// Whatever sits under the seat button in pickerRow, if anything - a ControlSchemePicker, a raw
|
|
62
69
|
// SectionedDropdown, or nothing at all. The five apps this component was extracted from each
|
|
63
70
|
// duplicated this entire panel and differed only in what (if anything) occupied this one slot,
|
|
64
71
|
// each wanting a genuinely different concept there (which physical input drives a seat, vs. a
|
|
@@ -69,7 +76,7 @@ export interface PlayerSetupPanelProps<C extends string = string> {
|
|
|
69
76
|
secondPicker?: ReactNode
|
|
70
77
|
// The popover id secondPicker opens under, if it opens one at all - lets pickerRow's own
|
|
71
78
|
// "elevate above later siblings while one of my own popovers is open" logic (see pickerRowOpen)
|
|
72
|
-
// react to secondPicker's popover the same way it already does for the
|
|
79
|
+
// react to secondPicker's popover the same way it already does for the fallback swatch's, without
|
|
73
80
|
// this component needing any opinion on what secondPicker actually renders. Omit when secondPicker
|
|
74
81
|
// has no popover of its own, or isn't provided at all.
|
|
75
82
|
secondPickerId?: string
|
|
@@ -82,7 +89,7 @@ export interface PlayerSetupPanelProps<C extends string = string> {
|
|
|
82
89
|
profiles?: Profile[]
|
|
83
90
|
selectedProfileId?: string | null
|
|
84
91
|
takenProfileId?: string | null
|
|
85
|
-
// What the
|
|
92
|
+
// What the seat button shows before any profile's selected - this seat's own 'P1'/'P2' (see
|
|
86
93
|
// ProfilePicker's own doc for why it's never "Player"). Required whenever profiles is provided,
|
|
87
94
|
// since the trigger always needs some idle text.
|
|
88
95
|
guestLabel?: string
|
|
@@ -95,29 +102,30 @@ export interface PlayerSetupPanelProps<C extends string = string> {
|
|
|
95
102
|
cpuDifficulty?: C
|
|
96
103
|
cpuDifficultyOptions?: LabeledDropdownOption<C>[]
|
|
97
104
|
onCpuDifficultyChange?: (value: C) => void
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
// consumer's own convention. A caller with its own extra CPU states (e.g. a "no CPU" slot) computes
|
|
104
|
-
// its own icon string and passes it through cpuIcon rather than this component needing any opinion
|
|
105
|
-
// on what those states are.
|
|
105
|
+
// The icon on a non-human seat's button chip, and on the fallback color swatch for a human vs. a
|
|
106
|
+
// non-human seat (overridden there by a selected profile's own `tag`). A human seat's button chip
|
|
107
|
+
// shows the selected profile's tag, or ProfilePicker's guest icon. A caller with its own extra CPU
|
|
108
|
+
// states (e.g. a "no CPU" slot) computes its own icon string and passes it through cpuIcon rather
|
|
109
|
+
// than this component needing any opinion on what those states are.
|
|
106
110
|
humanIcon?: string
|
|
107
111
|
cpuIcon?: string
|
|
108
112
|
}
|
|
109
113
|
|
|
110
|
-
// One panel per player slot in a local-multiplayer loadout/lobby screen:
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
114
|
+
// One panel per player slot in a local-multiplayer loadout/lobby screen: one seat button, an
|
|
115
|
+
// optional second, per-app picker under it (see `secondPicker`'s own doc), and a Ready toggle for
|
|
116
|
+
// human seats. A human seat's button is @tastic/profile's ProfilePicker and a CPU seat's is its
|
|
117
|
+
// OptionPicker (the difficulty levels), each with the seat's color behind a Color row at the top of
|
|
118
|
+
// the same dropdown: Solitaire's player button, made the fleet's seat control (Jay, 2026-10-16). It
|
|
119
|
+
// used to be a profile (or difficulty) dropdown with a separate color swatch beside it, which read
|
|
120
|
+
// as two unrelated things. A seat with neither picker wired keeps the plain label and the separate
|
|
121
|
+
// swatch. Ported from five apps' own near-identical PlayerSetupPanel.tsx/LobbyPlayerPanel.tsx
|
|
122
|
+
// (AirHockey, BoxHockey, Pong, LightCycles, Snake), which had all independently converged on this
|
|
123
|
+
// exact skeleton.
|
|
116
124
|
//
|
|
117
|
-
// Owns its own popover host shared by its own
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
export function PlayerSetupPanel<C extends string = string>({ idPrefix, host, label, labelFontFamily, color, onColorChange, swatches, takenColor, allowSwapTaken, colorAlignOverride, colorRotation, isHuman, secondPicker, secondPickerId, ready, onToggleReady, dark, showReadyButton = true, profiles, selectedProfileId, takenProfileId, guestLabel, onProfileSelect, onManageProfiles, cpuDifficulty, cpuDifficultyOptions, onCpuDifficultyChange,
|
|
125
|
+
// Owns its own popover host shared by its own pickers unless a `host` is passed in to share with
|
|
126
|
+
// another panel (or a shared-controls row) instead - same convention as every popover-hosting
|
|
127
|
+
// component in this package.
|
|
128
|
+
export function PlayerSetupPanel<C extends string = string>({ idPrefix, host, label, labelFontFamily, color, onColorChange, swatches, takenColor, allowSwapTaken, colorCaption, showColor = true, colorAlignOverride, colorRotation, isHuman, secondPicker, secondPickerId, ready, onToggleReady, dark, showReadyButton = true, profiles, selectedProfileId, takenProfileId, guestLabel, onProfileSelect, onManageProfiles, cpuDifficulty, cpuDifficultyOptions, onCpuDifficultyChange, humanIcon = 'face-man', cpuIcon = 'robot' }: PlayerSetupPanelProps<C>) {
|
|
121
129
|
const ownHost = usePopoverHost()
|
|
122
130
|
const popover = host ?? ownHost
|
|
123
131
|
// Checking *this panel's own* ids, not just "is anything open on the host" - when `host` is
|
|
@@ -126,22 +134,33 @@ export function PlayerSetupPanel<C extends string = string>({ idPrefix, host, la
|
|
|
126
134
|
// letting DOM order (wrongly) decide which one paints on top.
|
|
127
135
|
const ownPopoverOpen = popover.openId?.startsWith(`${idPrefix}-`) ?? false
|
|
128
136
|
// Narrower than ownPopoverOpen on purpose - only true for a popover actually inside pickerRow
|
|
129
|
-
// itself (color, or secondPicker's own popover when it has one and
|
|
130
|
-
// pickerRowOpen's own style comment for why pickerRow needs this in
|
|
137
|
+
// itself (the fallback color swatch, or secondPicker's own popover when it has one and
|
|
138
|
+
// secondPickerId names it). See pickerRowOpen's own style comment for why pickerRow needs this in
|
|
139
|
+
// addition to panelOpen above.
|
|
131
140
|
const pickerRowPopoverOpen = popover.openId === getColorPopoverId(idPrefix) || (secondPickerId !== undefined && popover.openId === secondPickerId)
|
|
132
141
|
const showProfilePicker = isHuman && profiles !== undefined && !!onProfileSelect
|
|
133
142
|
const showCpuDifficulty = !isHuman && cpuDifficulty !== undefined && cpuDifficultyOptions !== undefined && !!onCpuDifficultyChange
|
|
134
|
-
|
|
135
|
-
// profile rather than snapshotted at selection time, since a caller
|
|
136
|
-
// editing surface for it on this same screen.
|
|
143
|
+
const seatButton = showProfilePicker || showCpuDifficulty
|
|
144
|
+
// Read live from the selected profile rather than snapshotted at selection time, since a caller
|
|
145
|
+
// typically has no other editing surface for it on this same screen.
|
|
137
146
|
const selectedProfile = profiles?.find((p) => p.id === selectedProfileId) ?? null
|
|
138
147
|
|
|
148
|
+
const colorSection: ProfilePickerColorSection | undefined = showColor ? { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, caption: showProfilePicker && selectedProfile ? colorCaption : undefined } : undefined
|
|
149
|
+
// The seat button's dropdown is placed here rather than by the caller: inside a
|
|
150
|
+
// @tastic/split-screen zone (2 Player) it has to stay in that player's half, which
|
|
151
|
+
// useZoneClampedAlign does, and outside one it's useAutoAlign's own placement. Sized from the same
|
|
152
|
+
// section and rows the picker draws. Rotation 0, the same as the profile, color and difficulty
|
|
153
|
+
// dropdowns this button replaced were all placed with.
|
|
154
|
+
const seatPickerId = showProfilePicker ? getProfilePopoverId(idPrefix) : getCpuDifficultyPopoverId(idPrefix)
|
|
155
|
+
const seatPickerSize = showProfilePicker ? getProfilePickerContentSize(profiles!.length, !!onManageProfiles, colorSection) : getOptionPickerContentSize(cpuDifficultyOptions?.length ?? 0, colorSection)
|
|
156
|
+
const seatPickerAlign = useZoneClampedAlign(seatButton && popover.openId === seatPickerId, seatPickerSize.width, seatPickerSize.height)
|
|
157
|
+
|
|
139
158
|
return (
|
|
140
159
|
<View style={[styles.panel, ownPopoverOpen && styles.panelOpen]}>
|
|
141
160
|
{showProfilePicker ? (
|
|
142
|
-
<ProfilePicker idPrefix={idPrefix} host={popover} profiles={profiles!} selectedId={selectedProfileId ?? null} takenId={takenProfileId} color={color} dark={dark} guestLabel={guestLabel ?? 'GUEST'} onSelect={(profile: Profile | null) => onProfileSelect?.(profile)} onManage={onManageProfiles} />
|
|
161
|
+
<ProfilePicker idPrefix={idPrefix} host={popover} profiles={profiles!} selectedId={selectedProfileId ?? null} takenId={takenProfileId} color={color} dark={dark} guestLabel={guestLabel ?? 'GUEST'} onSelect={(profile: Profile | null) => onProfileSelect?.(profile)} onManage={onManageProfiles} colorSection={colorSection} alignOverride={seatPickerAlign} />
|
|
143
162
|
) : showCpuDifficulty ? (
|
|
144
|
-
<
|
|
163
|
+
<OptionPicker id={getCpuDifficultyPopoverId(idPrefix)} host={popover} options={cpuDifficultyOptions!} value={cpuDifficulty!} onChange={onCpuDifficultyChange!} color={color} dark={dark} icon={cpuIcon} colorSection={colorSection} alignOverride={seatPickerAlign} />
|
|
145
164
|
) : (
|
|
146
165
|
label && (
|
|
147
166
|
<Text variant='labelSmall' style={{ color, fontFamily: labelFontFamily }}>
|
|
@@ -150,14 +169,16 @@ export function PlayerSetupPanel<C extends string = string>({ idPrefix, host, la
|
|
|
150
169
|
)
|
|
151
170
|
)}
|
|
152
171
|
|
|
153
|
-
|
|
154
|
-
{
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
172
|
+
{(!seatButton || secondPicker) && (
|
|
173
|
+
<View style={[styles.pickerRow, pickerRowPopoverOpen && styles.pickerRowOpen]}>
|
|
174
|
+
{/* The fallback swatch, for a seat with no button: a human slot shows the selected
|
|
175
|
+
profile's own tag when it has one, falling back to humanIcon; a non-human slot always
|
|
176
|
+
falls back to cpuIcon. */}
|
|
177
|
+
{!seatButton && <InlineColorPicker id={getColorPopoverId(idPrefix)} host={popover} value={color} onChange={onColorChange} swatches={swatches} takenValue={takenColor} allowSwapTaken={allowSwapTaken} alignOverride={colorAlignOverride} rotation={colorRotation} dark={dark} tag={isHuman ? selectedProfile?.tag : undefined} icon={isHuman ? humanIcon : cpuIcon} />}
|
|
158
178
|
|
|
159
|
-
|
|
160
|
-
|
|
179
|
+
{secondPicker}
|
|
180
|
+
</View>
|
|
181
|
+
)}
|
|
161
182
|
|
|
162
183
|
{isHuman && showReadyButton && onToggleReady && <ReadyButton color={color} ready={ready ?? false} onToggleReady={onToggleReady} />}
|
|
163
184
|
</View>
|
|
@@ -175,9 +196,9 @@ const styles = StyleSheet.create({
|
|
|
175
196
|
panelOpen: {
|
|
176
197
|
zIndex: 100
|
|
177
198
|
},
|
|
178
|
-
// Same gap as `panel`'s own vertical rhythm, reused horizontally - the color
|
|
179
|
-
// present) secondPicker sit side by side within this row instead of stacked in the panel's
|
|
180
|
-
// column.
|
|
199
|
+
// Same gap as `panel`'s own vertical rhythm, reused horizontally - the fallback color swatch and
|
|
200
|
+
// (when present) secondPicker sit side by side within this row instead of stacked in the panel's
|
|
201
|
+
// own column.
|
|
181
202
|
pickerRow: {
|
|
182
203
|
alignItems: 'center',
|
|
183
204
|
flexDirection: 'row',
|
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
|
|
9
|
-
//
|
|
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
|
|
18
|
-
//
|
|
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
|
|
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
|
|
25
|
-
const fg = fgOverride ??
|
|
26
|
-
const fgMuted = fgMutedOverride ??
|
|
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}>
|
package/src/StatSection.tsx
CHANGED
|
@@ -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
|
|
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
|
|
17
|
-
// shape from LightCycles' own achievements.tsx, generalized to any
|
|
18
|
-
// StatRow). fg/sectionBg default to the
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
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
|
|
25
|
-
const fg = fgOverride ??
|
|
26
|
-
const sectionBg = sectionBgOverride ??
|
|
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
|
}
|
package/src/chrome/index.ts
CHANGED
|
@@ -23,3 +23,4 @@ export { StatRow } from '../StatRow'
|
|
|
23
23
|
export { StatSection } from '../StatSection'
|
|
24
24
|
export { UpdateDialog, type UpdateDialogProps } from '../UpdateDialog'
|
|
25
25
|
export { useCompactDialog } from '../useCompactDialog'
|
|
26
|
+
export { useWellColors, WELL_SHADOW, type WellColors } from '../wellSurface'
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ export { getLabeledDropdownContentHeight, LABELED_DROPDOWN_POPOVER_WIDTH, Labele
|
|
|
16
16
|
export { loadSkiaWeb } from './loadSkiaWeb'
|
|
17
17
|
export { overlayActionStyles, overlayStyles } from './overlayCard'
|
|
18
18
|
export { OverlayBackdrop, type OverlayBackdropProps, OverlayCard, type OverlayCardProps, type OverlayCardThemeColors, useOverlayCardColors } from './OverlaySurface'
|
|
19
|
-
export { getColorPopoverId, getCpuDifficultyPopoverId, PlayerSetupPanel, type PlayerSetupPanelProps } from './PlayerSetupPanel'
|
|
19
|
+
export { getColorPopoverId, getCpuDifficultyPopoverId, getProfilePopoverId, PlayerSetupPanel, type PlayerSetupPanelProps } from './PlayerSetupPanel'
|
|
20
20
|
export { PopoverBody } from './PopoverBody'
|
|
21
21
|
export { PressAwayOverlay } from './PressAwayOverlay'
|
|
22
22
|
export { ReadyButton } from './ReadyButton'
|
|
@@ -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
|
+
}
|