@tastic/hud 0.8.1 → 0.8.2

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.d.mts CHANGED
@@ -57,11 +57,9 @@ interface Props$b {
57
57
  resetConfirmBody?: string;
58
58
  fg?: string;
59
59
  bg?: string;
60
- cardBg?: string;
61
- accentColor?: string;
62
60
  children: ReactNode;
63
61
  }
64
- declare function BaseStatsScreen({ rotation: rotationOverride, title, titleVariant, onBack, insets, onReset, resetLabel, resetConfirmTitle, resetConfirmBody, fg: fgOverride, bg: bgOverride, cardBg: cardBgOverride, accentColor: accentColorOverride, children }: Props$b): react.JSX.Element;
62
+ declare function BaseStatsScreen({ rotation: rotationOverride, title, titleVariant, onBack, insets, onReset, resetLabel, resetConfirmTitle, resetConfirmBody, fg: fgOverride, bg: bgOverride, children }: Props$b): react.JSX.Element;
65
63
 
66
64
  interface ConfirmDialogProps {
67
65
  visible: boolean;
package/dist/index.d.ts CHANGED
@@ -57,11 +57,9 @@ interface Props$b {
57
57
  resetConfirmBody?: string;
58
58
  fg?: string;
59
59
  bg?: string;
60
- cardBg?: string;
61
- accentColor?: string;
62
60
  children: ReactNode;
63
61
  }
64
- declare function BaseStatsScreen({ rotation: rotationOverride, title, titleVariant, onBack, insets, onReset, resetLabel, resetConfirmTitle, resetConfirmBody, fg: fgOverride, bg: bgOverride, cardBg: cardBgOverride, accentColor: accentColorOverride, children }: Props$b): react.JSX.Element;
62
+ declare function BaseStatsScreen({ rotation: rotationOverride, title, titleVariant, onBack, insets, onReset, resetLabel, resetConfirmTitle, resetConfirmBody, fg: fgOverride, bg: bgOverride, children }: Props$b): react.JSX.Element;
65
63
 
66
64
  interface ConfirmDialogProps {
67
65
  visible: boolean;
package/dist/index.js CHANGED
@@ -459,139 +459,56 @@ var styles2 = import_react_native2.StyleSheet.create({
459
459
  });
460
460
 
461
461
  // src/BaseStatsScreen.tsx
462
- var import_auto_paper3 = require("@rific/auto-paper");
463
- var import_feedback_press2 = require("@rific/feedback-press");
462
+ var import_auto_paper4 = require("@rific/auto-paper");
463
+ var import_feedback_press3 = require("@rific/feedback-press");
464
464
  var import_core2 = require("@tastic/core");
465
465
  var import_react2 = require("react");
466
+ var import_react_native4 = require("react-native");
467
+ var import_react_native_paper4 = require("react-native-paper");
468
+
469
+ // src/ConfirmDialog.tsx
470
+ var import_auto_paper3 = require("@rific/auto-paper");
471
+ var import_feedback_press2 = require("@rific/feedback-press");
466
472
  var import_react_native3 = require("react-native");
467
473
  var import_react_native_paper3 = require("react-native-paper");
468
474
  var import_jsx_runtime3 = require("react/jsx-runtime");
469
- 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, cardBg: cardBgOverride, accentColor: accentColorOverride, children }) {
470
- const { dark, colors } = (0, import_auto_paper3.useAutoPaperTheme)();
471
- const ambientRotation = (0, import_core2.useRotation)();
472
- const rotation = rotationOverride ?? ambientRotation;
473
- const [confirmResetVisible, setConfirmResetVisible] = (0, import_react2.useState)(false);
474
- const showReset = !!onReset;
475
- const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
476
- const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
477
- const cardBg = cardBgOverride ?? (dark ? "#111111" : "#F2F2F2");
478
- const cardBorder = dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)";
479
- const accentColor = accentColorOverride ?? colors.secondary;
480
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: [styles3.container, { backgroundColor: bg }], children: [
481
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: [styles3.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
482
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press2.IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: onBack }),
483
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: titleVariant, style: [styles3.title, { color: fg }], children: title })
484
- ] }),
485
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.ScrollView, { style: styles3.scrollView, contentContainerStyle: [styles3.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, children: [
486
- children,
487
- showReset && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press2.Button, { mode: "contained", onPress: () => setConfirmResetVisible(true), buttonColor: colors.secondary, textColor: colors.onSecondary, style: styles3.resetButton, children: resetLabel })
488
- ] }),
489
- confirmResetVisible && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style: [styles3.overlay, rotation % 360 !== 0 && { transform: [{ rotate: `${rotation}deg` }] }], children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: [styles3.overlayCard, { backgroundColor: cardBg, borderColor: cardBorder }], children: [
490
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Icon, { source: "alert-outline", size: 64, color: accentColor }),
491
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "headlineLarge", style: [styles3.overlayTitle, { color: accentColor }], children: resetConfirmTitle }),
492
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "bodyLarge", style: [styles3.overlayBody, { color: fg }], children: resetConfirmBody }),
493
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press2.Button, { mode: "contained", onPress: () => setConfirmResetVisible(false), style: styles3.overlayButton, children: "Cancel" }),
494
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
495
- import_feedback_press2.Button,
496
- {
497
- mode: "contained",
498
- onPress: () => {
499
- onReset?.();
500
- setConfirmResetVisible(false);
501
- },
502
- style: styles3.overlayButton,
503
- buttonColor: colors.secondary,
504
- textColor: colors.onSecondary,
505
- children: "Reset"
506
- }
507
- )
508
- ] }) }) })
509
- ] });
510
- }
511
- var styles3 = import_react_native3.StyleSheet.create({
512
- container: {
513
- flex: 1
514
- },
515
- content: {
516
- gap: 12,
517
- paddingHorizontal: 20
518
- },
519
- header: {
520
- alignItems: "center",
521
- flexDirection: "row",
522
- gap: 4,
523
- paddingBottom: 8
524
- },
525
- // Same overlay shape BaseSettingsDialog's own info overlay uses (backdrop + centered card),
526
- // rotated in place the same way rather than via Portal-root transform for the same reason — see
527
- // that component's own identical styles for the full rationale.
528
- overlay: {
529
- alignItems: "center",
530
- backgroundColor: "rgba(0,0,0,0.72)",
531
- bottom: 0,
532
- justifyContent: "center",
533
- left: 0,
534
- position: "absolute",
535
- right: 0,
536
- top: 0
537
- },
538
- overlayBody: { textAlign: "center" },
539
- overlayButton: { width: 160 },
540
- overlayCard: {
541
- alignItems: "center",
542
- borderRadius: 20,
543
- borderWidth: 1,
544
- gap: 16,
545
- maxWidth: 360,
546
- padding: 32
547
- },
548
- overlayTitle: { fontWeight: "bold", marginBottom: -8 },
549
- resetButton: {
550
- marginTop: 16
551
- },
552
- // Bounds the ScrollView to the space `container`'s flex:1 actually gives it — without this, a
553
- // ScrollView with only a contentContainerStyle isn't reliably height-constrained on native (it
554
- // can render at its full unclipped content height instead of the screen's), which leaves the
555
- // header's back button touch target unreliable underneath it.
556
- scrollView: {
557
- flex: 1
558
- },
559
- title: {
560
- flexShrink: 1,
561
- fontWeight: "bold"
562
- }
563
- });
564
-
565
- // src/ConfirmDialog.tsx
566
- var import_auto_paper4 = require("@rific/auto-paper");
567
- var import_feedback_press3 = require("@rific/feedback-press");
568
- var import_react_native4 = require("react-native");
569
- var import_react_native_paper4 = require("react-native-paper");
570
- var import_jsx_runtime4 = require("react/jsx-runtime");
571
475
  function ConfirmDialog({ visible, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, rotation = 0, icon = "help-circle-outline", destructive = false }) {
572
- const { dark, colors } = (0, import_auto_paper4.useAutoPaperTheme)();
476
+ const { dark, colors } = (0, import_auto_paper3.useAutoPaperTheme)();
573
477
  if (!visible) return null;
574
478
  const fg = dark ? "#FFFFFF" : "#000000";
575
479
  const cardBg = dark ? "#111111" : "#F2F2F2";
576
480
  const cardBorder = dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)";
