@tastic/hud 0.14.2 → 0.15.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.
Files changed (53) hide show
  1. package/README.md +110 -79
  2. package/dist/chrome/index.d.mts +239 -0
  3. package/dist/chrome/index.d.ts +239 -0
  4. package/dist/chrome/index.js +772 -0
  5. package/dist/chrome/index.mjs +44 -0
  6. package/dist/chunk-OTMMST5M.mjs +645 -0
  7. package/dist/guide/index.js +3 -3
  8. package/dist/guide/index.mjs +3 -3
  9. package/dist/index.d.mts +21 -252
  10. package/dist/index.d.ts +21 -252
  11. package/dist/index.js +20 -20
  12. package/dist/index.mjs +186 -791
  13. package/package.json +8 -1
  14. package/src/AchievementCatalogSection.tsx +1 -1
  15. package/src/AchievementRow.tsx +9 -9
  16. package/src/BaseSettingsDialog.tsx +31 -31
  17. package/src/BaseStatsScreen.tsx +13 -13
  18. package/src/ConfirmDialog.tsx +2 -2
  19. package/src/ControlSchemePicker.tsx +9 -9
  20. package/src/CornerActionButtons.tsx +6 -6
  21. package/src/CornerStatusBadges.tsx +8 -8
  22. package/src/InlineColorPicker.tsx +30 -30
  23. package/src/LabeledDropdown.tsx +8 -8
  24. package/src/PlayerSetupPanel.tsx +25 -25
  25. package/src/PopoverBody.tsx +16 -16
  26. package/src/PressAwayOverlay.tsx +2 -2
  27. package/src/ReadyButton.tsx +3 -3
  28. package/src/SectionedDropdown.tsx +31 -31
  29. package/src/SharedActionBand.tsx +6 -6
  30. package/src/SkiaGate.tsx +5 -5
  31. package/src/SkiaGate.web.tsx +2 -2
  32. package/src/StaggeredWord.tsx +9 -9
  33. package/src/StatRow.tsx +3 -3
  34. package/src/StatSection.tsx +3 -3
  35. package/src/TriggerGauge.tsx +54 -54
  36. package/src/TriggerGaugeHost.tsx +2 -2
  37. package/src/chrome/index.ts +22 -0
  38. package/src/fonts.ts +1 -1
  39. package/src/guide/DirectionKeysHint.tsx +3 -3
  40. package/src/guide/GuideProvider.tsx +7 -7
  41. package/src/guide/HowToPlayDialog.tsx +7 -7
  42. package/src/guide/SeatDiagram.tsx +3 -3
  43. package/src/guide/SwipeHint.tsx +4 -4
  44. package/src/guide/guideSlice.ts +7 -7
  45. package/src/guide/index.ts +4 -3
  46. package/src/guide/types.ts +1 -1
  47. package/src/index.ts +1 -1
  48. package/src/loadSkiaWeb.ts +2 -2
  49. package/src/loadSkiaWeb.web.ts +2 -2
  50. package/src/overlayCard.ts +3 -3
  51. package/src/useAutoAlign.ts +23 -23
  52. package/src/usePopoverHost.ts +1 -1
  53. package/src/useZoneClampedAlign.ts +23 -23
