@tastic/hud 0.8.5 → 0.13.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/dist/index.mjs CHANGED
@@ -1,6 +1,17 @@
1
1
  import {
2
2
  SkiaGate
3
3
  } from "./chunk-XEOFR236.mjs";
4
+ import {
5
+ getOverlayCardColors,
6
+ overlayActionStyles,
7
+ overlayStyles
8
+ } from "./chunk-32U2OIRW.mjs";
9
+
10
+ // src/AchievementCatalogSection.tsx
11
+ import { useAutoPaperTheme as useAutoPaperTheme2 } from "@rific/auto-paper";
12
+ import { Fragment } from "react";
13
+ import { StyleSheet as StyleSheet2 } from "react-native";
14
+ import { Text as Text2 } from "react-native-paper";
4
15
 
5
16
  // src/AchievementRow.tsx
6
17
  import { useAutoPaperTheme } from "@rific/auto-paper";
@@ -70,112 +81,170 @@ var styles = StyleSheet.create({
70
81
  }
71
82
  });
72
83
 
73
- // src/BaseSettingsDialog.tsx
74
- import { AutoAppearancePicker, Dialog, useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
75
- import { Button as Button2, SoundContext, TouchableRipple, useHapticSettings, useSoundSettings, useVibration } from "@rific/feedback-press";
76
- import { useUpdater } from "@rific/updater";
77
- import { toRotationStyle as toRotationStyle2, useIsTouchPrimaryDevice, useRotation } from "@tastic/core";
78
- import { useContext, useState as useState2 } from "react";
79
- import { Platform, ScrollView, StyleSheet as StyleSheet4, View as View3 } from "react-native";
80
- import { Icon as Icon3, Text as Text3 } from "react-native-paper";
84
+ // src/fonts.ts
85
+ import { Platform } from "react-native";
86
+ var MONO_FONT = Platform.select({ ios: "Menlo", android: "monospace", default: "monospace" });
81
87
 
82
- // src/ConfirmDialog.tsx
83
- import { useAutoPaperTheme as useAutoPaperTheme2 } from "@rific/auto-paper";
84
- import { Button } from "@rific/feedback-press";
85
- import { toRotationStyle } from "@tastic/core";
86
- import { StyleSheet as StyleSheet3, View as View2 } from "react-native";
87
- import { Icon as Icon2, Portal, Text as Text2 } from "react-native-paper";
88
+ // src/AchievementCatalogSection.tsx
89
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
90
+ function AchievementCatalogSection({ rows, label = "ALL ACHIEVEMENTS", fg, fgMuted: fgMutedOverride }) {
91
+ const { dark } = useAutoPaperTheme2();
92
+ const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
93
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
94
+ /* @__PURE__ */ jsx2(Text2, { variant: "labelMedium", style: [styles2.label, { color: fgMuted, fontFamily: MONO_FONT }], children: label }),
95
+ rows.map((row) => /* @__PURE__ */ jsx2(AchievementRow, { icon: row.icon, title: row.title, description: row.description, badgeColor: row.unlockedAt !== void 0 ? row.tierColor : LOCKED_BADGE_COLOR, checkColor: row.tierColor, unlockedLabel: row.unlockedLabel, progress: row.progress, deviceMarker: row.deviceMarker, fg, fgMuted: fgMutedOverride }, row.id))
96
+ ] });
97
+ }
98
+ var styles2 = StyleSheet2.create({
99
+ label: {
100
+ letterSpacing: 2,
101
+ marginTop: 8
102
+ }
103
+ });
88
104
 
