@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 +1 -3
- package/dist/index.d.ts +1 -3
- package/dist/index.js +114 -115
- package/dist/index.mjs +126 -127
- package/package.json +1 -1
- package/src/BaseStatsScreen.tsx +50 -92
- package/src/ConfirmDialog.tsx +29 -3
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,
|
|
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,
|
|
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
|
|
463
|
-
var
|
|
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,
|
|
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,
|
|
580
|
-
/* @__PURE__ */ (0,
|
|
581
|
-
/* @__PURE__ */ (0,
|
|
582
|
-
/* @__PURE__ */ (0,
|
|
583
|
-
/* @__PURE__ */ (0,
|
|
584
|
-
/* @__PURE__ */ (0,
|
|
585
|
-
/* @__PURE__ */ (0,
|
|
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
|
|
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
|
-
|
|
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
|
|
279
|
-
import { Button as
|
|
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
|
|
283
|
-
import {
|
|
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
|
|
383
|
-
import { Button as
|
|
384
|
-
import { StyleSheet as
|
|
385
|
-
import { Icon as
|
|
386
|
-
import { jsx as
|
|
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 } =
|
|
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__ */
|
|
396
|
-
/* @__PURE__ */
|
|
397
|
-
/* @__PURE__ */
|
|
398
|
-
/* @__PURE__ */
|
|
399
|
-
/* @__PURE__ */
|
|
400
|
-
/* @__PURE__ */
|
|
401
|
-
/* @__PURE__ */
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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.
|
|
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",
|
package/src/BaseStatsScreen.tsx
CHANGED
|
@@ -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 {
|
|
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
|
|
16
|
-
// content) is unaffected by whichever screen's own
|
|
17
|
-
// to rotate itself. Defaults to a live ambient
|
|
18
|
-
// does NOT make the header/scrollable content
|
|
19
|
-
//
|
|
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
|
|
40
|
-
// to the literal-black/white-by-appearance formula described below when omitted, so
|
|
41
|
-
// existing caller (none of which pass these) renders identically to before. A caller with
|
|
42
|
-
// app-wide chrome palette (e.g. a tinted background rather than neutral black/white)
|
|
43
|
-
// whichever of the
|
|
44
|
-
//
|
|
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
|
|
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
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
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,
|
|
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
|
|
81
|
-
//
|
|
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.
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
},
|
package/src/ConfirmDialog.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
})
|