@tastic/hud 0.18.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 +14 -5
- package/dist/chrome/index.d.mts +7 -3
- package/dist/chrome/index.d.ts +7 -3
- package/dist/chrome/index.js +42 -20
- package/dist/chrome/index.mjs +1 -1
- package/dist/{chunk-Z2WUUYTE.mjs → chunk-DK3HOZC5.mjs} +33 -11
- package/dist/index.js +78 -56
- package/dist/index.mjs +1 -1
- package/package.json +8 -8
- package/src/AchievementCatalogSection.tsx +9 -2
- package/src/AchievementRow.tsx +4 -4
- package/src/BaseSettingsDialog.tsx +32 -5
- 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.
|
|
@@ -207,9 +210,11 @@ the fleet with real Redux Toolkit state - reads `lockOrientation`/`deferBottomEd
|
|
|
207
210
|
`useSelector` and writes them back via `dispatch(gameActions.setLockOrientation(...))` directly
|
|
208
211
|
inside the adapter, with nothing threaded through props at all. `BaseSettingsDialog` doesn't care
|
|
209
212
|
which of these an app uses: sound, haptics, appearance, and update-checking are identical across
|
|
210
|
-
every app and live entirely inside this package now, so `lockOrientation`/`onLockOrientationChange
|
|
211
|
-
|
|
212
|
-
actually needs to source itself, however it does that.
|
|
213
|
+
every app and live entirely inside this package now, so `lockOrientation`/`onLockOrientationChange`,
|
|
214
|
+
`deferBottomEdgeGestures`/`onDeferBottomEdgeGestures` and `keepAwake`/`onKeepAwakeChange` are the only
|
|
215
|
+
settings an adapter actually needs to source itself, however it does that. Each pair is optional: omit
|
|
216
|
+
one and its row doesn't show. The Keep Awake row only toggles the value; the app keeps the screen awake
|
|
217
|
+
itself while it's on (for example with `expo-keep-awake` on its match screen).
|
|
213
218
|
|
|
214
219
|
Every adapter also passes `version={release.otaVersion}` (so the dialog's own update-check button
|
|
215
220
|
knows what it's currently running) and `onUpdateError`, wired to a real `useToast()` call. Wire this
|
|
@@ -234,6 +239,8 @@ export function SettingsDialog({ visible, onDismiss, rotation = 0, settings, set
|
|
|
234
239
|
onLockOrientationChange={(value) => setSettings({ lockOrientation: value })}
|
|
235
240
|
deferBottomEdgeGestures={settings.deferBottomEdgeGestures}
|
|
236
241
|
onDeferBottomEdgeGestures={(value) => setSettings({ deferBottomEdgeGestures: value })}
|
|
242
|
+
keepAwake={settings.keepAwake}
|
|
243
|
+
onKeepAwakeChange={(value) => setSettings({ keepAwake: value })}
|
|
237
244
|
onUpdateError={(message) => error('Update check failed', message)}
|
|
238
245
|
/>
|
|
239
246
|
)
|
|
@@ -466,7 +473,9 @@ row. Returns a `Fragment`, not a `View`: every call site renders the heading and
|
|
|
466
473
|
siblings inside `BaseStatsScreen`'s own gap-spaced `ScrollView` content, and a wrapping container
|
|
467
474
|
here would double up that spacing. Resolves each row's locked-vs-unlocked `badgeColor` itself
|
|
468
475
|
(`row.tierColor` once `row.unlockedAt` is set, else the locked color) rather than upstream, since
|
|
469
|
-
`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.
|
|
470
479
|
|
|
471
480
|
```ts
|
|
472
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[];
|
|
@@ -47,6 +47,8 @@ interface BaseSettingsDialogProps {
|
|
|
47
47
|
onLockOrientationChange?: (value: boolean) => void;
|
|
48
48
|
deferBottomEdgeGestures?: boolean;
|
|
49
49
|
onDeferBottomEdgeGestures?: (value: boolean) => void;
|
|
50
|
+
keepAwake?: boolean;
|
|
51
|
+
onKeepAwakeChange?: (value: boolean) => void;
|
|
50
52
|
hideSound?: boolean;
|
|
51
53
|
hideHaptics?: boolean;
|
|
52
54
|
hideAppearance?: boolean;
|
|
@@ -55,7 +57,7 @@ interface BaseSettingsDialogProps {
|
|
|
55
57
|
onShowHowToPlay?: () => void;
|
|
56
58
|
children?: ReactNode;
|
|
57
59
|
}
|
|
58
|
-
declare function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound, hideHaptics, hideAppearance, hideUpdateCheck, onUpdateError, onShowHowToPlay, children }: BaseSettingsDialogProps): react.JSX.Element | null;
|
|
60
|
+
declare function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, keepAwake, onKeepAwakeChange, hideSound, hideHaptics, hideAppearance, hideUpdateCheck, onUpdateError, onShowHowToPlay, children }: BaseSettingsDialogProps): react.JSX.Element | null;
|
|
59
61
|
|
|
60
62
|
type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
|
|
61
63
|
interface Props$4 {
|
|
@@ -63,6 +65,8 @@ interface Props$4 {
|
|
|
63
65
|
title?: string;
|
|
64
66
|
titleVariant?: MD3TextVariant;
|
|
65
67
|
onBack: () => void;
|
|
68
|
+
backLabel?: string;
|
|
69
|
+
hitSlop?: number | Insets;
|
|
66
70
|
insets: {
|
|
67
71
|
top: number;
|
|
68
72
|
left: number;
|
|
@@ -77,7 +81,7 @@ interface Props$4 {
|
|
|
77
81
|
bg?: string;
|
|
78
82
|
children: ReactNode;
|
|
79
83
|
}
|
|
80
|
-
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;
|
|
81
85
|
|
|
82
86
|
interface ConfirmDialogProps {
|
|
83
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[];
|
|
@@ -47,6 +47,8 @@ interface BaseSettingsDialogProps {
|
|
|
47
47
|
onLockOrientationChange?: (value: boolean) => void;
|
|
48
48
|
deferBottomEdgeGestures?: boolean;
|
|
49
49
|
onDeferBottomEdgeGestures?: (value: boolean) => void;
|
|
50
|
+
keepAwake?: boolean;
|
|
51
|
+
onKeepAwakeChange?: (value: boolean) => void;
|
|
50
52
|
hideSound?: boolean;
|
|
51
53
|
hideHaptics?: boolean;
|
|
52
54
|
hideAppearance?: boolean;
|
|
@@ -55,7 +57,7 @@ interface BaseSettingsDialogProps {
|
|
|
55
57
|
onShowHowToPlay?: () => void;
|
|
56
58
|
children?: ReactNode;
|
|
57
59
|
}
|
|
58
|
-
declare function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound, hideHaptics, hideAppearance, hideUpdateCheck, onUpdateError, onShowHowToPlay, children }: BaseSettingsDialogProps): react.JSX.Element | null;
|
|
60
|
+
declare function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, keepAwake, onKeepAwakeChange, hideSound, hideHaptics, hideAppearance, hideUpdateCheck, onUpdateError, onShowHowToPlay, children }: BaseSettingsDialogProps): react.JSX.Element | null;
|
|
59
61
|
|
|
60
62
|
type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
|
|
61
63
|
interface Props$4 {
|
|
@@ -63,6 +65,8 @@ interface Props$4 {
|
|
|
63
65
|
title?: string;
|
|
64
66
|
titleVariant?: MD3TextVariant;
|
|
65
67
|
onBack: () => void;
|
|
68
|
+
backLabel?: string;
|
|
69
|
+
hitSlop?: number | Insets;
|
|
66
70
|
insets: {
|
|
67
71
|
top: number;
|
|
68
72
|
left: number;
|
|
@@ -77,7 +81,7 @@ interface Props$4 {
|
|
|
77
81
|
bg?: string;
|
|
78
82
|
children: ReactNode;
|
|
79
83
|
}
|
|
80
|
-
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;
|
|
81
85
|
|
|
82
86
|
interface ConfirmDialogProps {
|
|
83
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;
|
|
@@ -474,7 +488,7 @@ var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
|
474
488
|
function SettingIcon({ source, color, containerColor }) {
|
|
475
489
|
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native12.View, { style: [styles9.iconBadge, { backgroundColor: containerColor }], children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native_paper8.Icon, { source, size: 18, color }) });
|
|
476
490
|
}
|
|
477
|
-
function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }) {
|
|
491
|
+
function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, keepAwake, onKeepAwakeChange, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }) {
|
|
478
492
|
const { colors } = (0, import_auto_paper8.useAutoPaperTheme)();
|
|
479
493
|
const ambientRotation = (0, import_core4.useRotation)();
|
|
480
494
|
const rotation = rotationOverride ?? ambientRotation;
|
|
@@ -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,10 +514,11 @@ 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;
|
|
521
|
+
const showKeepAwake = isTouchPrimary && keepAwake !== void 0 && onKeepAwakeChange !== void 0;
|
|
507
522
|
const showHaptics = import_react_native12.Platform.OS !== "web" && !hideHaptics;
|
|
508
523
|
const showSound = !hideSound;
|
|
509
524
|
const showAppearance = !hideAppearance;
|
|
@@ -530,6 +545,13 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
|
|
|
530
545
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native_paper8.Text, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Require second swipe" })
|
|
531
546
|
] })
|
|
532
547
|
] }) }),
|
|
548
|
+
showKeepAwake && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_feedback_press2.TouchableRipple, { onPress: () => onKeepAwakeChange(!keepAwake), style: styles9.toggleButton, accessibilityLabel: `Keep awake ${keepAwake ? "on" : "off"}`, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native12.View, { style: styles9.toggleContent, children: [
|
|
549
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(SettingIcon, { source: keepAwake ? "eye" : "eye-closed", color: keepAwake ? colors.tertiary : colors.onSurfaceVariant, containerColor: keepAwake ? colors.tertiaryContainer : colors.surfaceVariant }),
|
|
550
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native12.View, { style: styles9.flexShrink, children: [
|
|
551
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native_paper8.Text, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Keep Awake" }),
|
|
552
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native_paper8.Text, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Stop auto-lock mid-match" })
|
|
553
|
+
] })
|
|
554
|
+
] }) }),
|
|
533
555
|
(showSound || showHaptics) && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native12.View, { style: styles9.toggleRow, children: [
|
|
534
556
|
showSound && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
535
557
|
import_feedback_press2.TouchableRipple,
|
|
@@ -610,7 +632,7 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
|
|
|
610
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" }) })
|
|
611
633
|
] }),
|
|
612
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) }),
|
|
613
|
-
/* @__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) })
|
|
614
636
|
] }) });
|
|
615
637
|
}
|
|
616
638
|
var SHEET_GUTTER = 24;
|
|
@@ -701,21 +723,21 @@ var styles9 = import_react_native12.StyleSheet.create({
|
|
|
701
723
|
var import_auto_paper9 = require("@rific/auto-paper");
|
|
702
724
|
var import_feedback_press3 = require("@rific/feedback-press");
|
|
703
725
|
var import_core5 = require("@tastic/core");
|
|
704
|
-
var
|
|
726
|
+
var import_react7 = require("react");
|
|
705
727
|
var import_react_native13 = require("react-native");
|
|
706
728
|
var import_react_native_paper9 = require("react-native-paper");
|
|
707
729
|
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
708
|
-
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 }) {
|
|
709
731
|
const { dark, colors } = (0, import_auto_paper9.useAutoPaperTheme)();
|
|
710
732
|
const ambientRotation = (0, import_core5.useRotation)();
|
|
711
733
|
const rotation = rotationOverride ?? ambientRotation;
|
|
712
|
-
const [confirmResetVisible, setConfirmResetVisible] = (0,
|
|
734
|
+
const [confirmResetVisible, setConfirmResetVisible] = (0, import_react7.useState)(false);
|
|
713
735
|
const showReset = !!onReset;
|
|
714
736
|
const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
|
|
715
737
|
const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
|
|
716
738
|
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native13.View, { style: [styles10.container, { backgroundColor: bg }], children: [
|
|
717
739
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native13.View, { style: [styles10.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
|
|
718
|
-
/* @__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 }),
|
|
719
741
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native_paper9.Text, { variant: titleVariant, style: [styles10.title, { color: fg }], children: title })
|
|
720
742
|
] }),
|
|
721
743
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native13.ScrollView, { style: styles10.scrollView, contentContainerStyle: [styles10.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, children: [
|
|
@@ -816,7 +838,7 @@ var styles12 = import_react_native15.StyleSheet.create({
|
|
|
816
838
|
});
|
|
817
839
|
|
|
818
840
|
// src/StaggeredWord.tsx
|
|
819
|
-
var
|
|
841
|
+
var import_react8 = require("react");
|
|
820
842
|
var import_react_native16 = require("react-native");
|
|
821
843
|
var import_react_native_reanimated = __toESM(require("react-native-reanimated"));
|
|
822
844
|
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
@@ -827,7 +849,7 @@ function getStaggeredWordDuration(letterCount) {
|
|
|
827
849
|
}
|
|
828
850
|
function AnimatedLetter({ char, index, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }) {
|
|
829
851
|
const progress = (0, import_react_native_reanimated.useSharedValue)(reducedMotion ? 1 : 0);
|
|
830
|
-
(0,
|
|
852
|
+
(0, import_react8.useEffect)(() => {
|
|
831
853
|
if (reducedMotion) return;
|
|
832
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)) }));
|
|
833
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;
|
|
@@ -293,7 +307,7 @@ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
|
293
307
|
function SettingIcon({ source, color, containerColor }) {
|
|
294
308
|
return /* @__PURE__ */ jsx8(View6, { style: [styles7.iconBadge, { backgroundColor: containerColor }], children: /* @__PURE__ */ jsx8(Icon2, { source, size: 18, color }) });
|
|
295
309
|
}
|
|
296
|
-
function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }) {
|
|
310
|
+
function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, keepAwake, onKeepAwakeChange, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }) {
|
|
297
311
|
const { colors } = useAutoPaperTheme6();
|
|
298
312
|
const ambientRotation = useRotation2();
|
|
299
313
|
const rotation = rotationOverride ?? ambientRotation;
|
|
@@ -323,6 +337,7 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
|
|
|
323
337
|
if (visible && !opened) setOpened(true);
|
|
324
338
|
const showLockOrientation = isTouchPrimary && lockOrientation !== void 0 && onLockOrientationChange !== void 0;
|
|
325
339
|
const showEdgeGuard = Platform2.OS === "ios" && deferBottomEdgeGestures !== void 0 && onDeferBottomEdgeGestures !== void 0;
|
|
340
|
+
const showKeepAwake = isTouchPrimary && keepAwake !== void 0 && onKeepAwakeChange !== void 0;
|
|
326
341
|
const showHaptics = Platform2.OS !== "web" && !hideHaptics;
|
|
327
342
|
const showSound = !hideSound;
|
|
328
343
|
const showAppearance = !hideAppearance;
|
|
@@ -349,6 +364,13 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
|
|
|
349
364
|
/* @__PURE__ */ jsx8(Text6, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Require second swipe" })
|
|
350
365
|
] })
|
|
351
366
|
] }) }),
|
|
367
|
+
showKeepAwake && /* @__PURE__ */ jsx8(TouchableRipple, { onPress: () => onKeepAwakeChange(!keepAwake), style: styles7.toggleButton, accessibilityLabel: `Keep awake ${keepAwake ? "on" : "off"}`, children: /* @__PURE__ */ jsxs7(View6, { style: styles7.toggleContent, children: [
|
|
368
|
+
/* @__PURE__ */ jsx8(SettingIcon, { source: keepAwake ? "eye" : "eye-closed", color: keepAwake ? colors.tertiary : colors.onSurfaceVariant, containerColor: keepAwake ? colors.tertiaryContainer : colors.surfaceVariant }),
|
|
369
|
+
/* @__PURE__ */ jsxs7(View6, { style: styles7.flexShrink, children: [
|
|
370
|
+
/* @__PURE__ */ jsx8(Text6, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Keep Awake" }),
|
|
371
|
+
/* @__PURE__ */ jsx8(Text6, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Stop auto-lock mid-match" })
|
|
372
|
+
] })
|
|
373
|
+
] }) }),
|
|
352
374
|
(showSound || showHaptics) && /* @__PURE__ */ jsxs7(View6, { style: styles7.toggleRow, children: [
|
|
353
375
|
showSound && /* @__PURE__ */ jsx8(
|
|
354
376
|
TouchableRipple,
|
|
@@ -429,7 +451,7 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
|
|
|
429
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" }) })
|
|
430
452
|
] }),
|
|
431
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) }),
|
|
432
|
-
/* @__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) })
|
|
433
455
|
] }) });
|
|
434
456
|
}
|
|
435
457
|
var SHEET_GUTTER = 24;
|
|
@@ -524,7 +546,7 @@ import { useState as useState4 } from "react";
|
|
|
524
546
|
import { ScrollView, StyleSheet as StyleSheet8, View as View7 } from "react-native";
|
|
525
547
|
import { Text as Text7 } from "react-native-paper";
|
|
526
548
|
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
527
|
-
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 }) {
|
|
528
550
|
const { dark, colors } = useAutoPaperTheme7();
|
|
529
551
|
const ambientRotation = useRotation3();
|
|
530
552
|
const rotation = rotationOverride ?? ambientRotation;
|
|
@@ -534,7 +556,7 @@ function BaseStatsScreen({ rotation: rotationOverride, title = "Achievements", t
|
|
|
534
556
|
const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
|
|
535
557
|
return /* @__PURE__ */ jsxs8(View7, { style: [styles8.container, { backgroundColor: bg }], children: [
|
|
536
558
|
/* @__PURE__ */ jsxs8(View7, { style: [styles8.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
|
|
537
|
-
/* @__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 }),
|
|
538
560
|
/* @__PURE__ */ jsx9(Text7, { variant: titleVariant, style: [styles8.title, { color: fg }], children: title })
|
|
539
561
|
] }),
|
|
540
562
|
/* @__PURE__ */ jsxs8(ScrollView, { style: styles8.scrollView, contentContainerStyle: [styles8.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, children: [
|
|
@@ -635,7 +657,7 @@ var styles10 = StyleSheet10.create({
|
|
|
635
657
|
});
|
|
636
658
|
|
|
637
659
|
// src/StaggeredWord.tsx
|
|
638
|
-
import { useEffect } from "react";
|
|
660
|
+
import { useEffect as useEffect2 } from "react";
|
|
639
661
|
import { StyleSheet as StyleSheet11, View as View9 } from "react-native";
|
|
640
662
|
import Animated, { Easing, useAnimatedStyle, useSharedValue, withDelay, withTiming } from "react-native-reanimated";
|
|
641
663
|
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
@@ -646,7 +668,7 @@ function getStaggeredWordDuration(letterCount) {
|
|
|
646
668
|
}
|
|
647
669
|
function AnimatedLetter({ char, index, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }) {
|
|
648
670
|
const progress = useSharedValue(reducedMotion ? 1 : 0);
|
|
649
|
-
|
|
671
|
+
useEffect2(() => {
|
|
650
672
|
if (reducedMotion) return;
|
|
651
673
|
progress.value = withDelay(index * STAGGER_MS, withTiming(1, { duration: LETTER_DURATION_MS, easing: Easing.out(Easing.back(1.5)) }));
|
|
652
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;
|
|
@@ -678,7 +692,7 @@ var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
|
678
692
|
function SettingIcon({ source, color, containerColor }) {
|
|
679
693
|
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native13.View, { style: [styles10.iconBadge, { backgroundColor: containerColor }], children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native_paper9.Icon, { source, size: 18, color }) });
|
|
680
694
|
}
|
|
681
|
-
function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }) {
|
|
695
|
+
function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, keepAwake, onKeepAwakeChange, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }) {
|
|
682
696
|
const { colors } = (0, import_auto_paper9.useAutoPaperTheme)();
|
|
683
697
|
const ambientRotation = (0, import_core4.useRotation)();
|
|
684
698
|
const rotation = rotationOverride ?? ambientRotation;
|
|
@@ -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,10 +718,11 @@ 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;
|
|
725
|
+
const showKeepAwake = isTouchPrimary && keepAwake !== void 0 && onKeepAwakeChange !== void 0;
|
|
711
726
|
const showHaptics = import_react_native13.Platform.OS !== "web" && !hideHaptics;
|
|
712
727
|
const showSound = !hideSound;
|
|
713
728
|
const showAppearance = !hideAppearance;
|
|
@@ -734,6 +749,13 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
|
|
|
734
749
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native_paper9.Text, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Require second swipe" })
|
|
735
750
|
] })
|
|
736
751
|
] }) }),
|
|
752
|
+
showKeepAwake && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_feedback_press2.TouchableRipple, { onPress: () => onKeepAwakeChange(!keepAwake), style: styles10.toggleButton, accessibilityLabel: `Keep awake ${keepAwake ? "on" : "off"}`, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native13.View, { style: styles10.toggleContent, children: [
|
|
753
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SettingIcon, { source: keepAwake ? "eye" : "eye-closed", color: keepAwake ? colors.tertiary : colors.onSurfaceVariant, containerColor: keepAwake ? colors.tertiaryContainer : colors.surfaceVariant }),
|
|
754
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native13.View, { style: styles10.flexShrink, children: [
|
|
755
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native_paper9.Text, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Keep Awake" }),
|
|
756
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native_paper9.Text, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Stop auto-lock mid-match" })
|
|
757
|
+
] })
|
|
758
|
+
] }) }),
|
|
737
759
|
(showSound || showHaptics) && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native13.View, { style: styles10.toggleRow, children: [
|
|
738
760
|
showSound && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
739
761
|
import_feedback_press2.TouchableRipple,
|
|
@@ -814,7 +836,7 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
|
|
|
814
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" }) })
|
|
815
837
|
] }),
|
|
816
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) }),
|
|
817
|
-
/* @__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) })
|
|
818
840
|
] }) });
|
|
819
841
|
}
|
|
820
842
|
var SHEET_GUTTER = 24;
|
|
@@ -905,21 +927,21 @@ var styles10 = import_react_native13.StyleSheet.create({
|
|
|
905
927
|
var import_auto_paper10 = require("@rific/auto-paper");
|
|
906
928
|
var import_feedback_press3 = require("@rific/feedback-press");
|
|
907
929
|
var import_core5 = require("@tastic/core");
|
|
908
|
-
var
|
|
930
|
+
var import_react7 = require("react");
|
|
909
931
|
var import_react_native14 = require("react-native");
|
|
910
932
|
var import_react_native_paper10 = require("react-native-paper");
|
|
911
933
|
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
912
|
-
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 }) {
|
|
913
935
|
const { dark, colors } = (0, import_auto_paper10.useAutoPaperTheme)();
|
|
914
936
|
const ambientRotation = (0, import_core5.useRotation)();
|
|
915
937
|
const rotation = rotationOverride ?? ambientRotation;
|
|
916
|
-
const [confirmResetVisible, setConfirmResetVisible] = (0,
|
|
938
|
+
const [confirmResetVisible, setConfirmResetVisible] = (0, import_react7.useState)(false);
|
|
917
939
|
const showReset = !!onReset;
|
|
918
940
|
const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
|
|
919
941
|
const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
|
|
920
942
|
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native14.View, { style: [styles11.container, { backgroundColor: bg }], children: [
|
|
921
943
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native14.View, { style: [styles11.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
|
|
922
|
-
/* @__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 }),
|
|
923
945
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native_paper10.Text, { variant: titleVariant, style: [styles11.title, { color: fg }], children: title })
|
|
924
946
|
] }),
|
|
925
947
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native14.ScrollView, { style: styles11.scrollView, contentContainerStyle: [styles11.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, children: [
|
|
@@ -1078,11 +1100,11 @@ var styles13 = import_react_native16.StyleSheet.create({
|
|
|
1078
1100
|
});
|
|
1079
1101
|
|
|
1080
1102
|
// src/SkiaGate.tsx
|
|
1081
|
-
var
|
|
1103
|
+
var import_react8 = require("react");
|
|
1082
1104
|
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
1083
1105
|
function SkiaGate({ getComponent, componentProps, fallback = null }) {
|
|
1084
|
-
const [Component, setComponent] = (0,
|
|
1085
|
-
(0,
|
|
1106
|
+
const [Component, setComponent] = (0, import_react8.useState)(null);
|
|
1107
|
+
(0, import_react8.useEffect)(() => {
|
|
1086
1108
|
let cancelled = false;
|
|
1087
1109
|
getComponent().then((mod) => {
|
|
1088
1110
|
if (!cancelled) setComponent(() => mod.default);
|
|
@@ -1103,7 +1125,7 @@ function TriggerGaugeHost(props) {
|
|
|
1103
1125
|
}
|
|
1104
1126
|
|
|
1105
1127
|
// src/useAutoAlign.ts
|
|
1106
|
-
var
|
|
1128
|
+
var import_react10 = require("react");
|
|
1107
1129
|
var import_react_native18 = require("react-native");
|
|
1108
1130
|
var EDGE_MARGIN = 12;
|
|
1109
1131
|
function rotateRect(x, y, width, height, rotation, windowWidth, windowHeight) {
|
|
@@ -1116,18 +1138,18 @@ function rotateRect(x, y, width, height, rotation, windowWidth, windowHeight) {
|
|
|
1116
1138
|
return { x: cx + newDx - newWidth / 2, y: cy + newDy - newHeight / 2, width: newWidth, height: newHeight };
|
|
1117
1139
|
}
|
|
1118
1140
|
function useAutoAlign(open, contentWidth, contentHeight, rotation = 0) {
|
|
1119
|
-
const triggerRef = (0,
|
|
1141
|
+
const triggerRef = (0, import_react10.useRef)(null);
|
|
1120
1142
|
const { width: windowWidth, height: windowHeight } = (0, import_react_native18.useWindowDimensions)();
|
|
1121
|
-
const [align, setAlign] = (0,
|
|
1122
|
-
const [verticalAlign, setVerticalAlign] = (0,
|
|
1123
|
-
const [maxHeight, setMaxHeight] = (0,
|
|
1124
|
-
const [measured, setMeasured] = (0,
|
|
1125
|
-
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);
|
|
1126
1148
|
if (open !== prevOpen) {
|
|
1127
1149
|
setPrevOpen(open);
|
|
1128
1150
|
if (open) setMeasured(false);
|
|
1129
1151
|
}
|
|
1130
|
-
const measure = (0,
|
|
1152
|
+
const measure = (0, import_react10.useCallback)(() => {
|
|
1131
1153
|
triggerRef.current?.measureInWindow((rawX, rawY, rawWidth, rawHeight) => {
|
|
1132
1154
|
const { x, y, width: triggerWidth, height: triggerHeight } = rotateRect(rawX, rawY, rawWidth, rawHeight, rotation, windowWidth, windowHeight);
|
|
1133
1155
|
const centerX = x + triggerWidth / 2;
|
|
@@ -1144,7 +1166,7 @@ function useAutoAlign(open, contentWidth, contentHeight, rotation = 0) {
|
|
|
1144
1166
|
setMeasured(true);
|
|
1145
1167
|
});
|
|
1146
1168
|
}, [contentWidth, contentHeight, windowWidth, windowHeight, rotation]);
|
|
1147
|
-
(0,
|
|
1169
|
+
(0, import_react10.useEffect)(() => {
|
|
1148
1170
|
if (open) measure();
|
|
1149
1171
|
}, [open, measure]);
|
|
1150
1172
|
return { align, maxHeight, measured, triggerRef, verticalAlign };
|
|
@@ -1673,13 +1695,13 @@ var styles20 = import_react_native25.StyleSheet.create({
|
|
|
1673
1695
|
});
|
|
1674
1696
|
|
|
1675
1697
|
// src/usePopoverHost.ts
|
|
1676
|
-
var
|
|
1698
|
+
var import_react11 = require("react");
|
|
1677
1699
|
function usePopoverHost() {
|
|
1678
|
-
const [openId, setOpenId] = (0,
|
|
1679
|
-
const toggle = (0,
|
|
1700
|
+
const [openId, setOpenId] = (0, import_react11.useState)(null);
|
|
1701
|
+
const toggle = (0, import_react11.useCallback)((id) => {
|
|
1680
1702
|
setOpenId((prev) => prev === id ? null : id);
|
|
1681
1703
|
}, []);
|
|
1682
|
-
const close = (0,
|
|
1704
|
+
const close = (0, import_react11.useCallback)(() => setOpenId(null), []);
|
|
1683
1705
|
return { openId, toggle, close };
|
|
1684
1706
|
}
|
|
1685
1707
|
|
|
@@ -1778,7 +1800,7 @@ var styles22 = import_react_native28.StyleSheet.create({
|
|
|
1778
1800
|
});
|
|
1779
1801
|
|
|
1780
1802
|
// src/StaggeredWord.tsx
|
|
1781
|
-
var
|
|
1803
|
+
var import_react12 = require("react");
|
|
1782
1804
|
var import_react_native29 = require("react-native");
|
|
1783
1805
|
var import_react_native_reanimated2 = __toESM(require("react-native-reanimated"));
|
|
1784
1806
|
var import_jsx_runtime28 = require("react/jsx-runtime");
|
|
@@ -1789,7 +1811,7 @@ function getStaggeredWordDuration(letterCount) {
|
|
|
1789
1811
|
}
|
|
1790
1812
|
function AnimatedLetter({ char, index, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }) {
|
|
1791
1813
|
const progress = (0, import_react_native_reanimated2.useSharedValue)(reducedMotion ? 1 : 0);
|
|
1792
|
-
(0,
|
|
1814
|
+
(0, import_react12.useEffect)(() => {
|
|
1793
1815
|
if (reducedMotion) return;
|
|
1794
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)) }));
|
|
1795
1817
|
}, [reducedMotion]);
|
|
@@ -1819,28 +1841,28 @@ function UpdateDialog({ rotation = 0, autoCheck = true, autoPrompt = true, onErr
|
|
|
1819
1841
|
}
|
|
1820
1842
|
|
|
1821
1843
|
// src/useQuitConfirmation.ts
|
|
1822
|
-
var
|
|
1844
|
+
var import_react13 = require("react");
|
|
1823
1845
|
function useQuitConfirmation(hasProgress, onConfirmedBack) {
|
|
1824
|
-
const [confirmVisible, setConfirmVisible] = (0,
|
|
1825
|
-
const hasProgressRef = (0,
|
|
1826
|
-
(0,
|
|
1846
|
+
const [confirmVisible, setConfirmVisible] = (0, import_react13.useState)(false);
|
|
1847
|
+
const hasProgressRef = (0, import_react13.useRef)(hasProgress);
|
|
1848
|
+
(0, import_react13.useEffect)(() => {
|
|
1827
1849
|
hasProgressRef.current = hasProgress;
|
|
1828
1850
|
});
|
|
1829
|
-
const onConfirmedBackRef = (0,
|
|
1830
|
-
(0,
|
|
1851
|
+
const onConfirmedBackRef = (0, import_react13.useRef)(onConfirmedBack);
|
|
1852
|
+
(0, import_react13.useEffect)(() => {
|
|
1831
1853
|
onConfirmedBackRef.current = onConfirmedBack;
|
|
1832
1854
|
});
|
|
1833
|
-
const requestBack = (0,
|
|
1855
|
+
const requestBack = (0, import_react13.useCallback)(() => {
|
|
1834
1856
|
if (hasProgressRef.current()) setConfirmVisible(true);
|
|
1835
1857
|
else onConfirmedBackRef.current();
|
|
1836
1858
|
}, []);
|
|
1837
|
-
const cancelBack = (0,
|
|
1859
|
+
const cancelBack = (0, import_react13.useCallback)(() => setConfirmVisible(false), []);
|
|
1838
1860
|
return { confirmVisible, requestBack, cancelBack };
|
|
1839
1861
|
}
|
|
1840
1862
|
|
|
1841
1863
|
// src/useZoneClampedAlign.ts
|
|
1842
1864
|
var import_split_screen = require("@tastic/split-screen");
|
|
1843
|
-
var
|
|
1865
|
+
var import_react14 = require("react");
|
|
1844
1866
|
var import_react_native30 = require("react-native");
|
|
1845
1867
|
var import_react_native_safe_area_context3 = require("react-native-safe-area-context");
|
|
1846
1868
|
var EDGE_MARGIN2 = 12;
|
|
@@ -1850,13 +1872,13 @@ function useZoneClampedAlign(open, contentWidth, contentHeight, rotation = 0) {
|
|
|
1850
1872
|
const { height: windowHeight } = (0, import_react_native30.useWindowDimensions)();
|
|
1851
1873
|
const insets = (0, import_react_native_safe_area_context3.useSafeAreaInsets)();
|
|
1852
1874
|
const shouldMeasure = open && auto.measured && zone !== null;
|
|
1853
|
-
const [ownBounds, setOwnBounds] = (0,
|
|
1854
|
-
const [prevShouldMeasure, setPrevShouldMeasure] = (0,
|
|
1875
|
+
const [ownBounds, setOwnBounds] = (0, import_react14.useState)(null);
|
|
1876
|
+
const [prevShouldMeasure, setPrevShouldMeasure] = (0, import_react14.useState)(shouldMeasure);
|
|
1855
1877
|
if (shouldMeasure !== prevShouldMeasure) {
|
|
1856
1878
|
setPrevShouldMeasure(shouldMeasure);
|
|
1857
1879
|
setOwnBounds(null);
|
|
1858
1880
|
}
|
|
1859
|
-
(0,
|
|
1881
|
+
(0, import_react14.useEffect)(() => {
|
|
1860
1882
|
if (!shouldMeasure) return;
|
|
1861
1883
|
auto.triggerRef.current?.measureInWindow((_x, y, _width, height) => {
|
|
1862
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",
|
|
@@ -87,15 +87,15 @@
|
|
|
87
87
|
"@infinitetoken/eslint-config": "^0.2.0",
|
|
88
88
|
"@infinitetoken/jest-config": "^0.4.0",
|
|
89
89
|
"@infinitetoken/tsconfig": "^0.5.0",
|
|
90
|
-
"@rific/auto-paper": "^0.13.
|
|
91
|
-
"@rific/core": "^0.3.
|
|
90
|
+
"@rific/auto-paper": "^0.13.2",
|
|
91
|
+
"@rific/core": "^0.3.1",
|
|
92
92
|
"@rific/feedback-press": "^0.13.0",
|
|
93
|
-
"@rific/updater": "^0.
|
|
93
|
+
"@rific/updater": "^0.7.1",
|
|
94
94
|
"@shopify/react-native-skia": "^2.6.2",
|
|
95
|
-
"@tastic/achievements": "^0.
|
|
96
|
-
"@tastic/core": "^0.
|
|
97
|
-
"@tastic/profile": "^0.
|
|
98
|
-
"@tastic/split-screen": "^0.5.
|
|
95
|
+
"@tastic/achievements": "^0.6.0",
|
|
96
|
+
"@tastic/core": "^0.14.0",
|
|
97
|
+
"@tastic/profile": "^0.13.0",
|
|
98
|
+
"@tastic/split-screen": "^0.5.2",
|
|
99
99
|
"@testing-library/dom": "^10.4.1",
|
|
100
100
|
"@testing-library/react": "^16.3.2",
|
|
101
101
|
"@types/jest": "^30.0.0",
|
|
@@ -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
|
|
@@ -56,6 +56,13 @@ export interface BaseSettingsDialogProps {
|
|
|
56
56
|
// @tastic/edge-guard's own swizzle is a no-op on every other platform.
|
|
57
57
|
deferBottomEdgeGestures?: boolean
|
|
58
58
|
onDeferBottomEdgeGestures?: (value: boolean) => void
|
|
59
|
+
// Optional, and always passed as a pair - omit both for an app that doesn't keep the screen awake.
|
|
60
|
+
// This dialog only shows and reports the toggle; the app itself keeps the screen on while it's set
|
|
61
|
+
// (expo-keep-awake on its match screen), so this package needs no native dependency for it. Shown
|
|
62
|
+
// on the same devices as Lock Orientation (see isTouchPrimary below): a desktop/laptop browser
|
|
63
|
+
// stays awake on its own while someone is playing with its keyboard or mouse.
|
|
64
|
+
keepAwake?: boolean
|
|
65
|
+
onKeepAwakeChange?: (value: boolean) => void
|
|
59
66
|
// Explicit opt-outs for the rows that pull their own state straight from @rific/feedback-press,
|
|
60
67
|
// @rific/auto-paper, and @rific/updater's own shared contexts/hooks rather than from anything the
|
|
61
68
|
// caller passes in - unlike Lock Orientation/Edge Guard above, there's no "value" to omit for
|
|
@@ -83,14 +90,14 @@ export interface BaseSettingsDialogProps {
|
|
|
83
90
|
children?: ReactNode
|
|
84
91
|
}
|
|
85
92
|
|
|
86
|
-
// The settings shell every @tastic game shares - orientation lock, the optional edge-guard
|
|
87
|
-
// sound/haptics, appearance, and update checking - extracted after the same six rows were
|
|
93
|
+
// The settings shell every @tastic game shares - orientation lock, the optional edge-guard and
|
|
94
|
+
// keep-awake toggles, sound/haptics, appearance, and update checking - extracted after the same six rows were
|
|
88
95
|
// independently copied into LightCycles, AirHockey, BoxHockey, Pong, and Snake and then needed
|
|
89
96
|
// hand-editing in every one of them, twice, for what were meant to be identical fixes. `children`
|
|
90
97
|
// is the seam for whatever isn't shared: a game with its own board/difficulty settings (or a stats
|
|
91
98
|
// backup flow, or anything else genuinely specific to it) renders those below Appearance, not by
|
|
92
99
|
// forking this component.
|
|
93
|
-
export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }: BaseSettingsDialogProps) {
|
|
100
|
+
export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, keepAwake, onKeepAwakeChange, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }: BaseSettingsDialogProps) {
|
|
94
101
|
const { colors } = useAutoPaperTheme()
|
|
95
102
|
const ambientRotation = useRotation()
|
|
96
103
|
const rotation = rotationOverride ?? ambientRotation
|
|
@@ -106,7 +113,7 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
106
113
|
// Dialog's own 32pt padding is simply overhung.
|
|
107
114
|
const insets = rotateInsets(useSafeAreaInsets(), rotation)
|
|
108
115
|
const sheetWidth = Math.min(overlayStyles.card.maxWidth, (turned ? window.height : window.width) - 2 * Math.max(insets.left, insets.right, SHEET_GUTTER))
|
|
109
|
-
// Also gates the Lock Orientation
|
|
116
|
+
// Also gates the Lock Orientation and Keep Awake rows below (alongside their own props) - true
|
|
110
117
|
// unconditionally on native (see the hook's own doc), so this only actually excludes a
|
|
111
118
|
// desktop/laptop browser, where a mouse-driven window has no physical orientation to lock
|
|
112
119
|
// against; a resize there would just make the setting silently stop reflowing the layout instead
|
|
@@ -153,6 +160,7 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
153
160
|
|
|
154
161
|
const showLockOrientation = isTouchPrimary && lockOrientation !== undefined && onLockOrientationChange !== undefined
|
|
155
162
|
const showEdgeGuard = Platform.OS === 'ios' && deferBottomEdgeGestures !== undefined && onDeferBottomEdgeGestures !== undefined
|
|
163
|
+
const showKeepAwake = isTouchPrimary && keepAwake !== undefined && onKeepAwakeChange !== undefined
|
|
156
164
|
const showHaptics = Platform.OS !== 'web' && !hideHaptics
|
|
157
165
|
const showSound = !hideSound
|
|
158
166
|
const showAppearance = !hideAppearance
|
|
@@ -235,6 +243,23 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
235
243
|
</TouchableRipple>
|
|
236
244
|
)}
|
|
237
245
|
|
|
246
|
+
{/* See the keepAwake prop's own doc: the app does the keeping awake, this only toggles it. */}
|
|
247
|
+
{showKeepAwake && (
|
|
248
|
+
<TouchableRipple onPress={() => onKeepAwakeChange!(!keepAwake)} style={styles.toggleButton} accessibilityLabel={`Keep awake ${keepAwake ? 'on' : 'off'}`}>
|
|
249
|
+
<View style={styles.toggleContent}>
|
|
250
|
+
<SettingIcon source={keepAwake ? 'eye' : 'eye-closed'} color={keepAwake ? colors.tertiary : colors.onSurfaceVariant} containerColor={keepAwake ? colors.tertiaryContainer : colors.surfaceVariant} />
|
|
251
|
+
<View style={styles.flexShrink}>
|
|
252
|
+
<Text variant='bodyLarge' style={{ color: colors.onSurface }}>
|
|
253
|
+
Keep Awake
|
|
254
|
+
</Text>
|
|
255
|
+
<Text variant='bodySmall' numberOfLines={1} style={{ color: colors.onSurfaceVariant }}>
|
|
256
|
+
Stop auto-lock mid-match
|
|
257
|
+
</Text>
|
|
258
|
+
</View>
|
|
259
|
+
</View>
|
|
260
|
+
</TouchableRipple>
|
|
261
|
+
)}
|
|
262
|
+
|
|
238
263
|
{/* Side by side, not stacked - neither needs a full row's width (single-line label,
|
|
239
264
|
no description under it the way Lock Orientation has), so sharing one row reads
|
|
240
265
|
just as clearly and takes half the vertical space. toggleRow carries the same
|
|
@@ -375,7 +400,9 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
|
|
|
375
400
|
</View>
|
|
376
401
|
</Dialog>
|
|
377
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)} />
|
|
378
|
-
|
|
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)} />
|
|
379
406
|
</RotatedFrame>
|
|
380
407
|
</Portal>
|
|
381
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
|