577
481
  const accent = destructive ? colors.danger : colors.primary;
578
482
  const onAccent = destructive ? colors.onDanger : colors.onPrimary;
579
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper4.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: styles4.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: [styles4.card, { backgroundColor: cardBg, borderColor: cardBorder }, rotation % 360 !== 0 && { transform: [{ rotate: `${rotation}deg` }] }], children: [
580
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper4.Icon, { source: icon, size: 64, color: accent }),
581
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper4.Text, { variant: "headlineLarge", style: [styles4.title, { color: accent }], children: title }),
582
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper4.Text, { variant: "bodyLarge", style: [styles4.body, { color: fg }], children: message }),
583
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: styles4.actions, children: [
584
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press3.Button, { mode: "outlined", onPress: onCancel, style: styles4.actionButton, children: cancelLabel }),
585
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press3.Button, { mode: "contained", onPress: onConfirm, style: styles4.actionButton, buttonColor: accent, textColor: onAccent, children: confirmLabel })
483
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style: styles3.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: [styles3.card, { backgroundColor: cardBg, borderColor: cardBorder }, rotation % 360 !== 0 && { transform: [{ rotate: `${rotation}deg` }] }], children: [
484
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Icon, { source: icon, size: 64, color: accent }),
485
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "headlineLarge", style: [styles3.title, { color: accent }], children: title }),
486
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "bodyLarge", style: [styles3.body, { color: fg }], children: message }),
487
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: styles3.actions, children: [
488
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press2.Button, { mode: "outlined", onPress: onCancel, style: styles3.actionButton, children: cancelLabel }),
489
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press2.Button, { mode: "contained", onPress: onConfirm, style: styles3.actionButton, buttonColor: accent, textColor: onAccent, children: confirmLabel })
586
490
  ] })
587
491
  ] }) }) });
588
492
  }
589
- var styles4 = import_react_native4.StyleSheet.create({
493
+ var styles3 = import_react_native3.StyleSheet.create({
494
+ // flexBasis (not flex: 1's own 0%) is each button's real floor: whenever both fit side by side,
495
+ // flexGrow still splits any leftover width evenly beyond that floor, same visual result as the
496
+ // old flex: 1 on a roomy screen - but a button can never be squeezed narrower than 128, only
497
+ // wrap to its own row instead (see actions' flexWrap below). react-native-paper's Button
498
+ // hardcodes numberOfLines={1} on its label with no override, so a button squeezed under its
499
+ // label's natural width silently ellipsizes ("Cancel" -> "Can...") instead of wrapping - assumed
500
+ // fixable by just giving the row more width, until confirmed live that these two-word labels
501
+ // (Cancel/Reset/New Game) can still lose the truncation fight even with the extra room card's own
502
+ // width fix below provides, on a narrow enough screen. flexBasis is the real fix: past a certain
503
+ // width there's no side-by-side arrangement that fits both buttons at their natural label size,
504
+ // so the layout has to give (stack) rather than the label (truncate).
590
505
  actionButton: {
591
- flex: 1
506
+ flexBasis: 128,
507
+ flexGrow: 1
592
508
  },
593
509
  actions: {
594
510
  flexDirection: "row",
511
+ flexWrap: "wrap",
595
512
  gap: 12,
596
513
  width: "100%"
597
514
  },
@@ -604,7 +521,12 @@ var styles4 = import_react_native4.StyleSheet.create({
604
521
  borderWidth: 1,
605
522
  gap: 16,
606
523
  maxWidth: 360,
607
- padding: 32
524
+ padding: 32,
525
+ // Claims its full allowance instead of just maxWidth alone, which only caps how wide the card
526
+ // is allowed to get - without an explicit width, a flex-centered child sizes to its own content
527
+ // instead of stretching, so the card (and therefore actions below) can end up narrower than the
528
+ // screen actually has room for, needlessly forcing the action row to wrap sooner than it has to.
529
+ width: "100%"
608
530
  },
609
531
  overlay: {
610
532
  alignItems: "center",
@@ -612,11 +534,88 @@ var styles4 = import_react_native4.StyleSheet.create({
612
534
  bottom: 0,
613
535
  justifyContent: "center",
614
536
  left: 0,
537
+ // Gives the card room to breathe from the screen edges now that it reliably stretches to fill
538
+ // its full width allowance (see card's own width comment) - without this, width: '100%' would
539
+ // run the card edge-to-edge on a screen narrower than maxWidth's own 360.
540
+ paddingHorizontal: 24,
615
541
  position: "absolute",
616
542
  right: 0,
617
543
  top: 0
618
544
  },
545
+ // textAlign is load-bearing, not decorative, once `title` wraps to more than one line - centered
546
+ // within card's own alignItems: 'center' only centers the Text's block as a whole; each wrapped
547
+ // line inside that block still defaults to left-aligned without this.
619
548
  title: {
549
+ fontWeight: "bold",
550
+ textAlign: "center"
551
+ }
552
+ });
553
+
554
+ // src/BaseStatsScreen.tsx
555
+ var import_jsx_runtime4 = require("react/jsx-runtime");
556
+ 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 }) {
557
+ const { dark, colors } = (0, import_auto_paper4.useAutoPaperTheme)();
558
+ const ambientRotation = (0, import_core2.useRotation)();
559
+ const rotation = rotationOverride ?? ambientRotation;
560
+ const [confirmResetVisible, setConfirmResetVisible] = (0, import_react2.useState)(false);
561
+ const showReset = !!onReset;
562
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
563
+ const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
564
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: [styles4.container, { backgroundColor: bg }], children: [
565
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: [styles4.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
566
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press3.IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: onBack }),
567
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper4.Text, { variant: titleVariant, style: [styles4.title, { color: fg }], children: title })
568
+ ] }),
569
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.ScrollView, { style: styles4.scrollView, contentContainerStyle: [styles4.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, children: [
570
+ children,
571
+ showReset && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press3.Button, { mode: "contained", onPress: () => setConfirmResetVisible(true), buttonColor: colors.danger, textColor: colors.onDanger, style: styles4.resetButton, children: resetLabel })
572
+ ] }),
573
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
574
+ ConfirmDialog,
575
+ {
576
+ visible: confirmResetVisible,
577
+ title: resetConfirmTitle,
578
+ message: resetConfirmBody,
579
+ confirmLabel: "Reset",
580
+ cancelLabel: "Cancel",
581
+ icon: "alert-outline",
582
+ destructive: true,
583
+ rotation,
584
+ onCancel: () => setConfirmResetVisible(false),
585
+ onConfirm: () => {
586
+ onReset?.();
587
+ setConfirmResetVisible(false);
588
+ }
589
+ }
590
+ )
591
+ ] });
592
+ }
593
+ var styles4 = import_react_native4.StyleSheet.create({
594
+ container: {
595
+ flex: 1
596
+ },
597
+ content: {
598
+ gap: 12,
599
+ paddingHorizontal: 20
600
+ },
601
+ header: {
602
+ alignItems: "center",
603
+ flexDirection: "row",
604
+ gap: 4,
605
+ paddingBottom: 8
606
+ },
607
+ resetButton: {
608
+ marginTop: 16
609
+ },
610
+ // Bounds the ScrollView to the space `container`'s flex:1 actually gives it — without this, a
611
+ // ScrollView with only a contentContainerStyle isn't reliably height-constrained on native (it
612
+ // can render at its full unclipped content height instead of the screen's), which leaves the
613
+ // header's back button touch target unreliable underneath it.
614
+ scrollView: {
615
+ flex: 1
616
+ },
617
+ title: {
618
+ flexShrink: 1,
620
619
  fontWeight: "bold"
621
620
  }
622
621
  });
