@tastic/hud 0.14.1 → 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 +22 -22
  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 +36 -34
  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,772 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/chrome/index.ts
31
+ var chrome_exports = {};
32
+ __export(chrome_exports, {
33
+ AchievementCatalogSection: () => AchievementCatalogSection,
34
+ AchievementRow: () => AchievementRow,
35
+ ActivityStatSection: () => ActivityStatSection,
36
+ BaseSettingsDialog: () => BaseSettingsDialog,
37
+ BaseStatsScreen: () => BaseStatsScreen,
38
+ ConfirmDialog: () => ConfirmDialog,
39
+ ContentGutter: () => ContentGutter,
40
+ CornerActionButtons: () => CornerActionButtons,
41
+ LOCKED_BADGE_COLOR: () => LOCKED_BADGE_COLOR,
42
+ MONO_FONT: () => MONO_FONT,
43
+ RotatedFrame: () => RotatedFrame,
44
+ StaggeredWord: () => StaggeredWord,
45
+ StatRow: () => StatRow,
46
+ StatSection: () => StatSection,
47
+ UpdateDialog: () => UpdateDialog,
48
+ getOverlayCardColors: () => getOverlayCardColors,
49
+ getStaggeredWordDuration: () => getStaggeredWordDuration,
50
+ overlayActionStyles: () => overlayActionStyles,
51
+ overlayStyles: () => overlayStyles
52
+ });
53
+ module.exports = __toCommonJS(chrome_exports);
54
+
55
+ // src/AchievementCatalogSection.tsx
56
+ var import_auto_paper2 = require("@rific/auto-paper");
57
+ var import_react = require("react");
58
+ var import_react_native3 = require("react-native");
59
+ var import_react_native_paper2 = require("react-native-paper");
60
+
61
+ // src/AchievementRow.tsx
62
+ var import_auto_paper = require("@rific/auto-paper");
63
+ var import_react_native = require("react-native");
64
+ var import_react_native_paper = require("react-native-paper");
65
+ var import_jsx_runtime = require("react/jsx-runtime");
66
+ var LOCKED_BADGE_COLOR = "rgba(128,128,128,0.3)";
67
+ function AchievementRow({ icon, title, description, badgeColor, unlockedLabel, checkColor, progress, deviceMarker, fg: fgOverride, fgMuted: fgMutedOverride, sectionBg: sectionBgOverride }) {
68
+ const { dark } = (0, import_auto_paper.useAutoPaperTheme)();
69
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
70
+ const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
71
+ const sectionBg = sectionBgOverride ?? (dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)");
72
+ const unlocked = unlockedLabel !== void 0;
73
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native.View, { style: [styles.row, { backgroundColor: sectionBg }], children: [
74
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: [styles.badge, { backgroundColor: badgeColor }], children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_paper.Icon, { source: icon, size: 20, color: unlocked ? "#000000" : fgMuted }) }),
75
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native.View, { style: styles.text, children: [
76
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_paper.Text, { variant: "bodyLarge", style: [styles.boldText, { color: fg }], children: title }),
77
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_paper.Text, { variant: "bodySmall", style: { color: fgMuted }, children: description }),
78
+ deviceMarker && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native.View, { style: styles.deviceMarker, children: [
79
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_paper.Icon, { source: "earth", size: 11, color: fgMuted }),
80
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_paper.Text, { variant: "labelSmall", style: { color: fgMuted }, children: "Same for every profile" })
81
+ ] }),
82
+ !unlocked && progress !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_paper.ProgressBar, { progress, color: checkColor ?? badgeColor, style: styles.progressBar })
83
+ ] }),
84
+ unlocked ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native.View, { style: styles.status, children: [
85
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_paper.Icon, { source: "check-circle", size: 20, color: checkColor ?? badgeColor }),
86
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_paper.Text, { variant: "labelSmall", style: { color: fgMuted }, children: unlockedLabel })
87
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_paper.Icon, { source: "lock-outline", size: 20, color: fgMuted })
88
+ ] });
89
+ }
90
+ var styles = import_react_native.StyleSheet.create({
91
+ badge: {
92
+ alignItems: "center",
93
+ borderRadius: 10,
94
+ height: 36,
95
+ justifyContent: "center",
96
+ width: 36
97
+ },
98
+ boldText: {
99
+ fontWeight: "bold"
100
+ },
101
+ deviceMarker: {
102
+ alignItems: "center",
103
+ flexDirection: "row",
104
+ gap: 4
105
+ },
106
+ progressBar: {
107
+ borderRadius: 4,
108
+ height: 6,
109
+ marginTop: 6
110
+ },
111
+ row: {
112
+ alignItems: "center",
113
+ borderRadius: 12,
114
+ flexDirection: "row",
115
+ gap: 12,
116
+ padding: 12
117
+ },
118
+ status: {
119
+ alignItems: "center",
120
+ gap: 2,
121
+ width: 64
122
+ },
123
+ text: {
124
+ flex: 1,
125
+ gap: 2
126
+ }
127
+ });
128
+
129
+ // src/fonts.ts
130
+ var import_react_native2 = require("react-native");
131
+ var MONO_FONT = import_react_native2.Platform.select({ ios: "Menlo", android: "monospace", default: "monospace" });
132
+
133
+ // src/AchievementCatalogSection.tsx
134
+ var import_jsx_runtime2 = require("react/jsx-runtime");
135
+ function AchievementCatalogSection({ rows, label = "ALL ACHIEVEMENTS", fg, fgMuted: fgMutedOverride }) {
136
+ const { dark } = (0, import_auto_paper2.useAutoPaperTheme)();
137
+ const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
138
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react.Fragment, { children: [
139
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper2.Text, { variant: "labelMedium", style: [styles2.label, { color: fgMuted, fontFamily: MONO_FONT }], children: label }),
140
+ rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(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))
141
+ ] });
142
+ }
143
+ var styles2 = import_react_native3.StyleSheet.create({
144
+ label: {
145
+ letterSpacing: 2,
146
+ marginTop: 8
147
+ }
148
+ });
149
+
150
+ // src/StatRow.tsx
151
+ var import_auto_paper3 = require("@rific/auto-paper");
152
+ var import_react_native4 = require("react-native");
153
+ var import_react_native_paper3 = require("react-native-paper");
154
+ var import_jsx_runtime3 = require("react/jsx-runtime");
155
+ function StatRow({ label, value, fg: fgOverride, fgMuted: fgMutedOverride }) {
156
+ const { dark } = (0, import_auto_paper3.useAutoPaperTheme)();
157
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
158
+ const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
159
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native4.View, { style: styles3.row, children: [
160
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "bodyMedium", style: { color: fgMuted }, children: label }),
161
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "bodyMedium", style: [styles3.value, { color: fg }], children: value })
162
+ ] });
163
+ }
164
+ var styles3 = import_react_native4.StyleSheet.create({
165
+ row: {
166
+ alignItems: "center",
167
+ flexDirection: "row",
168
+ justifyContent: "space-between"
169
+ },
170
+ value: {
171
+ fontWeight: "bold"
172
+ }
173
+ });
174
+
175
+ // src/StatSection.tsx
176
+ var import_auto_paper4 = require("@rific/auto-paper");
177
+ var import_react_native5 = require("react-native");
178
+ var import_react_native_paper4 = require("react-native-paper");
179
+ var import_jsx_runtime4 = require("react/jsx-runtime");
180
+ function StatSection({ label, fg: fgOverride, sectionBg: sectionBgOverride, children }) {
181
+ const { dark } = (0, import_auto_paper4.useAutoPaperTheme)();
182
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
183
+ const sectionBg = sectionBgOverride ?? (dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)");
184
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native5.View, { style: [styles4.section, { backgroundColor: sectionBg }], children: [
185
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper4.Text, { variant: "labelMedium", style: [styles4.label, { color: fg }], children: label }),
186
+ children
187
+ ] });
188
+ }
189
+ var styles4 = import_react_native5.StyleSheet.create({
190
+ // Bold and full-contrast (fg), not the original fontless/fgMuted treatment - that read at the
191
+ // same visual weight as StatRow's own row labels below it (both used the identical muted color;
192
+ // letterSpacing/variant alone didn't separate them enough to read as "this is the group header"
193
+ // at a glance). Matches AchievementRow's own title styling (bold + fg) below it in the same list.
194
+ label: {
195
+ fontWeight: "bold",
196
+ letterSpacing: 2
197
+ },
198
+ section: {
199
+ borderRadius: 12,
200
+ gap: 8,
201
+ padding: 16
202
+ }
203
+ });
204
+
205
+ // src/ActivityStatSection.tsx
206
+ var import_jsx_runtime5 = require("react/jsx-runtime");
207
+ function ActivityStatSection({ stats }) {
208
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(StatSection, { label: "ACTIVITY", children: [
209
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(StatRow, { label: "Days Played", value: String(stats.distinctDaysPlayed) }),
210
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(StatRow, { label: "Day Streak", value: String(stats.currentDayStreak) }),
211
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(StatRow, { label: "Best Day Streak", value: String(stats.bestDayStreak) })
212
+ ] });
213
+ }
214
+
215
+ // src/BaseSettingsDialog.tsx
216
+ var import_auto_paper6 = require("@rific/auto-paper");
217
+ var import_feedback_press2 = require("@rific/feedback-press");
218
+ var import_updater = require("@rific/updater");
219
+ var import_core3 = require("@tastic/core");
220
+ var import_react4 = require("react");
221
+ var import_react_native9 = require("react-native");
222
+ var import_react_native_paper7 = require("react-native-paper");
223
+
224
+ // src/ConfirmDialog.tsx
225
+ var import_auto_paper5 = require("@rific/auto-paper");
226
+ var import_feedback_press = require("@rific/feedback-press");
227
+ var import_core = require("@tastic/core");
228
+ var import_react_native7 = require("react-native");
229
+ var import_react_native_paper5 = require("react-native-paper");
230
+
231
+ // src/overlayCard.ts
232
+ var import_react_native6 = require("react-native");
233
+ function getOverlayCardColors(dark) {
234
+ return {
235
+ fg: dark ? "#FFFFFF" : "#000000",
236
+ fgMuted: dark ? "rgba(255,255,255,0.4)" : "rgba(0,0,0,0.4)",
237
+ cardBg: dark ? "#111111" : "#F2F2F2",
238
+ cardBorder: dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)"
239
+ };
240
+ }
241
+ var overlayStyles = import_react_native6.StyleSheet.create({
242
+ card: {
243
+ alignItems: "center",
244
+ borderRadius: 20,
245
+ borderWidth: 1,
246
+ gap: 16,
247
+ maxWidth: 360,
248
+ padding: 32
249
+ },
250
+ overlay: {
251
+ alignItems: "center",
252
+ backgroundColor: "rgba(0,0,0,0.72)",
253
+ bottom: 0,
254
+ justifyContent: "center",
255
+ left: 0,
256
+ position: "absolute",
257
+ right: 0,
258
+ top: 0
259
+ }
260
+ });
261
+ var overlayActionStyles = import_react_native6.StyleSheet.create({
262
+ // minWidth (not flexBasis) is each button's real floor - whenever both fit side by side, flexGrow
263
+ // still splits any leftover width evenly beyond that floor, same visual result as a plain flex: 1
264
+ // on a roomy screen, but a button can never be squeezed narrower than 128, only wrap to its own
265
+ // row instead (see actions' flexWrap below). This used to be flexBasis: 128, which measured
266
+ // correctly on web but rendered every button roughly SQUARE on iOS - confirmed live (see the
267
+ // "Quit Match?" Cancel/Quit buttons and the single-button "OK" dialogs both coming out ~128pt
268
+ // tall instead of wide). Root cause: react-native-paper's Button renders its Surface as two
269
+ // nested views on iOS only (Surface.tsx's SurfaceIOS, for shadow rendering) - only a curated
270
+ // allowlist of style keys (flex/flexGrow/flexShrink/width/height/position/... - notably NOT
271
+ // flexBasis) reaches the outer view, which is the real flex item inside this row; every other
272
+ // key, flexBasis included, falls through to an inner view whose own unspecified flexDirection
273
+ // defaults to column. flexGrow: 1 landed on the (row-context) outer view and grew width
274
+ // correctly; flexBasis: 128 landed on the (column-context) inner view instead, where a flexBasis
275
+ // sets HEIGHT, not width. minWidth isn't axis-relative like flexBasis, so it constrains width
276
+ // correctly regardless of which of the two views it ends up on. react-native-paper's Button also
277
+ // hardcodes numberOfLines={1} on its label with no override, so a button squeezed under its
278
+ // label's natural width silently ellipsizes ("Cancel" -> "Can...") instead of wrapping - assumed
279
+ // fixable by just giving the row more width, until confirmed live that these two-word labels
280
+ // (Cancel/Reset/New Game) can still lose the truncation fight even with the extra room ConfirmDialog's own
281
+ // card width fix provides, on a narrow enough screen.
282
+ actionButton: {
283
+ flexGrow: 1,
284
+ minWidth: 128
285
+ },
286
+ // A lone confirm button (no cancel - BaseSettingsDialog's info-only "OK" dialogs, e.g. the
287
+ // update-check "no update found" message) has nothing left to share actions' width with, so
288
+ // actionButton's own flexGrow: 1 would stretch it across the entire row instead of reading as a
289
+ // normal button. Same 128 floor, no flexGrow, so it stays at that natural size instead of filling
290
+ // the card - centered by actions' own justifyContent below.
291
+ actionButtonSingle: {
292
+ minWidth: 128
293
+ },
294
+ actions: {
295
+ flexDirection: "row",
296
+ flexWrap: "wrap",
297
+ gap: 12,
298
+ justifyContent: "center",
299
+ width: "100%"
300
+ }
301
+ });
302
+
303
+ // src/ConfirmDialog.tsx
304
+ var import_jsx_runtime6 = require("react/jsx-runtime");
305
+ function ConfirmDialog({ visible, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, rotation = 0, icon = "help-circle-outline", destructive = false }) {
306
+ const { dark, colors } = (0, import_auto_paper5.useAutoPaperTheme)();
307
+ if (!visible) return null;
308
+ const { fg, cardBg, cardBorder } = getOverlayCardColors(dark);
309
+ const accent = destructive ? colors.danger : colors.primary;
310
+ const onAccent = destructive ? colors.onDanger : colors.onPrimary;
311
+ const showCancel = cancelLabel !== void 0 && onCancel !== void 0;
312
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_paper5.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native7.View, { style: [overlayStyles.overlay, styles5.overlay], children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native7.View, { style: [overlayStyles.card, styles5.card, { backgroundColor: cardBg, borderColor: cardBorder }, (0, import_core.toRotationStyle)(rotation)], children: [
313
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_paper5.Icon, { source: icon, size: 64, color: accent }),
314
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_paper5.Text, { variant: "headlineLarge", style: [styles5.title, { color: accent }], children: title }),
315
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_paper5.Text, { variant: "bodyLarge", style: [styles5.body, { color: fg }], children: message }),
316
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native7.View, { style: overlayActionStyles.actions, children: [
317
+ showCancel && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_feedback_press.Button, { mode: "outlined", onPress: onCancel, style: overlayActionStyles.actionButton, children: cancelLabel }),
318
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_feedback_press.Button, { mode: "contained", onPress: onConfirm, style: showCancel ? overlayActionStyles.actionButton : overlayActionStyles.actionButtonSingle, buttonColor: accent, textColor: onAccent, children: confirmLabel })
319
+ ] })
320
+ ] }) }) });
321
+ }
322
+ var styles5 = import_react_native7.StyleSheet.create({
323
+ body: {
324
+ textAlign: "center"
325
+ },
326
+ // Additive on top of overlayCard's own shared `card` shape (borderRadius/borderWidth/gap/maxWidth/
327
+ // padding) - claims its full allowance instead of just maxWidth alone, which only caps how wide the
328
+ // card is allowed to get - without an explicit width, a flex-centered child sizes to its own content
329
+ // instead of stretching, so the card (and therefore actions below) can end up narrower than the
330
+ // screen actually has room for, needlessly forcing the action row to wrap sooner than it has to.
331
+ card: {
332
+ width: "100%"
333
+ },
334
+ // Additive on top of overlayCard's own shared `overlay` backdrop - gives the card room to breathe
335
+ // from the screen edges now that it reliably stretches to fill its full width allowance (see
336
+ // card's own width comment) - without this, width: '100%' would run the card edge-to-edge on a
337
+ // screen narrower than maxWidth's own 360.
338
+ overlay: {
339
+ paddingHorizontal: 24
340
+ },
341
+ // textAlign is load-bearing, not decorative, once `title` wraps to more than one line - centered
342
+ // within card's own alignItems: 'center' only centers the Text's block as a whole; each wrapped
343
+ // line inside that block still defaults to left-aligned without this.
344
+ title: {
345
+ fontWeight: "bold",
346
+ textAlign: "center"
347
+ }
348
+ });
349
+
350
+ // src/RotatedFrame.tsx
351
+ var import_core2 = require("@tastic/core");
352
+ var import_react2 = require("react");
353
+ var import_react_native8 = require("react-native");
354
+ var import_react_native_paper6 = require("react-native-paper");
355
+ var import_react_native_safe_area_context = require("react-native-safe-area-context");
356
+ var import_jsx_runtime7 = require("react/jsx-runtime");
357
+ function RotatedFrame({ children, rotation: rotationOverride, rotatedSafeArea = false, portalHost = false, passThrough = false, maxContentWidth }) {
358
+ const ambientRotation = (0, import_core2.useRotation)();
359
+ const rotation = rotationOverride ?? ambientRotation;
360
+ const window = (0, import_react_native8.useWindowDimensions)();
361
+ const { width, height } = (0, import_core2.rotateDimensions)(window.width, window.height, rotation);
362
+ const rawInsets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
363
+ const insets = (0, import_react2.useMemo)(() => rotatedSafeArea ? (0, import_core2.rotateInsets)(rawInsets, rotation) : rawInsets, [rotatedSafeArea, rawInsets, rotation]);
364
+ const cappedWidth = maxContentWidth !== void 0 && width > height ? maxContentWidth : void 0;
365
+ const column = /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native8.View, { style: [styles6.column, cappedWidth !== void 0 && { maxWidth: cappedWidth }], pointerEvents: passThrough ? "box-none" : void 0, children });
366
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_core2.FakeLandscapeView, { rotation, passThrough, style: styles6.root, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native_safe_area_context.SafeAreaInsetsContext.Provider, { value: insets, children: portalHost ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native_paper6.Portal.Host, { children: column }) : column }) });
367
+ }
368
+ var styles6 = import_react_native8.StyleSheet.create({
369
+ // A centred column: full width up to `maxWidth`, so a capped frame sits in the middle of a wider window.
370
+ column: {
371
+ alignSelf: "center",
372
+ flex: 1,
373
+ width: "100%"
374
+ },
375
+ root: {
376
+ flex: 1
377
+ }
378
+ });
379
+
380
+ // src/useUpdateConfirmBridge.ts
381
+ var import_react3 = require("react");
382
+ function useUpdateConfirmBridge() {
383
+ const [manifest, setManifest] = (0, import_react3.useState)(null);
384
+ const resolveRef = (0, import_react3.useRef)(null);
385
+ const onConfirm = (0, import_react3.useCallback)(
386
+ (next) => new Promise((resolve) => {
387
+ resolveRef.current = resolve;
388
+ setManifest(next);
389
+ }),
390
+ []
391
+ );
392
+ const respond = (0, import_react3.useCallback)((confirmed) => {
393
+ setManifest(null);
394
+ resolveRef.current?.(confirmed);
395
+ resolveRef.current = null;
396
+ }, []);
397
+ return { manifest, onConfirm, respond };
398
+ }
399
+
400
+ // src/BaseSettingsDialog.tsx
401
+ var import_jsx_runtime8 = require("react/jsx-runtime");
402
+ function SettingIcon({ source, color, containerColor }) {
403
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native9.View, { style: [styles7.iconBadge, { backgroundColor: containerColor }], children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper7.Icon, { source, size: 18, color }) });
404
+ }
405
+ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }) {
406
+ const { colors } = (0, import_auto_paper6.useAutoPaperTheme)();
407
+ const ambientRotation = (0, import_core3.useRotation)();
408
+ const rotation = rotationOverride ?? ambientRotation;
409
+ const isTouchPrimary = (0, import_core3.useIsTouchPrimaryDevice)();
410
+ const { settings: hapticSettings, set: setHapticSettings } = (0, import_feedback_press2.useHapticSettings)();
411
+ const { settings: soundSettings, set: setSoundSettings } = (0, import_feedback_press2.useSoundSettings)();
412
+ const sound = (0, import_react4.useContext)(import_feedback_press2.SoundContext);
413
+ const { forceShort: forceHapticFeedback } = (0, import_feedback_press2.useVibration)();
414
+ const [infoMessage, setInfoMessage] = (0, import_react4.useState)(null);
415
+ const { manifest: updateManifest, onConfirm: onUpdateConfirm, respond: respondToUpdateConfirm } = useUpdateConfirmBridge();
416
+ const { check, checking, updateReady } = (0, import_updater.useUpdater)({
417
+ autoCheck: false,
418
+ autoPrompt: false,
419
+ onConfirm: onUpdateConfirm,
420
+ onError: onUpdateError,
421
+ onInfo: (title, message) => setInfoMessage({ title, message })
422
+ });
423
+ const updateManifestDate = updateManifest ? new Date(updateManifest.createdAt) : null;
424
+ const [opened, setOpened] = (0, import_react4.useState)(visible);
425
+ if (visible && !opened) setOpened(true);
426
+ const showLockOrientation = isTouchPrimary && lockOrientation !== void 0 && onLockOrientationChange !== void 0;
427
+ const showEdgeGuard = import_react_native9.Platform.OS === "ios" && deferBottomEdgeGestures !== void 0 && onDeferBottomEdgeGestures !== void 0;
428
+ const showHaptics = import_react_native9.Platform.OS !== "web" && !hideHaptics;
429
+ const showSound = !hideSound;
430
+ const showAppearance = !hideAppearance;
431
+ const showUpdateCheck = import_react_native9.Platform.OS !== "web" && !hideUpdateCheck;
432
+ if (!opened) return null;
433
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper7.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(RotatedFrame, { rotation, rotatedSafeArea: true, portalHost: true, passThrough: true, children: [
434
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_auto_paper6.Dialog, { visible, onDismiss, style: styles7.dialog, children: [
435
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_auto_paper6.Dialog.Title, { children: "Settings" }),
436
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_auto_paper6.Dialog.ScrollArea, { style: styles7.scrollArea, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native9.ScrollView, { contentContainerStyle: styles7.content, showsVerticalScrollIndicator: false, children: [
437
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native9.View, { style: styles7.toggleGroup, children: [
438
+ showLockOrientation && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_feedback_press2.TouchableRipple, { onPress: () => onLockOrientationChange(!lockOrientation), style: styles7.toggleButton, accessibilityLabel: `Lock orientation ${lockOrientation ? "on" : "off"}`, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native9.View, { style: styles7.toggleContent, children: [
439
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SettingIcon, { source: lockOrientation ? "lock" : "lock-open-variant-outline", color: lockOrientation ? colors.tertiary : colors.onSurfaceVariant, containerColor: lockOrientation ? colors.tertiaryContainer : colors.surfaceVariant }),
440
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native9.View, { style: styles7.flexShrink, children: [
441
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper7.Text, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Lock Orientation" }),
442
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper7.Text, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Pins the current layout" })
443
+ ] })
444
+ ] }) }),
445
+ showEdgeGuard && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_feedback_press2.TouchableRipple, { onPress: () => onDeferBottomEdgeGestures(!deferBottomEdgeGestures), style: styles7.toggleButton, accessibilityLabel: `Edge guard ${deferBottomEdgeGestures ? "on" : "off"}`, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native9.View, { style: styles7.toggleContent, children: [
446
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SettingIcon, { source: deferBottomEdgeGestures ? "shield-check-outline" : "shield-off-outline", color: deferBottomEdgeGestures ? colors.tertiary : colors.onSurfaceVariant, containerColor: deferBottomEdgeGestures ? colors.tertiaryContainer : colors.surfaceVariant }),
447
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native9.View, { style: styles7.flexShrink, children: [
448
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper7.Text, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Edge Guard" }),
449
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper7.Text, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Requires a second swipe" })
450
+ ] })
451
+ ] }) }),
452
+ (showSound || showHaptics) && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native9.View, { style: styles7.toggleRow, children: [
453
+ showSound && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
454
+ import_feedback_press2.TouchableRipple,
455
+ {
456
+ soundDisabled: true,
457
+ onPress: () => {
458
+ const enabled = !soundSettings.enabled;
459
+ setSoundSettings({ enabled });
460
+ if (enabled) sound.selection?.();
461
+ },
462
+ style: styles7.toggleButtonInRow,
463
+ accessibilityLabel: `Sound ${soundSettings.enabled ? "on" : "off"}`,
464
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native9.View, { style: styles7.toggleContent, children: [
465
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SettingIcon, { source: soundSettings.enabled ? "volume-high" : "volume-off", color: soundSettings.enabled ? colors.tertiary : colors.onSurfaceVariant, containerColor: soundSettings.enabled ? colors.tertiaryContainer : colors.surfaceVariant }),
466
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper7.Text, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Sound" })
467
+ ] })
468
+ }
469
+ ),
470
+ showHaptics && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
471
+ import_feedback_press2.TouchableRipple,
472
+ {
473
+ hapticDisabled: true,
474
+ onPress: () => {
475
+ const vibrate = !hapticSettings.vibrate;
476
+ setHapticSettings({ vibrate });
477
+ if (vibrate) forceHapticFeedback();
478
+ },
479
+ style: styles7.toggleButtonInRow,
480
+ accessibilityLabel: `Haptics ${hapticSettings.vibrate ? "on" : "off"}`,
481
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native9.View, { style: styles7.toggleContent, children: [
482
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SettingIcon, { source: hapticSettings.vibrate ? "vibrate" : "vibrate-off", color: hapticSettings.vibrate ? colors.tertiary : colors.onSurfaceVariant, containerColor: hapticSettings.vibrate ? colors.tertiaryContainer : colors.surfaceVariant }),
483
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper7.Text, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Haptics" })
484
+ ] })
485
+ }
486
+ )
487
+ ] }),
488
+ onShowHowToPlay && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
489
+ import_feedback_press2.TouchableRipple,
490
+ {
491
+ onPress: () => {
492
+ onDismiss();
493
+ onShowHowToPlay();
494
+ },
495
+ style: styles7.toggleButton,
496
+ accessibilityLabel: "How to Play",
497
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native9.View, { style: styles7.toggleContent, children: [
498
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SettingIcon, { source: "help-circle-outline", color: colors.secondary, containerColor: colors.secondaryContainer }),
499
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native9.View, { style: styles7.flexShrink, children: [
500
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper7.Text, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "How to Play" }),
501
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper7.Text, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Replay the quick intro" })
502
+ ] })
503
+ ] })
504
+ }
505
+ )
506
+ ] }),
507
+ showAppearance && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native9.View, { style: styles7.section, children: [
508
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper7.Text, { variant: "labelMedium", style: [styles7.sectionLabel, { color: colors.onSurfaceVariant }], children: "APPEARANCE" }),
509
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_auto_paper6.AutoAppearancePicker, { showLabels: false })
510
+ ] }),
511
+ children,
512
+ showUpdateCheck && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native9.View, { style: styles7.section, children: [
513
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native_paper7.Text, { variant: "labelSmall", style: [styles7.sectionLabel, { color: colors.onSurfaceVariant }], children: [
514
+ "VERSION ",
515
+ version,
516
+ updateReady ? " \xB7 UPDATE READY" : ""
517
+ ] }),
518
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_feedback_press2.Button, { mode: "outlined", onPress: check, loading: checking, disabled: checking, children: "Check for Updates" })
519
+ ] })
520
+ ] }) })
521
+ ] }),
522
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(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) }),
523
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ConfirmDialog, { visible: !!infoMessage, title: infoMessage?.title ?? "", message: infoMessage?.message ?? "", confirmLabel: "OK", icon: "information-outline", rotation: 0, onConfirm: () => setInfoMessage(null) })
524
+ ] }) });
525
+ }
526
+ var styles7 = import_react_native9.StyleSheet.create({
527
+ content: {
528
+ gap: 24,
529
+ paddingVertical: 20
530
+ },
531
+ // Caps the card so it never grows past the screen - without this the dialog just keeps
532
+ // growing to fit its content and the overflow gets clipped by the screen edge, which is
533
+ // what happened in landscape where there's less height to work with. Dialog.ScrollArea +
534
+ // ScrollView below then take over and let the content scroll within that bound.
535
+ dialog: {
536
+ maxHeight: "90%"
537
+ },
538
+ flexShrink: {
539
+ flexShrink: 1
540
+ },
541
+ iconBadge: {
542
+ alignItems: "center",
543
+ borderRadius: 10,
544
+ height: 36,
545
+ justifyContent: "center",
546
+ width: 36
547
+ },
548
+ scrollArea: {
549
+ marginBottom: 0
550
+ },
551
+ section: {
552
+ gap: 12
553
+ },
554
+ sectionLabel: {
555
+ letterSpacing: 2
556
+ },
557
+ // Used to carry marginHorizontal: -12 + paddingHorizontal: 12 (netting to the same inset as
558
+ // APPEARANCE/SOUND & HAPTICS below, while giving the ripple/hover highlight room to breathe
559
+ // into the dialog's own gutter beyond that). Removed: this dialog scrolls (Dialog.ScrollArea +
560
+ // ScrollView above), and on web a ScrollView's own content wrapper clips to its own bounds
561
+ // regardless of a child's negative margin - confirmed live, the icon's own rounded-square badge
562
+ // was getting its left edge sliced off by exactly that overhang. No horizontal inset of its own
563
+ // now - it relies purely on the same dialog padding APPEARANCE/every other row already does,
564
+ // which can never overflow because it's never negative.
565
+ toggleButton: {
566
+ borderRadius: 12,
567
+ overflow: "hidden",
568
+ paddingVertical: 10
569
+ },
570
+ // Same shape as toggleButton, `flex: 1` instead of its own width - two of these side by side in
571
+ // toggleRow share the row's own edge alignment instead of each carrying their own.
572
+ toggleButtonInRow: {
573
+ borderRadius: 12,
574
+ flex: 1,
575
+ overflow: "hidden",
576
+ paddingVertical: 10
577
+ },
578
+ toggleContent: {
579
+ alignItems: "center",
580
+ flexDirection: "row",
581
+ gap: 12
582
+ },
583
+ toggleGroup: {
584
+ gap: 4
585
+ },
586
+ // No horizontal margin of its own now either - see toggleButton's identical note.
587
+ toggleRow: {
588
+ flexDirection: "row",
589
+ gap: 4
590
+ }
591
+ });
592
+
593
+ // src/BaseStatsScreen.tsx
594
+ var import_auto_paper7 = require("@rific/auto-paper");
595
+ var import_feedback_press3 = require("@rific/feedback-press");
596
+ var import_core4 = require("@tastic/core");
597
+ var import_react5 = require("react");
598
+ var import_react_native10 = require("react-native");
599
+ var import_react_native_paper8 = require("react-native-paper");
600
+ var import_jsx_runtime9 = require("react/jsx-runtime");
601
+ 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 }) {
602
+ const { dark, colors } = (0, import_auto_paper7.useAutoPaperTheme)();
603
+ const ambientRotation = (0, import_core4.useRotation)();
604
+ const rotation = rotationOverride ?? ambientRotation;
605
+ const [confirmResetVisible, setConfirmResetVisible] = (0, import_react5.useState)(false);
606
+ const showReset = !!onReset;
607
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
608
+ const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
609
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_react_native10.View, { style: [styles8.container, { backgroundColor: bg }], children: [
610
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_react_native10.View, { style: [styles8.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
611
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_feedback_press3.IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: onBack }),
612
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper8.Text, { variant: titleVariant, style: [styles8.title, { color: fg }], children: title })
613
+ ] }),
614
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_react_native10.ScrollView, { style: styles8.scrollView, contentContainerStyle: [styles8.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, children: [
615
+ children,
616
+ showReset && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_feedback_press3.Button, { mode: "contained", onPress: () => setConfirmResetVisible(true), buttonColor: colors.danger, textColor: colors.onDanger, style: styles8.resetButton, children: resetLabel })
617
+ ] }),
618
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
619
+ ConfirmDialog,
620
+ {
621
+ visible: confirmResetVisible,
622
+ title: resetConfirmTitle,
623
+ message: resetConfirmBody,
624
+ confirmLabel: "Reset",
625
+ cancelLabel: "Cancel",
626
+ icon: "alert-outline",
627
+ destructive: true,
628
+ rotation,
629
+ onCancel: () => setConfirmResetVisible(false),
630
+ onConfirm: () => {
631
+ onReset?.();
632
+ setConfirmResetVisible(false);
633
+ }
634
+ }
635
+ )
636
+ ] });
637
+ }
638
+ var styles8 = import_react_native10.StyleSheet.create({
639
+ container: {
640
+ flex: 1
641
+ },
642
+ content: {
643
+ gap: 12,
644
+ paddingHorizontal: 20
645
+ },
646
+ header: {
647
+ alignItems: "center",
648
+ flexDirection: "row",
649
+ gap: 4,
650
+ paddingBottom: 8
651
+ },
652
+ resetButton: {
653
+ marginTop: 16
654
+ },
655
+ // Bounds the ScrollView to the space `container`'s flex:1 actually gives it - without this, a
656
+ // ScrollView with only a contentContainerStyle isn't reliably height-constrained on native (it
657
+ // can render at its full unclipped content height instead of the screen's), which leaves the
658
+ // header's back button touch target unreliable underneath it.
659
+ scrollView: {
660
+ flex: 1
661
+ },
662
+ title: {
663
+ flexShrink: 1,
664
+ fontWeight: "bold"
665
+ }
666
+ });
667
+
668
+ // src/ContentGutter.tsx
669
+ var import_core5 = require("@tastic/core");
670
+ var import_react_native11 = require("react-native");
671
+ var import_jsx_runtime10 = require("react/jsx-runtime");
672
+ function ContentGutter({ maxContentWidth, leftGutter, rightGutter, style, children }) {
673
+ const { width: windowWidth } = (0, import_core5.useRotatedWindowDimensions)();
674
+ const { contentWidth, gutterWidth } = (0, import_core5.computeContentBounds)(windowWidth, maxContentWidth ?? Infinity);
675
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native11.View, { style: [styles9.row, style], children: [
676
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native11.View, { style: [styles9.gutter, { width: gutterWidth }], children: leftGutter }),
677
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native11.View, { style: { width: contentWidth }, children }),
678
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native11.View, { style: [styles9.gutter, { width: gutterWidth }], children: rightGutter })
679
+ ] });
680
+ }
681
+ var styles9 = import_react_native11.StyleSheet.create({
682
+ // overflow:'hidden' keeps an oversized leftGutter/rightGutter from bleeding into the content
683
+ // column instead of clipping cleanly at its own real width.
684
+ gutter: {
685
+ overflow: "hidden"
686
+ },
687
+ row: {
688
+ flexDirection: "row"
689
+ }
690
+ });
691
+
692
+ // src/CornerActionButtons.tsx
693
+ var import_feedback_press4 = require("@rific/feedback-press");
694
+ var import_react_native12 = require("react-native");
695
+ var import_jsx_runtime11 = require("react/jsx-runtime");
696
+ function CornerActionButtons({ onBack, onSettings, fg, insets, backIcon = "arrow-left", backLabel = "Back" }) {
697
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
698
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_feedback_press4.IconButton, { icon: backIcon, iconColor: fg, size: 24, style: [styles10.back, { top: 8 + insets.top, left: 8 + insets.left }], onPress: onBack, accessibilityLabel: backLabel }),
699
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_feedback_press4.IconButton, { icon: "cog", iconColor: fg, size: 24, style: [styles10.topRight, { top: 8 + insets.top, right: 8 + insets.right }], onPress: onSettings, accessibilityLabel: "Settings" })
700
+ ] });
701
+ }
702
+ var styles10 = import_react_native12.StyleSheet.create({
703
+ back: {
704
+ position: "absolute"
705
+ },
706
+ topRight: {
707
+ position: "absolute"
708
+ }
709
+ });
710
+
711
+ // src/StaggeredWord.tsx
712
+ var import_react6 = require("react");
713
+ var import_react_native13 = require("react-native");
714
+ var import_react_native_reanimated = __toESM(require("react-native-reanimated"));
715
+ var import_jsx_runtime12 = require("react/jsx-runtime");
716
+ var STAGGER_MS = 45;
717
+ var LETTER_DURATION_MS = 320;
718
+ function getStaggeredWordDuration(letterCount) {
719
+ return (letterCount - 1) * STAGGER_MS + LETTER_DURATION_MS;
720
+ }
721
+ function AnimatedLetter({ char, index, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }) {
722
+ const progress = (0, import_react_native_reanimated.useSharedValue)(reducedMotion ? 1 : 0);
723
+ (0, import_react6.useEffect)(() => {
724
+ if (reducedMotion) return;
725
+ progress.value = (0, import_react_native_reanimated.withDelay)(index * STAGGER_MS, (0, import_react_native_reanimated.withTiming)(1, { duration: LETTER_DURATION_MS, easing: import_react_native_reanimated.Easing.out(import_react_native_reanimated.Easing.back(1.5)) }));
726
+ }, [reducedMotion]);
727
+ const style = (0, import_react_native_reanimated.useAnimatedStyle)(() => ({
728
+ opacity: progress.value,
729
+ transform: [{ translateY: (1 - progress.value) * 12 }, { scale: 0.85 + progress.value * 0.15 }]
730
+ }));
731
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native_reanimated.default.Text, { allowFontScaling: false, style: [{ color, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }, style], children: char });
732
+ }
733
+ function StaggeredWord({ word, color, fontFamily, fontSize, lineHeight, reducedMotion, startIndex = 0, fontWeight = "bold", letterSpacing, style, onLayout }) {
734
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native13.View, { style: [styles11.row, style], onLayout, children: word.split("").map((char, i) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(AnimatedLetter, { char, index: startIndex + i, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }, i)) });
735
+ }
736
+ var styles11 = import_react_native13.StyleSheet.create({
737
+ row: {
738
+ flexDirection: "row"
739
+ }
740
+ });
741
+
742
+ // src/UpdateDialog.tsx
743
+ var import_updater2 = require("@rific/updater");
744
+ var import_jsx_runtime13 = require("react/jsx-runtime");
745
+ function UpdateDialog({ rotation = 0, autoCheck = true, autoPrompt = true, onError }) {
746
+ const { manifest, onConfirm, respond } = useUpdateConfirmBridge();
747
+ (0, import_updater2.useUpdater)({ autoCheck, autoPrompt, onConfirm, onError });
748
+ const date = manifest ? new Date(manifest.createdAt) : null;
749
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(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) });
750
+ }
751
+ // Annotate the CommonJS export names for ESM import in node:
752
+ 0 && (module.exports = {
753
+ AchievementCatalogSection,
754
+ AchievementRow,
755
+ ActivityStatSection,
756
+ BaseSettingsDialog,
757
+ BaseStatsScreen,
758
+ ConfirmDialog,
759
+ ContentGutter,
760
+ CornerActionButtons,
761
+ LOCKED_BADGE_COLOR,
762
+ MONO_FONT,
763
+ RotatedFrame,
764
+ StaggeredWord,
765
+ StatRow,
766
+ StatSection,
767
+ UpdateDialog,
768
+ getOverlayCardColors,
769
+ getStaggeredWordDuration,
770
+ overlayActionStyles,
771
+ overlayStyles
772
+ });