@@ -0,0 +1,645 @@
1
+ import {
2
+ getOverlayCardColors,
3
+ overlayActionStyles,
4
+ overlayStyles
5
+ } from "./chunk-32U2OIRW.mjs";
6
+
7
+ // src/AchievementRow.tsx
8
+ import { useAutoPaperTheme } from "@rific/auto-paper";
9
+ import { StyleSheet, View } from "react-native";
10
+ import { Icon, ProgressBar, Text } from "react-native-paper";
11
+ import { jsx, jsxs } from "react/jsx-runtime";
12
+ var LOCKED_BADGE_COLOR = "rgba(128,128,128,0.3)";
13
+ function AchievementRow({ icon, title, description, badgeColor, unlockedLabel, checkColor, progress, deviceMarker, fg: fgOverride, fgMuted: fgMutedOverride, sectionBg: sectionBgOverride }) {
14
+ const { dark } = useAutoPaperTheme();
15
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
16
+ const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
17
+ const sectionBg = sectionBgOverride ?? (dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)");
18
+ const unlocked = unlockedLabel !== void 0;
19
+ return /* @__PURE__ */ jsxs(View, { style: [styles.row, { backgroundColor: sectionBg }], children: [
20
+ /* @__PURE__ */ jsx(View, { style: [styles.badge, { backgroundColor: badgeColor }], children: /* @__PURE__ */ jsx(Icon, { source: icon, size: 20, color: unlocked ? "#000000" : fgMuted }) }),
21
+ /* @__PURE__ */ jsxs(View, { style: styles.text, children: [
22
+ /* @__PURE__ */ jsx(Text, { variant: "bodyLarge", style: [styles.boldText, { color: fg }], children: title }),
23
+ /* @__PURE__ */ jsx(Text, { variant: "bodySmall", style: { color: fgMuted }, children: description }),
24
+ deviceMarker && /* @__PURE__ */ jsxs(View, { style: styles.deviceMarker, children: [
25
+ /* @__PURE__ */ jsx(Icon, { source: "earth", size: 11, color: fgMuted }),
26
+ /* @__PURE__ */ jsx(Text, { variant: "labelSmall", style: { color: fgMuted }, children: "Same for every profile" })
27
+ ] }),
28
+ !unlocked && progress !== void 0 && /* @__PURE__ */ jsx(ProgressBar, { progress, color: checkColor ?? badgeColor, style: styles.progressBar })
29
+ ] }),
30
+ unlocked ? /* @__PURE__ */ jsxs(View, { style: styles.status, children: [
31
+ /* @__PURE__ */ jsx(Icon, { source: "check-circle", size: 20, color: checkColor ?? badgeColor }),
32
+ /* @__PURE__ */ jsx(Text, { variant: "labelSmall", style: { color: fgMuted }, children: unlockedLabel })
33
+ ] }) : /* @__PURE__ */ jsx(Icon, { source: "lock-outline", size: 20, color: fgMuted })
34
+ ] });
35
+ }
36
+ var styles = StyleSheet.create({
37
+ badge: {
38
+ alignItems: "center",
39
+ borderRadius: 10,
40
+ height: 36,
41
+ justifyContent: "center",
42
+ width: 36
43
+ },
44
+ boldText: {
45
+ fontWeight: "bold"
46
+ },
47
+ deviceMarker: {
48
+ alignItems: "center",
49
+ flexDirection: "row",
50
+ gap: 4
51
+ },
52
+ progressBar: {
53
+ borderRadius: 4,
54
+ height: 6,
55
+ marginTop: 6
56
+ },
57
+ row: {
58
+ alignItems: "center",
59
+ borderRadius: 12,
60
+ flexDirection: "row",
61
+ gap: 12,
62
+ padding: 12
63
+ },
64
+ status: {
65
+ alignItems: "center",
66
+ gap: 2,
67
+ width: 64
68
+ },
69
+ text: {
70
+ flex: 1,
71
+ gap: 2
72
+ }
73
+ });
74
+
75
+ // src/fonts.ts
76
+ import { Platform } from "react-native";
77
+ var MONO_FONT = Platform.select({ ios: "Menlo", android: "monospace", default: "monospace" });
78
+
79
+ // src/AchievementCatalogSection.tsx
80
+ import { useAutoPaperTheme as useAutoPaperTheme2 } from "@rific/auto-paper";
81
+ import { Fragment } from "react";
82
+ import { StyleSheet as StyleSheet2 } from "react-native";
83
+ import { Text as Text2 } from "react-native-paper";
84
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
85
+ function AchievementCatalogSection({ rows, label = "ALL ACHIEVEMENTS", fg, fgMuted: fgMutedOverride }) {
86
+ const { dark } = useAutoPaperTheme2();
87
+ const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
88
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
89
+ /* @__PURE__ */ jsx2(Text2, { variant: "labelMedium", style: [styles2.label, { color: fgMuted, fontFamily: MONO_FONT }], children: label }),
90
+ 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))
91
+ ] });
92
+ }
93
+ var styles2 = StyleSheet2.create({
94
+ label: {
95
+ letterSpacing: 2,
96
+ marginTop: 8
97
+ }
98
+ });
99
+
100
+ // src/StatRow.tsx
101
+ import { useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
102
+ import { StyleSheet as StyleSheet3, View as View2 } from "react-native";
103
+ import { Text as Text3 } from "react-native-paper";
104
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
105
+ function StatRow({ label, value, fg: fgOverride, fgMuted: fgMutedOverride }) {
106
+ const { dark } = useAutoPaperTheme3();
107
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
108
+ const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
109
+ return /* @__PURE__ */ jsxs3(View2, { style: styles3.row, children: [
110
+ /* @__PURE__ */ jsx3(Text3, { variant: "bodyMedium", style: { color: fgMuted }, children: label }),
111
+ /* @__PURE__ */ jsx3(Text3, { variant: "bodyMedium", style: [styles3.value, { color: fg }], children: value })
112
+ ] });
113
+ }
114
+ var styles3 = StyleSheet3.create({
115
+ row: {
116
+ alignItems: "center",
117
+ flexDirection: "row",
118
+ justifyContent: "space-between"
119
+ },
120
+ value: {
121
+ fontWeight: "bold"
122
+ }
123
+ });
124
+
125
+ // src/StatSection.tsx
126
+ import { useAutoPaperTheme as useAutoPaperTheme4 } from "@rific/auto-paper";
127
+ import { StyleSheet as StyleSheet4, View as View3 } from "react-native";
128
+ import { Text as Text4 } from "react-native-paper";
129
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
130
+ function StatSection({ label, fg: fgOverride, sectionBg: sectionBgOverride, children }) {
131
+ const { dark } = useAutoPaperTheme4();
132
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
133
+ const sectionBg = sectionBgOverride ?? (dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)");
134
+ return /* @__PURE__ */ jsxs4(View3, { style: [styles4.section, { backgroundColor: sectionBg }], children: [
135
+ /* @__PURE__ */ jsx4(Text4, { variant: "labelMedium", style: [styles4.label, { color: fg }], children: label }),
136
+ children
137
+ ] });
138
+ }
139
+ var styles4 = StyleSheet4.create({
140
+ // Bold and full-contrast (fg), not the original fontless/fgMuted treatment - that read at the
141
+ // same visual weight as StatRow's own row labels below it (both used the identical muted color;
142
+ // letterSpacing/variant alone didn't separate them enough to read as "this is the group header"
143
+ // at a glance). Matches AchievementRow's own title styling (bold + fg) below it in the same list.
144
+ label: {
145
+ fontWeight: "bold",
146
+ letterSpacing: 2
147
+ },
148
+ section: {
149
+ borderRadius: 12,
150
+ gap: 8,
151
+ padding: 16
152
+ }
153
+ });
154
+
155
+ // src/ActivityStatSection.tsx
156
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
157
+ function ActivityStatSection({ stats }) {
158
+ return /* @__PURE__ */ jsxs5(StatSection, { label: "ACTIVITY", children: [
159
+ /* @__PURE__ */ jsx5(StatRow, { label: "Days Played", value: String(stats.distinctDaysPlayed) }),
160
+ /* @__PURE__ */ jsx5(StatRow, { label: "Day Streak", value: String(stats.currentDayStreak) }),
161
+ /* @__PURE__ */ jsx5(StatRow, { label: "Best Day Streak", value: String(stats.bestDayStreak) })
162
+ ] });
163
+ }
164
+
165
+ // src/ConfirmDialog.tsx
166
+ import { useAutoPaperTheme as useAutoPaperTheme5 } from "@rific/auto-paper";
167
+ import { Button } from "@rific/feedback-press";
168
+ import { toRotationStyle } from "@tastic/core";
169
+ import { StyleSheet as StyleSheet5, View as View4 } from "react-native";
170
+ import { Icon as Icon2, Portal, Text as Text5 } from "react-native-paper";
171
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
172
+ function ConfirmDialog({ visible, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, rotation = 0, icon = "help-circle-outline", destructive = false }) {
173
+ const { dark, colors } = useAutoPaperTheme5();
174
+ if (!visible) return null;
175
+ const { fg, cardBg, cardBorder } = getOverlayCardColors(dark);
176
+ const accent = destructive ? colors.danger : colors.primary;
177
+ const onAccent = destructive ? colors.onDanger : colors.onPrimary;
178
+ const showCancel = cancelLabel !== void 0 && onCancel !== void 0;
179
+ return /* @__PURE__ */ jsx6(Portal, { children: /* @__PURE__ */ jsx6(View4, { style: [overlayStyles.overlay, styles5.overlay], children: /* @__PURE__ */ jsxs6(View4, { style: [overlayStyles.card, styles5.card, { backgroundColor: cardBg, borderColor: cardBorder }, toRotationStyle(rotation)], children: [
180
+ /* @__PURE__ */ jsx6(Icon2, { source: icon, size: 64, color: accent }),
181
+ /* @__PURE__ */ jsx6(Text5, { variant: "headlineLarge", style: [styles5.title, { color: accent }], children: title }),
182
+ /* @__PURE__ */ jsx6(Text5, { variant: "bodyLarge", style: [styles5.body, { color: fg }], children: message }),
183
+ /* @__PURE__ */ jsxs6(View4, { style: overlayActionStyles.actions, children: [
184
+ showCancel && /* @__PURE__ */ jsx6(Button, { mode: "outlined", onPress: onCancel, style: overlayActionStyles.actionButton, children: cancelLabel }),
185
+ /* @__PURE__ */ jsx6(Button, { mode: "contained", onPress: onConfirm, style: showCancel ? overlayActionStyles.actionButton : overlayActionStyles.actionButtonSingle, buttonColor: accent, textColor: onAccent, children: confirmLabel })
186
+ ] })
187
+ ] }) }) });
188
+ }
189
+ var styles5 = StyleSheet5.create({
190
+ body: {
191
+ textAlign: "center"
192
+ },
193
+ // Additive on top of overlayCard's own shared `card` shape (borderRadius/borderWidth/gap/maxWidth/
194
+ // padding) - claims its full allowance instead of just maxWidth alone, which only caps how wide the
195
+ // card is allowed to get - without an explicit width, a flex-centered child sizes to its own content
196
+ // instead of stretching, so the card (and therefore actions below) can end up narrower than the
197
+ // screen actually has room for, needlessly forcing the action row to wrap sooner than it has to.
198
+ card: {
199
+ width: "100%"
200
+ },
201
+ // Additive on top of overlayCard's own shared `overlay` backdrop - gives the card room to breathe
202
+ // from the screen edges now that it reliably stretches to fill its full width allowance (see
203
+ // card's own width comment) - without this, width: '100%' would run the card edge-to-edge on a
204
+ // screen narrower than maxWidth's own 360.
205
+ overlay: {
206
+ paddingHorizontal: 24
207
+ },
208
+ // textAlign is load-bearing, not decorative, once `title` wraps to more than one line - centered
209
+ // within card's own alignItems: 'center' only centers the Text's block as a whole; each wrapped
210
+ // line inside that block still defaults to left-aligned without this.
211
+ title: {
212
+ fontWeight: "bold",
213
+ textAlign: "center"
214
+ }
215
+ });
216
+
217
+ // src/RotatedFrame.tsx
218
+ import { FakeLandscapeView, rotateDimensions, rotateInsets, useRotation } from "@tastic/core";
219
+ import { useMemo } from "react";
220
+ import { StyleSheet as StyleSheet6, useWindowDimensions, View as View5 } from "react-native";
221
+ import { Portal as Portal2 } from "react-native-paper";
222
+ import { SafeAreaInsetsContext, useSafeAreaInsets } from "react-native-safe-area-context";
223
+ import { jsx as jsx7 } from "react/jsx-runtime";
224
+ function RotatedFrame({ children, rotation: rotationOverride, rotatedSafeArea = false, portalHost = false, passThrough = false, maxContentWidth }) {
225
+ const ambientRotation = useRotation();
226
+ const rotation = rotationOverride ?? ambientRotation;
227
+ const window = useWindowDimensions();
228
+ const { width, height } = rotateDimensions(window.width, window.height, rotation);
229
+ const rawInsets = useSafeAreaInsets();
230
+ const insets = useMemo(() => rotatedSafeArea ? rotateInsets(rawInsets, rotation) : rawInsets, [rotatedSafeArea, rawInsets, rotation]);
231
+ const cappedWidth = maxContentWidth !== void 0 && width > height ? maxContentWidth : void 0;
232
+ const column = /* @__PURE__ */ jsx7(View5, { style: [styles6.column, cappedWidth !== void 0 && { maxWidth: cappedWidth }], pointerEvents: passThrough ? "box-none" : void 0, children });
233
+ return /* @__PURE__ */ jsx7(FakeLandscapeView, { rotation, passThrough, style: styles6.root, children: /* @__PURE__ */ jsx7(SafeAreaInsetsContext.Provider, { value: insets, children: portalHost ? /* @__PURE__ */ jsx7(Portal2.Host, { children: column }) : column }) });
234
+ }
235
+ var styles6 = StyleSheet6.create({
236
+ // A centred column: full width up to `maxWidth`, so a capped frame sits in the middle of a wider window.
237
+ column: {
238
+ alignSelf: "center",
239
+ flex: 1,
240
+ width: "100%"
241
+ },
242
+ root: {
243
+ flex: 1
244
+ }
245
+ });
246
+
247
+ // src/BaseSettingsDialog.tsx
248
+ import { AutoAppearancePicker, Dialog, useAutoPaperTheme as useAutoPaperTheme6 } from "@rific/auto-paper";
249
+ import { Button as Button2, SoundContext, TouchableRipple, useHapticSettings, useSoundSettings, useVibration } from "@rific/feedback-press";
250
+ import { useUpdater } from "@rific/updater";
251
+ import { useIsTouchPrimaryDevice, useRotation as useRotation2 } from "@tastic/core";
252
+ import { useContext, useState as useState2 } from "react";
253
+ import { Platform as Platform2, ScrollView, StyleSheet as StyleSheet7, View as View6 } from "react-native";
254
+ import { Icon as Icon3, Portal as Portal3, Text as Text6 } from "react-native-paper";
255
+
256
+ // src/useUpdateConfirmBridge.ts
257
+ import { useCallback, useRef, useState } from "react";
258
+ function useUpdateConfirmBridge() {
259
+ const [manifest, setManifest] = useState(null);
260
+ const resolveRef = useRef(null);
261
+ const onConfirm = useCallback(
262
+ (next) => new Promise((resolve) => {
263
+ resolveRef.current = resolve;
264
+ setManifest(next);
265
+ }),
266
+ []
267
+ );
268
+ const respond = useCallback((confirmed) => {
269
+ setManifest(null);
270
+ resolveRef.current?.(confirmed);
271
+ resolveRef.current = null;
272
+ }, []);
273
+ return { manifest, onConfirm, respond };
274
+ }
275
+
276
+ // src/BaseSettingsDialog.tsx
277
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
278
+ function SettingIcon({ source, color, containerColor }) {
279
+ return /* @__PURE__ */ jsx8(View6, { style: [styles7.iconBadge, { backgroundColor: containerColor }], children: /* @__PURE__ */ jsx8(Icon3, { source, size: 18, color }) });
280
+ }
281
+ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }) {
282
+ const { colors } = useAutoPaperTheme6();
283
+ const ambientRotation = useRotation2();
284
+ const rotation = rotationOverride ?? ambientRotation;
285
+ const isTouchPrimary = useIsTouchPrimaryDevice();
286
+ const { settings: hapticSettings, set: setHapticSettings } = useHapticSettings();
287
+ const { settings: soundSettings, set: setSoundSettings } = useSoundSettings();
288
+ const sound = useContext(SoundContext);
289
+ const { forceShort: forceHapticFeedback } = useVibration();
290
+ const [infoMessage, setInfoMessage] = useState2(null);
291
+ const { manifest: updateManifest, onConfirm: onUpdateConfirm, respond: respondToUpdateConfirm } = useUpdateConfirmBridge();
292
+ const { check, checking, updateReady } = useUpdater({
293
+ autoCheck: false,
294
+ autoPrompt: false,
295
+ onConfirm: onUpdateConfirm,
296
+ onError: onUpdateError,
297
+ onInfo: (title, message) => setInfoMessage({ title, message })
298
+ });
299
+ const updateManifestDate = updateManifest ? new Date(updateManifest.createdAt) : null;
300
+ const [opened, setOpened] = useState2(visible);
301
+ if (visible && !opened) setOpened(true);
302
+ const showLockOrientation = isTouchPrimary && lockOrientation !== void 0 && onLockOrientationChange !== void 0;
303
+ const showEdgeGuard = Platform2.OS === "ios" && deferBottomEdgeGestures !== void 0 && onDeferBottomEdgeGestures !== void 0;
304
+ const showHaptics = Platform2.OS !== "web" && !hideHaptics;
305
+ const showSound = !hideSound;
306
+ const showAppearance = !hideAppearance;
307
+ const showUpdateCheck = Platform2.OS !== "web" && !hideUpdateCheck;
308
+ if (!opened) return null;
309
+ return /* @__PURE__ */ jsx8(Portal3, { children: /* @__PURE__ */ jsxs7(RotatedFrame, { rotation, rotatedSafeArea: true, portalHost: true, passThrough: true, children: [
310
+ /* @__PURE__ */ jsxs7(Dialog, { visible, onDismiss, style: styles7.dialog, children: [
311
+ /* @__PURE__ */ jsx8(Dialog.Title, { children: "Settings" }),
312
+ /* @__PURE__ */ jsx8(Dialog.ScrollArea, { style: styles7.scrollArea, children: /* @__PURE__ */ jsxs7(ScrollView, { contentContainerStyle: styles7.content, showsVerticalScrollIndicator: false, children: [
313
+ /* @__PURE__ */ jsxs7(View6, { style: styles7.toggleGroup, children: [
314
+ showLockOrientation && /* @__PURE__ */ jsx8(TouchableRipple, { onPress: () => onLockOrientationChange(!lockOrientation), style: styles7.toggleButton, accessibilityLabel: `Lock orientation ${lockOrientation ? "on" : "off"}`, children: /* @__PURE__ */ jsxs7(View6, { style: styles7.toggleContent, children: [
315
+ /* @__PURE__ */ jsx8(SettingIcon, { source: lockOrientation ? "lock" : "lock-open-variant-outline", color: lockOrientation ? colors.tertiary : colors.onSurfaceVariant, containerColor: lockOrientation ? colors.tertiaryContainer : colors.surfaceVariant }),
316
+ /* @__PURE__ */ jsxs7(View6, { style: styles7.flexShrink, children: [
317
+ /* @__PURE__ */ jsx8(Text6, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Lock Orientation" }),
318
+ /* @__PURE__ */ jsx8(Text6, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Pins the current layout" })
319
+ ] })
320
+ ] }) }),
321
+ showEdgeGuard && /* @__PURE__ */ jsx8(TouchableRipple, { onPress: () => onDeferBottomEdgeGestures(!deferBottomEdgeGestures), style: styles7.toggleButton, accessibilityLabel: `Edge guard ${deferBottomEdgeGestures ? "on" : "off"}`, children: /* @__PURE__ */ jsxs7(View6, { style: styles7.toggleContent, children: [
322
+ /* @__PURE__ */ jsx8(SettingIcon, { source: deferBottomEdgeGestures ? "shield-check-outline" : "shield-off-outline", color: deferBottomEdgeGestures ? colors.tertiary : colors.onSurfaceVariant, containerColor: deferBottomEdgeGestures ? colors.tertiaryContainer : colors.surfaceVariant }),
323
+ /* @__PURE__ */ jsxs7(View6, { style: styles7.flexShrink, children: [
324
+ /* @__PURE__ */ jsx8(Text6, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Edge Guard" }),
325
+ /* @__PURE__ */ jsx8(Text6, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Requires a second swipe" })
326
+ ] })
327
+ ] }) }),
328
+ (showSound || showHaptics) && /* @__PURE__ */ jsxs7(View6, { style: styles7.toggleRow, children: [
329
+ showSound && /* @__PURE__ */ jsx8(
330
+ TouchableRipple,
331
+ {
332
+ soundDisabled: true,
333
+ onPress: () => {
334
+ const enabled = !soundSettings.enabled;
335
+ setSoundSettings({ enabled });
336
+ if (enabled) sound.selection?.();
337
+ },
338
+ style: styles7.toggleButtonInRow,
339
+ accessibilityLabel: `Sound ${soundSettings.enabled ? "on" : "off"}`,
340
+ children: /* @__PURE__ */ jsxs7(View6, { style: styles7.toggleContent, children: [
341
+ /* @__PURE__ */ jsx8(SettingIcon, { source: soundSettings.enabled ? "volume-high" : "volume-off", color: soundSettings.enabled ? colors.tertiary : colors.onSurfaceVariant, containerColor: soundSettings.enabled ? colors.tertiaryContainer : colors.surfaceVariant }),
342
+ /* @__PURE__ */ jsx8(Text6, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Sound" })
343
+ ] })
344
+ }
345
+ ),
346
+ showHaptics && /* @__PURE__ */ jsx8(
347
+ TouchableRipple,
348
+ {
349
+ hapticDisabled: true,
350
+ onPress: () => {
351
+ const vibrate = !hapticSettings.vibrate;
352
+ setHapticSettings({ vibrate });
353
+ if (vibrate) forceHapticFeedback();
354
+ },
355
+ style: styles7.toggleButtonInRow,
356
+ accessibilityLabel: `Haptics ${hapticSettings.vibrate ? "on" : "off"}`,
357
+ children: /* @__PURE__ */ jsxs7(View6, { style: styles7.toggleContent, children: [
358
+ /* @__PURE__ */ jsx8(SettingIcon, { source: hapticSettings.vibrate ? "vibrate" : "vibrate-off", color: hapticSettings.vibrate ? colors.tertiary : colors.onSurfaceVariant, containerColor: hapticSettings.vibrate ? colors.tertiaryContainer : colors.surfaceVariant }),
359
+ /* @__PURE__ */ jsx8(Text6, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Haptics" })
360
+ ] })
361
+ }
362
+ )
363
+ ] }),
364
+ onShowHowToPlay && /* @__PURE__ */ jsx8(
365
+ TouchableRipple,
366
+ {
367
+ onPress: () => {
368
+ onDismiss();
369
+ onShowHowToPlay();
370
+ },
371
+ style: styles7.toggleButton,
372
+ accessibilityLabel: "How to Play",
373
+ children: /* @__PURE__ */ jsxs7(View6, { style: styles7.toggleContent, children: [
374
+ /* @__PURE__ */ jsx8(SettingIcon, { source: "help-circle-outline", color: colors.secondary, containerColor: colors.secondaryContainer }),
375
+ /* @__PURE__ */ jsxs7(View6, { style: styles7.flexShrink, children: [
376
+ /* @__PURE__ */ jsx8(Text6, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "How to Play" }),
377
+ /* @__PURE__ */ jsx8(Text6, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Replay the quick intro" })
378
+ ] })
379
+ ] })
380
+ }
381
+ )
382
+ ] }),
383
+ showAppearance && /* @__PURE__ */ jsxs7(View6, { style: styles7.section, children: [
384
+ /* @__PURE__ */ jsx8(Text6, { variant: "labelMedium", style: [styles7.sectionLabel, { color: colors.onSurfaceVariant }], children: "APPEARANCE" }),
385
+ /* @__PURE__ */ jsx8(AutoAppearancePicker, { showLabels: false })
386
+ ] }),
387
+ children,
388
+ showUpdateCheck && /* @__PURE__ */ jsxs7(View6, { style: styles7.section, children: [
389
+ /* @__PURE__ */ jsxs7(Text6, { variant: "labelSmall", style: [styles7.sectionLabel, { color: colors.onSurfaceVariant }], children: [
390
+ "VERSION ",
391
+ version,
392
+ updateReady ? " \xB7 UPDATE READY" : ""
393
+ ] }),
394
+ /* @__PURE__ */ jsx8(Button2, { mode: "outlined", onPress: check, loading: checking, disabled: checking, children: "Check for Updates" })
395
+ ] })
396
+ ] }) })
397
+ ] }),
398
+ /* @__PURE__ */ jsx8(ConfirmDialog, { visible: !!updateManifest, title: "Update Available", message: updateManifestDate ? `Released ${updateManifestDate.toLocaleDateString()} at ${updateManifestDate.toLocaleTimeString()}` : "", confirmLabel: "Restart", cancelLabel: "Later", icon: "update", rotation: 0, onConfirm: () => respondToUpdateConfirm(true), onCancel: () => respondToUpdateConfirm(false) }),
399
+ /* @__PURE__ */ jsx8(ConfirmDialog, { visible: !!infoMessage, title: infoMessage?.title ?? "", message: infoMessage?.message ?? "", confirmLabel: "OK", icon: "information-outline", rotation: 0, onConfirm: () => setInfoMessage(null) })
400
+ ] }) });
401
+ }
402
+ var styles7 = StyleSheet7.create({
403
+ content: {
404
+ gap: 24,
405
+ paddingVertical: 20
406
+ },
407
+ // Caps the card so it never grows past the screen - without this the dialog just keeps
408
+ // growing to fit its content and the overflow gets clipped by the screen edge, which is
409
+ // what happened in landscape where there's less height to work with. Dialog.ScrollArea +
410
+ // ScrollView below then take over and let the content scroll within that bound.
411
+ dialog: {
412
+ maxHeight: "90%"
413
+ },
414
+ flexShrink: {
415
+ flexShrink: 1
416
+ },
417
+ iconBadge: {
418
+ alignItems: "center",
419
+ borderRadius: 10,
420
+ height: 36,
421
+ justifyContent: "center",
422
+ width: 36
423
+ },
424
+ scrollArea: {
425
+ marginBottom: 0
426
+ },
427
+ section: {
428
+ gap: 12
429
+ },
430
+ sectionLabel: {
431
+ letterSpacing: 2
432
+ },
433
+ // Used to carry marginHorizontal: -12 + paddingHorizontal: 12 (netting to the same inset as
434
+ // APPEARANCE/SOUND & HAPTICS below, while giving the ripple/hover highlight room to breathe
435
+ // into the dialog's own gutter beyond that). Removed: this dialog scrolls (Dialog.ScrollArea +
436
+ // ScrollView above), and on web a ScrollView's own content wrapper clips to its own bounds
437
+ // regardless of a child's negative margin - confirmed live, the icon's own rounded-square badge
438
+ // was getting its left edge sliced off by exactly that overhang. No horizontal inset of its own
439
+ // now - it relies purely on the same dialog padding APPEARANCE/every other row already does,
440
+ // which can never overflow because it's never negative.
441
+ toggleButton: {
442
+ borderRadius: 12,
443
+ overflow: "hidden",
444
+ paddingVertical: 10
445
+ },
446
+ // Same shape as toggleButton, `flex: 1` instead of its own width - two of these side by side in
447
+ // toggleRow share the row's own edge alignment instead of each carrying their own.
448
+ toggleButtonInRow: {
449
+ borderRadius: 12,
450
+ flex: 1,
451
+ overflow: "hidden",
452
+ paddingVertical: 10
453
+ },
454
+ toggleContent: {
455
+ alignItems: "center",
456
+ flexDirection: "row",
457
+ gap: 12
458
+ },
459
+ toggleGroup: {
460
+ gap: 4
461
+ },
462
+ // No horizontal margin of its own now either - see toggleButton's identical note.
463
+ toggleRow: {
464
+ flexDirection: "row",
465
+ gap: 4
466
+ }
467
+ });
468
+
469
+ // src/BaseStatsScreen.tsx
470
+ import { useAutoPaperTheme as useAutoPaperTheme7 } from "@rific/auto-paper";
471
+ import { Button as Button3, IconButton } from "@rific/feedback-press";
472
+ import { useRotation as useRotation3 } from "@tastic/core";
473
+ import { useState as useState3 } from "react";
474
+ import { ScrollView as ScrollView2, StyleSheet as StyleSheet8, View as View7 } from "react-native";
475
+ import { Text as Text7 } from "react-native-paper";
476
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
477
+ 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 }) {
478
+ const { dark, colors } = useAutoPaperTheme7();
479
+ const ambientRotation = useRotation3();
480
+ const rotation = rotationOverride ?? ambientRotation;
481
+ const [confirmResetVisible, setConfirmResetVisible] = useState3(false);
482
+ const showReset = !!onReset;
483
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
484
+ const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
485
+ return /* @__PURE__ */ jsxs8(View7, { style: [styles8.container, { backgroundColor: bg }], children: [
486
+ /* @__PURE__ */ jsxs8(View7, { style: [styles8.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
487
+ /* @__PURE__ */ jsx9(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: onBack }),
488
+ /* @__PURE__ */ jsx9(Text7, { variant: titleVariant, style: [styles8.title, { color: fg }], children: title })
489
+ ] }),
490
+ /* @__PURE__ */ jsxs8(ScrollView2, { style: styles8.scrollView, contentContainerStyle: [styles8.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, children: [
491
+ children,
492
+ showReset && /* @__PURE__ */ jsx9(Button3, { mode: "contained", onPress: () => setConfirmResetVisible(true), buttonColor: colors.danger, textColor: colors.onDanger, style: styles8.resetButton, children: resetLabel })
493
+ ] }),
494
+ /* @__PURE__ */ jsx9(
495
+ ConfirmDialog,
496
+ {
497
+ visible: confirmResetVisible,
498
+ title: resetConfirmTitle,
499
+ message: resetConfirmBody,
500
+ confirmLabel: "Reset",
501
+ cancelLabel: "Cancel",
502
+ icon: "alert-outline",
503
+ destructive: true,
504
+ rotation,
505
+ onCancel: () => setConfirmResetVisible(false),
506
+ onConfirm: () => {
507
+ onReset?.();
508
+ setConfirmResetVisible(false);
509
+ }
510
+ }
511
+ )
512
+ ] });
513
+ }
514
+ var styles8 = StyleSheet8.create({
515
+ container: {
516
+ flex: 1
517
+ },
518
+ content: {
519
+ gap: 12,
520
+ paddingHorizontal: 20
521
+ },
522
+ header: {
523
+ alignItems: "center",
524
+ flexDirection: "row",
525
+ gap: 4,
526
+ paddingBottom: 8
527
+ },
528
+ resetButton: {
529
+ marginTop: 16
530
+ },
531
+ // Bounds the ScrollView to the space `container`'s flex:1 actually gives it - without this, a
532
+ // ScrollView with only a contentContainerStyle isn't reliably height-constrained on native (it
533
+ // can render at its full unclipped content height instead of the screen's), which leaves the
534
+ // header's back button touch target unreliable underneath it.
535
+ scrollView: {
536
+ flex: 1
537
+ },
538
+ title: {
539
+ flexShrink: 1,
540
+ fontWeight: "bold"
541
+ }
542
+ });
543
+
544
+ // src/ContentGutter.tsx
545
+ import { computeContentBounds, useRotatedWindowDimensions } from "@tastic/core";
546
+ import { StyleSheet as StyleSheet9, View as View8 } from "react-native";
547
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
548
+ function ContentGutter({ maxContentWidth, leftGutter, rightGutter, style, children }) {
549
+ const { width: windowWidth } = useRotatedWindowDimensions();
550
+ const { contentWidth, gutterWidth } = computeContentBounds(windowWidth, maxContentWidth ?? Infinity);
551
+ return /* @__PURE__ */ jsxs9(View8, { style: [styles9.row, style], children: [
552
+ /* @__PURE__ */ jsx10(View8, { style: [styles9.gutter, { width: gutterWidth }], children: leftGutter }),
553
+ /* @__PURE__ */ jsx10(View8, { style: { width: contentWidth }, children }),
554
+ /* @__PURE__ */ jsx10(View8, { style: [styles9.gutter, { width: gutterWidth }], children: rightGutter })
555
+ ] });
556
+ }
557
+ var styles9 = StyleSheet9.create({
558
+ // overflow:'hidden' keeps an oversized leftGutter/rightGutter from bleeding into the content
559
+ // column instead of clipping cleanly at its own real width.
560
+ gutter: {
561
+ overflow: "hidden"
562
+ },
563
+ row: {
564
+ flexDirection: "row"
565
+ }
566
+ });
567
+
568
+ // src/CornerActionButtons.tsx
569
+ import { IconButton as IconButton2 } from "@rific/feedback-press";
570
+ import { StyleSheet as StyleSheet10 } from "react-native";
571
+ import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
572
+ function CornerActionButtons({ onBack, onSettings, fg, insets, backIcon = "arrow-left", backLabel = "Back" }) {
573
+ return /* @__PURE__ */ jsxs10(Fragment2, { children: [
574
+ /* @__PURE__ */ jsx11(IconButton2, { icon: backIcon, iconColor: fg, size: 24, style: [styles10.back, { top: 8 + insets.top, left: 8 + insets.left }], onPress: onBack, accessibilityLabel: backLabel }),
575
+ /* @__PURE__ */ jsx11(IconButton2, { icon: "cog", iconColor: fg, size: 24, style: [styles10.topRight, { top: 8 + insets.top, right: 8 + insets.right }], onPress: onSettings, accessibilityLabel: "Settings" })
576
+ ] });
577
+ }
578
+ var styles10 = StyleSheet10.create({
579
+ back: {
580
+ position: "absolute"
581
+ },
582
+ topRight: {
583
+ position: "absolute"
584
+ }
585
+ });
586
+
587
+ // src/StaggeredWord.tsx
588
+ import { useEffect } from "react";
589
+ import { StyleSheet as StyleSheet11, View as View9 } from "react-native";
590
+ import Animated, { Easing, useAnimatedStyle, useSharedValue, withDelay, withTiming } from "react-native-reanimated";
591
+ import { jsx as jsx12 } from "react/jsx-runtime";
592
+ var STAGGER_MS = 45;
593
+ var LETTER_DURATION_MS = 320;
594
+ function getStaggeredWordDuration(letterCount) {
595
+ return (letterCount - 1) * STAGGER_MS + LETTER_DURATION_MS;
596
+ }
597
+ function AnimatedLetter({ char, index, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }) {
598
+ const progress = useSharedValue(reducedMotion ? 1 : 0);
599
+ useEffect(() => {
600
+ if (reducedMotion) return;
601
+ progress.value = withDelay(index * STAGGER_MS, withTiming(1, { duration: LETTER_DURATION_MS, easing: Easing.out(Easing.back(1.5)) }));
602
+ }, [reducedMotion]);
603
+ const style = useAnimatedStyle(() => ({
604
+ opacity: progress.value,
605
+ transform: [{ translateY: (1 - progress.value) * 12 }, { scale: 0.85 + progress.value * 0.15 }]
606
+ }));
607
+ return /* @__PURE__ */ jsx12(Animated.Text, { allowFontScaling: false, style: [{ color, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }, style], children: char });
608
+ }
609
+ function StaggeredWord({ word, color, fontFamily, fontSize, lineHeight, reducedMotion, startIndex = 0, fontWeight = "bold", letterSpacing, style, onLayout }) {
610
+ return /* @__PURE__ */ jsx12(View9, { style: [styles11.row, style], onLayout, children: word.split("").map((char, i) => /* @__PURE__ */ jsx12(AnimatedLetter, { char, index: startIndex + i, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }, i)) });
611
+ }
612
+ var styles11 = StyleSheet11.create({
613
+ row: {
614
+ flexDirection: "row"
615
+ }
616
+ });
617
+
618
+ // src/UpdateDialog.tsx
619
+ import { useUpdater as useUpdater2 } from "@rific/updater";
620
+ import { jsx as jsx13 } from "react/jsx-runtime";
621
+ function UpdateDialog({ rotation = 0, autoCheck = true, autoPrompt = true, onError }) {
622
+ const { manifest, onConfirm, respond } = useUpdateConfirmBridge();
623
+ useUpdater2({ autoCheck, autoPrompt, onConfirm, onError });
624
+ const date = manifest ? new Date(manifest.createdAt) : null;
625
+ return /* @__PURE__ */ jsx13(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) });
626
+ }
627
+
628
+ export {
629
+ LOCKED_BADGE_COLOR,
630
+ AchievementRow,
631
+ MONO_FONT,
632
+ AchievementCatalogSection,
633
+ StatRow,
634
+ StatSection,
635
+ ActivityStatSection,
636
+ ConfirmDialog,
637
+ RotatedFrame,
638
+ BaseSettingsDialog,
639
+ BaseStatsScreen,
640
+ ContentGutter,
641
+ CornerActionButtons,
642
+ getStaggeredWordDuration,
643
+ StaggeredWord,
644
+ UpdateDialog
645
+ };
@@ -270,7 +270,7 @@ var styles2 = import_react_native3.StyleSheet.create({
270
270
  body: {
271
271
  textAlign: "center"
272
272
  },
273
- // Additive on top of overlayCard's own shared `card` shape — its default 32px padding is tighter
273
+ // Additive on top of overlayCard's own shared `card` shape - its default 32px padding is tighter
274
274
  // here (a paged card wants more width for its art) and the shared 16px gap is right.
275
275
  card: {
276
276
  padding: CARD_PADDING
@@ -448,7 +448,7 @@ var styles4 = import_react_native5.StyleSheet.create({
448
448
  label: {
449
449
  fontWeight: "bold"
450
450
  },
451
- // A wash of the seat color behind its own label rather than an rgba() fill — the theme's colors
451
+ // A wash of the seat color behind its own label rather than an rgba() fill - the theme's colors
452
452
  // aren't guaranteed to be 6-digit hex, so an opacity layer is the only alpha that always works.
453
453
  tint: {
454
454
  opacity: 0.2
@@ -508,7 +508,7 @@ var styles5 = import_react_native6.StyleSheet.create({
508
508
  justifyContent: "space-between"
509
509
  },
510
510
  // Centered on the whole path (not on the dots row alone) by the container's own alignItems/
511
- // justifyContent, then pushed along the axis by its own animated translate — absolute so it never
511
+ // justifyContent, then pushed along the axis by its own animated translate - absolute so it never
512
512
  // takes space from the dots and arrowhead it's drawn over.
513
513
  finger: {
514
514
  borderRadius: FINGER_SIZE / 2,