package/dist/index.mjs CHANGED
@@ -275,139 +275,56 @@ var styles2 = StyleSheet2.create({
275
275
  });
276
276
 
277
277
  // src/BaseStatsScreen.tsx
278
- import { useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
279
- import { Button as Button2, IconButton } from "@rific/feedback-press";
278
+ import { useAutoPaperTheme as useAutoPaperTheme4 } from "@rific/auto-paper";
279
+ import { Button as Button3, IconButton } from "@rific/feedback-press";
280
280
  import { useRotation as useRotation2 } from "@tastic/core";
281
281
  import { useState as useState2 } from "react";
282
- import { ScrollView as ScrollView2, StyleSheet as StyleSheet3, View as View3 } from "react-native";
283
- import { Icon as Icon3, Portal as Portal2, Text as Text3 } from "react-native-paper";
284
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
285
- 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, cardBg: cardBgOverride, accentColor: accentColorOverride, children }) {
286
- const { dark, colors } = useAutoPaperTheme3();
287
- const ambientRotation = useRotation2();
288
- const rotation = rotationOverride ?? ambientRotation;
289
- const [confirmResetVisible, setConfirmResetVisible] = useState2(false);
290
- const showReset = !!onReset;
291
- const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
292
- const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
293
- const cardBg = cardBgOverride ?? (dark ? "#111111" : "#F2F2F2");
294
- const cardBorder = dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)";
295
- const accentColor = accentColorOverride ?? colors.secondary;
296
- return /* @__PURE__ */ jsxs3(View3, { style: [styles3.container, { backgroundColor: bg }], children: [
297
- /* @__PURE__ */ jsxs3(View3, { style: [styles3.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
298
- /* @__PURE__ */ jsx3(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: onBack }),
299
- /* @__PURE__ */ jsx3(Text3, { variant: titleVariant, style: [styles3.title, { color: fg }], children: title })
300
- ] }),
301
- /* @__PURE__ */ jsxs3(ScrollView2, { style: styles3.scrollView, contentContainerStyle: [styles3.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, children: [
302
- children,
303
- showReset && /* @__PURE__ */ jsx3(Button2, { mode: "contained", onPress: () => setConfirmResetVisible(true), buttonColor: colors.secondary, textColor: colors.onSecondary, style: styles3.resetButton, children: resetLabel })
304
- ] }),
305
- confirmResetVisible && /* @__PURE__ */ jsx3(Portal2, { children: /* @__PURE__ */ jsx3(View3, { style: [styles3.overlay, rotation % 360 !== 0 && { transform: [{ rotate: `${rotation}deg` }] }], children: /* @__PURE__ */ jsxs3(View3, { style: [styles3.overlayCard, { backgroundColor: cardBg, borderColor: cardBorder }], children: [
306
- /* @__PURE__ */ jsx3(Icon3, { source: "alert-outline", size: 64, color: accentColor }),
307
- /* @__PURE__ */ jsx3(Text3, { variant: "headlineLarge", style: [styles3.overlayTitle, { color: accentColor }], children: resetConfirmTitle }),
308
- /* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: [styles3.overlayBody, { color: fg }], children: resetConfirmBody }),
309
- /* @__PURE__ */ jsx3(Button2, { mode: "contained", onPress: () => setConfirmResetVisible(false), style: styles3.overlayButton, children: "Cancel" }),
310
- /* @__PURE__ */ jsx3(
311
- Button2,
312
- {
313
- mode: "contained",
314
- onPress: () => {
315
- onReset?.();
316
- setConfirmResetVisible(false);
317
- },
318
- style: styles3.overlayButton,
319
- buttonColor: colors.secondary,
320
- textColor: colors.onSecondary,
321
- children: "Reset"
322
- }
323
- )
324
- ] }) }) })
325
- ] });
326
- }
327
- var styles3 = StyleSheet3.create({
328
- container: {
329
- flex: 1
330
- },
331
- content: {
332
- gap: 12,
333
- paddingHorizontal: 20
334
- },
335
- header: {
336
- alignItems: "center",
337
- flexDirection: "row",
338
- gap: 4,
339
- paddingBottom: 8
340
- },
341
- // Same overlay shape BaseSettingsDialog's own info overlay uses (backdrop + centered card),
342
- // rotated in place the same way rather than via Portal-root transform for the same reason — see
343
- // that component's own identical styles for the full rationale.
344
- overlay: {
345
- alignItems: "center",
346
- backgroundColor: "rgba(0,0,0,0.72)",
347
- bottom: 0,
348
- justifyContent: "center",
349
- left: 0,
350
- position: "absolute",
351
- right: 0,
352
- top: 0
353
- },
354
- overlayBody: { textAlign: "center" },
355
- overlayButton: { width: 160 },
356
- overlayCard: {
357
- alignItems: "center",
358
- borderRadius: 20,
359
- borderWidth: 1,
360
- gap: 16,
361
- maxWidth: 360,
362
- padding: 32
363
- },
364
- overlayTitle: { fontWeight: "bold", marginBottom: -8 },
365
- resetButton: {
366
- marginTop: 16
367
- },
368
- // Bounds the ScrollView to the space `container`'s flex:1 actually gives it — without this, a
369
- // ScrollView with only a contentContainerStyle isn't reliably height-constrained on native (it
370
- // can render at its full unclipped content height instead of the screen's), which leaves the
371
- // header's back button touch target unreliable underneath it.
372
- scrollView: {
373
- flex: 1
374
- },
375
- title: {
376
- flexShrink: 1,
377
- fontWeight: "bold"
378
- }
379
- });
282
+ import { ScrollView as ScrollView2, StyleSheet as StyleSheet4, View as View4 } from "react-native";
283
+ import { Text as Text4 } from "react-native-paper";
380
284
 
