@tastic/hud 0.18.0 → 0.19.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 CHANGED
@@ -207,9 +207,11 @@ the fleet with real Redux Toolkit state - reads `lockOrientation`/`deferBottomEd
207
207
  `useSelector` and writes them back via `dispatch(gameActions.setLockOrientation(...))` directly
208
208
  inside the adapter, with nothing threaded through props at all. `BaseSettingsDialog` doesn't care
209
209
  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
- and `deferBottomEdgeGestures`/`onDeferBottomEdgeGestures` are the only two settings an adapter
212
- actually needs to source itself, however it does that.
210
+ every app and live entirely inside this package now, so `lockOrientation`/`onLockOrientationChange`,
211
+ `deferBottomEdgeGestures`/`onDeferBottomEdgeGestures` and `keepAwake`/`onKeepAwakeChange` are the only
212
+ settings an adapter actually needs to source itself, however it does that. Each pair is optional: omit
213
+ one and its row doesn't show. The Keep Awake row only toggles the value; the app keeps the screen awake
214
+ itself while it's on (for example with `expo-keep-awake` on its match screen).
213
215
 
214
216
  Every adapter also passes `version={release.otaVersion}` (so the dialog's own update-check button
215
217
  knows what it's currently running) and `onUpdateError`, wired to a real `useToast()` call. Wire this
@@ -234,6 +236,8 @@ export function SettingsDialog({ visible, onDismiss, rotation = 0, settings, set
234
236
  onLockOrientationChange={(value) => setSettings({ lockOrientation: value })}
235
237
  deferBottomEdgeGestures={settings.deferBottomEdgeGestures}
236
238
  onDeferBottomEdgeGestures={(value) => setSettings({ deferBottomEdgeGestures: value })}
239
+ keepAwake={settings.keepAwake}
240
+ onKeepAwakeChange={(value) => setSettings({ keepAwake: value })}
237
241
  onUpdateError={(message) => error('Update check failed', message)}
238
242
  />
239
243
  )
@@ -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 {
@@ -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 {
@@ -474,7 +474,7 @@ var import_jsx_runtime10 = require("react/jsx-runtime");
474
474
  function SettingIcon({ source, color, containerColor }) {
475
475
  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
476
  }
477
- function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }) {
477
+ 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
478
  const { colors } = (0, import_auto_paper8.useAutoPaperTheme)();
479
479
  const ambientRotation = (0, import_core4.useRotation)();
480
480
  const rotation = rotationOverride ?? ambientRotation;
@@ -504,6 +504,7 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
504
504
  if (visible && !opened) setOpened(true);
505
505
  const showLockOrientation = isTouchPrimary && lockOrientation !== void 0 && onLockOrientationChange !== void 0;
506
506
  const showEdgeGuard = import_react_native12.Platform.OS === "ios" && deferBottomEdgeGestures !== void 0 && onDeferBottomEdgeGestures !== void 0;
507
+ const showKeepAwake = isTouchPrimary && keepAwake !== void 0 && onKeepAwakeChange !== void 0;
507
508
  const showHaptics = import_react_native12.Platform.OS !== "web" && !hideHaptics;
508
509
  const showSound = !hideSound;
509
510
  const showAppearance = !hideAppearance;
@@ -530,6 +531,13 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
530
531
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native_paper8.Text, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Require second swipe" })
531
532
  ] })
532
533
  ] }) }),
534
+ 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: [
535
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(SettingIcon, { source: keepAwake ? "eye" : "eye-closed", color: keepAwake ? colors.tertiary : colors.onSurfaceVariant, containerColor: keepAwake ? colors.tertiaryContainer : colors.surfaceVariant }),
536
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native12.View, { style: styles9.flexShrink, children: [
537
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native_paper8.Text, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Keep Awake" }),
538
+ /* @__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" })
539
+ ] })
540
+ ] }) }),
533
541
  (showSound || showHaptics) && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native12.View, { style: styles9.toggleRow, children: [
534
542
  showSound && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
535
543
  import_feedback_press2.TouchableRipple,
@@ -16,7 +16,7 @@ import {
16
16
  UpdateDialog,
17
17
  getStaggeredWordDuration,
18
18
  useCompactDialog
19
- } from "../chunk-Z2WUUYTE.mjs";
19
+ } from "../chunk-HGJKNLMD.mjs";
20
20
  import {
21
21
  COMPACT_DIALOG_HEIGHT,
22
22
  DialogHeader,
@@ -293,7 +293,7 @@ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
293
293
  function SettingIcon({ source, color, containerColor }) {
294
294
  return /* @__PURE__ */ jsx8(View6, { style: [styles7.iconBadge, { backgroundColor: containerColor }], children: /* @__PURE__ */ jsx8(Icon2, { source, size: 18, color }) });
295
295
  }
296
- function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }) {
296
+ 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
297
  const { colors } = useAutoPaperTheme6();
298
298
  const ambientRotation = useRotation2();
299
299
  const rotation = rotationOverride ?? ambientRotation;
@@ -323,6 +323,7 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
323
323
  if (visible && !opened) setOpened(true);
324
324
  const showLockOrientation = isTouchPrimary && lockOrientation !== void 0 && onLockOrientationChange !== void 0;
325
325
  const showEdgeGuard = Platform2.OS === "ios" && deferBottomEdgeGestures !== void 0 && onDeferBottomEdgeGestures !== void 0;
326
+ const showKeepAwake = isTouchPrimary && keepAwake !== void 0 && onKeepAwakeChange !== void 0;
326
327
  const showHaptics = Platform2.OS !== "web" && !hideHaptics;
327
328
  const showSound = !hideSound;
328
329
  const showAppearance = !hideAppearance;
@@ -349,6 +350,13 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
349
350
  /* @__PURE__ */ jsx8(Text6, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Require second swipe" })
350
351
  ] })
351
352
  ] }) }),
353
+ showKeepAwake && /* @__PURE__ */ jsx8(TouchableRipple, { onPress: () => onKeepAwakeChange(!keepAwake), style: styles7.toggleButton, accessibilityLabel: `Keep awake ${keepAwake ? "on" : "off"}`, children: /* @__PURE__ */ jsxs7(View6, { style: styles7.toggleContent, children: [
354
+ /* @__PURE__ */ jsx8(SettingIcon, { source: keepAwake ? "eye" : "eye-closed", color: keepAwake ? colors.tertiary : colors.onSurfaceVariant, containerColor: keepAwake ? colors.tertiaryContainer : colors.surfaceVariant }),
355
+ /* @__PURE__ */ jsxs7(View6, { style: styles7.flexShrink, children: [
356
+ /* @__PURE__ */ jsx8(Text6, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Keep Awake" }),
357
+ /* @__PURE__ */ jsx8(Text6, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Stop auto-lock mid-match" })
358
+ ] })
359
+ ] }) }),
352
360
  (showSound || showHaptics) && /* @__PURE__ */ jsxs7(View6, { style: styles7.toggleRow, children: [
353
361
  showSound && /* @__PURE__ */ jsx8(
354
362
  TouchableRipple,
package/dist/index.js CHANGED
@@ -678,7 +678,7 @@ var import_jsx_runtime11 = require("react/jsx-runtime");
678
678
  function SettingIcon({ source, color, containerColor }) {
679
679
  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
680
  }
681
- function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }) {
681
+ 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
682
  const { colors } = (0, import_auto_paper9.useAutoPaperTheme)();
683
683
  const ambientRotation = (0, import_core4.useRotation)();
684
684
  const rotation = rotationOverride ?? ambientRotation;
@@ -708,6 +708,7 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
708
708
  if (visible && !opened) setOpened(true);
709
709
  const showLockOrientation = isTouchPrimary && lockOrientation !== void 0 && onLockOrientationChange !== void 0;
710
710
  const showEdgeGuard = import_react_native13.Platform.OS === "ios" && deferBottomEdgeGestures !== void 0 && onDeferBottomEdgeGestures !== void 0;
711
+ const showKeepAwake = isTouchPrimary && keepAwake !== void 0 && onKeepAwakeChange !== void 0;
711
712
  const showHaptics = import_react_native13.Platform.OS !== "web" && !hideHaptics;
712
713
  const showSound = !hideSound;
713
714
  const showAppearance = !hideAppearance;
@@ -734,6 +735,13 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
734
735
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native_paper9.Text, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Require second swipe" })
735
736
  ] })
736
737
  ] }) }),
738
+ 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: [
739
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SettingIcon, { source: keepAwake ? "eye" : "eye-closed", color: keepAwake ? colors.tertiary : colors.onSurfaceVariant, containerColor: keepAwake ? colors.tertiaryContainer : colors.surfaceVariant }),
740
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native13.View, { style: styles10.flexShrink, children: [
741
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native_paper9.Text, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Keep Awake" }),
742
+ /* @__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" })
743
+ ] })
744
+ ] }) }),
737
745
  (showSound || showHaptics) && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native13.View, { style: styles10.toggleRow, children: [
738
746
  showSound && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
739
747
  import_feedback_press2.TouchableRipple,
package/dist/index.mjs CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  UpdateDialog,
20
20
  getStaggeredWordDuration,
21
21
  useCompactDialog
22
- } from "./chunk-Z2WUUYTE.mjs";
22
+ } from "./chunk-HGJKNLMD.mjs";
23
23
  import {
24
24
  COMPACT_DIALOG_HEIGHT,
25
25
  DialogHeader,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/hud",
3
- "version": "0.18.0",
3
+ "version": "0.19.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.0",
91
- "@rific/core": "^0.3.0",
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.6.0",
93
+ "@rific/updater": "^0.7.1",
94
94
  "@shopify/react-native-skia": "^2.6.2",
95
- "@tastic/achievements": "^0.5.0",
96
- "@tastic/core": "^0.13.0",
95
+ "@tastic/achievements": "^0.6.0",
96
+ "@tastic/core": "^0.14.0",
97
97
  "@tastic/profile": "^0.12.0",
98
- "@tastic/split-screen": "^0.5.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",
@@ -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 toggle,
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 row below (alongside showLockOrientation itself) - true
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