@tastic/hud 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/dist/chrome/index.d.mts +4 -2
- package/dist/chrome/index.d.ts +4 -2
- package/dist/chrome/index.js +33 -19
- package/dist/chrome/index.mjs +1 -1
- package/dist/{chunk-HGJKNLMD.mjs → chunk-DK3HOZC5.mjs} +24 -10
- package/dist/index.js +69 -55
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/AchievementCatalogSection.tsx +9 -2
- package/src/AchievementRow.tsx +4 -4
- package/src/BaseSettingsDialog.tsx +3 -1
- package/src/BaseStatsScreen.tsx +11 -3
- package/src/ConfirmDialog.tsx +25 -2
package/README.md
CHANGED
|
@@ -75,7 +75,10 @@ corner of the screen.
|
|
|
75
75
|
settings sheet, an achievements/stats screen, and a plain confirm-or-cancel dialog, each factored
|
|
76
76
|
out after being independently copied into several games. See Rotation below for how each of these
|
|
77
77
|
(plus `SectionedDropdown`/`InlineColorPicker`) handles rotating inside a `@tastic/split-screen`
|
|
78
|
-
zone.
|
|
78
|
+
zone. `BaseStatsScreen`'s header arrow is labeled 'Back' for screen readers (`backLabel` to change
|
|
79
|
+
it) and takes the same opt-in `hitSlop` as `CornerActionButtons`. On Android, back while a
|
|
80
|
+
`ConfirmDialog` is open calls its `onCancel` and goes no further (with no `onCancel`, it does
|
|
81
|
+
nothing), so it never reaches the screen underneath.
|
|
79
82
|
- **`OverlayCard`** / **`OverlayBackdrop`** / **`useOverlayCardColors`** - the card and dim behind
|
|
80
83
|
`ConfirmDialog` and the how-to-play guide, in the app's own theme colors and blurred when the
|
|
81
84
|
player's Blur setting allows it. See Overlay cards below.
|
|
@@ -470,7 +473,9 @@ row. Returns a `Fragment`, not a `View`: every call site renders the heading and
|
|
|
470
473
|
siblings inside `BaseStatsScreen`'s own gap-spaced `ScrollView` content, and a wrapping container
|
|
471
474
|
here would double up that spacing. Resolves each row's locked-vs-unlocked `badgeColor` itself
|
|
472
475
|
(`row.tierColor` once `row.unlockedAt` is set, else the locked color) rather than upstream, since
|
|
473
|
-
`getAchievementCatalogRows` deliberately leaves that choice to whatever renders the rows.
|
|
476
|
+
`getAchievementCatalogRows` deliberately leaves that choice to whatever renders the rows. In light
|
|
477
|
+
mode the check mark and progress bar are the tier color mixed half with black, since raw gold and
|
|
478
|
+
silver barely show on a white screen; the badge keeps the true tier color.
|
|
474
479
|
|
|
475
480
|
```ts
|
|
476
481
|
interface AchievementCatalogSectionProps {
|
package/dist/chrome/index.d.mts
CHANGED
|
@@ -2,7 +2,7 @@ import * as react from 'react';
|
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
import { AchievementCatalogRow } from '@tastic/achievements';
|
|
4
4
|
import { ViewRotation } from '@tastic/core';
|
|
5
|
-
import { StyleProp, ViewStyle,
|
|
5
|
+
import { Insets, StyleProp, ViewStyle, LayoutChangeEvent, TextStyle } from 'react-native';
|
|
6
6
|
|
|
7
7
|
interface Props$7 {
|
|
8
8
|
rows: AchievementCatalogRow[];
|
|
@@ -65,6 +65,8 @@ interface Props$4 {
|
|
|
65
65
|
title?: string;
|
|
66
66
|
titleVariant?: MD3TextVariant;
|
|
67
67
|
onBack: () => void;
|
|
68
|
+
backLabel?: string;
|
|
69
|
+
hitSlop?: number | Insets;
|
|
68
70
|
insets: {
|
|
69
71
|
top: number;
|
|
70
72
|
left: number;
|
|
@@ -79,7 +81,7 @@ interface Props$4 {
|
|
|
79
81
|
bg?: string;
|
|
80
82
|
children: ReactNode;
|
|
81
83
|
}
|
|
82
|
-
declare function BaseStatsScreen({ rotation: rotationOverride, title, titleVariant, onBack, insets, onReset, resetLabel, resetConfirmTitle, resetConfirmBody, fg: fgOverride, bg: bgOverride, children }: Props$4): react.JSX.Element;
|
|
84
|
+
declare function BaseStatsScreen({ rotation: rotationOverride, title, titleVariant, onBack, backLabel, hitSlop, insets, onReset, resetLabel, resetConfirmTitle, resetConfirmBody, fg: fgOverride, bg: bgOverride, children }: Props$4): react.JSX.Element;
|
|
83
85
|
|
|
84
86
|
interface ConfirmDialogProps {
|
|
85
87
|
visible: boolean;
|
package/dist/chrome/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as react from 'react';
|
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
import { AchievementCatalogRow } from '@tastic/achievements';
|
|
4
4
|
import { ViewRotation } from '@tastic/core';
|
|
5
|
-
import { StyleProp, ViewStyle,
|
|
5
|
+
import { Insets, StyleProp, ViewStyle, LayoutChangeEvent, TextStyle } from 'react-native';
|
|
6
6
|
|
|
7
7
|
interface Props$7 {
|
|
8
8
|
rows: AchievementCatalogRow[];
|
|
@@ -65,6 +65,8 @@ interface Props$4 {
|
|
|
65
65
|
title?: string;
|
|
66
66
|
titleVariant?: MD3TextVariant;
|
|
67
67
|
onBack: () => void;
|
|
68
|
+
backLabel?: string;
|
|
69
|
+
hitSlop?: number | Insets;
|
|
68
70
|
insets: {
|
|
69
71
|
top: number;
|
|
70
72
|
left: number;
|
|
@@ -79,7 +81,7 @@ interface Props$4 {
|
|
|
79
81
|
bg?: string;
|
|
80
82
|
children: ReactNode;
|
|
81
83
|
}
|
|
82
|
-
declare function BaseStatsScreen({ rotation: rotationOverride, title, titleVariant, onBack, insets, onReset, resetLabel, resetConfirmTitle, resetConfirmBody, fg: fgOverride, bg: bgOverride, children }: Props$4): react.JSX.Element;
|
|
84
|
+
declare function BaseStatsScreen({ rotation: rotationOverride, title, titleVariant, onBack, backLabel, hitSlop, insets, onReset, resetLabel, resetConfirmTitle, resetConfirmBody, fg: fgOverride, bg: bgOverride, children }: Props$4): react.JSX.Element;
|
|
83
85
|
|
|
84
86
|
interface ConfirmDialogProps {
|
|
85
87
|
visible: boolean;
|
package/dist/chrome/index.js
CHANGED
|
@@ -141,9 +141,10 @@ var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
|
141
141
|
function AchievementCatalogSection({ rows, label = "ALL ACHIEVEMENTS", fg, fgMuted: fgMutedOverride }) {
|
|
142
142
|
const { dark } = (0, import_auto_paper2.useAutoPaperTheme)();
|
|
143
143
|
const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
|
|
144
|
+
const tierAccent = (tierColor) => dark ? tierColor : (0, import_auto_paper2.getBlendedColor)("#000000", tierColor, 0.5);
|
|
144
145
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react.Fragment, { children: [
|
|
145
146
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper2.Text, { variant: "labelMedium", style: [styles2.label, { color: fgMuted, fontFamily: MONO_FONT }], children: label }),
|
|
146
|
-
rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(AchievementRow, { icon: row.icon, title: row.title, description: row.description, badgeColor: row.unlockedAt !== void 0 ? row.tierColor : LOCKED_BADGE_COLOR, checkColor: row.tierColor, unlockedLabel: row.unlockedLabel, progress: row.progress, deviceMarker: row.deviceMarker, fg, fgMuted: fgMutedOverride }, row.id))
|
|
147
|
+
rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(AchievementRow, { icon: row.icon, title: row.title, description: row.description, badgeColor: row.unlockedAt !== void 0 ? row.tierColor : LOCKED_BADGE_COLOR, checkColor: tierAccent(row.tierColor), unlockedLabel: row.unlockedLabel, progress: row.progress, deviceMarker: row.deviceMarker, fg, fgMuted: fgMutedOverride }, row.id))
|
|
147
148
|
] });
|
|
148
149
|
}
|
|
149
150
|
var styles2 = import_react_native3.StyleSheet.create({
|
|
@@ -223,7 +224,7 @@ var import_auto_paper8 = require("@rific/auto-paper");
|
|
|
223
224
|
var import_feedback_press2 = require("@rific/feedback-press");
|
|
224
225
|
var import_updater = require("@rific/updater");
|
|
225
226
|
var import_core4 = require("@tastic/core");
|
|
226
|
-
var
|
|
227
|
+
var import_react6 = require("react");
|
|
227
228
|
var import_react_native12 = require("react-native");
|
|
228
229
|
var import_react_native_paper8 = require("react-native-paper");
|
|
229
230
|
var import_react_native_safe_area_context2 = require("react-native-safe-area-context");
|
|
@@ -232,6 +233,7 @@ var import_react_native_safe_area_context2 = require("react-native-safe-area-con
|
|
|
232
233
|
var import_auto_paper7 = require("@rific/auto-paper");
|
|
233
234
|
var import_feedback_press = require("@rific/feedback-press");
|
|
234
235
|
var import_core2 = require("@tastic/core");
|
|
236
|
+
var import_react3 = require("react");
|
|
235
237
|
var import_react_native10 = require("react-native");
|
|
236
238
|
var import_react_native_paper6 = require("react-native-paper");
|
|
237
239
|
|
|
@@ -385,6 +387,18 @@ function ConfirmDialog({ visible, title, message, confirmLabel, cancelLabel, onC
|
|
|
385
387
|
const { colors } = (0, import_auto_paper7.useAutoPaperTheme)();
|
|
386
388
|
const { fg } = useOverlayCardColors();
|
|
387
389
|
const { compact, onLayout } = useCompactDialog(rotation);
|
|
390
|
+
const onCancelRef = (0, import_react3.useRef)(onCancel);
|
|
391
|
+
(0, import_react3.useEffect)(() => {
|
|
392
|
+
onCancelRef.current = onCancel;
|
|
393
|
+
});
|
|
394
|
+
(0, import_react3.useEffect)(() => {
|
|
395
|
+
if (!visible) return;
|
|
396
|
+
const subscription = import_react_native10.BackHandler.addEventListener("hardwareBackPress", () => {
|
|
397
|
+
onCancelRef.current?.();
|
|
398
|
+
return true;
|
|
399
|
+
});
|
|
400
|
+
return () => subscription.remove();
|
|
401
|
+
}, [visible]);
|
|
388
402
|
if (!visible) return null;
|
|
389
403
|
const accent = destructive ? colors.danger : colors.primary;
|
|
390
404
|
const onAccent = destructive ? colors.onDanger : colors.onPrimary;
|
|
@@ -421,7 +435,7 @@ var styles7 = import_react_native10.StyleSheet.create({
|
|
|
421
435
|
|
|
422
436
|
// src/RotatedFrame.tsx
|
|
423
437
|
var import_core3 = require("@tastic/core");
|
|
424
|
-
var
|
|
438
|
+
var import_react4 = require("react");
|
|
425
439
|
var import_react_native11 = require("react-native");
|
|
426
440
|
var import_react_native_paper7 = require("react-native-paper");
|
|
427
441
|
var import_react_native_safe_area_context = require("react-native-safe-area-context");
|
|
@@ -432,7 +446,7 @@ function RotatedFrame({ children, rotation: rotationOverride, rotatedSafeArea =
|
|
|
432
446
|
const window = (0, import_react_native11.useWindowDimensions)();
|
|
433
447
|
const { width, height } = (0, import_core3.rotateDimensions)(window.width, window.height, rotation);
|
|
434
448
|
const rawInsets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
|
|
435
|
-
const insets = (0,
|
|
449
|
+
const insets = (0, import_react4.useMemo)(() => rotatedSafeArea ? (0, import_core3.rotateInsets)(rawInsets, rotation) : rawInsets, [rotatedSafeArea, rawInsets, rotation]);
|
|
436
450
|
const cappedWidth = maxContentWidth !== void 0 && width > height ? maxContentWidth : void 0;
|
|
437
451
|
const column = /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native11.View, { style: [styles8.column, cappedWidth !== void 0 && { maxWidth: cappedWidth }], pointerEvents: passThrough ? "box-none" : void 0, children });
|
|
438
452
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_core3.FakeLandscapeView, { rotation, passThrough, style: styles8.root, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_safe_area_context.SafeAreaInsetsContext.Provider, { value: insets, children: portalHost ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper7.Portal.Host, { children: column }) : column }) });
|
|
@@ -450,18 +464,18 @@ var styles8 = import_react_native11.StyleSheet.create({
|
|
|
450
464
|
});
|
|
451
465
|
|
|
452
466
|
// src/useUpdateConfirmBridge.ts
|
|
453
|
-
var
|
|
467
|
+
var import_react5 = require("react");
|
|
454
468
|
function useUpdateConfirmBridge() {
|
|
455
|
-
const [manifest, setManifest] = (0,
|
|
456
|
-
const resolveRef = (0,
|
|
457
|
-
const onConfirm = (0,
|
|
469
|
+
const [manifest, setManifest] = (0, import_react5.useState)(null);
|
|
470
|
+
const resolveRef = (0, import_react5.useRef)(null);
|
|
471
|
+
const onConfirm = (0, import_react5.useCallback)(
|
|
458
472
|
(next) => new Promise((resolve) => {
|
|
459
473
|
resolveRef.current = resolve;
|
|
460
474
|
setManifest(next);
|
|
461
475
|
}),
|
|
462
476
|
[]
|
|
463
477
|
);
|
|
464
|
-
const respond = (0,
|
|
478
|
+
const respond = (0, import_react5.useCallback)((confirmed) => {
|
|
465
479
|
setManifest(null);
|
|
466
480
|
resolveRef.current?.(confirmed);
|
|
467
481
|
resolveRef.current = null;
|
|
@@ -486,11 +500,11 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
|
|
|
486
500
|
const isTouchPrimary = (0, import_core4.useIsTouchPrimaryDevice)();
|
|
487
501
|
const { settings: hapticSettings, set: setHapticSettings } = (0, import_feedback_press2.useHapticSettings)();
|
|
488
502
|
const { settings: soundSettings, set: setSoundSettings } = (0, import_feedback_press2.useSoundSettings)();
|
|
489
|
-
const sound = (0,
|
|
503
|
+
const sound = (0, import_react6.useContext)(import_feedback_press2.SoundContext);
|
|
490
504
|
const { forceShort: forceHapticFeedback } = (0, import_feedback_press2.useVibration)();
|
|
491
505
|
const { settings: themeSettings, set: setThemeSettings } = (0, import_auto_paper8.useThemeSettings)();
|
|
492
506
|
const expoBlur = (0, import_auto_paper8.useBlurModule)();
|
|
493
|
-
const [infoMessage, setInfoMessage] = (0,
|
|
507
|
+
const [infoMessage, setInfoMessage] = (0, import_react6.useState)(null);
|
|
494
508
|
const { manifest: updateManifest, onConfirm: onUpdateConfirm, respond: respondToUpdateConfirm } = useUpdateConfirmBridge();
|
|
495
509
|
const { check, checking, updateReady } = (0, import_updater.useUpdater)({
|
|
496
510
|
autoCheck: false,
|
|
@@ -500,7 +514,7 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
|
|
|
500
514
|
onInfo: (title, message) => setInfoMessage({ title, message })
|
|
501
515
|
});
|
|
502
516
|
const updateManifestDate = updateManifest ? new Date(updateManifest.createdAt) : null;
|
|
503
|
-
const [opened, setOpened] = (0,
|
|
517
|
+
const [opened, setOpened] = (0, import_react6.useState)(visible);
|
|
504
518
|
if (visible && !opened) setOpened(true);
|
|
505
519
|
const showLockOrientation = isTouchPrimary && lockOrientation !== void 0 && onLockOrientationChange !== void 0;
|
|
506
520
|
const showEdgeGuard = import_react_native12.Platform.OS === "ios" && deferBottomEdgeGestures !== void 0 && onDeferBottomEdgeGestures !== void 0;
|
|
@@ -618,7 +632,7 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
|
|
|
618
632
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native12.View, { style: [overlayActionStyles.actions, styles9.footer], children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_feedback_press2.Button, { mode: "contained", onPress: onDismiss, style: overlayActionStyles.actionButtonSingle, buttonColor: colors.primary, textColor: colors.onPrimary, children: "Done" }) })
|
|
619
633
|
] }),
|
|
620
634
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ConfirmDialog, { visible: !!updateManifest, title: "Update Available", message: updateManifestDate ? `Released ${updateManifestDate.toLocaleDateString()} at ${updateManifestDate.toLocaleTimeString()}` : "", confirmLabel: "Restart", cancelLabel: "Later", icon: "update", rotation: 0, onConfirm: () => respondToUpdateConfirm(true), onCancel: () => respondToUpdateConfirm(false) }),
|
|
621
|
-
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ConfirmDialog, { visible: !!infoMessage, title: infoMessage?.title ?? "", message: infoMessage?.message ?? "", confirmLabel: "OK", icon: "information-outline", rotation: 0, onConfirm: () => setInfoMessage(null) })
|
|
635
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ConfirmDialog, { visible: !!infoMessage, title: infoMessage?.title ?? "", message: infoMessage?.message ?? "", confirmLabel: "OK", icon: "information-outline", rotation: 0, onConfirm: () => setInfoMessage(null), onCancel: () => setInfoMessage(null) })
|
|
622
636
|
] }) });
|
|
623
637
|
}
|
|
624
638
|
var SHEET_GUTTER = 24;
|
|
@@ -709,21 +723,21 @@ var styles9 = import_react_native12.StyleSheet.create({
|
|
|
709
723
|
var import_auto_paper9 = require("@rific/auto-paper");
|
|
710
724
|
var import_feedback_press3 = require("@rific/feedback-press");
|
|
711
725
|
var import_core5 = require("@tastic/core");
|
|
712
|
-
var
|
|
726
|
+
var import_react7 = require("react");
|
|
713
727
|
var import_react_native13 = require("react-native");
|
|
714
728
|
var import_react_native_paper9 = require("react-native-paper");
|
|
715
729
|
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
716
|
-
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 }) {
|
|
730
|
+
function BaseStatsScreen({ rotation: rotationOverride, title = "Achievements", titleVariant = "headlineSmall", onBack, backLabel = "Back", hitSlop, 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 }) {
|
|
717
731
|
const { dark, colors } = (0, import_auto_paper9.useAutoPaperTheme)();
|
|
718
732
|
const ambientRotation = (0, import_core5.useRotation)();
|
|
719
733
|
const rotation = rotationOverride ?? ambientRotation;
|
|
720
|
-
const [confirmResetVisible, setConfirmResetVisible] = (0,
|
|
734
|
+
const [confirmResetVisible, setConfirmResetVisible] = (0, import_react7.useState)(false);
|
|
721
735
|
const showReset = !!onReset;
|
|
722
736
|
const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
|
|
723
737
|
const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
|
|
724
738
|
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native13.View, { style: [styles10.container, { backgroundColor: bg }], children: [
|
|
725
739
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native13.View, { style: [styles10.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
|
|
726
|
-
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_feedback_press3.IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: onBack }),
|
|
740
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_feedback_press3.IconButton, { icon: "arrow-left", iconColor: fg, size: 24, hitSlop, onPress: onBack, accessibilityLabel: backLabel }),
|
|
727
741
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native_paper9.Text, { variant: titleVariant, style: [styles10.title, { color: fg }], children: title })
|
|
728
742
|
] }),
|
|
729
743
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native13.ScrollView, { style: styles10.scrollView, contentContainerStyle: [styles10.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, children: [
|
|
@@ -824,7 +838,7 @@ var styles12 = import_react_native15.StyleSheet.create({
|
|
|
824
838
|
});
|
|
825
839
|
|
|
826
840
|
// src/StaggeredWord.tsx
|
|
827
|
-
var
|
|
841
|
+
var import_react8 = require("react");
|
|
828
842
|
var import_react_native16 = require("react-native");
|
|
829
843
|
var import_react_native_reanimated = __toESM(require("react-native-reanimated"));
|
|
830
844
|
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
@@ -835,7 +849,7 @@ function getStaggeredWordDuration(letterCount) {
|
|
|
835
849
|
}
|
|
836
850
|
function AnimatedLetter({ char, index, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }) {
|
|
837
851
|
const progress = (0, import_react_native_reanimated.useSharedValue)(reducedMotion ? 1 : 0);
|
|
838
|
-
(0,
|
|
852
|
+
(0, import_react8.useEffect)(() => {
|
|
839
853
|
if (reducedMotion) return;
|
|
840
854
|
progress.value = (0, import_react_native_reanimated.withDelay)(index * STAGGER_MS, (0, import_react_native_reanimated.withTiming)(1, { duration: LETTER_DURATION_MS, easing: import_react_native_reanimated.Easing.out(import_react_native_reanimated.Easing.back(1.5)) }));
|
|
841
855
|
}, [reducedMotion]);
|
package/dist/chrome/index.mjs
CHANGED
|
@@ -81,7 +81,7 @@ import { Platform } from "react-native";
|
|
|
81
81
|
var MONO_FONT = Platform.select({ ios: "Menlo", android: "monospace", default: "monospace" });
|
|
82
82
|
|
|
83
83
|
// src/AchievementCatalogSection.tsx
|
|
84
|
-
import { useAutoPaperTheme as useAutoPaperTheme2 } from "@rific/auto-paper";
|
|
84
|
+
import { getBlendedColor, useAutoPaperTheme as useAutoPaperTheme2 } from "@rific/auto-paper";
|
|
85
85
|
import { Fragment } from "react";
|
|
86
86
|
import { StyleSheet as StyleSheet2 } from "react-native";
|
|
87
87
|
import { Text as Text2 } from "react-native-paper";
|
|
@@ -89,9 +89,10 @@ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
|
89
89
|
function AchievementCatalogSection({ rows, label = "ALL ACHIEVEMENTS", fg, fgMuted: fgMutedOverride }) {
|
|
90
90
|
const { dark } = useAutoPaperTheme2();
|
|
91
91
|
const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
|
|
92
|
+
const tierAccent = (tierColor) => dark ? tierColor : getBlendedColor("#000000", tierColor, 0.5);
|
|
92
93
|
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
93
94
|
/* @__PURE__ */ jsx2(Text2, { variant: "labelMedium", style: [styles2.label, { color: fgMuted, fontFamily: MONO_FONT }], children: label }),
|
|
94
|
-
rows.map((row) => /* @__PURE__ */ jsx2(AchievementRow, { icon: row.icon, title: row.title, description: row.description, badgeColor: row.unlockedAt !== void 0 ? row.tierColor : LOCKED_BADGE_COLOR, checkColor: row.tierColor, unlockedLabel: row.unlockedLabel, progress: row.progress, deviceMarker: row.deviceMarker, fg, fgMuted: fgMutedOverride }, row.id))
|
|
95
|
+
rows.map((row) => /* @__PURE__ */ jsx2(AchievementRow, { icon: row.icon, title: row.title, description: row.description, badgeColor: row.unlockedAt !== void 0 ? row.tierColor : LOCKED_BADGE_COLOR, checkColor: tierAccent(row.tierColor), unlockedLabel: row.unlockedLabel, progress: row.progress, deviceMarker: row.deviceMarker, fg, fgMuted: fgMutedOverride }, row.id))
|
|
95
96
|
] });
|
|
96
97
|
}
|
|
97
98
|
var styles2 = StyleSheet2.create({
|
|
@@ -187,13 +188,26 @@ function useCompactDialog(rotation = 0) {
|
|
|
187
188
|
import { useAutoPaperTheme as useAutoPaperTheme5 } from "@rific/auto-paper";
|
|
188
189
|
import { Button } from "@rific/feedback-press";
|
|
189
190
|
import { toRotationStyle } from "@tastic/core";
|
|
190
|
-
import {
|
|
191
|
+
import { useEffect, useRef } from "react";
|
|
192
|
+
import { BackHandler, StyleSheet as StyleSheet5, View as View4 } from "react-native";
|
|
191
193
|
import { Portal, Text as Text5 } from "react-native-paper";
|
|
192
194
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
193
195
|
function ConfirmDialog({ visible, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, rotation = 0, icon = "help-circle-outline", destructive = false }) {
|
|
194
196
|
const { colors } = useAutoPaperTheme5();
|
|
195
197
|
const { fg } = useOverlayCardColors();
|
|
196
198
|
const { compact, onLayout } = useCompactDialog(rotation);
|
|
199
|
+
const onCancelRef = useRef(onCancel);
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
onCancelRef.current = onCancel;
|
|
202
|
+
});
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
if (!visible) return;
|
|
205
|
+
const subscription = BackHandler.addEventListener("hardwareBackPress", () => {
|
|
206
|
+
onCancelRef.current?.();
|
|
207
|
+
return true;
|
|
208
|
+
});
|
|
209
|
+
return () => subscription.remove();
|
|
210
|
+
}, [visible]);
|
|
197
211
|
if (!visible) return null;
|
|
198
212
|
const accent = destructive ? colors.danger : colors.primary;
|
|
199
213
|
const onAccent = destructive ? colors.onDanger : colors.onPrimary;
|
|
@@ -269,10 +283,10 @@ import { Icon as Icon2, Portal as Portal3, Text as Text6 } from "react-native-pa
|
|
|
269
283
|
import { useSafeAreaInsets as useSafeAreaInsets2 } from "react-native-safe-area-context";
|
|
270
284
|
|
|
271
285
|
// src/useUpdateConfirmBridge.ts
|
|
272
|
-
import { useCallback as useCallback2, useRef, useState as useState2 } from "react";
|
|
286
|
+
import { useCallback as useCallback2, useRef as useRef2, useState as useState2 } from "react";
|
|
273
287
|
function useUpdateConfirmBridge() {
|
|
274
288
|
const [manifest, setManifest] = useState2(null);
|
|
275
|
-
const resolveRef =
|
|
289
|
+
const resolveRef = useRef2(null);
|
|
276
290
|
const onConfirm = useCallback2(
|
|
277
291
|
(next) => new Promise((resolve) => {
|
|
278
292
|
resolveRef.current = resolve;
|
|
@@ -437,7 +451,7 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
|
|
|
437
451
|
/* @__PURE__ */ jsx8(View6, { style: [overlayActionStyles.actions, styles7.footer], children: /* @__PURE__ */ jsx8(Button2, { mode: "contained", onPress: onDismiss, style: overlayActionStyles.actionButtonSingle, buttonColor: colors.primary, textColor: colors.onPrimary, children: "Done" }) })
|
|
438
452
|
] }),
|
|
439
453
|
/* @__PURE__ */ jsx8(ConfirmDialog, { visible: !!updateManifest, title: "Update Available", message: updateManifestDate ? `Released ${updateManifestDate.toLocaleDateString()} at ${updateManifestDate.toLocaleTimeString()}` : "", confirmLabel: "Restart", cancelLabel: "Later", icon: "update", rotation: 0, onConfirm: () => respondToUpdateConfirm(true), onCancel: () => respondToUpdateConfirm(false) }),
|
|
440
|
-
/* @__PURE__ */ jsx8(ConfirmDialog, { visible: !!infoMessage, title: infoMessage?.title ?? "", message: infoMessage?.message ?? "", confirmLabel: "OK", icon: "information-outline", rotation: 0, onConfirm: () => setInfoMessage(null) })
|
|
454
|
+
/* @__PURE__ */ jsx8(ConfirmDialog, { visible: !!infoMessage, title: infoMessage?.title ?? "", message: infoMessage?.message ?? "", confirmLabel: "OK", icon: "information-outline", rotation: 0, onConfirm: () => setInfoMessage(null), onCancel: () => setInfoMessage(null) })
|
|
441
455
|
] }) });
|
|
442
456
|
}
|
|
443
457
|
var SHEET_GUTTER = 24;
|
|
@@ -532,7 +546,7 @@ import { useState as useState4 } from "react";
|
|
|
532
546
|
import { ScrollView, StyleSheet as StyleSheet8, View as View7 } from "react-native";
|
|
533
547
|
import { Text as Text7 } from "react-native-paper";
|
|
534
548
|
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
535
|
-
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 }) {
|
|
549
|
+
function BaseStatsScreen({ rotation: rotationOverride, title = "Achievements", titleVariant = "headlineSmall", onBack, backLabel = "Back", hitSlop, 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 }) {
|
|
536
550
|
const { dark, colors } = useAutoPaperTheme7();
|
|
537
551
|
const ambientRotation = useRotation3();
|
|
538
552
|
const rotation = rotationOverride ?? ambientRotation;
|
|
@@ -542,7 +556,7 @@ function BaseStatsScreen({ rotation: rotationOverride, title = "Achievements", t
|
|
|
542
556
|
const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
|
|
543
557
|
return /* @__PURE__ */ jsxs8(View7, { style: [styles8.container, { backgroundColor: bg }], children: [
|
|
544
558
|
/* @__PURE__ */ jsxs8(View7, { style: [styles8.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
|
|
545
|
-
/* @__PURE__ */ jsx9(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: onBack }),
|
|
559
|
+
/* @__PURE__ */ jsx9(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, hitSlop, onPress: onBack, accessibilityLabel: backLabel }),
|
|
546
560
|
/* @__PURE__ */ jsx9(Text7, { variant: titleVariant, style: [styles8.title, { color: fg }], children: title })
|
|
547
561
|
] }),
|
|
548
562
|
/* @__PURE__ */ jsxs8(ScrollView, { style: styles8.scrollView, contentContainerStyle: [styles8.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, children: [
|
|
@@ -643,7 +657,7 @@ var styles10 = StyleSheet10.create({
|
|
|
643
657
|
});
|
|
644
658
|
|
|
645
659
|
// src/StaggeredWord.tsx
|
|
646
|
-
import { useEffect } from "react";
|
|
660
|
+
import { useEffect as useEffect2 } from "react";
|
|
647
661
|
import { StyleSheet as StyleSheet11, View as View9 } from "react-native";
|
|
648
662
|
import Animated, { Easing, useAnimatedStyle, useSharedValue, withDelay, withTiming } from "react-native-reanimated";
|
|
649
663
|
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
@@ -654,7 +668,7 @@ function getStaggeredWordDuration(letterCount) {
|
|
|
654
668
|
}
|
|
655
669
|
function AnimatedLetter({ char, index, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }) {
|
|
656
670
|
const progress = useSharedValue(reducedMotion ? 1 : 0);
|
|
657
|
-
|
|
671
|
+
useEffect2(() => {
|
|
658
672
|
if (reducedMotion) return;
|
|
659
673
|
progress.value = withDelay(index * STAGGER_MS, withTiming(1, { duration: LETTER_DURATION_MS, easing: Easing.out(Easing.back(1.5)) }));
|
|
660
674
|
}, [reducedMotion]);
|
package/dist/index.js
CHANGED
|
@@ -38,22 +38,22 @@ __export(TriggerGauge_exports, {
|
|
|
38
38
|
function TriggerGauge({ segments, litIndices, size, accentColor, mutedColor, dashWidth, startDeg = DEFAULT_START_DEG, endDeg = startDeg }) {
|
|
39
39
|
const totalDurationMs = MOUNT_DURATION_MS + Math.max(0, segments - 1) * MOUNT_STAGGER_MS;
|
|
40
40
|
const mountProgress = (0, import_react_native_reanimated.useSharedValue)(0);
|
|
41
|
-
(0,
|
|
41
|
+
(0, import_react9.useLayoutEffect)(() => {
|
|
42
42
|
mountProgress.value = (0, import_react_native_reanimated.withTiming)(1, { duration: totalDurationMs, easing: import_react_native_reanimated.Easing.out(import_react_native_reanimated.Easing.cubic) });
|
|
43
43
|
}, []);
|
|
44
44
|
const rawSweepDeg = ((endDeg - startDeg) % 360 + 360) % 360;
|
|
45
45
|
const totalSweepDeg = rawSweepDeg === 0 ? 360 : rawSweepDeg;
|
|
46
46
|
const step = segments > 0 ? totalSweepDeg / segments : 0;
|
|
47
|
-
const centerDeg = (0,
|
|
47
|
+
const centerDeg = (0, import_react9.useCallback)((i) => startDeg + (i + 0.5) * step, [startDeg, step]);
|
|
48
48
|
const singleSelect = litIndices.length === 1;
|
|
49
49
|
const angleDeg = (0, import_react_native_reanimated.useSharedValue)(centerDeg(litIndices[0] ?? 0));
|
|
50
50
|
const litIndicesKey = [...litIndices].sort((a, b) => a - b).join(",");
|
|
51
|
-
const [transitionFrom, setTransitionFrom] = (0,
|
|
51
|
+
const [transitionFrom, setTransitionFrom] = (0, import_react9.useState)(litIndices);
|
|
52
52
|
const transitionFromKey = [...transitionFrom].sort((a, b) => a - b).join(",");
|
|
53
|
-
const lastKeyRef = (0,
|
|
54
|
-
const lastIndicesRef = (0,
|
|
53
|
+
const lastKeyRef = (0, import_react9.useRef)(litIndicesKey);
|
|
54
|
+
const lastIndicesRef = (0, import_react9.useRef)(litIndices);
|
|
55
55
|
const selectionProgress = (0, import_react_native_reanimated.useSharedValue)(1);
|
|
56
|
-
(0,
|
|
56
|
+
(0, import_react9.useLayoutEffect)(() => {
|
|
57
57
|
if (litIndicesKey !== lastKeyRef.current) {
|
|
58
58
|
if (singleSelect && lastIndicesRef.current.length === 1) {
|
|
59
59
|
angleDeg.value = (0, import_react_native_reanimated.withTiming)(centerDeg(litIndices[0]), { duration: SELECTION_TRANSITION_DURATION_MS, easing: import_react_native_reanimated.Easing.out(import_react_native_reanimated.Easing.cubic) });
|
|
@@ -135,13 +135,13 @@ function TriggerGauge({ segments, litIndices, size, accentColor, mutedColor, das
|
|
|
135
135
|
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_skia.Path, { path: litPath, style: "stroke", strokeWidth: LIT_THICKNESS, strokeCap: "round", color: accentColor, opacity: 1 })
|
|
136
136
|
] }) });
|
|
137
137
|
}
|
|
138
|
-
var import_react_native_skia, import_core7,
|
|
138
|
+
var import_react_native_skia, import_core7, import_react9, import_react_native17, import_react_native_reanimated, import_jsx_runtime16, UNLIT_THICKNESS, LIT_THICKNESS, DASH_GAP_PX, ARC_GAP, DEFAULT_START_DEG, MIN_DASH_ANGLE_DEG, MOUNT_DURATION_MS, MOUNT_STAGGER_MS, SELECTION_TRANSITION_DURATION_MS, styles14;
|
|
139
139
|
var init_TriggerGauge = __esm({
|
|
140
140
|
"src/TriggerGauge.tsx"() {
|
|
141
141
|
"use strict";
|
|
142
142
|
import_react_native_skia = require("@shopify/react-native-skia");
|
|
143
143
|
import_core7 = require("@tastic/core");
|
|
144
|
-
|
|
144
|
+
import_react9 = require("react");
|
|
145
145
|
import_react_native17 = require("react-native");
|
|
146
146
|
import_react_native_reanimated = require("react-native-reanimated");
|
|
147
147
|
import_jsx_runtime16 = require("react/jsx-runtime");
|
|
@@ -301,9 +301,10 @@ var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
|
301
301
|
function AchievementCatalogSection({ rows, label = "ALL ACHIEVEMENTS", fg, fgMuted: fgMutedOverride }) {
|
|
302
302
|
const { dark } = (0, import_auto_paper2.useAutoPaperTheme)();
|
|
303
303
|
const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
|
|
304
|
+
const tierAccent = (tierColor) => dark ? tierColor : (0, import_auto_paper2.getBlendedColor)("#000000", tierColor, 0.5);
|
|
304
305
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react.Fragment, { children: [
|
|
305
306
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper2.Text, { variant: "labelMedium", style: [styles2.label, { color: fgMuted, fontFamily: MONO_FONT }], children: label }),
|
|
306
|
-
rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(AchievementRow, { icon: row.icon, title: row.title, description: row.description, badgeColor: row.unlockedAt !== void 0 ? row.tierColor : LOCKED_BADGE_COLOR, checkColor: row.tierColor, unlockedLabel: row.unlockedLabel, progress: row.progress, deviceMarker: row.deviceMarker, fg, fgMuted: fgMutedOverride }, row.id))
|
|
307
|
+
rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(AchievementRow, { icon: row.icon, title: row.title, description: row.description, badgeColor: row.unlockedAt !== void 0 ? row.tierColor : LOCKED_BADGE_COLOR, checkColor: tierAccent(row.tierColor), unlockedLabel: row.unlockedLabel, progress: row.progress, deviceMarker: row.deviceMarker, fg, fgMuted: fgMutedOverride }, row.id))
|
|
307
308
|
] });
|
|
308
309
|
}
|
|
309
310
|
var styles2 = import_react_native3.StyleSheet.create({
|
|
@@ -427,7 +428,7 @@ var import_auto_paper9 = require("@rific/auto-paper");
|
|
|
427
428
|
var import_feedback_press2 = require("@rific/feedback-press");
|
|
428
429
|
var import_updater = require("@rific/updater");
|
|
429
430
|
var import_core4 = require("@tastic/core");
|
|
430
|
-
var
|
|
431
|
+
var import_react6 = require("react");
|
|
431
432
|
var import_react_native13 = require("react-native");
|
|
432
433
|
var import_react_native_paper9 = require("react-native-paper");
|
|
433
434
|
var import_react_native_safe_area_context2 = require("react-native-safe-area-context");
|
|
@@ -436,6 +437,7 @@ var import_react_native_safe_area_context2 = require("react-native-safe-area-con
|
|
|
436
437
|
var import_auto_paper8 = require("@rific/auto-paper");
|
|
437
438
|
var import_feedback_press = require("@rific/feedback-press");
|
|
438
439
|
var import_core2 = require("@tastic/core");
|
|
440
|
+
var import_react3 = require("react");
|
|
439
441
|
var import_react_native11 = require("react-native");
|
|
440
442
|
var import_react_native_paper7 = require("react-native-paper");
|
|
441
443
|
|
|
@@ -589,6 +591,18 @@ function ConfirmDialog({ visible, title, message, confirmLabel, cancelLabel, onC
|
|
|
589
591
|
const { colors } = (0, import_auto_paper8.useAutoPaperTheme)();
|
|
590
592
|
const { fg } = useOverlayCardColors();
|
|
591
593
|
const { compact, onLayout } = useCompactDialog(rotation);
|
|
594
|
+
const onCancelRef = (0, import_react3.useRef)(onCancel);
|
|
595
|
+
(0, import_react3.useEffect)(() => {
|
|
596
|
+
onCancelRef.current = onCancel;
|
|
597
|
+
});
|
|
598
|
+
(0, import_react3.useEffect)(() => {
|
|
599
|
+
if (!visible) return;
|
|
600
|
+
const subscription = import_react_native11.BackHandler.addEventListener("hardwareBackPress", () => {
|
|
601
|
+
onCancelRef.current?.();
|
|
602
|
+
return true;
|
|
603
|
+
});
|
|
604
|
+
return () => subscription.remove();
|
|
605
|
+
}, [visible]);
|
|
592
606
|
if (!visible) return null;
|
|
593
607
|
const accent = destructive ? colors.danger : colors.primary;
|
|
594
608
|
const onAccent = destructive ? colors.onDanger : colors.onPrimary;
|
|
@@ -625,7 +639,7 @@ var styles8 = import_react_native11.StyleSheet.create({
|
|
|
625
639
|
|
|
626
640
|
// src/RotatedFrame.tsx
|
|
627
641
|
var import_core3 = require("@tastic/core");
|
|
628
|
-
var
|
|
642
|
+
var import_react4 = require("react");
|
|
629
643
|
var import_react_native12 = require("react-native");
|
|
630
644
|
var import_react_native_paper8 = require("react-native-paper");
|
|
631
645
|
var import_react_native_safe_area_context = require("react-native-safe-area-context");
|
|
@@ -636,7 +650,7 @@ function RotatedFrame({ children, rotation: rotationOverride, rotatedSafeArea =
|
|
|
636
650
|
const window = (0, import_react_native12.useWindowDimensions)();
|
|
637
651
|
const { width, height } = (0, import_core3.rotateDimensions)(window.width, window.height, rotation);
|
|
638
652
|
const rawInsets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
|
|
639
|
-
const insets = (0,
|
|
653
|
+
const insets = (0, import_react4.useMemo)(() => rotatedSafeArea ? (0, import_core3.rotateInsets)(rawInsets, rotation) : rawInsets, [rotatedSafeArea, rawInsets, rotation]);
|
|
640
654
|
const cappedWidth = maxContentWidth !== void 0 && width > height ? maxContentWidth : void 0;
|
|
641
655
|
const column = /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native12.View, { style: [styles9.column, cappedWidth !== void 0 && { maxWidth: cappedWidth }], pointerEvents: passThrough ? "box-none" : void 0, children });
|
|
642
656
|
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_core3.FakeLandscapeView, { rotation, passThrough, style: styles9.root, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native_safe_area_context.SafeAreaInsetsContext.Provider, { value: insets, children: portalHost ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native_paper8.Portal.Host, { children: column }) : column }) });
|
|
@@ -654,18 +668,18 @@ var styles9 = import_react_native12.StyleSheet.create({
|
|
|
654
668
|
});
|
|
655
669
|
|
|
656
670
|
// src/useUpdateConfirmBridge.ts
|
|
657
|
-
var
|
|
671
|
+
var import_react5 = require("react");
|
|
658
672
|
function useUpdateConfirmBridge() {
|
|
659
|
-
const [manifest, setManifest] = (0,
|
|
660
|
-
const resolveRef = (0,
|
|
661
|
-
const onConfirm = (0,
|
|
673
|
+
const [manifest, setManifest] = (0, import_react5.useState)(null);
|
|
674
|
+
const resolveRef = (0, import_react5.useRef)(null);
|
|
675
|
+
const onConfirm = (0, import_react5.useCallback)(
|
|
662
676
|
(next) => new Promise((resolve) => {
|
|
663
677
|
resolveRef.current = resolve;
|
|
664
678
|
setManifest(next);
|
|
665
679
|
}),
|
|
666
680
|
[]
|
|
667
681
|
);
|
|
668
|
-
const respond = (0,
|
|
682
|
+
const respond = (0, import_react5.useCallback)((confirmed) => {
|
|
669
683
|
setManifest(null);
|
|
670
684
|
resolveRef.current?.(confirmed);
|
|
671
685
|
resolveRef.current = null;
|
|
@@ -690,11 +704,11 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
|
|
|
690
704
|
const isTouchPrimary = (0, import_core4.useIsTouchPrimaryDevice)();
|
|
691
705
|
const { settings: hapticSettings, set: setHapticSettings } = (0, import_feedback_press2.useHapticSettings)();
|
|
692
706
|
const { settings: soundSettings, set: setSoundSettings } = (0, import_feedback_press2.useSoundSettings)();
|
|
693
|
-
const sound = (0,
|
|
707
|
+
const sound = (0, import_react6.useContext)(import_feedback_press2.SoundContext);
|
|
694
708
|
const { forceShort: forceHapticFeedback } = (0, import_feedback_press2.useVibration)();
|
|
695
709
|
const { settings: themeSettings, set: setThemeSettings } = (0, import_auto_paper9.useThemeSettings)();
|
|
696
710
|
const expoBlur = (0, import_auto_paper9.useBlurModule)();
|
|
697
|
-
const [infoMessage, setInfoMessage] = (0,
|
|
711
|
+
const [infoMessage, setInfoMessage] = (0, import_react6.useState)(null);
|
|
698
712
|
const { manifest: updateManifest, onConfirm: onUpdateConfirm, respond: respondToUpdateConfirm } = useUpdateConfirmBridge();
|
|
699
713
|
const { check, checking, updateReady } = (0, import_updater.useUpdater)({
|
|
700
714
|
autoCheck: false,
|
|
@@ -704,7 +718,7 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
|
|
|
704
718
|
onInfo: (title, message) => setInfoMessage({ title, message })
|
|
705
719
|
});
|
|
706
720
|
const updateManifestDate = updateManifest ? new Date(updateManifest.createdAt) : null;
|
|
707
|
-
const [opened, setOpened] = (0,
|
|
721
|
+
const [opened, setOpened] = (0, import_react6.useState)(visible);
|
|
708
722
|
if (visible && !opened) setOpened(true);
|
|
709
723
|
const showLockOrientation = isTouchPrimary && lockOrientation !== void 0 && onLockOrientationChange !== void 0;
|
|
710
724
|
const showEdgeGuard = import_react_native13.Platform.OS === "ios" && deferBottomEdgeGestures !== void 0 && onDeferBottomEdgeGestures !== void 0;
|
|
@@ -822,7 +836,7 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
|
|
|
822
836
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native13.View, { style: [overlayActionStyles.actions, styles10.footer], children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_feedback_press2.Button, { mode: "contained", onPress: onDismiss, style: overlayActionStyles.actionButtonSingle, buttonColor: colors.primary, textColor: colors.onPrimary, children: "Done" }) })
|
|
823
837
|
] }),
|
|
824
838
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ConfirmDialog, { visible: !!updateManifest, title: "Update Available", message: updateManifestDate ? `Released ${updateManifestDate.toLocaleDateString()} at ${updateManifestDate.toLocaleTimeString()}` : "", confirmLabel: "Restart", cancelLabel: "Later", icon: "update", rotation: 0, onConfirm: () => respondToUpdateConfirm(true), onCancel: () => respondToUpdateConfirm(false) }),
|
|
825
|
-
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ConfirmDialog, { visible: !!infoMessage, title: infoMessage?.title ?? "", message: infoMessage?.message ?? "", confirmLabel: "OK", icon: "information-outline", rotation: 0, onConfirm: () => setInfoMessage(null) })
|
|
839
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ConfirmDialog, { visible: !!infoMessage, title: infoMessage?.title ?? "", message: infoMessage?.message ?? "", confirmLabel: "OK", icon: "information-outline", rotation: 0, onConfirm: () => setInfoMessage(null), onCancel: () => setInfoMessage(null) })
|
|
826
840
|
] }) });
|
|
827
841
|
}
|
|
828
842
|
var SHEET_GUTTER = 24;
|
|
@@ -913,21 +927,21 @@ var styles10 = import_react_native13.StyleSheet.create({
|
|
|
913
927
|
var import_auto_paper10 = require("@rific/auto-paper");
|
|
914
928
|
var import_feedback_press3 = require("@rific/feedback-press");
|
|
915
929
|
var import_core5 = require("@tastic/core");
|
|
916
|
-
var
|
|
930
|
+
var import_react7 = require("react");
|
|
917
931
|
var import_react_native14 = require("react-native");
|
|
918
932
|
var import_react_native_paper10 = require("react-native-paper");
|
|
919
933
|
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
920
|
-
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 }) {
|
|
934
|
+
function BaseStatsScreen({ rotation: rotationOverride, title = "Achievements", titleVariant = "headlineSmall", onBack, backLabel = "Back", hitSlop, 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 }) {
|
|
921
935
|
const { dark, colors } = (0, import_auto_paper10.useAutoPaperTheme)();
|
|
922
936
|
const ambientRotation = (0, import_core5.useRotation)();
|
|
923
937
|
const rotation = rotationOverride ?? ambientRotation;
|
|
924
|
-
const [confirmResetVisible, setConfirmResetVisible] = (0,
|
|
938
|
+
const [confirmResetVisible, setConfirmResetVisible] = (0, import_react7.useState)(false);
|
|
925
939
|
const showReset = !!onReset;
|
|
926
940
|
const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
|
|
927
941
|
const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
|
|
928
942
|
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native14.View, { style: [styles11.container, { backgroundColor: bg }], children: [
|
|
929
943
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native14.View, { style: [styles11.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
|
|
930
|
-
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_feedback_press3.IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: onBack }),
|
|
944
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_feedback_press3.IconButton, { icon: "arrow-left", iconColor: fg, size: 24, hitSlop, onPress: onBack, accessibilityLabel: backLabel }),
|
|
931
945
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native_paper10.Text, { variant: titleVariant, style: [styles11.title, { color: fg }], children: title })
|
|
932
946
|
] }),
|
|
933
947
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native14.ScrollView, { style: styles11.scrollView, contentContainerStyle: [styles11.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, children: [
|
|
@@ -1086,11 +1100,11 @@ var styles13 = import_react_native16.StyleSheet.create({
|
|
|
1086
1100
|
});
|
|
1087
1101
|
|
|
1088
1102
|
// src/SkiaGate.tsx
|
|
1089
|
-
var
|
|
1103
|
+
var import_react8 = require("react");
|
|
1090
1104
|
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
1091
1105
|
function SkiaGate({ getComponent, componentProps, fallback = null }) {
|
|
1092
|
-
const [Component, setComponent] = (0,
|
|
1093
|
-
(0,
|
|
1106
|
+
const [Component, setComponent] = (0, import_react8.useState)(null);
|
|
1107
|
+
(0, import_react8.useEffect)(() => {
|
|
1094
1108
|
let cancelled = false;
|
|
1095
1109
|
getComponent().then((mod) => {
|
|
1096
1110
|
if (!cancelled) setComponent(() => mod.default);
|
|
@@ -1111,7 +1125,7 @@ function TriggerGaugeHost(props) {
|
|
|
1111
1125
|
}
|
|
1112
1126
|
|
|
1113
1127
|
// src/useAutoAlign.ts
|
|
1114
|
-
var
|
|
1128
|
+
var import_react10 = require("react");
|
|
1115
1129
|
var import_react_native18 = require("react-native");
|
|
1116
1130
|
var EDGE_MARGIN = 12;
|
|
1117
1131
|
function rotateRect(x, y, width, height, rotation, windowWidth, windowHeight) {
|
|
@@ -1124,18 +1138,18 @@ function rotateRect(x, y, width, height, rotation, windowWidth, windowHeight) {
|
|
|
1124
1138
|
return { x: cx + newDx - newWidth / 2, y: cy + newDy - newHeight / 2, width: newWidth, height: newHeight };
|
|
1125
1139
|
}
|
|
1126
1140
|
function useAutoAlign(open, contentWidth, contentHeight, rotation = 0) {
|
|
1127
|
-
const triggerRef = (0,
|
|
1141
|
+
const triggerRef = (0, import_react10.useRef)(null);
|
|
1128
1142
|
const { width: windowWidth, height: windowHeight } = (0, import_react_native18.useWindowDimensions)();
|
|
1129
|
-
const [align, setAlign] = (0,
|
|
1130
|
-
const [verticalAlign, setVerticalAlign] = (0,
|
|
1131
|
-
const [maxHeight, setMaxHeight] = (0,
|
|
1132
|
-
const [measured, setMeasured] = (0,
|
|
1133
|
-
const [prevOpen, setPrevOpen] = (0,
|
|
1143
|
+
const [align, setAlign] = (0, import_react10.useState)("center");
|
|
1144
|
+
const [verticalAlign, setVerticalAlign] = (0, import_react10.useState)("below");
|
|
1145
|
+
const [maxHeight, setMaxHeight] = (0, import_react10.useState)(contentHeight);
|
|
1146
|
+
const [measured, setMeasured] = (0, import_react10.useState)(false);
|
|
1147
|
+
const [prevOpen, setPrevOpen] = (0, import_react10.useState)(open);
|
|
1134
1148
|
if (open !== prevOpen) {
|
|
1135
1149
|
setPrevOpen(open);
|
|
1136
1150
|
if (open) setMeasured(false);
|
|
1137
1151
|
}
|
|
1138
|
-
const measure = (0,
|
|
1152
|
+
const measure = (0, import_react10.useCallback)(() => {
|
|
1139
1153
|
triggerRef.current?.measureInWindow((rawX, rawY, rawWidth, rawHeight) => {
|
|
1140
1154
|
const { x, y, width: triggerWidth, height: triggerHeight } = rotateRect(rawX, rawY, rawWidth, rawHeight, rotation, windowWidth, windowHeight);
|
|
1141
1155
|
const centerX = x + triggerWidth / 2;
|
|
@@ -1152,7 +1166,7 @@ function useAutoAlign(open, contentWidth, contentHeight, rotation = 0) {
|
|
|
1152
1166
|
setMeasured(true);
|
|
1153
1167
|
});
|
|
1154
1168
|
}, [contentWidth, contentHeight, windowWidth, windowHeight, rotation]);
|
|
1155
|
-
(0,
|
|
1169
|
+
(0, import_react10.useEffect)(() => {
|
|
1156
1170
|
if (open) measure();
|
|
1157
1171
|
}, [open, measure]);
|
|
1158
1172
|
return { align, maxHeight, measured, triggerRef, verticalAlign };
|
|
@@ -1681,13 +1695,13 @@ var styles20 = import_react_native25.StyleSheet.create({
|
|
|
1681
1695
|
});
|
|
1682
1696
|
|
|
1683
1697
|
// src/usePopoverHost.ts
|
|
1684
|
-
var
|
|
1698
|
+
var import_react11 = require("react");
|
|
1685
1699
|
function usePopoverHost() {
|
|
1686
|
-
const [openId, setOpenId] = (0,
|
|
1687
|
-
const toggle = (0,
|
|
1700
|
+
const [openId, setOpenId] = (0, import_react11.useState)(null);
|
|
1701
|
+
const toggle = (0, import_react11.useCallback)((id) => {
|
|
1688
1702
|
setOpenId((prev) => prev === id ? null : id);
|
|
1689
1703
|
}, []);
|
|
1690
|
-
const close = (0,
|
|
1704
|
+
const close = (0, import_react11.useCallback)(() => setOpenId(null), []);
|
|
1691
1705
|
return { openId, toggle, close };
|
|
1692
1706
|
}
|
|
1693
1707
|
|
|
@@ -1786,7 +1800,7 @@ var styles22 = import_react_native28.StyleSheet.create({
|
|
|
1786
1800
|
});
|
|
1787
1801
|
|
|
1788
1802
|
// src/StaggeredWord.tsx
|
|
1789
|
-
var
|
|
1803
|
+
var import_react12 = require("react");
|
|
1790
1804
|
var import_react_native29 = require("react-native");
|
|
1791
1805
|
var import_react_native_reanimated2 = __toESM(require("react-native-reanimated"));
|
|
1792
1806
|
var import_jsx_runtime28 = require("react/jsx-runtime");
|
|
@@ -1797,7 +1811,7 @@ function getStaggeredWordDuration(letterCount) {
|
|
|
1797
1811
|
}
|
|
1798
1812
|
function AnimatedLetter({ char, index, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }) {
|
|
1799
1813
|
const progress = (0, import_react_native_reanimated2.useSharedValue)(reducedMotion ? 1 : 0);
|
|
1800
|
-
(0,
|
|
1814
|
+
(0, import_react12.useEffect)(() => {
|
|
1801
1815
|
if (reducedMotion) return;
|
|
1802
1816
|
progress.value = (0, import_react_native_reanimated2.withDelay)(index * STAGGER_MS, (0, import_react_native_reanimated2.withTiming)(1, { duration: LETTER_DURATION_MS, easing: import_react_native_reanimated2.Easing.out(import_react_native_reanimated2.Easing.back(1.5)) }));
|
|
1803
1817
|
}, [reducedMotion]);
|
|
@@ -1827,28 +1841,28 @@ function UpdateDialog({ rotation = 0, autoCheck = true, autoPrompt = true, onErr
|
|
|
1827
1841
|
}
|
|
1828
1842
|
|
|
1829
1843
|
// src/useQuitConfirmation.ts
|
|
1830
|
-
var
|
|
1844
|
+
var import_react13 = require("react");
|
|
1831
1845
|
function useQuitConfirmation(hasProgress, onConfirmedBack) {
|
|
1832
|
-
const [confirmVisible, setConfirmVisible] = (0,
|
|
1833
|
-
const hasProgressRef = (0,
|
|
1834
|
-
(0,
|
|
1846
|
+
const [confirmVisible, setConfirmVisible] = (0, import_react13.useState)(false);
|
|
1847
|
+
const hasProgressRef = (0, import_react13.useRef)(hasProgress);
|
|
1848
|
+
(0, import_react13.useEffect)(() => {
|
|
1835
1849
|
hasProgressRef.current = hasProgress;
|
|
1836
1850
|
});
|
|
1837
|
-
const onConfirmedBackRef = (0,
|
|
1838
|
-
(0,
|
|
1851
|
+
const onConfirmedBackRef = (0, import_react13.useRef)(onConfirmedBack);
|
|
1852
|
+
(0, import_react13.useEffect)(() => {
|
|
1839
1853
|
onConfirmedBackRef.current = onConfirmedBack;
|
|
1840
1854
|
});
|
|
1841
|
-
const requestBack = (0,
|
|
1855
|
+
const requestBack = (0, import_react13.useCallback)(() => {
|
|
1842
1856
|
if (hasProgressRef.current()) setConfirmVisible(true);
|
|
1843
1857
|
else onConfirmedBackRef.current();
|
|
1844
1858
|
}, []);
|
|
1845
|
-
const cancelBack = (0,
|
|
1859
|
+
const cancelBack = (0, import_react13.useCallback)(() => setConfirmVisible(false), []);
|
|
1846
1860
|
return { confirmVisible, requestBack, cancelBack };
|
|
1847
1861
|
}
|
|
1848
1862
|
|
|
1849
1863
|
// src/useZoneClampedAlign.ts
|
|
1850
1864
|
var import_split_screen = require("@tastic/split-screen");
|
|
1851
|
-
var
|
|
1865
|
+
var import_react14 = require("react");
|
|
1852
1866
|
var import_react_native30 = require("react-native");
|
|
1853
1867
|
var import_react_native_safe_area_context3 = require("react-native-safe-area-context");
|
|
1854
1868
|
var EDGE_MARGIN2 = 12;
|
|
@@ -1858,13 +1872,13 @@ function useZoneClampedAlign(open, contentWidth, contentHeight, rotation = 0) {
|
|
|
1858
1872
|
const { height: windowHeight } = (0, import_react_native30.useWindowDimensions)();
|
|
1859
1873
|
const insets = (0, import_react_native_safe_area_context3.useSafeAreaInsets)();
|
|
1860
1874
|
const shouldMeasure = open && auto.measured && zone !== null;
|
|
1861
|
-
const [ownBounds, setOwnBounds] = (0,
|
|
1862
|
-
const [prevShouldMeasure, setPrevShouldMeasure] = (0,
|
|
1875
|
+
const [ownBounds, setOwnBounds] = (0, import_react14.useState)(null);
|
|
1876
|
+
const [prevShouldMeasure, setPrevShouldMeasure] = (0, import_react14.useState)(shouldMeasure);
|
|
1863
1877
|
if (shouldMeasure !== prevShouldMeasure) {
|
|
1864
1878
|
setPrevShouldMeasure(shouldMeasure);
|
|
1865
1879
|
setOwnBounds(null);
|
|
1866
1880
|
}
|
|
1867
|
-
(0,
|
|
1881
|
+
(0, import_react14.useEffect)(() => {
|
|
1868
1882
|
if (!shouldMeasure) return;
|
|
1869
1883
|
auto.triggerRef.current?.measureInWindow((_x, y, _width, height) => {
|
|
1870
1884
|
const belowShared = zone.zoneSide === "belowShared";
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastic/hud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
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",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"@shopify/react-native-skia": "^2.6.2",
|
|
95
95
|
"@tastic/achievements": "^0.6.0",
|
|
96
96
|
"@tastic/core": "^0.14.0",
|
|
97
|
-
"@tastic/profile": "^0.
|
|
97
|
+
"@tastic/profile": "^0.13.0",
|
|
98
98
|
"@tastic/split-screen": "^0.5.2",
|
|
99
99
|
"@testing-library/dom": "^10.4.1",
|
|
100
100
|
"@testing-library/react": "^16.3.2",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useAutoPaperTheme } from '@rific/auto-paper'
|
|
1
|
+
import { getBlendedColor, useAutoPaperTheme } from '@rific/auto-paper'
|
|
2
2
|
import type { AchievementCatalogRow } from '@tastic/achievements'
|
|
3
3
|
import { Fragment } from 'react'
|
|
4
4
|
import { StyleSheet } from 'react-native'
|
|
@@ -36,9 +36,16 @@ interface Props {
|
|
|
36
36
|
// AchievementRow's own "badge color is a rendering decision" doc comment - @tastic/achievements'
|
|
37
37
|
// (pending) getAchievementCatalogRows deliberately leaves tierColor unresolved-to-badgeColor for
|
|
38
38
|
// the same reason.
|
|
39
|
+
//
|
|
40
|
+
// The check mark and progress bar (AchievementRow's checkColor) are the tier color in dark mode, and
|
|
41
|
+
// the tier color mixed half with black in light mode. Raw gold (#FFD54F) and silver (#C0C0C8) are
|
|
42
|
+
// about 1.4:1 and 1.8:1 against the light screen's white, so a bar or check in either barely shows;
|
|
43
|
+
// mixed, they're about 5:1 and 6:1 (bronze goes from about 3:1 to 9:1). The badge keeps the true tier
|
|
44
|
+
// color: its glyph is black, which reads on all three.
|
|
39
45
|
export function AchievementCatalogSection({ rows, label = 'ALL ACHIEVEMENTS', fg, fgMuted: fgMutedOverride }: Props) {
|
|
40
46
|
const { dark } = useAutoPaperTheme()
|
|
41
47
|
const fgMuted = fgMutedOverride ?? (dark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)')
|
|
48
|
+
const tierAccent = (tierColor: string) => (dark ? tierColor : getBlendedColor('#000000', tierColor, 0.5))
|
|
42
49
|
|
|
43
50
|
return (
|
|
44
51
|
<Fragment>
|
|
@@ -46,7 +53,7 @@ export function AchievementCatalogSection({ rows, label = 'ALL ACHIEVEMENTS', fg
|
|
|
46
53
|
{label}
|
|
47
54
|
</Text>
|
|
48
55
|
{rows.map((row) => (
|
|
49
|
-
<AchievementRow key={row.id} icon={row.icon} title={row.title} description={row.description} badgeColor={row.unlockedAt !== undefined ? row.tierColor : LOCKED_BADGE_COLOR} checkColor={row.tierColor} unlockedLabel={row.unlockedLabel} progress={row.progress} deviceMarker={row.deviceMarker} fg={fg} fgMuted={fgMutedOverride} />
|
|
56
|
+
<AchievementRow key={row.id} icon={row.icon} title={row.title} description={row.description} badgeColor={row.unlockedAt !== undefined ? row.tierColor : LOCKED_BADGE_COLOR} checkColor={tierAccent(row.tierColor)} unlockedLabel={row.unlockedLabel} progress={row.progress} deviceMarker={row.deviceMarker} fg={fg} fgMuted={fgMutedOverride} />
|
|
50
57
|
))}
|
|
51
58
|
</Fragment>
|
|
52
59
|
)
|
package/src/AchievementRow.tsx
CHANGED
|
@@ -21,10 +21,10 @@ interface Props {
|
|
|
21
21
|
// timestamp - this component has no opinion on date formatting or streak conventions, which vary
|
|
22
22
|
// per app (see LightCycles' own calendar-day-difference convention in its achievements.tsx).
|
|
23
23
|
unlockedLabel?: string
|
|
24
|
-
// Color for the unlocked check-circle icon
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
24
|
+
// Color for the unlocked check-circle icon and the locked progress bar - defaults to badgeColor.
|
|
25
|
+
// Its own prop so a caller can tune it apart from the badge: AchievementCatalogSection passes the
|
|
26
|
+
// tier color darkened in light mode, where raw gold and silver barely show on white, while the badge
|
|
27
|
+
// keeps the true tier color behind its black glyph.
|
|
28
28
|
checkColor?: string
|
|
29
29
|
// Only rendered while locked (unlockedLabel is undefined) - 0..1.
|
|
30
30
|
progress?: number
|
|
@@ -400,7 +400,9 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
400
400
|
</View>
|
|
401
401
|
</Dialog>
|
|
402
402
|
<ConfirmDialog visible={!!updateManifest} title='Update Available' message={updateManifestDate ? `Released ${updateManifestDate.toLocaleDateString()} at ${updateManifestDate.toLocaleTimeString()}` : ''} confirmLabel='Restart' cancelLabel='Later' icon='update' rotation={0} onConfirm={() => respondToUpdateConfirm(true)} onCancel={() => respondToUpdateConfirm(false)} />
|
|
403
|
-
|
|
403
|
+
{/* onCancel with no cancelLabel adds no button: it's only so Android back dismisses the
|
|
404
|
+
message the way OK does (ConfirmDialog consumes back either way). */}
|
|
405
|
+
<ConfirmDialog visible={!!infoMessage} title={infoMessage?.title ?? ''} message={infoMessage?.message ?? ''} confirmLabel='OK' icon='information-outline' rotation={0} onConfirm={() => setInfoMessage(null)} onCancel={() => setInfoMessage(null)} />
|
|
404
406
|
</RotatedFrame>
|
|
405
407
|
</Portal>
|
|
406
408
|
)
|
package/src/BaseStatsScreen.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { useAutoPaperTheme } from '@rific/auto-paper'
|
|
|
2
2
|
import { Button, IconButton } from '@rific/feedback-press'
|
|
3
3
|
import { useRotation, type ViewRotation } from '@tastic/core'
|
|
4
4
|
import { ReactNode, useState } from 'react'
|
|
5
|
-
import { ScrollView, StyleSheet, View } from 'react-native'
|
|
5
|
+
import { type Insets, ScrollView, StyleSheet, View } from 'react-native'
|
|
6
6
|
import { Text } from 'react-native-paper'
|
|
7
7
|
|
|
8
8
|
import { ConfirmDialog } from './ConfirmDialog'
|
|
@@ -30,6 +30,14 @@ interface Props {
|
|
|
30
30
|
// caller that wants something else entirely.
|
|
31
31
|
titleVariant?: MD3TextVariant
|
|
32
32
|
onBack: () => void
|
|
33
|
+
// The header arrow's accessibility label, default 'Back' (same name and default as
|
|
34
|
+
// CornerActionButtons' own backLabel). Without one, VoiceOver and TalkBack announce only "button".
|
|
35
|
+
backLabel?: string
|
|
36
|
+
// Opt-in extra touch area on the header arrow, forwarded as its IconButton's own hitSlop, exactly
|
|
37
|
+
// like CornerActionButtons' hitSlop: the target is the 40pt box, so 4 makes it 48pt, and nothing
|
|
38
|
+
// visible moves. Needs @rific/feedback-press 0.13.0 or later (ignored on older versions, and on
|
|
39
|
+
// web). Omitted means no slop, as before.
|
|
40
|
+
hitSlop?: number | Insets
|
|
33
41
|
insets: { top: number; left: number; bottom: number; right: number }
|
|
34
42
|
// Optional, and always passed as a pair - a caller with nothing resettable (or that deliberately
|
|
35
43
|
// doesn't want a reset action on this screen) omits both rather than one alone. Confirmation copy
|
|
@@ -70,7 +78,7 @@ interface Props {
|
|
|
70
78
|
// convention. `children` is the seam for whatever isn't shared: this component has no opinion on
|
|
71
79
|
// what a "stat" is or how achievements are catalogued - see StatRow/StatSection/AchievementRow for
|
|
72
80
|
// 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) {
|
|
81
|
+
export function BaseStatsScreen({ rotation: rotationOverride, title = 'Achievements', titleVariant = 'headlineSmall', onBack, backLabel = 'Back', hitSlop, 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) {
|
|
74
82
|
const { dark, colors } = useAutoPaperTheme()
|
|
75
83
|
const ambientRotation = useRotation()
|
|
76
84
|
const rotation = rotationOverride ?? ambientRotation
|
|
@@ -87,7 +95,7 @@ export function BaseStatsScreen({ rotation: rotationOverride, title = 'Achieveme
|
|
|
87
95
|
return (
|
|
88
96
|
<View style={[styles.container, { backgroundColor: bg }]}>
|
|
89
97
|
<View style={[styles.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }]}>
|
|
90
|
-
<IconButton icon='arrow-left' iconColor={fg} size={24} onPress={onBack} />
|
|
98
|
+
<IconButton icon='arrow-left' iconColor={fg} size={24} hitSlop={hitSlop} onPress={onBack} accessibilityLabel={backLabel} />
|
|
91
99
|
<Text variant={titleVariant} style={[styles.title, { color: fg }]}>
|
|
92
100
|
{title}
|
|
93
101
|
</Text>
|
package/src/ConfirmDialog.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useAutoPaperTheme } from '@rific/auto-paper'
|
|
2
2
|
import { Button } from '@rific/feedback-press'
|
|
3
3
|
import { toRotationStyle, type ViewRotation } from '@tastic/core'
|
|
4
|
-
import { type ReactNode } from 'react'
|
|
5
|
-
import { StyleSheet, View } from 'react-native'
|
|
4
|
+
import { type ReactNode, useEffect, useRef } from 'react'
|
|
5
|
+
import { BackHandler, StyleSheet, View } from 'react-native'
|
|
6
6
|
import { Portal, Text } from 'react-native-paper'
|
|
7
7
|
|
|
8
8
|
import { DialogHeader } from './DialogHeader'
|
|
@@ -43,6 +43,29 @@ export function ConfirmDialog({ visible, title, message, confirmLabel, cancelLab
|
|
|
43
43
|
const { colors } = useAutoPaperTheme()
|
|
44
44
|
const { fg } = useOverlayCardColors()
|
|
45
45
|
const { compact, onLayout } = useCompactDialog(rotation)
|
|
46
|
+
|
|
47
|
+
// Android's back means Cancel while the dialog is up. It's a Portal over a View, not a Modal, so
|
|
48
|
+
// nothing else catches the press: it would fall through to whatever is underneath (the router, or a
|
|
49
|
+
// screen's own back listener) and close that screen with the dialog still up. RN calls the newest
|
|
50
|
+
// listener first, and this one is added when the dialog opens. Always consumed, even with no
|
|
51
|
+
// onCancel: a dialog with only a confirm button has to be answered, not backed out from under.
|
|
52
|
+
//
|
|
53
|
+
// onCancel goes through a ref (kept current after every commit, the useQuitConfirmation idiom) so the
|
|
54
|
+
// listener is added once per opening. Callers pass a new arrow each render, and re-adding the
|
|
55
|
+
// listener on each would move it to the top of the stack, ahead of anything opened above it since.
|
|
56
|
+
const onCancelRef = useRef(onCancel)
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
onCancelRef.current = onCancel
|
|
59
|
+
})
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!visible) return
|
|
62
|
+
const subscription = BackHandler.addEventListener('hardwareBackPress', () => {
|
|
63
|
+
onCancelRef.current?.()
|
|
64
|
+
return true
|
|
65
|
+
})
|
|
66
|
+
return () => subscription.remove()
|
|
67
|
+
}, [visible])
|
|
68
|
+
|
|
46
69
|
if (!visible) return null
|
|
47
70
|
|
|
48
71
|
const accent = destructive ? colors.danger : colors.primary
|