381
285
  // src/ConfirmDialog.tsx
382
- import { useAutoPaperTheme as useAutoPaperTheme4 } from "@rific/auto-paper";
383
- import { Button as Button3 } from "@rific/feedback-press";
384
- import { StyleSheet as StyleSheet4, View as View4 } from "react-native";
385
- import { Icon as Icon4, Portal as Portal3, Text as Text4 } from "react-native-paper";
386
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
286
+ import { useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
287
+ import { Button as Button2 } from "@rific/feedback-press";
288
+ import { StyleSheet as StyleSheet3, View as View3 } from "react-native";
289
+ import { Icon as Icon3, Portal as Portal2, Text as Text3 } from "react-native-paper";
290
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
387
291
  function ConfirmDialog({ visible, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, rotation = 0, icon = "help-circle-outline", destructive = false }) {
388
- const { dark, colors } = useAutoPaperTheme4();
292
+ const { dark, colors } = useAutoPaperTheme3();
389
293
  if (!visible) return null;
390
294
  const fg = dark ? "#FFFFFF" : "#000000";
391
295
  const cardBg = dark ? "#111111" : "#F2F2F2";
392
296
  const cardBorder = dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)";
393
297
  const accent = destructive ? colors.danger : colors.primary;
394
298
  const onAccent = destructive ? colors.onDanger : colors.onPrimary;
395
- return /* @__PURE__ */ jsx4(Portal3, { children: /* @__PURE__ */ jsx4(View4, { style: styles4.overlay, children: /* @__PURE__ */ jsxs4(View4, { style: [styles4.card, { backgroundColor: cardBg, borderColor: cardBorder }, rotation % 360 !== 0 && { transform: [{ rotate: `${rotation}deg` }] }], children: [
396
- /* @__PURE__ */ jsx4(Icon4, { source: icon, size: 64, color: accent }),
397
- /* @__PURE__ */ jsx4(Text4, { variant: "headlineLarge", style: [styles4.title, { color: accent }], children: title }),
398
- /* @__PURE__ */ jsx4(Text4, { variant: "bodyLarge", style: [styles4.body, { color: fg }], children: message }),
399
- /* @__PURE__ */ jsxs4(View4, { style: styles4.actions, children: [
400
- /* @__PURE__ */ jsx4(Button3, { mode: "outlined", onPress: onCancel, style: styles4.actionButton, children: cancelLabel }),
401
- /* @__PURE__ */ jsx4(Button3, { mode: "contained", onPress: onConfirm, style: styles4.actionButton, buttonColor: accent, textColor: onAccent, children: confirmLabel })
299
+ return /* @__PURE__ */ jsx3(Portal2, { children: /* @__PURE__ */ jsx3(View3, { style: styles3.overlay, children: /* @__PURE__ */ jsxs3(View3, { style: [styles3.card, { backgroundColor: cardBg, borderColor: cardBorder }, rotation % 360 !== 0 && { transform: [{ rotate: `${rotation}deg` }] }], children: [
300
+ /* @__PURE__ */ jsx3(Icon3, { source: icon, size: 64, color: accent }),
301
+ /* @__PURE__ */ jsx3(Text3, { variant: "headlineLarge", style: [styles3.title, { color: accent }], children: title }),
302
+ /* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: [styles3.body, { color: fg }], children: message }),
303
+ /* @__PURE__ */ jsxs3(View3, { style: styles3.actions, children: [
304
+ /* @__PURE__ */ jsx3(Button2, { mode: "outlined", onPress: onCancel, style: styles3.actionButton, children: cancelLabel }),
305
+ /* @__PURE__ */ jsx3(Button2, { mode: "contained", onPress: onConfirm, style: styles3.actionButton, buttonColor: accent, textColor: onAccent, children: confirmLabel })
402
306
  ] })
403
307
  ] }) }) });
404
308
  }
405
- var styles4 = StyleSheet4.create({
309
+ var styles3 = StyleSheet3.create({
310
+ // flexBasis (not flex: 1's own 0%) is each button's real floor: whenever both fit side by side,
311
+ // flexGrow still splits any leftover width evenly beyond that floor, same visual result as the
312
+ // old flex: 1 on a roomy screen - but a button can never be squeezed narrower than 128, only
313
+ // wrap to its own row instead (see actions' flexWrap below). react-native-paper's Button
314
+ // hardcodes numberOfLines={1} on its label with no override, so a button squeezed under its
315
+ // label's natural width silently ellipsizes ("Cancel" -> "Can...") instead of wrapping - assumed
316
+ // fixable by just giving the row more width, until confirmed live that these two-word labels
317
+ // (Cancel/Reset/New Game) can still lose the truncation fight even with the extra room card's own
318
+ // width fix below provides, on a narrow enough screen. flexBasis is the real fix: past a certain
319
+ // width there's no side-by-side arrangement that fits both buttons at their natural label size,
320
+ // so the layout has to give (stack) rather than the label (truncate).
406
321
  actionButton: {
407
- flex: 1
322
+ flexBasis: 128,
323
+ flexGrow: 1
408
324
  },
409
325
  actions: {
410
326
  flexDirection: "row",
327
+ flexWrap: "wrap",
411
328
  gap: 12,
412
329
  width: "100%"
413
330
  },
@@ -420,7 +337,12 @@ var styles4 = StyleSheet4.create({
420
337
  borderWidth: 1,
421
338
  gap: 16,
422
339
  maxWidth: 360,
423
- padding: 32
340
+ padding: 32,
341
+ // Claims its full allowance instead of just maxWidth alone, which only caps how wide the card
342
+ // is allowed to get - without an explicit width, a flex-centered child sizes to its own content
343
+ // instead of stretching, so the card (and therefore actions below) can end up narrower than the
344
+ // screen actually has room for, needlessly forcing the action row to wrap sooner than it has to.
345
+ width: "100%"
424
346
  },
425
347
  overlay: {
426
348
  alignItems: "center",
@@ -428,11 +350,88 @@ var styles4 = StyleSheet4.create({
428
350
  bottom: 0,
429
351
  justifyContent: "center",
430
352
  left: 0,
353
+ // Gives the card room to breathe from the screen edges now that it reliably stretches to fill
354
+ // its full width allowance (see card's own width comment) - without this, width: '100%' would
355
+ // run the card edge-to-edge on a screen narrower than maxWidth's own 360.
356
+ paddingHorizontal: 24,
431
357
  position: "absolute",
432
358
  right: 0,
433
359
  top: 0
434
360
  },
361
+ // textAlign is load-bearing, not decorative, once `title` wraps to more than one line - centered
362
+ // within card's own alignItems: 'center' only centers the Text's block as a whole; each wrapped
363
+ // line inside that block still defaults to left-aligned without this.
435
364
  title: {
365
+ fontWeight: "bold",
366
+ textAlign: "center"
367
+ }
368
+ });
369
+
370
+ // src/BaseStatsScreen.tsx
371
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
372
+ 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 }) {
373
+ const { dark, colors } = useAutoPaperTheme4();
374
+ const ambientRotation = useRotation2();
375
+ const rotation = rotationOverride ?? ambientRotation;
376
+ const [confirmResetVisible, setConfirmResetVisible] = useState2(false);
377
+ const showReset = !!onReset;
378
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
379
+ const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
380
+ return /* @__PURE__ */ jsxs4(View4, { style: [styles4.container, { backgroundColor: bg }], children: [
381
+ /* @__PURE__ */ jsxs4(View4, { style: [styles4.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
382
+ /* @__PURE__ */ jsx4(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: onBack }),
383
+ /* @__PURE__ */ jsx4(Text4, { variant: titleVariant, style: [styles4.title, { color: fg }], children: title })
384
+ ] }),
385
+ /* @__PURE__ */ jsxs4(ScrollView2, { style: styles4.scrollView, contentContainerStyle: [styles4.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, children: [
386
+ children,
387
+ showReset && /* @__PURE__ */ jsx4(Button3, { mode: "contained", onPress: () => setConfirmResetVisible(true), buttonColor: colors.danger, textColor: colors.onDanger, style: styles4.resetButton, children: resetLabel })
388
+ ] }),
389
+ /* @__PURE__ */ jsx4(
390
+ ConfirmDialog,
391
+ {
392
+ visible: confirmResetVisible,
393
+ title: resetConfirmTitle,
394
+ message: resetConfirmBody,
395
+ confirmLabel: "Reset",
396
+ cancelLabel: "Cancel",
397
+ icon: "alert-outline",
398
+ destructive: true,
399
+ rotation,
400
+ onCancel: () => setConfirmResetVisible(false),
401
+ onConfirm: () => {
402
+ onReset?.();
403
+ setConfirmResetVisible(false);
404
+ }
405
+ }
406
+ )
407
+ ] });
408
+ }
409
+ var styles4 = StyleSheet4.create({
410
+ container: {
411
+ flex: 1
412
+ },
413
+ content: {
414
+ gap: 12,
415
+ paddingHorizontal: 20
416
+ },
417
+ header: {
418
+ alignItems: "center",
419
+ flexDirection: "row",
420
+ gap: 4,
421
+ paddingBottom: 8
422
+ },
423
+ resetButton: {
424
+ marginTop: 16
425
+ },
426
+ // Bounds the ScrollView to the space `container`'s flex:1 actually gives it — without this, a
427
+ // ScrollView with only a contentContainerStyle isn't reliably height-constrained on native (it
428
+ // can render at its full unclipped content height instead of the screen's), which leaves the
429
+ // header's back button touch target unreliable underneath it.
430
+ scrollView: {
431
+ flex: 1
432
+ },
433
+ title: {
434
+ flexShrink: 1,
436
435
  fontWeight: "bold"
437
436
  }
438
437
  });
@@ -470,7 +469,7 @@ import { getBlendedColor, getColorRoles } from "@rific/auto-paper";
470
469
  import { IconButton as IconButton2, TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
471
470
  import { useRotation as useRotation3 } from "@tastic/core";
472
471
  import { ScrollView as ScrollView3, StyleSheet as StyleSheet7, View as View8 } from "react-native";
473
- import { Icon as Icon5, Text as Text5 } from "react-native-paper";
472
+ import { Icon as Icon4, Text as Text5 } from "react-native-paper";
474
473
 
475
474
  // src/fonts.ts
476
475
  import { Platform as Platform2 } from "react-native";
@@ -689,7 +688,7 @@ function SingleSection({
689
688
  const labelStyle = { fontFamily: labelFontFamily, color: rowColor, fontWeight: selected ? "bold" : "normal" };
690
689
  const descriptionStyle = { fontFamily: labelFontFamily, color: rowColor };
691
690
  return /* @__PURE__ */ jsx8(TouchableRipple2, { disabled: taken, onPress: () => onSelect(option.value), style: itemStyle, children: /* @__PURE__ */ jsxs7(View8, { style: styles7.itemRow, children: [
692
- option.icon && /* @__PURE__ */ jsx8(Icon5, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
691
+ option.icon && /* @__PURE__ */ jsx8(Icon4, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
693
692
  /* @__PURE__ */ jsxs7(View8, { style: styles7.itemLabelColumn, children: [
694
693
  /* @__PURE__ */ jsx8(Text5, { variant: "labelLarge", style: labelStyle, children: option.label }),
695
694
  option.description && /* @__PURE__ */ jsx8(Text5, { variant: "labelSmall", style: descriptionStyle, children: option.description })
@@ -715,7 +714,7 @@ function MultiSection({
715
714
  const itemStyle = [styles7.item, selected && { backgroundColor: accentColor }, prevSelected && { borderTopLeftRadius: 0, borderTopRightRadius: 0 }, nextSelected && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0 }];
716
715
  const labelStyle = { fontFamily: labelFontFamily, color: rowColor, fontWeight: selected ? "bold" : "normal" };
717
716
  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: [
718
- option.icon && /* @__PURE__ */ jsx8(Icon5, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
717
+ option.icon && /* @__PURE__ */ jsx8(Icon4, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
719
718
  /* @__PURE__ */ jsx8(Text5, { variant: "labelLarge", style: [styles7.itemLabelFlex, labelStyle], children: option.label })
720
719
  ] }) }, option.value);
721
720
  }),
@@ -820,11 +819,11 @@ var styles8 = StyleSheet8.create({
820
819
 
821
820
  // src/CornerStatusBadges.tsx
822
821
  import { StyleSheet as StyleSheet9, View as View9 } from "react-native";
823
- import { Icon as Icon6 } from "react-native-paper";
822
+ import { Icon as Icon5 } from "react-native-paper";
824
823
  import { Fragment as Fragment5, jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
825
824
  var BADGE_SIZE = 30;
826
825
  function StatusBadge({ icon }) {
827
- return /* @__PURE__ */ jsx11(View9, { style: styles9.badge, children: icon && /* @__PURE__ */ jsx11(Icon6, { source: icon, size: 16, color: "rgba(255,255,255,0.85)" }) });
826
+ return /* @__PURE__ */ jsx11(View9, { style: styles9.badge, children: icon && /* @__PURE__ */ jsx11(Icon5, { source: icon, size: 16, color: "rgba(255,255,255,0.85)" }) });
828
827
  }
829
828
  function CornerStatusBadges({ orientationMode, p1OnRight, p1, p2 }) {
830
829
  const p1OnRightSide = orientationMode === "sideBySide" && p1OnRight;
@@ -857,7 +856,7 @@ import { defaultColors, getContrastColor } from "@rific/auto-paper";
857
856
  import { TouchableRipple as TouchableRipple3 } from "@rific/feedback-press";
858
857
  import { clamp, useRotation as useRotation4 } from "@tastic/core";
859
858
  import { ScrollView as ScrollView4, StyleSheet as StyleSheet10, useWindowDimensions as useWindowDimensions3, View as View10 } from "react-native";
860
- import { Icon as Icon7, Text as Text6 } from "react-native-paper";
859
+ import { Icon as Icon6, Text as Text6 } from "react-native-paper";
861
860
  import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
862
861
  var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
863
862
  var DEFAULT_SIZE = 48;
@@ -889,7 +888,7 @@ function InlineColorPicker({ id, host, value, onChange, previewValue, swatches =
889
888
  const contrastColor = getContrastColor(displayValue);
890
889
  const tagFontSize = size * (tag && EMOJI_PATTERN.test(tag) ? 0.6 : 0.4);
891
890
  return /* @__PURE__ */ jsxs10(View10, { style: [styles10.anchor, open && styles10.anchorOpen], children: [
892
- /* @__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(Icon7, { source: icon, size: size * 0.6, color: contrastColor }) }) }),
891
+ /* @__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 }) }) }),
893
892
  /* @__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) => {
894
893
  const selected = swatch.value.toLowerCase() === value.toLowerCase();
895
894
  const taken = !selected && !!takenValue && swatch.value.toLowerCase() === takenValue.toLowerCase();
@@ -905,7 +904,7 @@ function InlineColorPicker({ id, host, value, onChange, previewValue, swatches =
905
904
  },
906
905
  borderless: true,
907
906
  style: [styles10.swatch, { backgroundColor: swatchDisplay }, selected && styles10.swatchSelected, taken && !swappable && styles10.swatchTaken],
908
- children: selected ? /* @__PURE__ */ jsx12(Icon7, { source: "check", size: 16, color: getContrastColor(swatchDisplay) }) : swappable ? /* @__PURE__ */ jsx12(Icon7, { source: "swap-horizontal", size: 16, color: getContrastColor(swatchDisplay) }) : taken ? /* @__PURE__ */ jsx12(Icon7, { source: "close", size: 16, color: getContrastColor(swatchDisplay) }) : /* @__PURE__ */ jsx12(View10, {})
907
+ 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, {})
909
908
  },
910
909
  swatch.value
911
910
  );
@@ -987,7 +986,7 @@ var styles10 = StyleSheet10.create({
987
986
  import { getContrastColor as getContrastColor2 } from "@rific/auto-paper";
988
987
  import { TouchableRipple as TouchableRipple4 } from "@rific/feedback-press";
989
988
  import { ScrollView as ScrollView5, StyleSheet as StyleSheet11, View as View11 } from "react-native";
990
- import { Icon as Icon8, Text as Text7 } from "react-native-paper";
989
+ import { Icon as Icon7, Text as Text7 } from "react-native-paper";
991
990
  import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
992
991
  var TRIGGER_HEIGHT = 28;
993
992
  var POPOVER_WIDTH = 180;
@@ -1014,13 +1013,13 @@ function LabeledDropdown({ id, host, options, value, onChange, color, dark, alig
1014
1013
  return /* @__PURE__ */ jsxs11(View11, { style: [styles11.anchor, open && styles11.anchorOpen], children: [
1015
1014
  /* @__PURE__ */ jsx13(TouchableRipple4, { onPress: () => host.toggle(id), style: styles11.trigger, children: /* @__PURE__ */ jsxs11(View11, { ref: triggerRef, collapsable: false, style: styles11.triggerInner, children: [
1016
1015
  /* @__PURE__ */ jsx13(Text7, { style: [styles11.triggerLabel, { color }], numberOfLines: 1, children: (selected?.label ?? "").toUpperCase() }),
1017
- /* @__PURE__ */ jsx13(Icon8, { source: "menu-down", size: 16, color })
1016
+ /* @__PURE__ */ jsx13(Icon7, { source: "menu-down", size: 16, color })
1018
1017
  ] }) }),
1019
1018
  /* @__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) => {
1020
1019
  const isSelected = option.value === value;
1021
1020
  const onColor = getContrastColor2(color);
1022
1021
  return /* @__PURE__ */ jsx13(TouchableRipple4, { onPress: () => handleSelect(option), style: [styles11.row, isSelected && { backgroundColor: color }], children: /* @__PURE__ */ jsxs11(View11, { style: styles11.rowInner, children: [
1023
- option.icon && /* @__PURE__ */ jsx13(Icon8, { source: option.icon, size: 18, color: isSelected ? onColor : fg }),
1022
+ option.icon && /* @__PURE__ */ jsx13(Icon7, { source: option.icon, size: 18, color: isSelected ? onColor : fg }),
1024
1023
  /* @__PURE__ */ jsx13(Text7, { style: [styles11.rowLabel, { color: isSelected ? onColor : fg }], numberOfLines: 1, children: option.label })
1025
1024
  ] }) }, option.value);
1026
1025
  }) }) })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/hud",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Visual component kit for local-multiplayer React Native games: inline (non-modal) popovers, dropdowns, color pickers, gauges, ready buttons, and dialogs, built to stay scoped to one player's own zone on a shared screen",
5
5
  "keywords": [
6
6
  "react-native",
@@ -3,7 +3,9 @@ import { Button, IconButton } from '@rific/feedback-press'
3
3
  import { useRotation } from '@tastic/core'
4
4
  import { ReactNode, useState } from 'react'
5
5
  import { ScrollView, StyleSheet, View } from 'react-native'
6
- import { Icon, Portal, Text } from 'react-native-paper'
6
+ import { Text } from 'react-native-paper'
7
+
8
+ import { ConfirmDialog } from './ConfirmDialog'
7
9
 
8
10
  // react-native-paper's own MD3 Text variant names — mirrored locally rather than importing its
9
11
  // internal VariantProp/MD3TypescaleKey (not exported from the package's public entry point) just
@@ -12,11 +14,12 @@ type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headl
12
14
 
13
15
  interface Props {
14
16
  // Explicit override for the live physical-hold rotation (see @tastic/core's useRotation) — only
15
- // affects this component's own Portal-rendered reset-confirm overlay, which (like any Portal
16
- // content) is unaffected by whichever screen's own FakeLandscapeView wraps the trigger and so has
17
- // to rotate itself. Defaults to a live ambient read via useRotation() when omitted — note this
18
- // does NOT make the header/scrollable content below rotate; the caller's own screen still needs
19
- // its own FakeLandscapeView wrap for that (this component has no transform on its own main body).
17
+ // affects this component's own Portal-rendered reset-confirm dialog (via ConfirmDialog's own
18
+ // rotation prop), which (like any Portal content) is unaffected by whichever screen's own
19
+ // FakeLandscapeView wraps the trigger and so has to rotate itself. Defaults to a live ambient
20
+ // read via useRotation() when omitted — note this does NOT make the header/scrollable content
21
+ // below rotate; the caller's own screen still needs its own FakeLandscapeView wrap for that
22
+ // (this component has no transform on its own main body).
20
23
  rotation?: number
21
24
  title?: string
22
25
  // 'headlineSmall' (this screen's own default, below) — was originally 'displaySmall' (the full
@@ -36,39 +39,38 @@ interface Props {
36
39
  resetLabel?: string
37
40
  resetConfirmTitle?: string
38
41
  resetConfirmBody?: string
39
- // Independently optional overrides for this screen's own background/text colors — each defaults
40
- // to the literal-black/white-by-appearance formula described below when omitted, so every
41
- // existing caller (none of which pass these) renders identically to before. A caller with its own
42
- // app-wide chrome palette (e.g. a tinted background rather than neutral black/white) passes
43
- // whichever of the three it needs; cardBorder on the reset-confirm overlay stays fixed regardless,
44
- // since its low alpha already reads fine against any cardBg.
42
+ // Independently optional overrides for this screen's own header/background colors — each
43
+ // defaults to the literal-black/white-by-appearance formula described below when omitted, so
44
+ // every existing caller (none of which pass these) renders identically to before. A caller with
45
+ // its own app-wide chrome palette (e.g. a tinted background rather than neutral black/white)
46
+ // passes whichever of the two it needs.
47
+ //
48
+ // There used to be a third override here, cardBg, plus a separate accentColor override for the
49
+ // reset-confirm dialog's own icon/title/card. Both are gone: the reset-confirm step used to be a
50
+ // hand-rolled lookalike of ConfirmDialog with its own copy of the overlay/card/button styling,
51
+ // which is exactly how it drifted from ConfirmDialog's real look in the first place (different
52
+ // button layout, different Cancel styling, and — via cardBg — a different, caller-tintable card
53
+ // background no other ConfirmDialog in the fleet has). It now renders a real ConfirmDialog
54
+ // instance below instead of its own copy, which by construction can never drift from every other
55
+ // confirm prompt in the app again — at the cost of losing that per-caller tinting, which is the
56
+ // right trade: a "you're about to erase everything" prompt should look and read identically
57
+ // everywhere, not take on whichever screen happened to trigger it.
45
58
  fg?: string
46
59
  bg?: string
47
- cardBg?: string
48
- // Overrides the confirm-overlay's alert icon/title, which otherwise default to auto-paper's
49
- // colors.secondary. That default is tuned to read against auto-paper's OWN background role, not
50
- // necessarily against a cardBg override above — a caller passing cardBg should generally pass a
51
- // matching accentColor too, rather than risk colors.secondary landing close in hue/lightness to
52
- // its own custom card background (this is what actually happened with a tinted felt cardBg in the
53
- // app this was first overridden for). Every button on this screen (the reset trigger included)
54
- // fills with colors.secondary/onSecondary as a self-contained, always-legible pair instead of
55
- // relying on a text-only color read against the surrounding chrome, so none of them need this —
56
- // only the bare icon/title do.
57
- accentColor?: string
58
60
  children: ReactNode
59
61
  }
60
62
 
61
63
  // The stats/achievements screen shell every @tastic game shares — back button + title header,
62
64
  // scrollable content area, and an optional reset-everything action with its own confirm-before-
63
- // destroying overlay — extracted after the same header/scroll/reset-confirm shape was independently
65
+ // destroying dialog — extracted after the same header/scroll/reset-confirm shape was independently
64
66
  // built into LightCycles' own achievements.tsx. Colors default to the same literal-black/white-by-
65
67
  // appearance formula BaseSettingsDialog hardcodes internally (same as this package's own
66
- // BaseSettingsDialog) — zero-config for every existing caller — but fg/bg/cardBg are each
67
- // independently overridable for a caller whose own app-wide chrome palette isn't that literal
68
- // black/white convention. `children` is the seam for whatever isn't shared: this component has no
69
- // opinion on what a "stat" is or how achievements are catalogued — see StatRow/StatSection/
70
- // AchievementRow for the smaller presentational pieces built to go inside it.
71
- export 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, cardBg: cardBgOverride, accentColor: accentColorOverride, children }: Props) {
68
+ // BaseSettingsDialog) — zero-config for every existing caller — but fg/bg are each independently
69
+ // overridable for a caller whose own app-wide chrome palette isn't that literal black/white
70
+ // convention. `children` is the seam for whatever isn't shared: this component has no opinion on
71
+ // what a "stat" is or how achievements are catalogued — see StatRow/StatSection/AchievementRow for
72
+ // the smaller presentational pieces built to go inside it.
73
+ export 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 }: Props) {
72
74
  const { dark, colors } = useAutoPaperTheme()
73
75
  const ambientRotation = useRotation()
74
76
  const rotation = rotationOverride ?? ambientRotation
@@ -77,15 +79,10 @@ export function BaseStatsScreen({ rotation: rotationOverride, title = 'Achieveme
77
79
 
78
80
  // High-contrast retro look by default: literal black/white by appearance, matching every other
79
81
  // screen in this ecosystem (title/loadout/settings all use this identical formula) rather than
80
- // auto-paper's own (slightly tinted) background role — same convention BaseSettingsDialog's own
81
- // fg/cardBg/cardBorder use internally. Overridable per-instance via the props above for a caller
82
- // whose own chrome isn't that literal black/white convention; cardBorder has no override (see the
83
- // Props doc above for why).
82
+ // auto-paper's own (slightly tinted) background role. Overridable per-instance via the props
83
+ // above for a caller whose own chrome isn't that literal black/white convention.
84
84
  const fg = fgOverride ?? (dark ? '#FFFFFF' : '#000000')
85
85
  const bg = bgOverride ?? (dark ? '#000000' : '#FFFFFF')
86
- const cardBg = cardBgOverride ?? (dark ? '#111111' : '#F2F2F2')
87
- const cardBorder = dark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'
88
- const accentColor = accentColorOverride ?? colors.secondary
89
86
 
90
87
  return (
91
88
  <View style={[styles.container, { backgroundColor: bg }]}>
@@ -100,42 +97,27 @@ export function BaseStatsScreen({ rotation: rotationOverride, title = 'Achieveme
100
97
  {children}
101
98
 
102
99
  {showReset && (
103
- <Button mode='contained' onPress={() => setConfirmResetVisible(true)} buttonColor={colors.secondary} textColor={colors.onSecondary} style={styles.resetButton}>
100
+ <Button mode='contained' onPress={() => setConfirmResetVisible(true)} buttonColor={colors.danger} textColor={colors.onDanger} style={styles.resetButton}>
104
101
  {resetLabel}
105
102
  </Button>
106
103
  )}
107
104
  </ScrollView>
108
105
 
109
- {confirmResetVisible && (
110
- <Portal>
111
- <View style={[styles.overlay, rotation % 360 !== 0 && { transform: [{ rotate: `${rotation}deg` }] }]}>
112
- <View style={[styles.overlayCard, { backgroundColor: cardBg, borderColor: cardBorder }]}>
113
- <Icon source='alert-outline' size={64} color={accentColor} />
114
- <Text variant='headlineLarge' style={[styles.overlayTitle, { color: accentColor }]}>
115
- {resetConfirmTitle}
116
- </Text>
117
- <Text variant='bodyLarge' style={[styles.overlayBody, { color: fg }]}>
118
- {resetConfirmBody}
119
- </Text>
120
- <Button mode='contained' onPress={() => setConfirmResetVisible(false)} style={styles.overlayButton}>
121
- Cancel
122
- </Button>
123
- <Button
124
- mode='contained'
125
- onPress={() => {
126
- onReset?.()
127
- setConfirmResetVisible(false)
128
- }}
129
- style={styles.overlayButton}
130
- buttonColor={colors.secondary}
131
- textColor={colors.onSecondary}
132
- >
133
- Reset
134
- </Button>
135
- </View>
136
- </View>
137
- </Portal>
138
- )}
106
+ <ConfirmDialog
107
+ visible={confirmResetVisible}
108
+ title={resetConfirmTitle}
109
+ message={resetConfirmBody}
110
+ confirmLabel='Reset'
111
+ cancelLabel='Cancel'
112
+ icon='alert-outline'
113
+ destructive
114
+ rotation={rotation}
115
+ onCancel={() => setConfirmResetVisible(false)}
116
+ onConfirm={() => {
117
+ onReset?.()
118
+ setConfirmResetVisible(false)
119
+ }}
120
+ />
139
121
  </View>
140
122
  )
141
123
  }
@@ -154,30 +136,6 @@ const styles = StyleSheet.create({
154
136
  gap: 4,
155
137
  paddingBottom: 8
156
138
  },
157
- // Same overlay shape BaseSettingsDialog's own info overlay uses (backdrop + centered card),
158
- // rotated in place the same way rather than via Portal-root transform for the same reason — see
159
- // that component's own identical styles for the full rationale.
160
- overlay: {
161
- alignItems: 'center',
162
- backgroundColor: 'rgba(0,0,0,0.72)',
163
- bottom: 0,
164
- justifyContent: 'center',
165
- left: 0,
166
- position: 'absolute',
167
- right: 0,
168
- top: 0
169
- },
170
- overlayBody: { textAlign: 'center' },
171
- overlayButton: { width: 160 },
172
- overlayCard: {
173
- alignItems: 'center',
174
- borderRadius: 20,
175
- borderWidth: 1,
176
- gap: 16,
177
- maxWidth: 360,
178
- padding: 32
179
- },
180
- overlayTitle: { fontWeight: 'bold', marginBottom: -8 },
181
139
  resetButton: {
182
140
  marginTop: 16
183
141
  },
@@ -69,11 +69,24 @@ export function ConfirmDialog({ visible, title, message, confirmLabel, cancelLab
69
69
  }
70
70
 
71
71
  const styles = StyleSheet.create({
72
+ // flexBasis (not flex: 1's own 0%) is each button's real floor: whenever both fit side by side,
73
+ // flexGrow still splits any leftover width evenly beyond that floor, same visual result as the
74
+ // old flex: 1 on a roomy screen - but a button can never be squeezed narrower than 128, only
75
+ // wrap to its own row instead (see actions' flexWrap below). react-native-paper's Button
76
+ // hardcodes numberOfLines={1} on its label with no override, so a button squeezed under its
77
+ // label's natural width silently ellipsizes ("Cancel" -> "Can...") instead of wrapping - assumed
78
+ // fixable by just giving the row more width, until confirmed live that these two-word labels
79
+ // (Cancel/Reset/New Game) can still lose the truncation fight even with the extra room card's own
80
+ // width fix below provides, on a narrow enough screen. flexBasis is the real fix: past a certain
81
+ // width there's no side-by-side arrangement that fits both buttons at their natural label size,
82
+ // so the layout has to give (stack) rather than the label (truncate).
72
83
  actionButton: {
73
- flex: 1
84
+ flexBasis: 128,
85
+ flexGrow: 1
74
86
  },
75
87
  actions: {
76
88
  flexDirection: 'row',
89
+ flexWrap: 'wrap',
77
90
  gap: 12,
78
91
  width: '100%'
79
92
  },
@@ -86,7 +99,12 @@ const styles = StyleSheet.create({
86
99
  borderWidth: 1,
87
100
  gap: 16,
88
101
  maxWidth: 360,
89
- padding: 32
102
+ padding: 32,
103
+ // Claims its full allowance instead of just maxWidth alone, which only caps how wide the card
104
+ // is allowed to get - without an explicit width, a flex-centered child sizes to its own content
105
+ // instead of stretching, so the card (and therefore actions below) can end up narrower than the
106
+ // screen actually has room for, needlessly forcing the action row to wrap sooner than it has to.
107
+ width: '100%'
90
108
  },
91
109
  overlay: {
92
110
  alignItems: 'center',
@@ -94,11 +112,19 @@ const styles = StyleSheet.create({
94
112
  bottom: 0,
95
113
  justifyContent: 'center',
96
114
  left: 0,
115
+ // Gives the card room to breathe from the screen edges now that it reliably stretches to fill
116
+ // its full width allowance (see card's own width comment) - without this, width: '100%' would
117
+ // run the card edge-to-edge on a screen narrower than maxWidth's own 360.
118
+ paddingHorizontal: 24,
97
119
  position: 'absolute',
98
120
  right: 0,
99
121
  top: 0
100
122
  },
123
+ // textAlign is load-bearing, not decorative, once `title` wraps to more than one line - centered
124
+ // within card's own alignItems: 'center' only centers the Text's block as a whole; each wrapped
125
+ // line inside that block still defaults to left-aligned without this.
101
126
  title: {
102
- fontWeight: 'bold'
127
+ fontWeight: 'bold',
128
+ textAlign: 'center'
103
129
  }
104
130
  })