89
- // src/overlayCard.ts
90
- import { StyleSheet as StyleSheet2 } from "react-native";
91
- function getOverlayCardColors(dark) {
92
- return {
93
- fg: dark ? "#FFFFFF" : "#000000",
94
- fgMuted: dark ? "rgba(255,255,255,0.4)" : "rgba(0,0,0,0.4)",
95
- cardBg: dark ? "#111111" : "#F2F2F2",
96
- cardBorder: dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)"
97
- };
105
+ // src/AchievementUnlockList.tsx
106
+ import { getContrastColor, useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
107
+ import { ProfileChip } from "@tastic/profile";
108
+ import { StyleSheet as StyleSheet3, View as View2 } from "react-native";
109
+ import { Icon as Icon2, Text as Text3 } from "react-native-paper";
110
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
111
+ var AVATAR_SIZE = 18;
112
+ function AchievementUnlockList({ unlocks, seats, owners, tierColors, fg: fgOverride, style }) {
113
+ const { dark } = useAutoPaperTheme3();
114
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
115
+ const rows = seats.flatMap((seat) => (unlocks[seat] ?? []).map((achievement) => ({ seat, achievement })));
116
+ if (rows.length === 0) return null;
117
+ return /* @__PURE__ */ jsx3(View2, { style: [styles3.list, style], children: rows.map(({ seat, achievement }) => {
118
+ const owner = owners[seat];
119
+ return /* @__PURE__ */ jsxs3(View2, { style: styles3.row, children: [
120
+ owner.profile ? /* @__PURE__ */ jsx3(ProfileChip, { profile: owner.profile, size: AVATAR_SIZE }) : /* @__PURE__ */ jsx3(View2, { style: [styles3.circle, { backgroundColor: owner.color }], children: /* @__PURE__ */ jsx3(Icon2, { source: "account", size: 10, color: getContrastColor(owner.color) }) }),
121
+ /* @__PURE__ */ jsx3(View2, { style: [styles3.circle, { backgroundColor: tierColors[achievement.tier] }], children: /* @__PURE__ */ jsx3(Icon2, { source: achievement.icon, size: 12, color: "#000000" }) }),
122
+ /* @__PURE__ */ jsx3(Text3, { style: [styles3.label, { color: fg }], numberOfLines: 1, children: achievement.title })
123
+ ] }, `${seat}-${achievement.id}`);
124
+ }) });
98
125
  }
99
- var overlayStyles = StyleSheet2.create({
100
- card: {
126
+ var styles3 = StyleSheet3.create({
127
+ circle: {
101
128
  alignItems: "center",
102
- borderRadius: 20,
103
- borderWidth: 1,
104
- gap: 16,
105
- maxWidth: 360,
106
- padding: 32
129
+ borderRadius: AVATAR_SIZE / 2,
130
+ height: AVATAR_SIZE,
131
+ justifyContent: "center",
132
+ width: AVATAR_SIZE
107
133
  },
108
- overlay: {
134
+ label: {
135
+ flexShrink: 1,
136
+ fontSize: 12,
137
+ fontWeight: "700"
138
+ },
139
+ list: {
140
+ gap: 6
141
+ },
142
+ row: {
109
143
  alignItems: "center",
110
- backgroundColor: "rgba(0,0,0,0.72)",
111
- bottom: 0,
112
- justifyContent: "center",
113
- left: 0,
114
- position: "absolute",
115
- right: 0,
116
- top: 0
144
+ flexDirection: "row",
145
+ gap: 6
146
+ }
147
+ });
148
+
149
+ // src/StatRow.tsx
150
+ import { useAutoPaperTheme as useAutoPaperTheme4 } from "@rific/auto-paper";
151
+ import { StyleSheet as StyleSheet4, View as View3 } from "react-native";
152
+ import { Text as Text4 } from "react-native-paper";
153
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
154
+ function StatRow({ label, value, fg: fgOverride, fgMuted: fgMutedOverride }) {
155
+ const { dark } = useAutoPaperTheme4();
156
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
157
+ const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
158
+ return /* @__PURE__ */ jsxs4(View3, { style: styles4.row, children: [
159
+ /* @__PURE__ */ jsx4(Text4, { variant: "bodyMedium", style: { color: fgMuted }, children: label }),
160
+ /* @__PURE__ */ jsx4(Text4, { variant: "bodyMedium", style: [styles4.value, { color: fg }], children: value })
161
+ ] });
162
+ }
163
+ var styles4 = StyleSheet4.create({
164
+ row: {
165
+ alignItems: "center",
166
+ flexDirection: "row",
167
+ justifyContent: "space-between"
168
+ },
169
+ value: {
170
+ fontWeight: "bold"
171
+ }
172
+ });
173
+
174
+ // src/StatSection.tsx
175
+ import { useAutoPaperTheme as useAutoPaperTheme5 } from "@rific/auto-paper";
176
+ import { StyleSheet as StyleSheet5, View as View4 } from "react-native";
177
+ import { Text as Text5 } from "react-native-paper";
178
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
179
+ function StatSection({ label, fg: fgOverride, sectionBg: sectionBgOverride, children }) {
180
+ const { dark } = useAutoPaperTheme5();
181
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
182
+ const sectionBg = sectionBgOverride ?? (dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)");
183
+ return /* @__PURE__ */ jsxs5(View4, { style: [styles5.section, { backgroundColor: sectionBg }], children: [
184
+ /* @__PURE__ */ jsx5(Text5, { variant: "labelMedium", style: [styles5.label, { color: fg }], children: label }),
185
+ children
186
+ ] });
187
+ }
188
+ var styles5 = StyleSheet5.create({
189
+ // Bold and full-contrast (fg), not the original fontless/fgMuted treatment — that read at the
190
+ // same visual weight as StatRow's own row labels below it (both used the identical muted color;
191
+ // letterSpacing/variant alone didn't separate them enough to read as "this is the group header"
192
+ // at a glance). Matches AchievementRow's own title styling (bold + fg) below it in the same list.
193
+ label: {
194
+ fontWeight: "bold",
195
+ letterSpacing: 2
196
+ },
197
+ section: {
198
+ borderRadius: 12,
199
+ gap: 8,
200
+ padding: 16
117
201
  }
118
202
  });
119
203
 
204
+ // src/ActivityStatSection.tsx
205
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
206
+ function ActivityStatSection({ stats }) {
207
+ return /* @__PURE__ */ jsxs6(StatSection, { label: "ACTIVITY", children: [
208
+ /* @__PURE__ */ jsx6(StatRow, { label: "Days Played", value: String(stats.distinctDaysPlayed) }),
209
+ /* @__PURE__ */ jsx6(StatRow, { label: "Day Streak", value: String(stats.currentDayStreak) }),
210
+ /* @__PURE__ */ jsx6(StatRow, { label: "Best Day Streak", value: String(stats.bestDayStreak) })
211
+ ] });
212
+ }
213
+
214
+ // src/BaseSettingsDialog.tsx
215
+ import { AutoAppearancePicker, Dialog, useAutoPaperTheme as useAutoPaperTheme7 } from "@rific/auto-paper";
216
+ import { Button as Button2, SoundContext, TouchableRipple, useHapticSettings, useSoundSettings, useVibration } from "@rific/feedback-press";
217
+ import { useUpdater } from "@rific/updater";
218
+ import { toRotationStyle as toRotationStyle2, useIsTouchPrimaryDevice, useRotation } from "@tastic/core";
219
+ import { useContext, useState as useState2 } from "react";
220
+ import { Platform as Platform2, ScrollView, StyleSheet as StyleSheet7, View as View6 } from "react-native";
221
+ import { Icon as Icon4, Text as Text7 } from "react-native-paper";
222
+
120
223
  // src/ConfirmDialog.tsx
121
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
224
+ import { useAutoPaperTheme as useAutoPaperTheme6 } from "@rific/auto-paper";
225
+ import { Button } from "@rific/feedback-press";
226
+ import { toRotationStyle } from "@tastic/core";
227
+ import { StyleSheet as StyleSheet6, View as View5 } from "react-native";
228
+ import { Icon as Icon3, Portal, Text as Text6 } from "react-native-paper";
229
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
122
230
  function ConfirmDialog({ visible, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, rotation = 0, icon = "help-circle-outline", destructive = false }) {
123
- const { dark, colors } = useAutoPaperTheme2();
231
+ const { dark, colors } = useAutoPaperTheme6();
124
232
  if (!visible) return null;
125
233
  const { fg, cardBg, cardBorder } = getOverlayCardColors(dark);
126
234
  const accent = destructive ? colors.danger : colors.primary;
127
235
  const onAccent = destructive ? colors.onDanger : colors.onPrimary;
128
236
  const showCancel = cancelLabel !== void 0 && onCancel !== void 0;
129
- return /* @__PURE__ */ jsx2(Portal, { children: /* @__PURE__ */ jsx2(View2, { style: [overlayStyles.overlay, styles2.overlay], children: /* @__PURE__ */ jsxs2(View2, { style: [overlayStyles.card, styles2.card, { backgroundColor: cardBg, borderColor: cardBorder }, toRotationStyle(rotation)], children: [
130
- /* @__PURE__ */ jsx2(Icon2, { source: icon, size: 64, color: accent }),
131
- /* @__PURE__ */ jsx2(Text2, { variant: "headlineLarge", style: [styles2.title, { color: accent }], children: title }),
132
- /* @__PURE__ */ jsx2(Text2, { variant: "bodyLarge", style: [styles2.body, { color: fg }], children: message }),
133
- /* @__PURE__ */ jsxs2(View2, { style: styles2.actions, children: [
134
- showCancel && /* @__PURE__ */ jsx2(Button, { mode: "outlined", onPress: onCancel, style: styles2.actionButton, children: cancelLabel }),
135
- /* @__PURE__ */ jsx2(Button, { mode: "contained", onPress: onConfirm, style: showCancel ? styles2.actionButton : styles2.actionButtonSingle, buttonColor: accent, textColor: onAccent, children: confirmLabel })
237
+ return /* @__PURE__ */ jsx7(Portal, { children: /* @__PURE__ */ jsx7(View5, { style: [overlayStyles.overlay, styles6.overlay], children: /* @__PURE__ */ jsxs7(View5, { style: [overlayStyles.card, styles6.card, { backgroundColor: cardBg, borderColor: cardBorder }, toRotationStyle(rotation)], children: [
238
+ /* @__PURE__ */ jsx7(Icon3, { source: icon, size: 64, color: accent }),
239
+ /* @__PURE__ */ jsx7(Text6, { variant: "headlineLarge", style: [styles6.title, { color: accent }], children: title }),
240
+ /* @__PURE__ */ jsx7(Text6, { variant: "bodyLarge", style: [styles6.body, { color: fg }], children: message }),
241
+ /* @__PURE__ */ jsxs7(View5, { style: overlayActionStyles.actions, children: [
242
+ showCancel && /* @__PURE__ */ jsx7(Button, { mode: "outlined", onPress: onCancel, style: overlayActionStyles.actionButton, children: cancelLabel }),
243
+ /* @__PURE__ */ jsx7(Button, { mode: "contained", onPress: onConfirm, style: showCancel ? overlayActionStyles.actionButton : overlayActionStyles.actionButtonSingle, buttonColor: accent, textColor: onAccent, children: confirmLabel })
136
244
  ] })
137
245
  ] }) }) });
138
246
  }
139
- var styles2 = StyleSheet3.create({
140
- // minWidth (not flexBasis) is each button's real floor - whenever both fit side by side, flexGrow
141
- // still splits any leftover width evenly beyond that floor, same visual result as a plain flex: 1
142
- // on a roomy screen, but a button can never be squeezed narrower than 128, only wrap to its own
143
- // row instead (see actions' flexWrap below). This used to be flexBasis: 128, which measured
144
- // correctly on web but rendered every button roughly SQUARE on iOS - confirmed live (see the
145
- // "Quit Match?" Cancel/Quit buttons and the single-button "OK" dialogs both coming out ~128pt
146
- // tall instead of wide). Root cause: react-native-paper's Button renders its Surface as two
147
- // nested views on iOS only (Surface.tsx's SurfaceIOS, for shadow rendering) - only a curated
148
- // allowlist of style keys (flex/flexGrow/flexShrink/width/height/position/... - notably NOT
149
- // flexBasis) reaches the outer view, which is the real flex item inside this row; every other
150
- // key, flexBasis included, falls through to an inner view whose own unspecified flexDirection
151
- // defaults to column. flexGrow: 1 landed on the (row-context) outer view and grew width
152
- // correctly; flexBasis: 128 landed on the (column-context) inner view instead, where a flexBasis
153
- // sets HEIGHT, not width. minWidth isn't axis-relative like flexBasis, so it constrains width
154
- // correctly regardless of which of the two views it ends up on. react-native-paper's Button also
155
- // hardcodes numberOfLines={1} on its label with no override, so a button squeezed under its
156
- // label's natural width silently ellipsizes ("Cancel" -> "Can...") instead of wrapping - assumed
157
- // fixable by just giving the row more width, until confirmed live that these two-word labels
158
- // (Cancel/Reset/New Game) can still lose the truncation fight even with the extra room card's own
159
- // width fix below provides, on a narrow enough screen.
160
- actionButton: {
161
- flexGrow: 1,
162
- minWidth: 128
163
- },
164
- // A lone confirm button (no cancel - BaseSettingsDialog's info-only "OK" dialogs, e.g. the
165
- // update-check "no update found" message) has nothing left to share actions' width with, so
166
- // actionButton's own flexGrow: 1 would stretch it across the entire row instead of reading as a
167
- // normal button. Same 128 floor, no flexGrow, so it stays at that natural size instead of filling
168
- // the card - centered by actions' own justifyContent below.
169
- actionButtonSingle: {
170
- minWidth: 128
171
- },
172
- actions: {
173
- flexDirection: "row",
174
- flexWrap: "wrap",
175
- gap: 12,
176
- justifyContent: "center",
177
- width: "100%"
178
- },
247
+ var styles6 = StyleSheet6.create({
179
248
  body: {
180
249
  textAlign: "center"
181
250
  },
@@ -224,12 +293,12 @@ function useUpdateConfirmBridge() {
224
293
  }
225
294
 
226
295
  // src/BaseSettingsDialog.tsx
227
- import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
296
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
228
297
  function SettingIcon({ source, color, containerColor }) {
229
- return /* @__PURE__ */ jsx3(View3, { style: [styles3.iconBadge, { backgroundColor: containerColor }], children: /* @__PURE__ */ jsx3(Icon3, { source, size: 18, color }) });
298
+ return /* @__PURE__ */ jsx8(View6, { style: [styles7.iconBadge, { backgroundColor: containerColor }], children: /* @__PURE__ */ jsx8(Icon4, { source, size: 18, color }) });
230
299
  }
231
- function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, children }) {
232
- const { colors } = useAutoPaperTheme3();
300
+ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }) {
301
+ const { colors } = useAutoPaperTheme7();
233
302
  const ambientRotation = useRotation();
234
303
  const rotation = rotationOverride ?? ambientRotation;
235
304
  const isTouchPrimary = useIsTouchPrimaryDevice();
@@ -248,32 +317,32 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
248
317
  });
249
318
  const updateManifestDate = updateManifest ? new Date(updateManifest.createdAt) : null;
250
319
  const showLockOrientation = isTouchPrimary && lockOrientation !== void 0 && onLockOrientationChange !== void 0;
251
- const showEdgeGuard = Platform.OS === "ios" && deferBottomEdgeGestures !== void 0 && onDeferBottomEdgeGestures !== void 0;
252
- const showHaptics = Platform.OS !== "web" && !hideHaptics;
320
+ const showEdgeGuard = Platform2.OS === "ios" && deferBottomEdgeGestures !== void 0 && onDeferBottomEdgeGestures !== void 0;
321
+ const showHaptics = Platform2.OS !== "web" && !hideHaptics;
253
322
  const showSound = !hideSound;
254
323
  const showAppearance = !hideAppearance;
255
- const showUpdateCheck = Platform.OS !== "web" && !hideUpdateCheck;
256
- return /* @__PURE__ */ jsxs3(Fragment, { children: [
257
- /* @__PURE__ */ jsxs3(Dialog, { visible, onDismiss, style: [styles3.dialog, toRotationStyle2(rotation)], children: [
258
- /* @__PURE__ */ jsx3(Dialog.Title, { children: "Settings" }),
259
- /* @__PURE__ */ jsx3(Dialog.ScrollArea, { style: styles3.scrollArea, children: /* @__PURE__ */ jsxs3(ScrollView, { contentContainerStyle: styles3.content, showsVerticalScrollIndicator: false, children: [
260
- /* @__PURE__ */ jsxs3(View3, { style: styles3.toggleGroup, children: [
261
- showLockOrientation && /* @__PURE__ */ jsx3(TouchableRipple, { onPress: () => onLockOrientationChange(!lockOrientation), style: styles3.toggleButton, accessibilityLabel: `Lock orientation ${lockOrientation ? "on" : "off"}`, children: /* @__PURE__ */ jsxs3(View3, { style: styles3.toggleContent, children: [
262
- /* @__PURE__ */ jsx3(SettingIcon, { source: lockOrientation ? "lock" : "lock-open-variant-outline", color: lockOrientation ? colors.secondary : colors.onSurfaceVariant, containerColor: lockOrientation ? colors.secondaryContainer : colors.surfaceVariant }),
263
- /* @__PURE__ */ jsxs3(View3, { style: styles3.flexShrink, children: [
264
- /* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Lock Orientation" }),
265
- /* @__PURE__ */ jsx3(Text3, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Pins the current layout" })
324
+ const showUpdateCheck = Platform2.OS !== "web" && !hideUpdateCheck;
325
+ return /* @__PURE__ */ jsxs8(Fragment2, { children: [
326
+ /* @__PURE__ */ jsxs8(Dialog, { visible, onDismiss, style: [styles7.dialog, toRotationStyle2(rotation)], children: [
327
+ /* @__PURE__ */ jsx8(Dialog.Title, { children: "Settings" }),
328
+ /* @__PURE__ */ jsx8(Dialog.ScrollArea, { style: styles7.scrollArea, children: /* @__PURE__ */ jsxs8(ScrollView, { contentContainerStyle: styles7.content, showsVerticalScrollIndicator: false, children: [
329
+ /* @__PURE__ */ jsxs8(View6, { style: styles7.toggleGroup, children: [
330
+ showLockOrientation && /* @__PURE__ */ jsx8(TouchableRipple, { onPress: () => onLockOrientationChange(!lockOrientation), style: styles7.toggleButton, accessibilityLabel: `Lock orientation ${lockOrientation ? "on" : "off"}`, children: /* @__PURE__ */ jsxs8(View6, { style: styles7.toggleContent, children: [
331
+ /* @__PURE__ */ jsx8(SettingIcon, { source: lockOrientation ? "lock" : "lock-open-variant-outline", color: lockOrientation ? colors.secondary : colors.onSurfaceVariant, containerColor: lockOrientation ? colors.secondaryContainer : colors.surfaceVariant }),
332
+ /* @__PURE__ */ jsxs8(View6, { style: styles7.flexShrink, children: [
333
+ /* @__PURE__ */ jsx8(Text7, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Lock Orientation" }),
334
+ /* @__PURE__ */ jsx8(Text7, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Pins the current layout" })
266
335
  ] })
267
336
  ] }) }),
268
- showEdgeGuard && /* @__PURE__ */ jsx3(TouchableRipple, { onPress: () => onDeferBottomEdgeGestures(!deferBottomEdgeGestures), style: styles3.toggleButton, accessibilityLabel: `Edge guard ${deferBottomEdgeGestures ? "on" : "off"}`, children: /* @__PURE__ */ jsxs3(View3, { style: styles3.toggleContent, children: [
269
- /* @__PURE__ */ jsx3(SettingIcon, { source: deferBottomEdgeGestures ? "shield-check-outline" : "shield-off-outline", color: deferBottomEdgeGestures ? colors.secondary : colors.onSurfaceVariant, containerColor: deferBottomEdgeGestures ? colors.secondaryContainer : colors.surfaceVariant }),
270
- /* @__PURE__ */ jsxs3(View3, { style: styles3.flexShrink, children: [
271
- /* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Edge Guard" }),
272
- /* @__PURE__ */ jsx3(Text3, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Requires a second swipe" })
337
+ showEdgeGuard && /* @__PURE__ */ jsx8(TouchableRipple, { onPress: () => onDeferBottomEdgeGestures(!deferBottomEdgeGestures), style: styles7.toggleButton, accessibilityLabel: `Edge guard ${deferBottomEdgeGestures ? "on" : "off"}`, children: /* @__PURE__ */ jsxs8(View6, { style: styles7.toggleContent, children: [
338
+ /* @__PURE__ */ jsx8(SettingIcon, { source: deferBottomEdgeGestures ? "shield-check-outline" : "shield-off-outline", color: deferBottomEdgeGestures ? colors.secondary : colors.onSurfaceVariant, containerColor: deferBottomEdgeGestures ? colors.secondaryContainer : colors.surfaceVariant }),
339
+ /* @__PURE__ */ jsxs8(View6, { style: styles7.flexShrink, children: [
340
+ /* @__PURE__ */ jsx8(Text7, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Edge Guard" }),
341
+ /* @__PURE__ */ jsx8(Text7, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Requires a second swipe" })
273
342
  ] })
274
343
  ] }) }),
275
- (showSound || showHaptics) && /* @__PURE__ */ jsxs3(View3, { style: styles3.toggleRow, children: [
276
- showSound && /* @__PURE__ */ jsx3(
344
+ (showSound || showHaptics) && /* @__PURE__ */ jsxs8(View6, { style: styles7.toggleRow, children: [
345
+ showSound && /* @__PURE__ */ jsx8(
277
346
  TouchableRipple,
278
347
  {
279
348
  soundDisabled: true,
@@ -282,15 +351,15 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
282
351
  setSoundSettings({ enabled });
283
352
  if (enabled) sound.selection?.();
284
353
  },
285
- style: styles3.toggleButtonInRow,
354
+ style: styles7.toggleButtonInRow,
286
355
  accessibilityLabel: `Sound ${soundSettings.enabled ? "on" : "off"}`,
287
- children: /* @__PURE__ */ jsxs3(View3, { style: styles3.toggleContent, children: [
288
- /* @__PURE__ */ jsx3(SettingIcon, { source: soundSettings.enabled ? "volume-high" : "volume-off", color: soundSettings.enabled ? colors.tertiary : colors.onSurfaceVariant, containerColor: soundSettings.enabled ? colors.tertiaryContainer : colors.surfaceVariant }),
289
- /* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Sound" })
356
+ children: /* @__PURE__ */ jsxs8(View6, { style: styles7.toggleContent, children: [
357
+ /* @__PURE__ */ jsx8(SettingIcon, { source: soundSettings.enabled ? "volume-high" : "volume-off", color: soundSettings.enabled ? colors.tertiary : colors.onSurfaceVariant, containerColor: soundSettings.enabled ? colors.tertiaryContainer : colors.surfaceVariant }),
358
+ /* @__PURE__ */ jsx8(Text7, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Sound" })
290
359
  ] })
291
360
  }
292
361
  ),
293
- showHaptics && /* @__PURE__ */ jsx3(
362
+ showHaptics && /* @__PURE__ */ jsx8(
294
363
  TouchableRipple,
295
364
  {
296
365
  hapticDisabled: true,
@@ -299,36 +368,54 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
299
368
  setHapticSettings({ vibrate });
300
369
  if (vibrate) forceHapticFeedback();
301
370
  },
302
- style: styles3.toggleButtonInRow,
371
+ style: styles7.toggleButtonInRow,
303
372
  accessibilityLabel: `Haptics ${hapticSettings.vibrate ? "on" : "off"}`,
304
- children: /* @__PURE__ */ jsxs3(View3, { style: styles3.toggleContent, children: [
305
- /* @__PURE__ */ jsx3(SettingIcon, { source: hapticSettings.vibrate ? "vibrate" : "vibrate-off", color: hapticSettings.vibrate ? colors.tertiary : colors.onSurfaceVariant, containerColor: hapticSettings.vibrate ? colors.tertiaryContainer : colors.surfaceVariant }),
306
- /* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Haptics" })
373
+ children: /* @__PURE__ */ jsxs8(View6, { style: styles7.toggleContent, children: [
374
+ /* @__PURE__ */ jsx8(SettingIcon, { source: hapticSettings.vibrate ? "vibrate" : "vibrate-off", color: hapticSettings.vibrate ? colors.tertiary : colors.onSurfaceVariant, containerColor: hapticSettings.vibrate ? colors.tertiaryContainer : colors.surfaceVariant }),
375
+ /* @__PURE__ */ jsx8(Text7, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Haptics" })
307
376
  ] })
308
377
  }
309
378
  )
310
- ] })
379
+ ] }),
380
+ onShowHowToPlay && /* @__PURE__ */ jsx8(
381
+ TouchableRipple,
382
+ {
383
+ onPress: () => {
384
+ onDismiss();
385
+ onShowHowToPlay();
386
+ },
387
+ style: styles7.toggleButton,
388
+ accessibilityLabel: "How to Play",
389
+ children: /* @__PURE__ */ jsxs8(View6, { style: styles7.toggleContent, children: [
390
+ /* @__PURE__ */ jsx8(SettingIcon, { source: "help-circle-outline", color: colors.secondary, containerColor: colors.secondaryContainer }),
391
+ /* @__PURE__ */ jsxs8(View6, { style: styles7.flexShrink, children: [
392
+ /* @__PURE__ */ jsx8(Text7, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "How to Play" }),
393
+ /* @__PURE__ */ jsx8(Text7, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Replay the quick intro" })
394
+ ] })
395
+ ] })
396
+ }
397
+ )
311
398
  ] }),
312
- showAppearance && /* @__PURE__ */ jsxs3(View3, { style: styles3.section, children: [
313
- /* @__PURE__ */ jsx3(Text3, { variant: "labelMedium", style: [styles3.sectionLabel, { color: colors.onSurfaceVariant }], children: "APPEARANCE" }),
314
- /* @__PURE__ */ jsx3(AutoAppearancePicker, { showLabels: false })
399
+ showAppearance && /* @__PURE__ */ jsxs8(View6, { style: styles7.section, children: [
400
+ /* @__PURE__ */ jsx8(Text7, { variant: "labelMedium", style: [styles7.sectionLabel, { color: colors.onSurfaceVariant }], children: "APPEARANCE" }),
401
+ /* @__PURE__ */ jsx8(AutoAppearancePicker, { showLabels: false })
315
402
  ] }),
316
403
  children,
317
- showUpdateCheck && /* @__PURE__ */ jsxs3(View3, { style: styles3.section, children: [
318
- /* @__PURE__ */ jsxs3(Text3, { variant: "labelSmall", style: [styles3.sectionLabel, { color: colors.onSurfaceVariant }], children: [
404
+ showUpdateCheck && /* @__PURE__ */ jsxs8(View6, { style: styles7.section, children: [
405
+ /* @__PURE__ */ jsxs8(Text7, { variant: "labelSmall", style: [styles7.sectionLabel, { color: colors.onSurfaceVariant }], children: [
319
406
  "VERSION ",
320
407
  version,
321
408
  updateReady ? " \xB7 UPDATE READY" : ""
322
409
  ] }),
323
- /* @__PURE__ */ jsx3(Button2, { mode: "outlined", onPress: check, loading: checking, disabled: checking, children: "Check for Updates" })
410
+ /* @__PURE__ */ jsx8(Button2, { mode: "outlined", onPress: check, loading: checking, disabled: checking, children: "Check for Updates" })
324
411
  ] })
325
412
  ] }) })
326
413
  ] }),
327
- /* @__PURE__ */ jsx3(ConfirmDialog, { visible: !!updateManifest, title: "Update Available", message: updateManifestDate ? `Released ${updateManifestDate.toLocaleDateString()} at ${updateManifestDate.toLocaleTimeString()}` : "", confirmLabel: "Restart", cancelLabel: "Later", icon: "update", rotation, onConfirm: () => respondToUpdateConfirm(true), onCancel: () => respondToUpdateConfirm(false) }),
328
- /* @__PURE__ */ jsx3(ConfirmDialog, { visible: !!infoMessage, title: infoMessage?.title ?? "", message: infoMessage?.message ?? "", confirmLabel: "OK", icon: "information-outline", rotation, onConfirm: () => setInfoMessage(null) })
414
+ /* @__PURE__ */ jsx8(ConfirmDialog, { visible: !!updateManifest, title: "Update Available", message: updateManifestDate ? `Released ${updateManifestDate.toLocaleDateString()} at ${updateManifestDate.toLocaleTimeString()}` : "", confirmLabel: "Restart", cancelLabel: "Later", icon: "update", rotation, onConfirm: () => respondToUpdateConfirm(true), onCancel: () => respondToUpdateConfirm(false) }),
415
+ /* @__PURE__ */ jsx8(ConfirmDialog, { visible: !!infoMessage, title: infoMessage?.title ?? "", message: infoMessage?.message ?? "", confirmLabel: "OK", icon: "information-outline", rotation, onConfirm: () => setInfoMessage(null) })
329
416
  ] });
330
417
  }
331
- var styles3 = StyleSheet4.create({
418
+ var styles7 = StyleSheet7.create({
332
419
  content: {
333
420
  gap: 24,
334
421
  paddingVertical: 20
@@ -396,31 +483,31 @@ var styles3 = StyleSheet4.create({
396
483
  });
397
484
 
398
485
  // src/BaseStatsScreen.tsx
399
- import { useAutoPaperTheme as useAutoPaperTheme4 } from "@rific/auto-paper";
486
+ import { useAutoPaperTheme as useAutoPaperTheme8 } from "@rific/auto-paper";
400
487
  import { Button as Button3, IconButton } from "@rific/feedback-press";
401
488
  import { useRotation as useRotation2 } from "@tastic/core";
402
489
  import { useState as useState3 } from "react";
403
- import { ScrollView as ScrollView2, StyleSheet as StyleSheet5, View as View4 } from "react-native";
404
- import { Text as Text4 } from "react-native-paper";
405
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
490
+ import { ScrollView as ScrollView2, StyleSheet as StyleSheet8, View as View7 } from "react-native";
491
+ import { Text as Text8 } from "react-native-paper";
492
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
406
493
  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 }) {
407
- const { dark, colors } = useAutoPaperTheme4();
494
+ const { dark, colors } = useAutoPaperTheme8();
408
495
  const ambientRotation = useRotation2();
409
496
  const rotation = rotationOverride ?? ambientRotation;
410
497
  const [confirmResetVisible, setConfirmResetVisible] = useState3(false);
411
498
  const showReset = !!onReset;
412
499
  const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
413
500
  const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
414
- return /* @__PURE__ */ jsxs4(View4, { style: [styles4.container, { backgroundColor: bg }], children: [
415
- /* @__PURE__ */ jsxs4(View4, { style: [styles4.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
416
- /* @__PURE__ */ jsx4(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: onBack }),
417
- /* @__PURE__ */ jsx4(Text4, { variant: titleVariant, style: [styles4.title, { color: fg }], children: title })
501
+ return /* @__PURE__ */ jsxs9(View7, { style: [styles8.container, { backgroundColor: bg }], children: [
502
+ /* @__PURE__ */ jsxs9(View7, { style: [styles8.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
503
+ /* @__PURE__ */ jsx9(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: onBack }),
504
+ /* @__PURE__ */ jsx9(Text8, { variant: titleVariant, style: [styles8.title, { color: fg }], children: title })
418
505
  ] }),
419
- /* @__PURE__ */ jsxs4(ScrollView2, { style: styles4.scrollView, contentContainerStyle: [styles4.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, children: [
506
+ /* @__PURE__ */ jsxs9(ScrollView2, { style: styles8.scrollView, contentContainerStyle: [styles8.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, children: [
420
507
  children,
421
- showReset && /* @__PURE__ */ jsx4(Button3, { mode: "contained", onPress: () => setConfirmResetVisible(true), buttonColor: colors.danger, textColor: colors.onDanger, style: styles4.resetButton, children: resetLabel })
508
+ showReset && /* @__PURE__ */ jsx9(Button3, { mode: "contained", onPress: () => setConfirmResetVisible(true), buttonColor: colors.danger, textColor: colors.onDanger, style: styles8.resetButton, children: resetLabel })
422
509
  ] }),
423
- /* @__PURE__ */ jsx4(
510
+ /* @__PURE__ */ jsx9(
424
511
  ConfirmDialog,
425
512
  {
426
513
  visible: confirmResetVisible,
@@ -440,7 +527,7 @@ function BaseStatsScreen({ rotation: rotationOverride, title = "Achievements", t
440
527
  )
441
528
  ] });
442
529
  }
443
- var styles4 = StyleSheet5.create({
530
+ var styles8 = StyleSheet8.create({
444
531
  container: {
445
532
  flex: 1
446
533
  },
@@ -471,19 +558,19 @@ var styles4 = StyleSheet5.create({
471
558
  });
472
559
 
473
560
  // src/ContentGutter.tsx
474
- import { computeContentBounds } from "@tastic/core";
475
- import { StyleSheet as StyleSheet6, useWindowDimensions, View as View5 } from "react-native";
476
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
561
+ import { computeContentBounds, useRotatedWindowDimensions } from "@tastic/core";
562
+ import { StyleSheet as StyleSheet9, View as View8 } from "react-native";
563
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
477
564
  function ContentGutter({ maxContentWidth, leftGutter, rightGutter, style, children }) {
478
- const { width: windowWidth } = useWindowDimensions();
565
+ const { width: windowWidth } = useRotatedWindowDimensions();
479
566
  const { contentWidth, gutterWidth } = computeContentBounds(windowWidth, maxContentWidth ?? Infinity);
480
- return /* @__PURE__ */ jsxs5(View5, { style: [styles5.row, style], children: [
481
- /* @__PURE__ */ jsx5(View5, { style: [styles5.gutter, { width: gutterWidth }], children: leftGutter }),
482
- /* @__PURE__ */ jsx5(View5, { style: { width: contentWidth }, children }),
483
- /* @__PURE__ */ jsx5(View5, { style: [styles5.gutter, { width: gutterWidth }], children: rightGutter })
567
+ return /* @__PURE__ */ jsxs10(View8, { style: [styles9.row, style], children: [
568
+ /* @__PURE__ */ jsx10(View8, { style: [styles9.gutter, { width: gutterWidth }], children: leftGutter }),
569
+ /* @__PURE__ */ jsx10(View8, { style: { width: contentWidth }, children }),
570
+ /* @__PURE__ */ jsx10(View8, { style: [styles9.gutter, { width: gutterWidth }], children: rightGutter })
484
571
  ] });
485
572
  }
486
- var styles5 = StyleSheet6.create({
573
+ var styles9 = StyleSheet9.create({
487
574
  // overflow:'hidden' keeps an oversized leftGutter/rightGutter from bleeding into the content
488
575
  // column instead of clipping cleanly at its own real width.
489
576
  gutter: {
@@ -502,38 +589,34 @@ import { Platform as Platform3 } from "react-native";
502
589
  import { getBlendedColor, getColorRoles } from "@rific/auto-paper";
503
590
  import { IconButton as IconButton2, TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
504
591
  import { useRotation as useRotation3 } from "@tastic/core";
505
- import { ScrollView as ScrollView3, StyleSheet as StyleSheet8, View as View8 } from "react-native";
506
- import { Icon as Icon4, Text as Text5 } from "react-native-paper";
507
-
508
- // src/fonts.ts
509
- import { Platform as Platform2 } from "react-native";
510
- var MONO_FONT = Platform2.select({ ios: "Menlo", android: "monospace", default: "monospace" });
592
+ import { ScrollView as ScrollView3, StyleSheet as StyleSheet11, View as View11 } from "react-native";
593
+ import { Icon as Icon5, Text as Text9 } from "react-native-paper";
511
594
 
512
595
  // src/PopoverBody.tsx
513
- import { StyleSheet as StyleSheet7, View as View6 } from "react-native";
514
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
596
+ import { StyleSheet as StyleSheet10, View as View9 } from "react-native";
597
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
515
598
  var CARET_SIZE = 7;
516
599
  var CARET_RING_OFFSET = 2;
517
600
  var CARET_DIP = 1;
518
601
  function PopoverBody({ visible, children, align = "center", verticalAlign = "below", caretColor, caretBorderColor, caretBorderWidth = 1, triggerSize = 0 }) {
519
602
  if (!visible) return null;
520
603
  const above = verticalAlign === "above";
521
- const alignStyle = align === "left" ? styles6.contentLeft : align === "right" ? styles6.contentRight : styles6.contentCenter;
522
- const verticalStyle = above ? styles6.contentAbove : styles6.contentBelow;
604
+ const alignStyle = align === "left" ? styles10.contentLeft : align === "right" ? styles10.contentRight : styles10.contentCenter;
605
+ const verticalStyle = above ? styles10.contentAbove : styles10.contentBelow;
523
606
  const ringSize = CARET_SIZE + caretBorderWidth;
524
607
  const caretOffset = (halfFootprint) => align === "right" ? { right: triggerSize / 2 - halfFootprint } : { left: triggerSize / 2 - halfFootprint };
525
608
  const caretFill = above ? "borderTopColor" : "borderBottomColor";
526
609
  const caretWidthProp = above ? "borderTopWidth" : "borderBottomWidth";
527
610
  const caretEdge = (size) => above ? { bottom: -size + CARET_DIP } : { top: -size + CARET_DIP };
528
- return /* @__PURE__ */ jsxs6(View6, { style: [styles6.content, alignStyle, verticalStyle], children: [
611
+ return /* @__PURE__ */ jsxs11(View9, { style: [styles10.content, alignStyle, verticalStyle], children: [
529
612
  children,
530
- caretColor && /* @__PURE__ */ jsxs6(Fragment2, { children: [
531
- /* @__PURE__ */ jsx6(View6, { style: [styles6.caret, caretOffset(ringSize), caretEdge(ringSize), { [caretFill]: caretBorderColor ?? caretColor, [caretWidthProp]: ringSize, borderLeftWidth: ringSize, borderRightWidth: ringSize }] }),
532
- /* @__PURE__ */ jsx6(View6, { style: [styles6.caret, caretOffset(CARET_SIZE), caretEdge(CARET_SIZE - CARET_RING_OFFSET), { [caretFill]: caretColor, [caretWidthProp]: CARET_SIZE, borderLeftWidth: CARET_SIZE, borderRightWidth: CARET_SIZE }] })
613
+ caretColor && /* @__PURE__ */ jsxs11(Fragment3, { children: [
614
+ /* @__PURE__ */ jsx11(View9, { style: [styles10.caret, caretOffset(ringSize), caretEdge(ringSize), { [caretFill]: caretBorderColor ?? caretColor, [caretWidthProp]: ringSize, borderLeftWidth: ringSize, borderRightWidth: ringSize }] }),
615
+ /* @__PURE__ */ jsx11(View9, { style: [styles10.caret, caretOffset(CARET_SIZE), caretEdge(CARET_SIZE - CARET_RING_OFFSET), { [caretFill]: caretColor, [caretWidthProp]: CARET_SIZE, borderLeftWidth: CARET_SIZE, borderRightWidth: CARET_SIZE }] })
533
616
  ] })
534
617
  ] });
535
618
  }
536
- var styles6 = StyleSheet7.create({
619
+ var styles10 = StyleSheet10.create({
537
620
  caret: {
538
621
  borderLeftColor: "transparent",
539
622
  borderRightColor: "transparent",
@@ -576,15 +659,15 @@ var styles6 = StyleSheet7.create({
576
659
  });
577
660
 
578
661
  // src/TriggerGaugeHost.tsx
579
- import { jsx as jsx7 } from "react/jsx-runtime";
662
+ import { jsx as jsx12 } from "react/jsx-runtime";
580
663
  var getTriggerGauge = () => import("./TriggerGauge-2OJOOF6I.mjs").then((mod) => ({ default: mod.TriggerGauge }));
581
664
  function TriggerGaugeHost(props) {
582
- return /* @__PURE__ */ jsx7(SkiaGate, { getComponent: getTriggerGauge, componentProps: props });
665
+ return /* @__PURE__ */ jsx12(SkiaGate, { getComponent: getTriggerGauge, componentProps: props });
583
666
  }
584
667
 
585
668
  // src/useAutoAlign.ts
586
669
  import { useCallback as useCallback2, useEffect, useRef as useRef2, useState as useState4 } from "react";
587
- import { useWindowDimensions as useWindowDimensions2 } from "react-native";
670
+ import { useWindowDimensions } from "react-native";
588
671
  var EDGE_MARGIN = 12;
589
672
  function rotateRect(x, y, width, height, rotation, windowWidth, windowHeight) {
590
673
  if (rotation === 0) return { x, y, width, height };
@@ -597,7 +680,7 @@ function rotateRect(x, y, width, height, rotation, windowWidth, windowHeight) {
597
680
  }
598
681
  function useAutoAlign(open, contentWidth, contentHeight, rotation = 0) {
599
682
  const triggerRef = useRef2(null);
600
- const { width: windowWidth, height: windowHeight } = useWindowDimensions2();
683
+ const { width: windowWidth, height: windowHeight } = useWindowDimensions();
601
684
  const [align, setAlign] = useState4("center");
602
685
  const [verticalAlign, setVerticalAlign] = useState4("below");
603
686
  const [maxHeight, setMaxHeight] = useState4(contentHeight);
@@ -631,7 +714,7 @@ function useAutoAlign(open, contentWidth, contentHeight, rotation = 0) {
631
714
  }
632
715
 
633
716
  // src/SectionedDropdown.tsx
634
- import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
717
+ import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
635
718
  var MENU_BORDER_WIDTH = 1;
636
719
  var TRIGGER_SIZE = 44;
637
720
  var MENU_MIN_WIDTH = 200;
@@ -642,7 +725,7 @@ var MENU_ITEM_HEIGHT = 40;
642
725
  var MENU_ITEM_HEIGHT_WITH_DESCRIPTION = 58;
643
726
  var MENU_DIVIDER_HEIGHT = 9;
644
727
  var MENU_VERTICAL_PADDING = 12;
645
- function SectionedDropdown({ id, host, icon, accessibilityLabel, sections, accentColor, mutedColor, onAccentColor, dark, align: alignOverride, rotation: rotationOverride, autoDismiss = true, labelFontFamily = MONO_FONT, allClearLabels = { all: "All", clear: "Clear" } }) {
728
+ function SectionedDropdown({ id, host, icon, accessibilityLabel, sections, accentColor, mutedColor, onAccentColor, dark, align: forcedAlign, alignOverride, rotation: rotationOverride, autoDismiss = true, labelFontFamily = MONO_FONT, allClearLabels = { all: "All", clear: "Clear" } }) {
646
729
  const ambientRotation = useRotation3();
647
730
  const rotation = rotationOverride ?? ambientRotation;
648
731
  const menuBg = dark ? "#000000" : "#FFFFFF";
@@ -656,8 +739,10 @@ function SectionedDropdown({ id, host, icon, accessibilityLabel, sections, accen
656
739
  const divider = sectionIndex > 0 ? MENU_DIVIDER_HEIGHT : 0;
657
740
  return sum + rows + footer + divider;
658
741
  }, 0);
659
- const { align: autoAlign, maxHeight, measured, triggerRef, verticalAlign } = useAutoAlign(open, MENU_MAX_WIDTH, estimatedHeight, rotation);
660
- const align = alignOverride ?? autoAlign;
742
+ const auto = useAutoAlign(open, MENU_MAX_WIDTH, estimatedHeight, rotation);
743
+ const placement = alignOverride ?? auto;
744
+ const { maxHeight, measured, triggerRef, verticalAlign } = placement;
745
+ const align = forcedAlign ?? placement.align;
661
746
  const onlySection = sections.length === 1 ? sections[0] : null;
662
747
  const selectedOption = onlySection?.kind === "single" ? onlySection.options.find((o) => o.value === onlySection.value) : void 0;
663
748
  const triggerIcon = selectedOption?.icon ?? icon;
@@ -679,17 +764,17 @@ function SectionedDropdown({ id, host, icon, accessibilityLabel, sections, accen
679
764
  }
680
765
  gaugeOffset += section.options.length;
681
766
  }
682
- const anchorStyle = [styles7.anchor, open && styles7.anchorOpen];
767
+ const anchorStyle = [styles11.anchor, open && styles11.anchorOpen];
683
768
  const menuStyle = { backgroundColor: menuBg, borderColor: menuBorderColor, maxHeight };
684
769
  const dividerStyle = { backgroundColor: menuBorderColor };
685
- return /* @__PURE__ */ jsxs7(View8, { style: anchorStyle, children: [
686
- /* @__PURE__ */ jsxs7(View8, { ref: triggerRef, collapsable: false, style: styles7.triggerBox, children: [
687
- /* @__PURE__ */ jsx8(TriggerGaugeHost, { segments: gaugeSegments, litIndices: gaugeLitIndices, size: TRIGGER_SIZE, accentColor, mutedColor }),
688
- /* @__PURE__ */ jsx8(IconButton2, { icon: triggerIcon, iconColor: triggerIconColor, size: triggerIconSize, accessibilityLabel, onPress: () => host.toggle(id) })
770
+ return /* @__PURE__ */ jsxs12(View11, { style: anchorStyle, children: [
771
+ /* @__PURE__ */ jsxs12(View11, { ref: triggerRef, collapsable: false, style: styles11.triggerBox, children: [
772
+ /* @__PURE__ */ jsx13(TriggerGaugeHost, { segments: gaugeSegments, litIndices: gaugeLitIndices, size: TRIGGER_SIZE, accentColor, mutedColor }),
773
+ /* @__PURE__ */ jsx13(IconButton2, { icon: triggerIcon, iconColor: triggerIconColor, size: triggerIconSize, accessibilityLabel, onPress: () => host.toggle(id) })
689
774
  ] }),
690
- /* @__PURE__ */ jsx8(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: menuBorderColor, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: TRIGGER_SIZE, children: /* @__PURE__ */ jsx8(ScrollView3, { style: [styles7.menu, menuStyle], contentContainerStyle: styles7.menuContent, showsVerticalScrollIndicator: false, children: sections.map((section, sectionIndex) => /* @__PURE__ */ jsxs7(View8, { children: [
691
- sectionIndex > 0 && /* @__PURE__ */ jsx8(View8, { style: [styles7.divider, dividerStyle] }),
692
- section.kind === "single" ? /* @__PURE__ */ jsx8(
775
+ /* @__PURE__ */ jsx13(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: menuBorderColor, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: TRIGGER_SIZE, children: /* @__PURE__ */ jsx13(ScrollView3, { style: [styles11.menu, menuStyle], contentContainerStyle: styles11.menuContent, showsVerticalScrollIndicator: false, children: sections.map((section, sectionIndex) => /* @__PURE__ */ jsxs12(View11, { children: [
776
+ sectionIndex > 0 && /* @__PURE__ */ jsx13(View11, { style: [styles11.divider, dividerStyle] }),
777
+ section.kind === "single" ? /* @__PURE__ */ jsx13(
693
778
  SingleSection,
694
779
  {
695
780
  section,
@@ -702,7 +787,7 @@ function SectionedDropdown({ id, host, icon, accessibilityLabel, sections, accen
702
787
  if (autoDismiss) host.close();
703
788
  }
704
789
  }
705
- ) : /* @__PURE__ */ jsx8(MultiSection, { section, accentColor, onAccentColor: resolvedOnAccentColor, mutedColor, labelFontFamily, allClearLabels })
790
+ ) : /* @__PURE__ */ jsx13(MultiSection, { section, accentColor, onAccentColor: resolvedOnAccentColor, mutedColor, labelFontFamily, allClearLabels })
706
791
  ] }, section.id)) }) })
707
792
  ] });
708
793
  }
@@ -714,18 +799,18 @@ function SingleSection({
714
799
  labelFontFamily,
715
800
  onSelect
716
801
  }) {
717
- return /* @__PURE__ */ jsx8(Fragment3, { children: section.options.map((option) => {
802
+ return /* @__PURE__ */ jsx13(Fragment4, { children: section.options.map((option) => {
718
803
  const selected = option.value === section.value;
719
804
  const taken = !selected && option.value === section.takenValue;
720
805
  const rowColor = selected ? onAccentColor : mutedColor;
721
- const itemStyle = [styles7.item, selected && { backgroundColor: accentColor }, taken && styles7.itemTaken];
806
+ const itemStyle = [styles11.item, selected && { backgroundColor: accentColor }, taken && styles11.itemTaken];
722
807
  const labelStyle = { fontFamily: labelFontFamily, color: rowColor, fontWeight: selected ? "bold" : "normal" };
723
808
  const descriptionStyle = { fontFamily: labelFontFamily, color: rowColor };
724
- return /* @__PURE__ */ jsx8(TouchableRipple2, { disabled: taken, onPress: () => onSelect(option.value), style: itemStyle, children: /* @__PURE__ */ jsxs7(View8, { style: styles7.itemRow, children: [
725
- option.icon && /* @__PURE__ */ jsx8(Icon4, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
726
- /* @__PURE__ */ jsxs7(View8, { style: styles7.itemLabelColumn, children: [
727
- /* @__PURE__ */ jsx8(Text5, { variant: "labelLarge", style: labelStyle, children: option.label }),
728
- option.description && /* @__PURE__ */ jsx8(Text5, { variant: "labelSmall", style: descriptionStyle, children: option.description })
809
+ return /* @__PURE__ */ jsx13(TouchableRipple2, { disabled: taken, onPress: () => onSelect(option.value), style: itemStyle, children: /* @__PURE__ */ jsxs12(View11, { style: styles11.itemRow, children: [
810
+ option.icon && /* @__PURE__ */ jsx13(Icon5, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
811
+ /* @__PURE__ */ jsxs12(View11, { style: styles11.itemLabelColumn, children: [
812
+ /* @__PURE__ */ jsx13(Text9, { variant: "labelLarge", style: labelStyle, children: option.label }),
813
+ option.description && /* @__PURE__ */ jsx13(Text9, { variant: "labelSmall", style: descriptionStyle, children: option.description })
729
814
  ] })
730
815
  ] }) }, option.value);
731
816
  }) });
@@ -739,23 +824,23 @@ function MultiSection({
739
824
  allClearLabels
740
825
  }) {
741
826
  const allSelected = section.value.length === section.options.length;
742
- return /* @__PURE__ */ jsxs7(Fragment3, { children: [
827
+ return /* @__PURE__ */ jsxs12(Fragment4, { children: [
743
828
  section.options.map((option, i) => {
744
829
  const selected = section.value.includes(option.value);
745
830
  const rowColor = selected ? onAccentColor : mutedColor;
746
831
  const prevSelected = selected && i > 0 && section.value.includes(section.options[i - 1].value);
747
832
  const nextSelected = selected && i < section.options.length - 1 && section.value.includes(section.options[i + 1].value);
748
- const itemStyle = [styles7.item, selected && { backgroundColor: accentColor }, prevSelected && { borderTopLeftRadius: 0, borderTopRightRadius: 0 }, nextSelected && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0 }];
833
+ const itemStyle = [styles11.item, selected && { backgroundColor: accentColor }, prevSelected && { borderTopLeftRadius: 0, borderTopRightRadius: 0 }, nextSelected && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0 }];
749
834
  const labelStyle = { fontFamily: labelFontFamily, color: rowColor, fontWeight: selected ? "bold" : "normal" };
750
- return /* @__PURE__ */ jsx8(TouchableRipple2, { onPress: () => section.onChange(selected ? section.value.filter((v) => v !== option.value) : [...section.value, option.value]), style: itemStyle, children: /* @__PURE__ */ jsxs7(View8, { style: styles7.itemRow, children: [
751
- option.icon && /* @__PURE__ */ jsx8(Icon4, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
752
- /* @__PURE__ */ jsx8(Text5, { variant: "labelLarge", style: [styles7.itemLabelFlex, labelStyle], children: option.label })
835
+ return /* @__PURE__ */ jsx13(TouchableRipple2, { onPress: () => section.onChange(selected ? section.value.filter((v) => v !== option.value) : [...section.value, option.value]), style: itemStyle, children: /* @__PURE__ */ jsxs12(View11, { style: styles11.itemRow, children: [
836
+ option.icon && /* @__PURE__ */ jsx13(Icon5, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
837
+ /* @__PURE__ */ jsx13(Text9, { variant: "labelLarge", style: [styles11.itemLabelFlex, labelStyle], children: option.label })
753
838
  ] }) }, option.value);
754
839
  }),
755
- section.allClear && /* @__PURE__ */ jsx8(TouchableRipple2, { onPress: () => section.onChange(allSelected ? [] : section.options.map((o) => o.value)), style: styles7.item, children: /* @__PURE__ */ jsx8(Text5, { variant: "labelLarge", style: [styles7.allClearLabel, { fontFamily: labelFontFamily, color: accentColor }], children: allSelected ? allClearLabels.clear : allClearLabels.all }) })
840
+ section.allClear && /* @__PURE__ */ jsx13(TouchableRipple2, { onPress: () => section.onChange(allSelected ? [] : section.options.map((o) => o.value)), style: styles11.item, children: /* @__PURE__ */ jsx13(Text9, { variant: "labelLarge", style: [styles11.allClearLabel, { fontFamily: labelFontFamily, color: accentColor }], children: allSelected ? allClearLabels.clear : allClearLabels.all }) })
756
841
  ] });
757
842
  }
758
- var styles7 = StyleSheet8.create({
843
+ var styles11 = StyleSheet11.create({
759
844
  allClearLabel: {
760
845
  fontWeight: "bold",
761
846
  textAlign: "center"
@@ -772,7 +857,7 @@ var styles7 = StyleSheet8.create({
772
857
  zIndex: 100
773
858
  },
774
859
  divider: {
775
- height: StyleSheet8.hairlineWidth,
860
+ height: StyleSheet11.hairlineWidth,
776
861
  marginVertical: 4
777
862
  },
778
863
  item: {
@@ -825,24 +910,24 @@ var styles7 = StyleSheet8.create({
825
910
  });
826
911
 
827
912
  // src/ControlSchemePicker.tsx
828
- import { jsx as jsx9 } from "react/jsx-runtime";
829
- function ControlSchemePicker({ id, host, value, onChange, options, takenValue, accentColor, mutedColor, onAccentColor, dark, align, labelFontFamily }) {
913
+ import { jsx as jsx14 } from "react/jsx-runtime";
914
+ function ControlSchemePicker({ id, host, value, onChange, options, takenValue, accentColor, mutedColor, onAccentColor, dark, align, alignOverride, rotation, labelFontFamily }) {
830
915
  const isTouchPrimary = useIsTouchPrimaryDevice2();
831
916
  if (Platform3.OS !== "web" || isTouchPrimary) return null;
832
- return /* @__PURE__ */ jsx9(SectionedDropdown, { id, host, icon: "keyboard-outline", accessibilityLabel: "Control scheme", sections: [{ kind: "single", id: "controls", options, value, onChange, takenValue }], accentColor, mutedColor, onAccentColor, dark, align, labelFontFamily });
917
+ return /* @__PURE__ */ jsx14(SectionedDropdown, { id, host, icon: "keyboard-outline", accessibilityLabel: "Control scheme", sections: [{ kind: "single", id: "controls", options, value, onChange, takenValue }], accentColor, mutedColor, onAccentColor, dark, align, alignOverride, rotation, labelFontFamily });
833
918
  }
834
919
 
835
920
  // src/CornerActionButtons.tsx
836
921
  import { IconButton as IconButton3 } from "@rific/feedback-press";
837
- import { StyleSheet as StyleSheet9 } from "react-native";
838
- import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
922
+ import { StyleSheet as StyleSheet12 } from "react-native";
923
+ import { Fragment as Fragment5, jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
839
924
  function CornerActionButtons({ onBack, onSettings, fg, insets, backIcon = "arrow-left", backLabel = "Back" }) {
840
- return /* @__PURE__ */ jsxs8(Fragment4, { children: [
841
- /* @__PURE__ */ jsx10(IconButton3, { icon: backIcon, iconColor: fg, size: 24, style: [styles8.back, { top: 8 + insets.top, left: 8 + insets.left }], onPress: onBack, accessibilityLabel: backLabel }),
842
- /* @__PURE__ */ jsx10(IconButton3, { icon: "cog", iconColor: fg, size: 24, style: [styles8.topRight, { top: 8 + insets.top, right: 8 + insets.right }], onPress: onSettings, accessibilityLabel: "Settings" })
925
+ return /* @__PURE__ */ jsxs13(Fragment5, { children: [
926
+ /* @__PURE__ */ jsx15(IconButton3, { icon: backIcon, iconColor: fg, size: 24, style: [styles12.back, { top: 8 + insets.top, left: 8 + insets.left }], onPress: onBack, accessibilityLabel: backLabel }),
927
+ /* @__PURE__ */ jsx15(IconButton3, { icon: "cog", iconColor: fg, size: 24, style: [styles12.topRight, { top: 8 + insets.top, right: 8 + insets.right }], onPress: onSettings, accessibilityLabel: "Settings" })
843
928
  ] });
844
929
  }
845
- var styles8 = StyleSheet9.create({
930
+ var styles12 = StyleSheet12.create({
846
931
  back: {
847
932
  position: "absolute"
848
933
  },
@@ -852,22 +937,22 @@ var styles8 = StyleSheet9.create({
852
937
  });
853
938
 
854
939
  // src/CornerStatusBadges.tsx
855
- import { StyleSheet as StyleSheet10, View as View9 } from "react-native";
856
- import { Icon as Icon5 } from "react-native-paper";
857
- import { Fragment as Fragment5, jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
940
+ import { StyleSheet as StyleSheet13, View as View12 } from "react-native";
941
+ import { Icon as Icon6 } from "react-native-paper";
942
+ import { Fragment as Fragment6, jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
858
943
  var BADGE_SIZE = 30;
859
944
  function StatusBadge({ icon }) {
860
- return /* @__PURE__ */ jsx11(View9, { style: styles9.badge, children: icon && /* @__PURE__ */ jsx11(Icon5, { source: icon, size: 16, color: "rgba(255,255,255,0.85)" }) });
945
+ return /* @__PURE__ */ jsx16(View12, { style: styles13.badge, children: icon && /* @__PURE__ */ jsx16(Icon6, { source: icon, size: 16, color: "rgba(255,255,255,0.85)" }) });
861
946
  }
862
947
  function CornerStatusBadges({ orientationMode, p1OnRight, p1, p2 }) {
863
948
  const p1OnRightSide = orientationMode === "sideBySide" && p1OnRight;
864
949
  const p2OnLeftSide = orientationMode === "sideBySide" && !p1OnRight;
865
- return /* @__PURE__ */ jsxs9(Fragment5, { children: [
866
- p1 && /* @__PURE__ */ jsx11(View9, { pointerEvents: "none", style: p1OnRightSide ? styles9.bottomRight : styles9.bottomLeft, children: /* @__PURE__ */ jsx11(StatusBadge, { icon: p1.icon }) }),
867
- p2 && /* @__PURE__ */ jsx11(View9, { pointerEvents: "none", style: p2OnLeftSide ? styles9.topLeft : styles9.topRight, children: /* @__PURE__ */ jsx11(StatusBadge, { icon: p2.icon }) })
950
+ return /* @__PURE__ */ jsxs14(Fragment6, { children: [
951
+ p1 && /* @__PURE__ */ jsx16(View12, { pointerEvents: "none", style: p1OnRightSide ? styles13.bottomRight : styles13.bottomLeft, children: /* @__PURE__ */ jsx16(StatusBadge, { icon: p1.icon }) }),
952
+ p2 && /* @__PURE__ */ jsx16(View12, { pointerEvents: "none", style: p2OnLeftSide ? styles13.topLeft : styles13.topRight, children: /* @__PURE__ */ jsx16(StatusBadge, { icon: p2.icon }) })
868
953
  ] });
869
954
  }
870
- var styles9 = StyleSheet10.create({
955
+ var styles13 = StyleSheet13.create({
871
956
  badge: {
872
957
  alignItems: "center",
873
958
  backgroundColor: "rgba(0,0,0,0.28)",
@@ -886,12 +971,12 @@ var styles9 = StyleSheet10.create({
886
971
  });
887
972
 
888
973
  // src/InlineColorPicker.tsx
889
- import { defaultColors, getContrastColor } from "@rific/auto-paper";
974
+ import { defaultColors, getContrastColor as getContrastColor2 } from "@rific/auto-paper";
890
975
  import { TouchableRipple as TouchableRipple3 } from "@rific/feedback-press";
891
976
  import { clamp, useRotation as useRotation4 } from "@tastic/core";
892
- import { ScrollView as ScrollView4, StyleSheet as StyleSheet11, useWindowDimensions as useWindowDimensions3, View as View10 } from "react-native";
893
- import { Icon as Icon6, Text as Text6 } from "react-native-paper";
894
- import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
977
+ import { ScrollView as ScrollView4, StyleSheet as StyleSheet14, useWindowDimensions as useWindowDimensions2, View as View13 } from "react-native";
978
+ import { Icon as Icon7, Text as Text10 } from "react-native-paper";
979
+ import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
895
980
  var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
896
981
  var DEFAULT_SIZE = 48;
897
982
  var SWATCH_SIZE = 28;
@@ -904,31 +989,37 @@ var SCREEN_MARGIN = 40;
904
989
  function widthForColumns(columns) {
905
990
  return SWATCHES_BORDER_WIDTH * 2 + SWATCHES_PADDING * 2 + SWATCH_SIZE * columns + SWATCHES_GAP * (columns - 1);
906
991
  }
907
- function InlineColorPicker({ id, host, value, onChange, previewValue, swatches = defaultColors, takenValue, allowSwapTaken, dark, align: alignOverride, rotation: rotationOverride, icon = "palette", tag, labelFontFamily = MONO_FONT, autoDismiss = true, columns, size = DEFAULT_SIZE }) {
992
+ function getInlineColorPickerContentSize(swatchCount, windowWidth, columns) {
993
+ const autoColumns = clamp(Math.floor((windowWidth - 2 * SCREEN_MARGIN + SWATCHES_GAP) / (SWATCH_SIZE + SWATCHES_GAP)), MIN_COLUMNS, MAX_COLUMNS);
994
+ const resolvedColumns = columns ?? autoColumns;
995
+ const width = widthForColumns(resolvedColumns);
996
+ const rows = Math.ceil(swatchCount / resolvedColumns);
997
+ const height = SWATCHES_BORDER_WIDTH * 2 + SWATCHES_PADDING * 2 + SWATCH_SIZE * rows + SWATCHES_GAP * (rows - 1);
998
+ return { width, height };
999
+ }
1000
+ function InlineColorPicker({ id, host, value, onChange, previewValue, swatches = defaultColors, takenValue, allowSwapTaken, dark, align: forcedAlign, alignOverride, rotation: rotationOverride, icon = "palette", tag, labelFontFamily = MONO_FONT, autoDismiss = true, columns, size = DEFAULT_SIZE }) {
908
1001
  const ambientRotation = useRotation4();
909
1002
  const rotation = rotationOverride ?? ambientRotation;
910
1003
  const menuBg = dark ? "#000000" : "#FFFFFF";
911
- const { width: windowWidth } = useWindowDimensions3();
912
- const autoColumns = clamp(Math.floor((windowWidth - 2 * SCREEN_MARGIN + SWATCHES_GAP) / (SWATCH_SIZE + SWATCHES_GAP)), MIN_COLUMNS, MAX_COLUMNS);
913
- const resolvedColumns = columns ?? autoColumns;
914
- const swatchesWidth = widthForColumns(resolvedColumns);
915
- const swatchRows = Math.ceil(swatches.length / resolvedColumns);
916
- const swatchesHeight = SWATCHES_BORDER_WIDTH * 2 + SWATCHES_PADDING * 2 + SWATCH_SIZE * swatchRows + SWATCHES_GAP * (swatchRows - 1);
1004
+ const { width: windowWidth } = useWindowDimensions2();
1005
+ const { width: swatchesWidth, height: swatchesHeight } = getInlineColorPickerContentSize(swatches.length, windowWidth, columns);
917
1006
  const open = host.openId === id;
918
- const { align: autoAlign, maxHeight, measured, triggerRef, verticalAlign } = useAutoAlign(open, swatchesWidth, swatchesHeight, rotation);
919
- const align = alignOverride ?? autoAlign;
1007
+ const auto = useAutoAlign(open, swatchesWidth, swatchesHeight, rotation);
1008
+ const placement = alignOverride ?? auto;
1009
+ const { maxHeight, measured, triggerRef, verticalAlign } = placement;
1010
+ const align = forcedAlign ?? placement.align;
920
1011
  const displayFor = previewValue ?? ((hex) => hex);
921
1012
  const displayValue = displayFor(value);
922
- const contrastColor = getContrastColor(displayValue);
1013
+ const contrastColor = getContrastColor2(displayValue);
923
1014
  const tagFontSize = size * (tag && EMOJI_PATTERN.test(tag) ? 0.6 : 0.4);
924
- return /* @__PURE__ */ jsxs10(View10, { style: [styles10.anchor, open && styles10.anchorOpen], children: [
925
- /* @__PURE__ */ jsx12(TouchableRipple3, { onPress: () => host.toggle(id), borderless: true, style: [styles10.trigger, { backgroundColor: displayValue, borderRadius: size / 2, height: size, width: size }], children: /* @__PURE__ */ jsx12(View10, { ref: triggerRef, collapsable: false, style: styles10.triggerMeasure, children: tag ? /* @__PURE__ */ jsx12(Text6, { style: [styles10.tagLabel, { color: contrastColor, fontFamily: labelFontFamily, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ jsx12(Icon6, { source: icon, size: size * 0.6, color: contrastColor }) }) }),
926
- /* @__PURE__ */ jsx12(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: displayValue, caretBorderWidth: SWATCHES_BORDER_WIDTH, triggerSize: size, children: /* @__PURE__ */ jsx12(ScrollView4, { style: [styles10.swatches, { backgroundColor: menuBg, borderColor: displayValue, width: swatchesWidth, maxHeight }], contentContainerStyle: styles10.swatchesContent, showsVerticalScrollIndicator: false, children: swatches.map((swatch) => {
1015
+ return /* @__PURE__ */ jsxs15(View13, { style: [styles14.anchor, open && styles14.anchorOpen], children: [
1016
+ /* @__PURE__ */ jsx17(TouchableRipple3, { onPress: () => host.toggle(id), borderless: true, style: [styles14.trigger, { backgroundColor: displayValue, borderRadius: size / 2, height: size, width: size }], children: /* @__PURE__ */ jsx17(View13, { ref: triggerRef, collapsable: false, style: styles14.triggerMeasure, children: tag ? /* @__PURE__ */ jsx17(Text10, { style: [styles14.tagLabel, { color: contrastColor, fontFamily: labelFontFamily, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ jsx17(Icon7, { source: icon, size: size * 0.6, color: contrastColor }) }) }),
1017
+ /* @__PURE__ */ jsx17(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: displayValue, caretBorderWidth: SWATCHES_BORDER_WIDTH, triggerSize: size, children: /* @__PURE__ */ jsx17(ScrollView4, { style: [styles14.swatches, { backgroundColor: menuBg, borderColor: displayValue, width: swatchesWidth, maxHeight }], contentContainerStyle: styles14.swatchesContent, showsVerticalScrollIndicator: false, children: swatches.map((swatch) => {
927
1018
  const selected = swatch.value.toLowerCase() === value.toLowerCase();
928
1019
  const taken = !selected && !!takenValue && swatch.value.toLowerCase() === takenValue.toLowerCase();
929
1020
  const swappable = taken && allowSwapTaken;
930
1021
  const swatchDisplay = displayFor(swatch.value);
931
- return /* @__PURE__ */ jsx12(
1022
+ return /* @__PURE__ */ jsx17(
932
1023
  TouchableRipple3,
933
1024
  {
934
1025
  disabled: taken && !swappable,
@@ -937,15 +1028,15 @@ function InlineColorPicker({ id, host, value, onChange, previewValue, swatches =
937
1028
  if (autoDismiss) host.close();
938
1029
  },
939
1030
  borderless: true,
940
- style: [styles10.swatch, { backgroundColor: swatchDisplay }, selected && styles10.swatchSelected, taken && !swappable && styles10.swatchTaken],
941
- children: selected ? /* @__PURE__ */ jsx12(Icon6, { source: "check", size: 16, color: getContrastColor(swatchDisplay) }) : swappable ? /* @__PURE__ */ jsx12(Icon6, { source: "swap-horizontal", size: 16, color: getContrastColor(swatchDisplay) }) : taken ? /* @__PURE__ */ jsx12(Icon6, { source: "close", size: 16, color: getContrastColor(swatchDisplay) }) : /* @__PURE__ */ jsx12(View10, {})
1031
+ style: [styles14.swatch, { backgroundColor: swatchDisplay }, selected && styles14.swatchSelected, taken && !swappable && styles14.swatchTaken],
1032
+ children: selected ? /* @__PURE__ */ jsx17(Icon7, { source: "check", size: 16, color: getContrastColor2(swatchDisplay) }) : swappable ? /* @__PURE__ */ jsx17(Icon7, { source: "swap-horizontal", size: 16, color: getContrastColor2(swatchDisplay) }) : taken ? /* @__PURE__ */ jsx17(Icon7, { source: "close", size: 16, color: getContrastColor2(swatchDisplay) }) : /* @__PURE__ */ jsx17(View13, {})
942
1033
  },
943
1034
  swatch.value
944
1035
  );
945
1036
  }) }) })
946
1037
  ] });
947
1038
  }
948
- var styles10 = StyleSheet11.create({
1039
+ var styles14 = StyleSheet14.create({
949
1040
  anchor: {
950
1041
  position: "relative"
951
1042
  },
@@ -1017,11 +1108,11 @@ var styles10 = StyleSheet11.create({
1017
1108
  });
1018
1109
 
1019
1110
  // src/LabeledDropdown.tsx
1020
- import { getContrastColor as getContrastColor2 } from "@rific/auto-paper";
1111
+ import { getContrastColor as getContrastColor3 } from "@rific/auto-paper";
1021
1112
  import { TouchableRipple as TouchableRipple4 } from "@rific/feedback-press";
1022
- import { ScrollView as ScrollView5, StyleSheet as StyleSheet12, View as View11 } from "react-native";
1023
- import { Icon as Icon7, Text as Text7 } from "react-native-paper";
1024
- import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
1113
+ import { ScrollView as ScrollView5, StyleSheet as StyleSheet15, View as View14 } from "react-native";
1114
+ import { Icon as Icon8, Text as Text11 } from "react-native-paper";
1115
+ import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
1025
1116
  var TRIGGER_HEIGHT = 28;
1026
1117
  var POPOVER_WIDTH = 180;
1027
1118
  var ROW_HEIGHT = 36;
@@ -1044,22 +1135,22 @@ function LabeledDropdown({ id, host, options, value, onChange, color, dark, alig
1044
1135
  onChange(option.value);
1045
1136
  host.close();
1046
1137
  };
1047
- return /* @__PURE__ */ jsxs11(View11, { style: [styles11.anchor, open && styles11.anchorOpen], children: [
1048
- /* @__PURE__ */ jsx13(TouchableRipple4, { onPress: () => host.toggle(id), style: styles11.trigger, children: /* @__PURE__ */ jsxs11(View11, { ref: triggerRef, collapsable: false, style: styles11.triggerInner, children: [
1049
- /* @__PURE__ */ jsx13(Text7, { style: [styles11.triggerLabel, { color }], numberOfLines: 1, children: (selected?.label ?? "").toUpperCase() }),
1050
- /* @__PURE__ */ jsx13(Icon7, { source: "menu-down", size: 16, color })
1138
+ return /* @__PURE__ */ jsxs16(View14, { style: [styles15.anchor, open && styles15.anchorOpen], children: [
1139
+ /* @__PURE__ */ jsx18(TouchableRipple4, { onPress: () => host.toggle(id), style: styles15.trigger, children: /* @__PURE__ */ jsxs16(View14, { ref: triggerRef, collapsable: false, style: styles15.triggerInner, children: [
1140
+ /* @__PURE__ */ jsx18(Text11, { style: [styles15.triggerLabel, { color }], numberOfLines: 1, children: (selected?.label ?? "").toUpperCase() }),
1141
+ /* @__PURE__ */ jsx18(Icon8, { source: "menu-down", size: 16, color })
1051
1142
  ] }) }),
1052
- /* @__PURE__ */ jsx13(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: 2, triggerSize: TRIGGER_HEIGHT, children: /* @__PURE__ */ jsx13(ScrollView5, { style: [styles11.menu, { backgroundColor: menuBg, borderColor: color, width: POPOVER_WIDTH, maxHeight }], contentContainerStyle: styles11.menuContent, showsVerticalScrollIndicator: false, children: options.map((option) => {
1143
+ /* @__PURE__ */ jsx18(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: 2, triggerSize: TRIGGER_HEIGHT, children: /* @__PURE__ */ jsx18(ScrollView5, { style: [styles15.menu, { backgroundColor: menuBg, borderColor: color, width: POPOVER_WIDTH, maxHeight }], contentContainerStyle: styles15.menuContent, showsVerticalScrollIndicator: false, children: options.map((option) => {
1053
1144
  const isSelected = option.value === value;
1054
- const onColor = getContrastColor2(color);
1055
- return /* @__PURE__ */ jsx13(TouchableRipple4, { onPress: () => handleSelect(option), style: [styles11.row, isSelected && { backgroundColor: color }], children: /* @__PURE__ */ jsxs11(View11, { style: styles11.rowInner, children: [
1056
- option.icon && /* @__PURE__ */ jsx13(Icon7, { source: option.icon, size: 18, color: isSelected ? onColor : fg }),
1057
- /* @__PURE__ */ jsx13(Text7, { style: [styles11.rowLabel, { color: isSelected ? onColor : fg }], numberOfLines: 1, children: option.label })
1145
+ const onColor = getContrastColor3(color);
1146
+ return /* @__PURE__ */ jsx18(TouchableRipple4, { onPress: () => handleSelect(option), style: [styles15.row, isSelected && { backgroundColor: color }], children: /* @__PURE__ */ jsxs16(View14, { style: styles15.rowInner, children: [
1147
+ option.icon && /* @__PURE__ */ jsx18(Icon8, { source: option.icon, size: 18, color: isSelected ? onColor : fg }),
1148
+ /* @__PURE__ */ jsx18(Text11, { style: [styles15.rowLabel, { color: isSelected ? onColor : fg }], numberOfLines: 1, children: option.label })
1058
1149
  ] }) }, option.value);
1059
1150
  }) }) })
1060
1151
  ] });
1061
1152
  }
1062
- var styles11 = StyleSheet12.create({
1153
+ var styles15 = StyleSheet15.create({
1063
1154
  anchor: {
1064
1155
  position: "relative"
1065
1156
  },
@@ -1116,26 +1207,23 @@ function loadSkiaWeb() {
1116
1207
  return Promise.resolve();
1117
1208
  }
1118
1209
 
1119
- // src/PressAwayOverlay.tsx
1120
- import { Pressable, StyleSheet as StyleSheet13 } from "react-native";
1121
- import { jsx as jsx14 } from "react/jsx-runtime";
1122
- function PressAwayOverlay({ active, onPress, style }) {
1123
- if (!active) return null;
1124
- return /* @__PURE__ */ jsx14(Pressable, { accessibilityLabel: "Dismiss", accessibilityRole: "button", onPress, style: [StyleSheet13.absoluteFill, style] });
1125
- }
1210
+ // src/PlayerSetupPanel.tsx
1211
+ import { ProfilePicker } from "@tastic/profile";
1212
+ import { StyleSheet as StyleSheet17, View as View15 } from "react-native";
1213
+ import { Text as Text13 } from "react-native-paper";
1126
1214
 
1127
1215
  // src/ReadyButton.tsx
1128
1216
  import { TouchableRipple as TouchableRipple5 } from "@rific/feedback-press";
1129
- import { StyleSheet as StyleSheet14 } from "react-native";
1130
- import { Text as Text8 } from "react-native-paper";
1131
- import { jsx as jsx15 } from "react/jsx-runtime";
1217
+ import { StyleSheet as StyleSheet16 } from "react-native";
1218
+ import { Text as Text12 } from "react-native-paper";
1219
+ import { jsx as jsx19 } from "react/jsx-runtime";
1132
1220
  function ReadyButton({ color, ready, onToggleReady, style, labelFontFamily = MONO_FONT }) {
1133
- const readyButtonStyle = [styles12.readyButton, { borderColor: color }, ready && { backgroundColor: color }, style];
1221
+ const readyButtonStyle = [styles16.readyButton, { borderColor: color }, ready && { backgroundColor: color }, style];
1134
1222
  const labelTextStyle = { fontFamily: labelFontFamily, fontWeight: "bold" };
1135
1223
  const labelColorStyle = { color: ready ? "#000000" : color };
1136
- return /* @__PURE__ */ jsx15(TouchableRipple5, { onPress: onToggleReady, borderless: true, style: readyButtonStyle, children: /* @__PURE__ */ jsx15(Text8, { variant: "labelLarge", style: [labelTextStyle, labelColorStyle], children: ready ? "READY \u2713" : "READY?" }) });
1224
+ return /* @__PURE__ */ jsx19(TouchableRipple5, { onPress: onToggleReady, borderless: true, style: readyButtonStyle, children: /* @__PURE__ */ jsx19(Text12, { variant: "labelLarge", style: [labelTextStyle, labelColorStyle], children: ready ? "READY \u2713" : "READY?" }) });
1137
1225
  }
1138
- var styles12 = StyleSheet14.create({
1226
+ var styles16 = StyleSheet16.create({
1139
1227
  readyButton: {
1140
1228
  alignItems: "center",
1141
1229
  borderRadius: 12,
@@ -1147,22 +1235,97 @@ var styles12 = StyleSheet14.create({
1147
1235
  }
1148
1236
  });
1149
1237
 
1238
+ // src/usePopoverHost.ts
1239
+ import { useCallback as useCallback3, useState as useState5 } from "react";
1240
+ function usePopoverHost() {
1241
+ const [openId, setOpenId] = useState5(null);
1242
+ const toggle = useCallback3((id) => {
1243
+ setOpenId((prev) => prev === id ? null : id);
1244
+ }, []);
1245
+ const close = useCallback3(() => setOpenId(null), []);
1246
+ return { openId, toggle, close };
1247
+ }
1248
+
1249
+ // src/PlayerSetupPanel.tsx
1250
+ import { jsx as jsx20, jsxs as jsxs17 } from "react/jsx-runtime";
1251
+ function getColorPopoverId(idPrefix) {
1252
+ return `${idPrefix}-color`;
1253
+ }
1254
+ function getCpuDifficultyPopoverId(idPrefix) {
1255
+ return `${idPrefix}-difficulty`;
1256
+ }
1257
+ function PlayerSetupPanel({ 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, cpuDifficultyAlignOverride, humanIcon = "face-man", cpuIcon = "robot" }) {
1258
+ const ownHost = usePopoverHost();
1259
+ const popover = host ?? ownHost;
1260
+ const ownPopoverOpen = popover.openId?.startsWith(`${idPrefix}-`) ?? false;
1261
+ const pickerRowPopoverOpen = popover.openId === getColorPopoverId(idPrefix) || secondPickerId !== void 0 && popover.openId === secondPickerId;
1262
+ const showProfilePicker = isHuman && profiles !== void 0 && !!onProfileSelect;
1263
+ const showCpuDifficulty = !isHuman && cpuDifficulty !== void 0 && cpuDifficultyOptions !== void 0 && !!onCpuDifficultyChange;
1264
+ const selectedProfile = profiles?.find((p) => p.id === selectedProfileId) ?? null;
1265
+ return /* @__PURE__ */ jsxs17(View15, { style: [styles17.panel, ownPopoverOpen && styles17.panelOpen], children: [
1266
+ showProfilePicker ? /* @__PURE__ */ jsx20(ProfilePicker, { idPrefix, host: popover, profiles, selectedId: selectedProfileId ?? null, takenId: takenProfileId, color, dark, guestLabel: guestLabel ?? "GUEST", onSelect: (profile) => onProfileSelect?.(profile), onManage: onManageProfiles }) : showCpuDifficulty ? /* @__PURE__ */ jsx20(LabeledDropdown, { id: getCpuDifficultyPopoverId(idPrefix), host: popover, options: cpuDifficultyOptions, value: cpuDifficulty, onChange: onCpuDifficultyChange, color, dark, alignOverride: cpuDifficultyAlignOverride }) : label && /* @__PURE__ */ jsx20(Text13, { variant: "labelSmall", style: { color, fontFamily: labelFontFamily }, children: label }),
1267
+ /* @__PURE__ */ jsxs17(View15, { style: [styles17.pickerRow, pickerRowPopoverOpen && styles17.pickerRowOpen], children: [
1268
+ /* @__PURE__ */ jsx20(InlineColorPicker, { id: getColorPopoverId(idPrefix), host: popover, value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, alignOverride: colorAlignOverride, rotation: colorRotation, dark, tag: isHuman ? selectedProfile?.tag : void 0, icon: isHuman ? humanIcon : cpuIcon }),
1269
+ secondPicker
1270
+ ] }),
1271
+ isHuman && showReadyButton && onToggleReady && /* @__PURE__ */ jsx20(ReadyButton, { color, ready: ready ?? false, onToggleReady })
1272
+ ] });
1273
+ }
1274
+ var styles17 = StyleSheet17.create({
1275
+ panel: {
1276
+ alignItems: "center",
1277
+ gap: 12
1278
+ },
1279
+ // See SectionedDropdown's anchorOpen comment — this panel is typically a sibling of another
1280
+ // panel (and/or a shared-controls row), so a popover escaping this panel's bounds needs the panel
1281
+ // itself elevated, not just the popover content, to paint above a later sibling.
1282
+ panelOpen: {
1283
+ zIndex: 100
1284
+ },
1285
+ // Same gap as `panel`'s own vertical rhythm, reused horizontally — the color picker and (when
1286
+ // present) secondPicker sit side by side within this row instead of stacked in the panel's own
1287
+ // column.
1288
+ pickerRow: {
1289
+ alignItems: "center",
1290
+ flexDirection: "row",
1291
+ gap: 12
1292
+ },
1293
+ // A popover is an absolutely-positioned sibling of its own trigger — so a tall one (the color
1294
+ // swatch grid) can extend down far enough to overlap the ReadyButton below, which paints on top of
1295
+ // it by default DOM order since it's a later sibling within `panel`. `panel`'s own panelOpen
1296
+ // elevation only helps this panel paint above OTHER panels (see its own comment) — it does nothing
1297
+ // for stacking *within* this panel, since React Native Web gives each View its own stacking
1298
+ // context. This is what actually fixes that: elevate pickerRow itself above its own later sibling
1299
+ // whenever one of its own popovers is open.
1300
+ pickerRowOpen: {
1301
+ zIndex: 100
1302
+ }
1303
+ });
1304
+
1305
+ // src/PressAwayOverlay.tsx
1306
+ import { Pressable, StyleSheet as StyleSheet18 } from "react-native";
1307
+ import { jsx as jsx21 } from "react/jsx-runtime";
1308
+ function PressAwayOverlay({ active, onPress, style }) {
1309
+ if (!active) return null;
1310
+ return /* @__PURE__ */ jsx21(Pressable, { accessibilityLabel: "Dismiss", accessibilityRole: "button", onPress, style: [StyleSheet18.absoluteFill, style] });
1311
+ }
1312
+
1150
1313
  // src/SharedActionBand.tsx
1151
1314
  import { IconButton as IconButton4 } from "@rific/feedback-press";
1152
- import { StyleSheet as StyleSheet15, View as View12 } from "react-native";
1153
- import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
1315
+ import { StyleSheet as StyleSheet19, View as View16 } from "react-native";
1316
+ import { jsx as jsx22, jsxs as jsxs18 } from "react/jsx-runtime";
1154
1317
  function SharedActionBand({ onBack, onSettings, onRandomize, onReset, fg, popoverOpen, children }) {
1155
- return /* @__PURE__ */ jsxs12(View12, { style: [styles13.column, popoverOpen && styles13.columnOpen], children: [
1156
- /* @__PURE__ */ jsxs12(View12, { style: styles13.actionsRow, children: [
1157
- /* @__PURE__ */ jsx16(IconButton4, { icon: "arrow-left", iconColor: fg, size: 20, onPress: onBack, accessibilityLabel: "Back" }),
1158
- onRandomize && /* @__PURE__ */ jsx16(IconButton4, { icon: "dice-multiple", iconColor: fg, size: 18, onPress: onRandomize, accessibilityLabel: "Randomize match settings" }),
1159
- onReset && /* @__PURE__ */ jsx16(IconButton4, { icon: "restore", iconColor: fg, size: 18, onPress: onReset, accessibilityLabel: "Reset match settings to defaults" }),
1160
- /* @__PURE__ */ jsx16(IconButton4, { icon: "cog", iconColor: fg, size: 20, onPress: onSettings, accessibilityLabel: "Settings" })
1318
+ return /* @__PURE__ */ jsxs18(View16, { style: [styles18.column, popoverOpen && styles18.columnOpen], children: [
1319
+ /* @__PURE__ */ jsxs18(View16, { style: styles18.actionsRow, children: [
1320
+ /* @__PURE__ */ jsx22(IconButton4, { icon: "arrow-left", iconColor: fg, size: 20, onPress: onBack, accessibilityLabel: "Back" }),
1321
+ onRandomize && /* @__PURE__ */ jsx22(IconButton4, { icon: "dice-multiple", iconColor: fg, size: 18, onPress: onRandomize, accessibilityLabel: "Randomize match settings" }),
1322
+ onReset && /* @__PURE__ */ jsx22(IconButton4, { icon: "restore", iconColor: fg, size: 18, onPress: onReset, accessibilityLabel: "Reset match settings to defaults" }),
1323
+ /* @__PURE__ */ jsx22(IconButton4, { icon: "cog", iconColor: fg, size: 20, onPress: onSettings, accessibilityLabel: "Settings" })
1161
1324
  ] }),
1162
1325
  children
1163
1326
  ] });
1164
1327
  }
1165
- var styles13 = StyleSheet15.create({
1328
+ var styles18 = StyleSheet19.create({
1166
1329
  actionsRow: {
1167
1330
  alignItems: "center",
1168
1331
  flexDirection: "row",
@@ -1177,83 +1340,110 @@ var styles13 = StyleSheet15.create({
1177
1340
  }
1178
1341
  });
1179
1342
 
1180
- // src/StatRow.tsx
1181
- import { useAutoPaperTheme as useAutoPaperTheme5 } from "@rific/auto-paper";
1182
- import { StyleSheet as StyleSheet16, View as View13 } from "react-native";
1183
- import { Text as Text9 } from "react-native-paper";
1184
- import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
1185
- function StatRow({ label, value, fg: fgOverride, fgMuted: fgMutedOverride }) {
1186
- const { dark } = useAutoPaperTheme5();
1187
- const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
1188
- const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
1189
- return /* @__PURE__ */ jsxs13(View13, { style: styles14.row, children: [
1190
- /* @__PURE__ */ jsx17(Text9, { variant: "bodyMedium", style: { color: fgMuted }, children: label }),
1191
- /* @__PURE__ */ jsx17(Text9, { variant: "bodyMedium", style: [styles14.value, { color: fg }], children: value })
1192
- ] });
1343
+ // src/StaggeredWord.tsx
1344
+ import { useEffect as useEffect2 } from "react";
1345
+ import { StyleSheet as StyleSheet20, View as View17 } from "react-native";
1346
+ import Animated, { Easing, useAnimatedStyle, useSharedValue, withDelay, withTiming } from "react-native-reanimated";
1347
+ import { jsx as jsx23 } from "react/jsx-runtime";
1348
+ var STAGGER_MS = 45;
1349
+ var LETTER_DURATION_MS = 320;
1350
+ function getStaggeredWordDuration(letterCount) {
1351
+ return (letterCount - 1) * STAGGER_MS + LETTER_DURATION_MS;
1193
1352
  }
1194
- var styles14 = StyleSheet16.create({
1195
- row: {
1196
- alignItems: "center",
1197
- flexDirection: "row",
1198
- justifyContent: "space-between"
1199
- },
1200
- value: {
1201
- fontWeight: "bold"
1202
- }
1203
- });
1204
-
1205
- // src/StatSection.tsx
1206
- import { useAutoPaperTheme as useAutoPaperTheme6 } from "@rific/auto-paper";
1207
- import { StyleSheet as StyleSheet17, View as View14 } from "react-native";
1208
- import { Text as Text10 } from "react-native-paper";
1209
- import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
1210
- function StatSection({ label, fg: fgOverride, sectionBg: sectionBgOverride, children }) {
1211
- const { dark } = useAutoPaperTheme6();
1212
- const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
1213
- const sectionBg = sectionBgOverride ?? (dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)");
1214
- return /* @__PURE__ */ jsxs14(View14, { style: [styles15.section, { backgroundColor: sectionBg }], children: [
1215
- /* @__PURE__ */ jsx18(Text10, { variant: "labelMedium", style: [styles15.label, { color: fg }], children: label }),
1216
- children
1217
- ] });
1353
+ function AnimatedLetter({ char, index, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }) {
1354
+ const progress = useSharedValue(reducedMotion ? 1 : 0);
1355
+ useEffect2(() => {
1356
+ if (reducedMotion) return;
1357
+ progress.value = withDelay(index * STAGGER_MS, withTiming(1, { duration: LETTER_DURATION_MS, easing: Easing.out(Easing.back(1.5)) }));
1358
+ }, [reducedMotion]);
1359
+ const style = useAnimatedStyle(() => ({
1360
+ opacity: progress.value,
1361
+ transform: [{ translateY: (1 - progress.value) * 12 }, { scale: 0.85 + progress.value * 0.15 }]
1362
+ }));
1363
+ return /* @__PURE__ */ jsx23(Animated.Text, { allowFontScaling: false, style: [{ color, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }, style], children: char });
1218
1364
  }
1219
- var styles15 = StyleSheet17.create({
1220
- // Bold and full-contrast (fg), not the original fontless/fgMuted treatment — that read at the
1221
- // same visual weight as StatRow's own row labels below it (both used the identical muted color;
1222
- // letterSpacing/variant alone didn't separate them enough to read as "this is the group header"
1223
- // at a glance). Matches AchievementRow's own title styling (bold + fg) below it in the same list.
1224
- label: {
1225
- fontWeight: "bold",
1226
- letterSpacing: 2
1227
- },
1228
- section: {
1229
- borderRadius: 12,
1230
- gap: 8,
1231
- padding: 16
1365
+ function StaggeredWord({ word, color, fontFamily, fontSize, lineHeight, reducedMotion, startIndex = 0, fontWeight = "bold", letterSpacing, style, onLayout }) {
1366
+ return /* @__PURE__ */ jsx23(View17, { style: [styles19.row, style], onLayout, children: word.split("").map((char, i) => /* @__PURE__ */ jsx23(AnimatedLetter, { char, index: startIndex + i, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }, i)) });
1367
+ }
1368
+ var styles19 = StyleSheet20.create({
1369
+ row: {
1370
+ flexDirection: "row"
1232
1371
  }
1233
1372
  });
1234
1373
 
1235
1374
  // src/UpdateDialog.tsx
1236
1375
  import { useUpdater as useUpdater2 } from "@rific/updater";
1237
- import { jsx as jsx19 } from "react/jsx-runtime";
1376
+ import { jsx as jsx24 } from "react/jsx-runtime";
1238
1377
  function UpdateDialog({ rotation = 0, autoCheck = true, autoPrompt = true, onError }) {
1239
1378
  const { manifest, onConfirm, respond } = useUpdateConfirmBridge();
1240
1379
  useUpdater2({ autoCheck, autoPrompt, onConfirm, onError });
1241
1380
  const date = manifest ? new Date(manifest.createdAt) : null;
1242
- return /* @__PURE__ */ jsx19(ConfirmDialog, { visible: !!manifest, title: "Update Available", message: date ? `Released ${date.toLocaleDateString()} at ${date.toLocaleTimeString()}` : "", confirmLabel: "Restart", cancelLabel: "Later", icon: "update", rotation, onConfirm: () => respond(true), onCancel: () => respond(false) });
1381
+ return /* @__PURE__ */ jsx24(ConfirmDialog, { visible: !!manifest, title: "Update Available", message: date ? `Released ${date.toLocaleDateString()} at ${date.toLocaleTimeString()}` : "", confirmLabel: "Restart", cancelLabel: "Later", icon: "update", rotation, onConfirm: () => respond(true), onCancel: () => respond(false) });
1243
1382
  }
1244
1383
 
1245
- // src/usePopoverHost.ts
1246
- import { useCallback as useCallback3, useState as useState5 } from "react";
1247
- function usePopoverHost() {
1248
- const [openId, setOpenId] = useState5(null);
1249
- const toggle = useCallback3((id) => {
1250
- setOpenId((prev) => prev === id ? null : id);
1384
+ // src/useQuitConfirmation.ts
1385
+ import { useCallback as useCallback4, useEffect as useEffect3, useRef as useRef3, useState as useState6 } from "react";
1386
+ function useQuitConfirmation(hasProgress, onConfirmedBack) {
1387
+ const [confirmVisible, setConfirmVisible] = useState6(false);
1388
+ const hasProgressRef = useRef3(hasProgress);
1389
+ useEffect3(() => {
1390
+ hasProgressRef.current = hasProgress;
1391
+ });
1392
+ const onConfirmedBackRef = useRef3(onConfirmedBack);
1393
+ useEffect3(() => {
1394
+ onConfirmedBackRef.current = onConfirmedBack;
1395
+ });
1396
+ const requestBack = useCallback4(() => {
1397
+ if (hasProgressRef.current()) setConfirmVisible(true);
1398
+ else onConfirmedBackRef.current();
1251
1399
  }, []);
1252
- const close = useCallback3(() => setOpenId(null), []);
1253
- return { openId, toggle, close };
1400
+ const cancelBack = useCallback4(() => setConfirmVisible(false), []);
1401
+ return { confirmVisible, requestBack, cancelBack };
1402
+ }
1403
+
1404
+ // src/useZoneClampedAlign.ts
1405
+ import { useZoneBounds } from "@tastic/split-screen";
1406
+ import { useEffect as useEffect4, useState as useState7 } from "react";
1407
+ import { useWindowDimensions as useWindowDimensions3 } from "react-native";
1408
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
1409
+ var EDGE_MARGIN2 = 12;
1410
+ function useZoneClampedAlign(open, contentWidth, contentHeight, rotation = 0) {
1411
+ const auto = useAutoAlign(open, contentWidth, contentHeight, rotation);
1412
+ const zone = useZoneBounds();
1413
+ const { height: windowHeight } = useWindowDimensions3();
1414
+ const insets = useSafeAreaInsets();
1415
+ const shouldMeasure = open && auto.measured && zone !== null;
1416
+ const [ownBounds, setOwnBounds] = useState7(null);
1417
+ const [prevShouldMeasure, setPrevShouldMeasure] = useState7(shouldMeasure);
1418
+ if (shouldMeasure !== prevShouldMeasure) {
1419
+ setPrevShouldMeasure(shouldMeasure);
1420
+ setOwnBounds(null);
1421
+ }
1422
+ useEffect4(() => {
1423
+ if (!shouldMeasure) return;
1424
+ auto.triggerRef.current?.measureInWindow((_x, y, _width, height) => {
1425
+ const belowShared = zone.zoneSide === "belowShared";
1426
+ const awayRoom = belowShared ? windowHeight - insets.bottom - (y + height) - EDGE_MARGIN2 : y - insets.top - EDGE_MARGIN2;
1427
+ const towardRoom = belowShared ? y - zone.sharedEdgeY - EDGE_MARGIN2 : zone.sharedEdgeY - (y + height) - EDGE_MARGIN2;
1428
+ const useToward = contentHeight > awayRoom && towardRoom > awayRoom;
1429
+ let awaySide = belowShared ? "below" : "above";
1430
+ let towardSide = belowShared ? "above" : "below";
1431
+ if (zone.rotated) {
1432
+ awaySide = awaySide === "above" ? "below" : "above";
1433
+ towardSide = towardSide === "above" ? "below" : "above";
1434
+ }
1435
+ setOwnBounds({ maxHeight: Math.max(useToward ? towardRoom : awayRoom, 0), verticalAlign: useToward ? towardSide : awaySide });
1436
+ });
1437
+ }, [shouldMeasure, zone, auto.triggerRef, contentHeight, windowHeight, insets.top, insets.bottom]);
1438
+ if (!zone) return auto;
1439
+ if (!ownBounds) return { ...auto, measured: false };
1440
+ return { ...auto, maxHeight: ownBounds.maxHeight, measured: auto.measured, verticalAlign: ownBounds.verticalAlign };
1254
1441
  }
1255
1442
  export {
1443
+ AchievementCatalogSection,
1256
1444
  AchievementRow,
1445
+ AchievementUnlockList,
1446
+ ActivityStatSection,
1257
1447
  BaseSettingsDialog,
1258
1448
  BaseStatsScreen,
1259
1449
  ConfirmDialog,
@@ -1266,20 +1456,29 @@ export {
1266
1456
  LOCKED_BADGE_COLOR,
1267
1457
  LabeledDropdown,
1268
1458
  MONO_FONT,
1459
+ PlayerSetupPanel,
1269
1460
  PopoverBody,
1270
1461
  PressAwayOverlay,
1271
1462
  ReadyButton,
1272
1463
  SectionedDropdown,
1273
1464
  SharedActionBand,
1274
1465
  SkiaGate,
1466
+ StaggeredWord,
1275
1467
  StatRow,
1276
1468
  StatSection,
1277
1469
  TriggerGaugeHost,
1278
1470
  UpdateDialog,
1471
+ getColorPopoverId,
1472
+ getCpuDifficultyPopoverId,
1473
+ getInlineColorPickerContentSize,
1279
1474
  getLabeledDropdownContentHeight,
1280
1475
  getOverlayCardColors,
1476
+ getStaggeredWordDuration,
1281
1477
  loadSkiaWeb,
1478
+ overlayActionStyles,
1282
1479
  overlayStyles,
1283
1480
  useAutoAlign,
1284
- usePopoverHost
1481
+ usePopoverHost,
1482
+ useQuitConfirmation,
1483
+ useZoneClampedAlign
1285
1484
  };