@tastic/hud 0.1.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/LICENSE +21 -0
- package/README.md +110 -0
- package/dist/TriggerGauge-HIPRGEWJ.mjs +117 -0
- package/dist/index.d.mts +127 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.js +649 -0
- package/dist/index.mjs +475 -0
- package/package.json +103 -0
- package/src/InlineColorPicker.tsx +183 -0
- package/src/PopoverBody.tsx +131 -0
- package/src/PressAwayOverlay.tsx +20 -0
- package/src/ReadyButton.tsx +43 -0
- package/src/SectionedDropdown.tsx +387 -0
- package/src/TriggerGauge.tsx +285 -0
- package/src/TriggerGaugeHost.tsx +27 -0
- package/src/fonts.ts +6 -0
- package/src/index.ts +18 -0
- package/src/loadSkiaWeb.ts +14 -0
- package/src/useAutoAlign.ts +88 -0
- package/src/usePopoverHost.ts +24 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { getBlendedColor, getColorRoles } from '@rific/auto-paper'
|
|
2
|
+
import { IconButton, TouchableRipple } from '@rific/feedback-press'
|
|
3
|
+
import { ScrollView, StyleSheet, View } from 'react-native'
|
|
4
|
+
import { Icon, Text } from 'react-native-paper'
|
|
5
|
+
|
|
6
|
+
import { MONO_FONT } from './fonts'
|
|
7
|
+
import { PopoverBody } from './PopoverBody'
|
|
8
|
+
import TriggerGaugeHost from './TriggerGaugeHost'
|
|
9
|
+
import { useAutoAlign } from './useAutoAlign'
|
|
10
|
+
import { PopoverHost } from './usePopoverHost'
|
|
11
|
+
|
|
12
|
+
const MENU_BORDER_WIDTH = 1
|
|
13
|
+
// Matches triggerBox's own height/width below — passed to PopoverBody so its caret can point at
|
|
14
|
+
// this trigger's actual center, not the (usually wider) menu's midpoint.
|
|
15
|
+
const TRIGGER_SIZE = 44
|
|
16
|
+
const MENU_MIN_WIDTH = 200
|
|
17
|
+
// Fed into useAutoAlign as the popover's assumed width — a real render can come in narrower
|
|
18
|
+
// (shrink-to-fit content), but never wider, since `menu` below caps at this same value. Alignment
|
|
19
|
+
// math staying in sync with the actual rendered cap is what keeps it from ever being wrong in a way
|
|
20
|
+
// that lets the popover clip off-screen.
|
|
21
|
+
const MENU_MAX_WIDTH = 220
|
|
22
|
+
const MENU_ITEM_ICON_SIZE = 18
|
|
23
|
+
// Tuned by eye: wide enough that a short one-word-ish label stays on one line, narrow enough that a
|
|
24
|
+
// sentence-length description reliably wraps onto a second line instead.
|
|
25
|
+
const MENU_ITEM_LABEL_MAX_WIDTH = 150
|
|
26
|
+
// Estimated per-row heights fed into useAutoAlign's vertical flip — approximate (item's own
|
|
27
|
+
// padding + a labelLarge line, give or take font metrics), not measured, for the same reason
|
|
28
|
+
// MENU_MAX_WIDTH is a fixed cap rather than a measured value: knowing it before the popover renders
|
|
29
|
+
// is what lets the flip decision happen in the same frame as opening, instead of a measure-then-
|
|
30
|
+
// reposition flicker.
|
|
31
|
+
const MENU_ITEM_HEIGHT = 40
|
|
32
|
+
const MENU_ITEM_HEIGHT_WITH_DESCRIPTION = 58
|
|
33
|
+
const MENU_DIVIDER_HEIGHT = 9
|
|
34
|
+
const MENU_VERTICAL_PADDING = 12
|
|
35
|
+
|
|
36
|
+
export interface MenuOption<T extends string | number> {
|
|
37
|
+
value: T
|
|
38
|
+
label: string
|
|
39
|
+
description?: string
|
|
40
|
+
// Optional per-option icon — when every option in a single-select section has one, the trigger
|
|
41
|
+
// shows the currently selected option's icon instead of the static fallback, so the value is
|
|
42
|
+
// readable at a glance without opening the dropdown. Unused by multi-select sections (there's no
|
|
43
|
+
// single "the" selected option to show on the trigger).
|
|
44
|
+
icon?: string
|
|
45
|
+
iconSize?: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Exactly one of these can be true at a time — radio-style. Tapping a row selects it; whether that
|
|
49
|
+
// also closes the popover is controlled by the dropdown's own `autoDismiss` prop.
|
|
50
|
+
export interface SingleSelectSection<T extends string | number> {
|
|
51
|
+
kind: 'single'
|
|
52
|
+
id: string
|
|
53
|
+
options: MenuOption<T>[]
|
|
54
|
+
value: T
|
|
55
|
+
onChange: (value: T) => void
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Any number of these can be true at once — checkbox-style. Tapping a row toggles just that one and
|
|
59
|
+
// never closes the popover on its own (unlike a single-select row, picking one of several things
|
|
60
|
+
// isn't "done" the way picking the one value in a single-select is).
|
|
61
|
+
export interface MultiSelectSection<T extends string | number> {
|
|
62
|
+
kind: 'multi'
|
|
63
|
+
id: string
|
|
64
|
+
options: MenuOption<T>[]
|
|
65
|
+
value: T[]
|
|
66
|
+
onChange: (value: T[]) => void
|
|
67
|
+
// Renders an alternating All/Clear bulk-toggle button beneath this section's rows — label reflects
|
|
68
|
+
// what tapping it would do next (every option already selected -> "Clear"; anything else -> "All").
|
|
69
|
+
allClear?: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
|
+
export type MenuSection = SingleSelectSection<any> | MultiSelectSection<any>
|
|
74
|
+
|
|
75
|
+
interface Props {
|
|
76
|
+
id: string
|
|
77
|
+
host: PopoverHost
|
|
78
|
+
// Static fallback trigger icon — used whenever there isn't a single unambiguous "selected option"
|
|
79
|
+
// to show instead (see the trigger-icon logic below).
|
|
80
|
+
icon: string
|
|
81
|
+
accessibilityLabel: string
|
|
82
|
+
sections: MenuSection[]
|
|
83
|
+
accentColor: string
|
|
84
|
+
mutedColor: string
|
|
85
|
+
// Foreground for a selected row's icon/text, painted on top of accentColor's own fill — accentColor
|
|
86
|
+
// isn't guaranteed to be a fixed brand tone (a consumer may derive it from a user-chosen color), so
|
|
87
|
+
// a hardcoded black or a guessed contrast color both risk landing unreadable against it. Omit to
|
|
88
|
+
// derive one automatically (getColorRoles' onColor) from accentColor against this popover's own
|
|
89
|
+
// background — pass a real design-system "on" color explicitly instead when you have one.
|
|
90
|
+
onAccentColor?: string
|
|
91
|
+
dark: boolean
|
|
92
|
+
// Manual override — omit to let the popover measure its own trigger and pick whichever alignment
|
|
93
|
+
// keeps it from overflowing the screen edge (see useAutoAlign). Only pass this to force a specific
|
|
94
|
+
// side regardless of where the trigger actually sits.
|
|
95
|
+
align?: 'left' | 'right' | 'center'
|
|
96
|
+
// Whether picking a value in a *single-select* section closes the popover. Defaults to true
|
|
97
|
+
// (the old, pre-press-away default: pick one thing, you're done). Multi-select rows never
|
|
98
|
+
// auto-close regardless of this — see MultiSelectSection's own comment. Worth setting false for a
|
|
99
|
+
// dropdown with multiple sections, where picking one value while another's still being decided
|
|
100
|
+
// shouldn't snap the whole thing shut; press-away (or the trigger itself) closes it instead.
|
|
101
|
+
autoDismiss?: boolean
|
|
102
|
+
// Defaults to the system monospace font — pass your own game's registered font to match its UI.
|
|
103
|
+
labelFontFamily?: string
|
|
104
|
+
allClearLabels?: { all: string; clear: string }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Single popover shell that can hold any mix of single-select ("pick one") and multi-select ("pick
|
|
108
|
+
// some") sections, divided by rules. A plain single-value picker (grid size, speed, CPU difficulty,
|
|
109
|
+
// control scheme, ...) is just this with one 'single' section; a multi-select toggle list (which
|
|
110
|
+
// powerups are enabled, say) is just this with one 'multi' section (optionally with allClear); a
|
|
111
|
+
// combined menu (e.g. spawn frequency + which types, in one menu) is just two sections in the same
|
|
112
|
+
// array.
|
|
113
|
+
export function SectionedDropdown({ id, host, icon, accessibilityLabel, sections, accentColor, mutedColor, onAccentColor, dark, align: alignOverride, autoDismiss = true, labelFontFamily = MONO_FONT, allClearLabels = { all: 'All', clear: 'Clear' } }: Props) {
|
|
114
|
+
const menuBg = dark ? '#000000' : '#FFFFFF'
|
|
115
|
+
// mutedColor is translucent (fine for the icon/text tints below, each a single paint), but the
|
|
116
|
+
// caret is deliberately drawn overlapping the box's own top border (PopoverBody's CARET_DIP,
|
|
117
|
+
// needed to avoid a subpixel seam between the two separately-drawn edges) — with a translucent
|
|
118
|
+
// color that overlap double-blends into a visibly lighter patch, unlike every other (opaque-
|
|
119
|
+
// bordered) popover in the app. Flattened once against this menu's own solid background so the
|
|
120
|
+
// box border and caret ring paint identically instead of compounding.
|
|
121
|
+
const menuBorderColor = getBlendedColor(mutedColor, menuBg, 0.5)
|
|
122
|
+
const resolvedOnAccentColor = onAccentColor ?? getColorRoles(accentColor, menuBg).onColor
|
|
123
|
+
// A bold, always-legible foreground — the trigger icon's job is category identity ("this is grid
|
|
124
|
+
// size"), not state, so it stays as bold as any other icon on screen (a back arrow, a settings
|
|
125
|
+
// gear) instead of dimmed. State lives entirely in TriggerGaugeHost's ring around it.
|
|
126
|
+
const fg = dark ? '#FFFFFF' : '#000000'
|
|
127
|
+
|
|
128
|
+
const open = host.openId === id
|
|
129
|
+
// Estimated, not measured — see MENU_ITEM_HEIGHT's own comment. Every row, plus a divider between
|
|
130
|
+
// sections, plus a footer row for whichever multi-select sections show one.
|
|
131
|
+
const estimatedHeight =
|
|
132
|
+
MENU_VERTICAL_PADDING +
|
|
133
|
+
sections.reduce((sum, section, sectionIndex) => {
|
|
134
|
+
const rows = section.options.reduce((rowSum, option) => rowSum + (option.description ? MENU_ITEM_HEIGHT_WITH_DESCRIPTION : MENU_ITEM_HEIGHT), 0)
|
|
135
|
+
const footer = section.kind === 'multi' && section.allClear ? MENU_ITEM_HEIGHT : 0
|
|
136
|
+
const divider = sectionIndex > 0 ? MENU_DIVIDER_HEIGHT : 0
|
|
137
|
+
return sum + rows + footer + divider
|
|
138
|
+
}, 0)
|
|
139
|
+
const { align: autoAlign, maxHeight, measured, triggerRef, verticalAlign } = useAutoAlign(open, MENU_MAX_WIDTH, estimatedHeight)
|
|
140
|
+
const align = alignOverride ?? autoAlign
|
|
141
|
+
|
|
142
|
+
// Trigger reflects the selected option's own icon only when there's exactly one section and it's
|
|
143
|
+
// single-select — any other shape (multi-select present, or more than one section) has no single
|
|
144
|
+
// unambiguous "the" value to show, so it falls back to the static icon instead.
|
|
145
|
+
const onlySection = sections.length === 1 ? sections[0] : null
|
|
146
|
+
const selectedOption = onlySection?.kind === 'single' ? onlySection.options.find((o) => o.value === onlySection.value) : undefined
|
|
147
|
+
const triggerIcon = selectedOption?.icon ?? icon
|
|
148
|
+
const triggerIconSize = selectedOption?.iconSize ?? 22
|
|
149
|
+
const hasMultiSection = sections.some((s) => s.kind === 'multi')
|
|
150
|
+
const anyMultiSelected = sections.some((s) => s.kind === 'multi' && s.value.length > 0)
|
|
151
|
+
// A single-select trigger always has exactly one active value, so there's no "off" state for its
|
|
152
|
+
// icon to distinguish — plain fg, unconditionally. A multi-select one (powerups, say) genuinely
|
|
153
|
+
// can be fully off (nothing enabled), which is different information than *which* ones are on
|
|
154
|
+
// (the gauge ring's job) — worth a real on/off read on the icon itself, especially once a section
|
|
155
|
+
// like this gains a combined off/few/many dimension alongside its checkboxes.
|
|
156
|
+
const triggerIconColor = hasMultiSection ? (anyMultiSelected ? accentColor : fg) : fg
|
|
157
|
+
|
|
158
|
+
// Unlike the trigger-icon override above (which only makes sense for a single unambiguous "the"
|
|
159
|
+
// value), the gauge ring has no such restriction — it just needs a position per option and which
|
|
160
|
+
// ones are lit, so it flattens every section's options into one combined ring instead of only
|
|
161
|
+
// rendering when there's exactly one section. A two-section dropdown (say, board variant + a
|
|
162
|
+
// rink toggle riding along as its own single-item multi-select) reads as one ring where the first
|
|
163
|
+
// N dashes are the single-select's tiers and the last dash is the toggle, each section's own
|
|
164
|
+
// selection state lighting independently within its own slice.
|
|
165
|
+
const gaugeSegments = sections.reduce((sum, section) => sum + section.options.length, 0)
|
|
166
|
+
const gaugeLitIndices: number[] = []
|
|
167
|
+
let gaugeOffset = 0
|
|
168
|
+
for (const section of sections) {
|
|
169
|
+
if (section.kind === 'single') {
|
|
170
|
+
const index = section.options.findIndex((o) => o.value === section.value)
|
|
171
|
+
if (index >= 0) gaugeLitIndices.push(gaugeOffset + index)
|
|
172
|
+
} else {
|
|
173
|
+
section.options.forEach((o, i) => {
|
|
174
|
+
if (section.value.includes(o.value)) gaugeLitIndices.push(gaugeOffset + i)
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
gaugeOffset += section.options.length
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<View style={[styles.anchor, open && styles.anchorOpen]}>
|
|
182
|
+
<View ref={triggerRef} collapsable={false} style={styles.triggerBox}>
|
|
183
|
+
<TriggerGaugeHost segments={gaugeSegments} litIndices={gaugeLitIndices} size={TRIGGER_SIZE} accentColor={accentColor} mutedColor={mutedColor} />
|
|
184
|
+
<IconButton icon={triggerIcon} iconColor={triggerIconColor} size={triggerIconSize} accessibilityLabel={accessibilityLabel} onPress={() => host.toggle(id)} />
|
|
185
|
+
</View>
|
|
186
|
+
|
|
187
|
+
{/* Gated on `measured`, not just `open` — see useAutoAlign's own comment. Without it, the
|
|
188
|
+
popover mounts for one frame at whatever align/verticalAlign the *previous* open (or the
|
|
189
|
+
initial center/below guess) left behind, then visibly jumps once this open's own measurement
|
|
190
|
+
lands — most noticeable near a screen edge, where the guess is furthest from correct. */}
|
|
191
|
+
<PopoverBody visible={open && measured} align={align} verticalAlign={verticalAlign} caretColor={menuBg} caretBorderColor={menuBorderColor} caretBorderWidth={MENU_BORDER_WIDTH} triggerSize={TRIGGER_SIZE}>
|
|
192
|
+
{/* maxHeight is the room useAutoAlign actually found in whichever direction it picked — on
|
|
193
|
+
a screen tall enough for the estimated content, this never kicks in (estimatedHeight itself
|
|
194
|
+
is always <= maxHeight then, so nothing scrolls); on a short landscape screen where neither
|
|
195
|
+
direction has enough room, this is what keeps the menu from just running off the screen
|
|
196
|
+
edge instead of flipping only made it run off a *smaller* amount. */}
|
|
197
|
+
<ScrollView style={[styles.menu, { backgroundColor: menuBg, borderColor: menuBorderColor, maxHeight }]} contentContainerStyle={styles.menuContent} showsVerticalScrollIndicator={false}>
|
|
198
|
+
{sections.map((section, sectionIndex) => (
|
|
199
|
+
<View key={section.id}>
|
|
200
|
+
{/* Every section after the first gets a rule above it — the divider belongs to the
|
|
201
|
+
boundary between sections, not to either section itself, so it's keyed off position
|
|
202
|
+
rather than each section carrying its own "divider below me" flag. */}
|
|
203
|
+
{sectionIndex > 0 && <View style={[styles.divider, { backgroundColor: menuBorderColor }]} />}
|
|
204
|
+
|
|
205
|
+
{section.kind === 'single' ? (
|
|
206
|
+
<SingleSection
|
|
207
|
+
section={section}
|
|
208
|
+
accentColor={accentColor}
|
|
209
|
+
onAccentColor={resolvedOnAccentColor}
|
|
210
|
+
mutedColor={mutedColor}
|
|
211
|
+
labelFontFamily={labelFontFamily}
|
|
212
|
+
onSelect={(value) => {
|
|
213
|
+
section.onChange(value)
|
|
214
|
+
if (autoDismiss) host.close()
|
|
215
|
+
}}
|
|
216
|
+
/>
|
|
217
|
+
) : (
|
|
218
|
+
<MultiSection section={section} accentColor={accentColor} onAccentColor={resolvedOnAccentColor} mutedColor={mutedColor} labelFontFamily={labelFontFamily} allClearLabels={allClearLabels} />
|
|
219
|
+
)}
|
|
220
|
+
</View>
|
|
221
|
+
))}
|
|
222
|
+
</ScrollView>
|
|
223
|
+
</PopoverBody>
|
|
224
|
+
</View>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function SingleSection({
|
|
229
|
+
section,
|
|
230
|
+
accentColor,
|
|
231
|
+
onAccentColor,
|
|
232
|
+
mutedColor,
|
|
233
|
+
labelFontFamily,
|
|
234
|
+
onSelect
|
|
235
|
+
}: {
|
|
236
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
237
|
+
section: SingleSelectSection<any>
|
|
238
|
+
accentColor: string
|
|
239
|
+
onAccentColor: string
|
|
240
|
+
mutedColor: string
|
|
241
|
+
labelFontFamily: string
|
|
242
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
243
|
+
onSelect: (value: any) => void
|
|
244
|
+
}) {
|
|
245
|
+
return (
|
|
246
|
+
<>
|
|
247
|
+
{section.options.map((option) => {
|
|
248
|
+
const selected = option.value === section.value
|
|
249
|
+
const rowColor = selected ? onAccentColor : mutedColor
|
|
250
|
+
return (
|
|
251
|
+
<TouchableRipple key={option.value} onPress={() => onSelect(option.value)} style={[styles.item, selected && { backgroundColor: accentColor }]}>
|
|
252
|
+
<View style={styles.itemRow}>
|
|
253
|
+
{option.icon && <Icon source={option.icon} size={MENU_ITEM_ICON_SIZE} color={rowColor} />}
|
|
254
|
+
<View style={styles.itemLabelColumn}>
|
|
255
|
+
<Text variant='labelLarge' style={[{ fontFamily: labelFontFamily }, { color: rowColor, fontWeight: selected ? 'bold' : 'normal' }]}>
|
|
256
|
+
{option.label}
|
|
257
|
+
</Text>
|
|
258
|
+
{option.description && (
|
|
259
|
+
<Text variant='labelSmall' style={[{ fontFamily: labelFontFamily }, { color: rowColor }]}>
|
|
260
|
+
{option.description}
|
|
261
|
+
</Text>
|
|
262
|
+
)}
|
|
263
|
+
</View>
|
|
264
|
+
</View>
|
|
265
|
+
</TouchableRipple>
|
|
266
|
+
)
|
|
267
|
+
})}
|
|
268
|
+
</>
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function MultiSection({
|
|
273
|
+
section,
|
|
274
|
+
accentColor,
|
|
275
|
+
onAccentColor,
|
|
276
|
+
mutedColor,
|
|
277
|
+
labelFontFamily,
|
|
278
|
+
allClearLabels
|
|
279
|
+
}: {
|
|
280
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
281
|
+
section: MultiSelectSection<any>
|
|
282
|
+
accentColor: string
|
|
283
|
+
onAccentColor: string
|
|
284
|
+
mutedColor: string
|
|
285
|
+
labelFontFamily: string
|
|
286
|
+
allClearLabels: { all: string; clear: string }
|
|
287
|
+
}) {
|
|
288
|
+
const allSelected = section.value.length === section.options.length
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<>
|
|
292
|
+
{section.options.map((option, i) => {
|
|
293
|
+
const selected = section.value.includes(option.value)
|
|
294
|
+
const rowColor = selected ? onAccentColor : mutedColor
|
|
295
|
+
// Adjacent rows sit flush against each other (no gap — see styles.item's own lack of
|
|
296
|
+
// margin), so two selected neighbors otherwise each carve a small rounded notch out of what
|
|
297
|
+
// should read as one continuous block. Squaring off just the touching edge is what makes a
|
|
298
|
+
// run of several selected options look like one seamless shape instead of a stack of
|
|
299
|
+
// separately-rounded pills.
|
|
300
|
+
const prevSelected = selected && i > 0 && section.value.includes(section.options[i - 1].value)
|
|
301
|
+
const nextSelected = selected && i < section.options.length - 1 && section.value.includes(section.options[i + 1].value)
|
|
302
|
+
return (
|
|
303
|
+
<TouchableRipple key={option.value} onPress={() => section.onChange(selected ? section.value.filter((v: unknown) => v !== option.value) : [...section.value, option.value])} style={[styles.item, selected && { backgroundColor: accentColor }, prevSelected && { borderTopLeftRadius: 0, borderTopRightRadius: 0 }, nextSelected && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0 }]}>
|
|
304
|
+
<View style={styles.itemRow}>
|
|
305
|
+
{option.icon && <Icon source={option.icon} size={MENU_ITEM_ICON_SIZE} color={rowColor} />}
|
|
306
|
+
<Text variant='labelLarge' style={[styles.itemLabelFlex, { fontFamily: labelFontFamily }, { color: rowColor, fontWeight: selected ? 'bold' : 'normal' }]}>
|
|
307
|
+
{option.label}
|
|
308
|
+
</Text>
|
|
309
|
+
</View>
|
|
310
|
+
</TouchableRipple>
|
|
311
|
+
)
|
|
312
|
+
})}
|
|
313
|
+
|
|
314
|
+
{section.allClear && (
|
|
315
|
+
<TouchableRipple onPress={() => section.onChange(allSelected ? [] : section.options.map((o) => o.value))} style={styles.item}>
|
|
316
|
+
<Text variant='labelLarge' style={[styles.allClearLabel, { fontFamily: labelFontFamily, color: accentColor }]}>
|
|
317
|
+
{allSelected ? allClearLabels.clear : allClearLabels.all}
|
|
318
|
+
</Text>
|
|
319
|
+
</TouchableRipple>
|
|
320
|
+
)}
|
|
321
|
+
</>
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const styles = StyleSheet.create({
|
|
326
|
+
allClearLabel: {
|
|
327
|
+
fontWeight: 'bold',
|
|
328
|
+
textAlign: 'center'
|
|
329
|
+
},
|
|
330
|
+
anchor: {
|
|
331
|
+
position: 'relative'
|
|
332
|
+
},
|
|
333
|
+
// React Native Web gives every position:'relative' view an explicit zIndex (0, not 'auto'),
|
|
334
|
+
// which makes each one its own stacking context — so a zIndex set only on the popover content
|
|
335
|
+
// below only wins comparisons within this anchor's own box, not against this anchor's own later
|
|
336
|
+
// siblings (e.g. a Ready button), which are otherwise painted on top by DOM-order tiebreak.
|
|
337
|
+
// Elevating the anchor itself, only while its popover is open, fixes that at the right level.
|
|
338
|
+
anchorOpen: {
|
|
339
|
+
zIndex: 100
|
|
340
|
+
},
|
|
341
|
+
divider: {
|
|
342
|
+
height: StyleSheet.hairlineWidth,
|
|
343
|
+
marginVertical: 4
|
|
344
|
+
},
|
|
345
|
+
item: {
|
|
346
|
+
borderRadius: 8,
|
|
347
|
+
paddingHorizontal: 12,
|
|
348
|
+
paddingVertical: 8
|
|
349
|
+
},
|
|
350
|
+
itemLabelColumn: {
|
|
351
|
+
maxWidth: MENU_ITEM_LABEL_MAX_WIDTH
|
|
352
|
+
},
|
|
353
|
+
itemLabelFlex: {
|
|
354
|
+
flex: 1
|
|
355
|
+
},
|
|
356
|
+
itemRow: {
|
|
357
|
+
alignItems: 'center',
|
|
358
|
+
flexDirection: 'row',
|
|
359
|
+
gap: 8
|
|
360
|
+
},
|
|
361
|
+
menu: {
|
|
362
|
+
borderRadius: 12,
|
|
363
|
+
borderWidth: MENU_BORDER_WIDTH,
|
|
364
|
+
elevation: 8,
|
|
365
|
+
maxWidth: MENU_MAX_WIDTH,
|
|
366
|
+
minWidth: MENU_MIN_WIDTH,
|
|
367
|
+
shadowColor: '#000000',
|
|
368
|
+
shadowOffset: { height: 2, width: 0 },
|
|
369
|
+
shadowOpacity: 0.3,
|
|
370
|
+
shadowRadius: 8
|
|
371
|
+
},
|
|
372
|
+
// Separate from `menu` itself — a ScrollView's own style sets the scrolling frame's bounds
|
|
373
|
+
// (including the maxHeight that actually caps it), while padding/gap belong to its
|
|
374
|
+
// contentContainerStyle, which sizes to the *unclamped* content instead of the frame.
|
|
375
|
+
menuContent: {
|
|
376
|
+
gap: 4,
|
|
377
|
+
padding: 6
|
|
378
|
+
},
|
|
379
|
+
// Fixed hit-area regardless of the current option's iconSize, so a small-vs-large trigger icon
|
|
380
|
+
// doesn't shift this picker's position within its row.
|
|
381
|
+
triggerBox: {
|
|
382
|
+
alignItems: 'center',
|
|
383
|
+
height: TRIGGER_SIZE,
|
|
384
|
+
justifyContent: 'center',
|
|
385
|
+
width: TRIGGER_SIZE
|
|
386
|
+
}
|
|
387
|
+
})
|