@tastic/hud 0.8.3 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -71,12 +71,19 @@ var styles = StyleSheet.create({
71
71
  });
72
72
 
73
73
  // src/BaseSettingsDialog.tsx
74
- import { AutoAppearancePicker, Dialog, useAutoPaperTheme as useAutoPaperTheme2 } from "@rific/auto-paper";
75
- import { Button, SoundContext, TouchableRipple, useHapticSettings, useSoundSettings, useVibration } from "@rific/feedback-press";
74
+ import { AutoAppearancePicker, Dialog, useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
75
+ import { Button as Button2, SoundContext, TouchableRipple, useHapticSettings, useSoundSettings, useVibration } from "@rific/feedback-press";
76
76
  import { useUpdater } from "@rific/updater";
77
- import { toRotationStyle, useIsTouchPrimaryDevice, useRotation } from "@tastic/core";
78
- import { useContext, useState } from "react";
79
- import { Platform, ScrollView, StyleSheet as StyleSheet3, View as View2 } from "react-native";
77
+ import { toRotationStyle as toRotationStyle2, useIsTouchPrimaryDevice, useRotation } from "@tastic/core";
78
+ import { useContext, useState as useState2 } from "react";
79
+ import { Platform, ScrollView, StyleSheet as StyleSheet4, View as View3 } from "react-native";
80
+ import { Icon as Icon3, Text as Text3 } from "react-native-paper";
81
+
82
+ // src/ConfirmDialog.tsx
83
+ import { useAutoPaperTheme as useAutoPaperTheme2 } from "@rific/auto-paper";
84
+ import { Button } from "@rific/feedback-press";
85
+ import { toRotationStyle } from "@tastic/core";
86
+ import { StyleSheet as StyleSheet3, View as View2 } from "react-native";
80
87
  import { Icon as Icon2, Portal, Text as Text2 } from "react-native-paper";
81
88
 
82
89
  // src/overlayCard.ts
@@ -110,13 +117,119 @@ var overlayStyles = StyleSheet2.create({
110
117
  }
111
118
  });
112
119
 
120
+ // src/ConfirmDialog.tsx
121
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
122
+ function ConfirmDialog({ visible, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, rotation = 0, icon = "help-circle-outline", destructive = false }) {
123
+ const { dark, colors } = useAutoPaperTheme2();
124
+ if (!visible) return null;
125
+ const { fg, cardBg, cardBorder } = getOverlayCardColors(dark);
126
+ const accent = destructive ? colors.danger : colors.primary;
127
+ const onAccent = destructive ? colors.onDanger : colors.onPrimary;
128
+ const showCancel = cancelLabel !== void 0 && onCancel !== void 0;
129
+ return /* @__PURE__ */ jsx2(Portal, { children: /* @__PURE__ */ jsx2(View2, { style: [overlayStyles.overlay, styles2.overlay], children: /* @__PURE__ */ jsxs2(View2, { style: [overlayStyles.card, styles2.card, { backgroundColor: cardBg, borderColor: cardBorder }, toRotationStyle(rotation)], children: [
130
+ /* @__PURE__ */ jsx2(Icon2, { source: icon, size: 64, color: accent }),
131
+ /* @__PURE__ */ jsx2(Text2, { variant: "headlineLarge", style: [styles2.title, { color: accent }], children: title }),
132
+ /* @__PURE__ */ jsx2(Text2, { variant: "bodyLarge", style: [styles2.body, { color: fg }], children: message }),
133
+ /* @__PURE__ */ jsxs2(View2, { style: styles2.actions, children: [
134
+ showCancel && /* @__PURE__ */ jsx2(Button, { mode: "outlined", onPress: onCancel, style: styles2.actionButton, children: cancelLabel }),
135
+ /* @__PURE__ */ jsx2(Button, { mode: "contained", onPress: onConfirm, style: showCancel ? styles2.actionButton : styles2.actionButtonSingle, buttonColor: accent, textColor: onAccent, children: confirmLabel })
136
+ ] })
137
+ ] }) }) });
138
+ }
139
+ var styles2 = StyleSheet3.create({
140
+ // minWidth (not flexBasis) is each button's real floor - whenever both fit side by side, flexGrow
141
+ // still splits any leftover width evenly beyond that floor, same visual result as a plain flex: 1
142
+ // on a roomy screen, but a button can never be squeezed narrower than 128, only wrap to its own
143
+ // row instead (see actions' flexWrap below). This used to be flexBasis: 128, which measured
144
+ // correctly on web but rendered every button roughly SQUARE on iOS - confirmed live (see the
145
+ // "Quit Match?" Cancel/Quit buttons and the single-button "OK" dialogs both coming out ~128pt
146
+ // tall instead of wide). Root cause: react-native-paper's Button renders its Surface as two
147
+ // nested views on iOS only (Surface.tsx's SurfaceIOS, for shadow rendering) - only a curated
148
+ // allowlist of style keys (flex/flexGrow/flexShrink/width/height/position/... - notably NOT
149
+ // flexBasis) reaches the outer view, which is the real flex item inside this row; every other
150
+ // key, flexBasis included, falls through to an inner view whose own unspecified flexDirection
151
+ // defaults to column. flexGrow: 1 landed on the (row-context) outer view and grew width
152
+ // correctly; flexBasis: 128 landed on the (column-context) inner view instead, where a flexBasis
153
+ // sets HEIGHT, not width. minWidth isn't axis-relative like flexBasis, so it constrains width
154
+ // correctly regardless of which of the two views it ends up on. react-native-paper's Button also
155
+ // hardcodes numberOfLines={1} on its label with no override, so a button squeezed under its
156
+ // label's natural width silently ellipsizes ("Cancel" -> "Can...") instead of wrapping - assumed
157
+ // fixable by just giving the row more width, until confirmed live that these two-word labels
158
+ // (Cancel/Reset/New Game) can still lose the truncation fight even with the extra room card's own
159
+ // width fix below provides, on a narrow enough screen.
160
+ actionButton: {
161
+ flexGrow: 1,
162
+ minWidth: 128
163
+ },
164
+ // A lone confirm button (no cancel - BaseSettingsDialog's info-only "OK" dialogs, e.g. the
165
+ // update-check "no update found" message) has nothing left to share actions' width with, so
166
+ // actionButton's own flexGrow: 1 would stretch it across the entire row instead of reading as a
167
+ // normal button. Same 128 floor, no flexGrow, so it stays at that natural size instead of filling
168
+ // the card - centered by actions' own justifyContent below.
169
+ actionButtonSingle: {
170
+ minWidth: 128
171
+ },
172
+ actions: {
173
+ flexDirection: "row",
174
+ flexWrap: "wrap",
175
+ gap: 12,
176
+ justifyContent: "center",
177
+ width: "100%"
178
+ },
179
+ body: {
180
+ textAlign: "center"
181
+ },
182
+ // Additive on top of overlayCard's own shared `card` shape (borderRadius/borderWidth/gap/maxWidth/
183
+ // padding) — claims its full allowance instead of just maxWidth alone, which only caps how wide the
184
+ // card is allowed to get - without an explicit width, a flex-centered child sizes to its own content
185
+ // instead of stretching, so the card (and therefore actions below) can end up narrower than the
186
+ // screen actually has room for, needlessly forcing the action row to wrap sooner than it has to.
187
+ card: {
188
+ width: "100%"
189
+ },
190
+ // Additive on top of overlayCard's own shared `overlay` backdrop — gives the card room to breathe
191
+ // from the screen edges now that it reliably stretches to fill its full width allowance (see
192
+ // card's own width comment) - without this, width: '100%' would run the card edge-to-edge on a
193
+ // screen narrower than maxWidth's own 360.
194
+ overlay: {
195
+ paddingHorizontal: 24
196
+ },
197
+ // textAlign is load-bearing, not decorative, once `title` wraps to more than one line - centered
198
+ // within card's own alignItems: 'center' only centers the Text's block as a whole; each wrapped
199
+ // line inside that block still defaults to left-aligned without this.
200
+ title: {
201
+ fontWeight: "bold",
202
+ textAlign: "center"
203
+ }
204
+ });
205
+
206
+ // src/useUpdateConfirmBridge.ts
207
+ import { useCallback, useRef, useState } from "react";
208
+ function useUpdateConfirmBridge() {
209
+ const [manifest, setManifest] = useState(null);
210
+ const resolveRef = useRef(null);
211
+ const onConfirm = useCallback(
212
+ (next) => new Promise((resolve) => {
213
+ resolveRef.current = resolve;
214
+ setManifest(next);
215
+ }),
216
+ []
217
+ );
218
+ const respond = useCallback((confirmed) => {
219
+ setManifest(null);
220
+ resolveRef.current?.(confirmed);
221
+ resolveRef.current = null;
222
+ }, []);
223
+ return { manifest, onConfirm, respond };
224
+ }
225
+
113
226
  // src/BaseSettingsDialog.tsx
114
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
227
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
115
228
  function SettingIcon({ source, color, containerColor }) {
116
- return /* @__PURE__ */ jsx2(View2, { style: [styles2.iconBadge, { backgroundColor: containerColor }], children: /* @__PURE__ */ jsx2(Icon2, { source, size: 18, color }) });
229
+ return /* @__PURE__ */ jsx3(View3, { style: [styles3.iconBadge, { backgroundColor: containerColor }], children: /* @__PURE__ */ jsx3(Icon3, { source, size: 18, color }) });
117
230
  }
118
231
  function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, children }) {
119
- const { dark, colors } = useAutoPaperTheme2();
232
+ const { colors } = useAutoPaperTheme3();
120
233
  const ambientRotation = useRotation();
121
234
  const rotation = rotationOverride ?? ambientRotation;
122
235
  const isTouchPrimary = useIsTouchPrimaryDevice();
@@ -124,41 +237,43 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
124
237
  const { settings: soundSettings, set: setSoundSettings } = useSoundSettings();
125
238
  const sound = useContext(SoundContext);
126
239
  const { forceShort: forceHapticFeedback } = useVibration();
127
- const [infoMessage, setInfoMessage] = useState(null);
240
+ const [infoMessage, setInfoMessage] = useState2(null);
241
+ const { manifest: updateManifest, onConfirm: onUpdateConfirm, respond: respondToUpdateConfirm } = useUpdateConfirmBridge();
128
242
  const { check, checking, updateReady } = useUpdater({
129
243
  autoCheck: false,
130
244
  autoPrompt: false,
245
+ onConfirm: onUpdateConfirm,
131
246
  onError: onUpdateError,
132
247
  onInfo: (title, message) => setInfoMessage({ title, message })
133
248
  });
249
+ const updateManifestDate = updateManifest ? new Date(updateManifest.createdAt) : null;
134
250
  const showLockOrientation = isTouchPrimary && lockOrientation !== void 0 && onLockOrientationChange !== void 0;
135
251
  const showEdgeGuard = Platform.OS === "ios" && deferBottomEdgeGestures !== void 0 && onDeferBottomEdgeGestures !== void 0;
136
252
  const showHaptics = Platform.OS !== "web" && !hideHaptics;
137
253
  const showSound = !hideSound;
138
254
  const showAppearance = !hideAppearance;
139
255
  const showUpdateCheck = Platform.OS !== "web" && !hideUpdateCheck;
140
- const { fg, cardBg, cardBorder } = getOverlayCardColors(dark);
141
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
142
- /* @__PURE__ */ jsxs2(Dialog, { visible, onDismiss, style: [styles2.dialog, toRotationStyle(rotation)], children: [
143
- /* @__PURE__ */ jsx2(Dialog.Title, { children: "Settings" }),
144
- /* @__PURE__ */ jsx2(Dialog.ScrollArea, { style: styles2.scrollArea, children: /* @__PURE__ */ jsxs2(ScrollView, { contentContainerStyle: styles2.content, showsVerticalScrollIndicator: false, children: [
145
- /* @__PURE__ */ jsxs2(View2, { style: styles2.toggleGroup, children: [
146
- showLockOrientation && /* @__PURE__ */ jsx2(TouchableRipple, { onPress: () => onLockOrientationChange(!lockOrientation), style: styles2.toggleButton, accessibilityLabel: `Lock orientation ${lockOrientation ? "on" : "off"}`, children: /* @__PURE__ */ jsxs2(View2, { style: styles2.toggleContent, children: [
147
- /* @__PURE__ */ jsx2(SettingIcon, { source: lockOrientation ? "lock" : "lock-open-variant-outline", color: lockOrientation ? colors.secondary : colors.onSurfaceVariant, containerColor: lockOrientation ? colors.secondaryContainer : colors.surfaceVariant }),
148
- /* @__PURE__ */ jsxs2(View2, { style: styles2.flexShrink, children: [
149
- /* @__PURE__ */ jsx2(Text2, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Lock Orientation" }),
150
- /* @__PURE__ */ jsx2(Text2, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Pins the current layout" })
256
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
257
+ /* @__PURE__ */ jsxs3(Dialog, { visible, onDismiss, style: [styles3.dialog, toRotationStyle2(rotation)], children: [
258
+ /* @__PURE__ */ jsx3(Dialog.Title, { children: "Settings" }),
259
+ /* @__PURE__ */ jsx3(Dialog.ScrollArea, { style: styles3.scrollArea, children: /* @__PURE__ */ jsxs3(ScrollView, { contentContainerStyle: styles3.content, showsVerticalScrollIndicator: false, children: [
260
+ /* @__PURE__ */ jsxs3(View3, { style: styles3.toggleGroup, children: [
261
+ showLockOrientation && /* @__PURE__ */ jsx3(TouchableRipple, { onPress: () => onLockOrientationChange(!lockOrientation), style: styles3.toggleButton, accessibilityLabel: `Lock orientation ${lockOrientation ? "on" : "off"}`, children: /* @__PURE__ */ jsxs3(View3, { style: styles3.toggleContent, children: [
262
+ /* @__PURE__ */ jsx3(SettingIcon, { source: lockOrientation ? "lock" : "lock-open-variant-outline", color: lockOrientation ? colors.secondary : colors.onSurfaceVariant, containerColor: lockOrientation ? colors.secondaryContainer : colors.surfaceVariant }),
263
+ /* @__PURE__ */ jsxs3(View3, { style: styles3.flexShrink, children: [
264
+ /* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Lock Orientation" }),
265
+ /* @__PURE__ */ jsx3(Text3, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Pins the current layout" })
151
266
  ] })
152
267
  ] }) }),
153
- showEdgeGuard && /* @__PURE__ */ jsx2(TouchableRipple, { onPress: () => onDeferBottomEdgeGestures(!deferBottomEdgeGestures), style: styles2.toggleButton, accessibilityLabel: `Edge guard ${deferBottomEdgeGestures ? "on" : "off"}`, children: /* @__PURE__ */ jsxs2(View2, { style: styles2.toggleContent, children: [
154
- /* @__PURE__ */ jsx2(SettingIcon, { source: deferBottomEdgeGestures ? "shield-check-outline" : "shield-off-outline", color: deferBottomEdgeGestures ? colors.secondary : colors.onSurfaceVariant, containerColor: deferBottomEdgeGestures ? colors.secondaryContainer : colors.surfaceVariant }),
155
- /* @__PURE__ */ jsxs2(View2, { style: styles2.flexShrink, children: [
156
- /* @__PURE__ */ jsx2(Text2, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Edge Guard" }),
157
- /* @__PURE__ */ jsx2(Text2, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Requires a second swipe" })
268
+ showEdgeGuard && /* @__PURE__ */ jsx3(TouchableRipple, { onPress: () => onDeferBottomEdgeGestures(!deferBottomEdgeGestures), style: styles3.toggleButton, accessibilityLabel: `Edge guard ${deferBottomEdgeGestures ? "on" : "off"}`, children: /* @__PURE__ */ jsxs3(View3, { style: styles3.toggleContent, children: [
269
+ /* @__PURE__ */ jsx3(SettingIcon, { source: deferBottomEdgeGestures ? "shield-check-outline" : "shield-off-outline", color: deferBottomEdgeGestures ? colors.secondary : colors.onSurfaceVariant, containerColor: deferBottomEdgeGestures ? colors.secondaryContainer : colors.surfaceVariant }),
270
+ /* @__PURE__ */ jsxs3(View3, { style: styles3.flexShrink, children: [
271
+ /* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Edge Guard" }),
272
+ /* @__PURE__ */ jsx3(Text3, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Requires a second swipe" })
158
273
  ] })
159
274
  ] }) }),
160
- (showSound || showHaptics) && /* @__PURE__ */ jsxs2(View2, { style: styles2.toggleRow, children: [
161
- showSound && /* @__PURE__ */ jsx2(
275
+ (showSound || showHaptics) && /* @__PURE__ */ jsxs3(View3, { style: styles3.toggleRow, children: [
276
+ showSound && /* @__PURE__ */ jsx3(
162
277
  TouchableRipple,
163
278
  {
164
279
  soundDisabled: true,
@@ -167,15 +282,15 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
167
282
  setSoundSettings({ enabled });
168
283
  if (enabled) sound.selection?.();
169
284
  },
170
- style: styles2.toggleButtonInRow,
285
+ style: styles3.toggleButtonInRow,
171
286
  accessibilityLabel: `Sound ${soundSettings.enabled ? "on" : "off"}`,
172
- children: /* @__PURE__ */ jsxs2(View2, { style: styles2.toggleContent, children: [
173
- /* @__PURE__ */ jsx2(SettingIcon, { source: soundSettings.enabled ? "volume-high" : "volume-off", color: soundSettings.enabled ? colors.tertiary : colors.onSurfaceVariant, containerColor: soundSettings.enabled ? colors.tertiaryContainer : colors.surfaceVariant }),
174
- /* @__PURE__ */ jsx2(Text2, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Sound" })
287
+ children: /* @__PURE__ */ jsxs3(View3, { style: styles3.toggleContent, children: [
288
+ /* @__PURE__ */ jsx3(SettingIcon, { source: soundSettings.enabled ? "volume-high" : "volume-off", color: soundSettings.enabled ? colors.tertiary : colors.onSurfaceVariant, containerColor: soundSettings.enabled ? colors.tertiaryContainer : colors.surfaceVariant }),
289
+ /* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Sound" })
175
290
  ] })
176
291
  }
177
292
  ),
178
- showHaptics && /* @__PURE__ */ jsx2(
293
+ showHaptics && /* @__PURE__ */ jsx3(
179
294
  TouchableRipple,
180
295
  {
181
296
  hapticDisabled: true,
@@ -184,40 +299,36 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
184
299
  setHapticSettings({ vibrate });
185
300
  if (vibrate) forceHapticFeedback();
186
301
  },
187
- style: styles2.toggleButtonInRow,
302
+ style: styles3.toggleButtonInRow,
188
303
  accessibilityLabel: `Haptics ${hapticSettings.vibrate ? "on" : "off"}`,
189
- children: /* @__PURE__ */ jsxs2(View2, { style: styles2.toggleContent, children: [
190
- /* @__PURE__ */ jsx2(SettingIcon, { source: hapticSettings.vibrate ? "vibrate" : "vibrate-off", color: hapticSettings.vibrate ? colors.tertiary : colors.onSurfaceVariant, containerColor: hapticSettings.vibrate ? colors.tertiaryContainer : colors.surfaceVariant }),
191
- /* @__PURE__ */ jsx2(Text2, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Haptics" })
304
+ children: /* @__PURE__ */ jsxs3(View3, { style: styles3.toggleContent, children: [
305
+ /* @__PURE__ */ jsx3(SettingIcon, { source: hapticSettings.vibrate ? "vibrate" : "vibrate-off", color: hapticSettings.vibrate ? colors.tertiary : colors.onSurfaceVariant, containerColor: hapticSettings.vibrate ? colors.tertiaryContainer : colors.surfaceVariant }),
306
+ /* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Haptics" })
192
307
  ] })
193
308
  }
194
309
  )
195
310
  ] })
196
311
  ] }),
197
- showAppearance && /* @__PURE__ */ jsxs2(View2, { style: styles2.section, children: [
198
- /* @__PURE__ */ jsx2(Text2, { variant: "labelMedium", style: [styles2.sectionLabel, { color: colors.onSurfaceVariant }], children: "APPEARANCE" }),
199
- /* @__PURE__ */ jsx2(AutoAppearancePicker, { showLabels: false })
312
+ showAppearance && /* @__PURE__ */ jsxs3(View3, { style: styles3.section, children: [
313
+ /* @__PURE__ */ jsx3(Text3, { variant: "labelMedium", style: [styles3.sectionLabel, { color: colors.onSurfaceVariant }], children: "APPEARANCE" }),
314
+ /* @__PURE__ */ jsx3(AutoAppearancePicker, { showLabels: false })
200
315
  ] }),
201
316
  children,
202
- showUpdateCheck && /* @__PURE__ */ jsxs2(View2, { style: styles2.section, children: [
203
- /* @__PURE__ */ jsxs2(Text2, { variant: "labelSmall", style: [styles2.sectionLabel, { color: colors.onSurfaceVariant }], children: [
317
+ showUpdateCheck && /* @__PURE__ */ jsxs3(View3, { style: styles3.section, children: [
318
+ /* @__PURE__ */ jsxs3(Text3, { variant: "labelSmall", style: [styles3.sectionLabel, { color: colors.onSurfaceVariant }], children: [
204
319
  "VERSION ",
205
320
  version,
206
321
  updateReady ? " \xB7 UPDATE READY" : ""
207
322
  ] }),
208
- /* @__PURE__ */ jsx2(Button, { mode: "outlined", onPress: check, loading: checking, disabled: checking, children: "Check for Updates" })
323
+ /* @__PURE__ */ jsx3(Button2, { mode: "outlined", onPress: check, loading: checking, disabled: checking, children: "Check for Updates" })
209
324
  ] })
210
325
  ] }) })
211
326
  ] }),
212
- infoMessage && /* @__PURE__ */ jsx2(Portal, { children: /* @__PURE__ */ jsx2(View2, { style: overlayStyles.overlay, children: /* @__PURE__ */ jsxs2(View2, { style: [overlayStyles.card, { backgroundColor: cardBg, borderColor: cardBorder }, toRotationStyle(rotation)], children: [
213
- /* @__PURE__ */ jsx2(Icon2, { source: "information-outline", size: 64, color: colors.secondary }),
214
- /* @__PURE__ */ jsx2(Text2, { variant: "headlineLarge", style: [styles2.overlayTitle, { color: colors.secondary }], children: infoMessage.title }),
215
- /* @__PURE__ */ jsx2(Text2, { variant: "bodyLarge", style: [styles2.overlayBody, { color: fg }], children: infoMessage.message }),
216
- /* @__PURE__ */ jsx2(Button, { mode: "contained", onPress: () => setInfoMessage(null), style: styles2.overlayButton, buttonColor: colors.primary, textColor: colors.onPrimary, children: "OK" })
217
- ] }) }) })
327
+ /* @__PURE__ */ jsx3(ConfirmDialog, { visible: !!updateManifest, title: "Update Available", message: updateManifestDate ? `Released ${updateManifestDate.toLocaleDateString()} at ${updateManifestDate.toLocaleTimeString()}` : "", confirmLabel: "Restart", cancelLabel: "Later", icon: "update", rotation, onConfirm: () => respondToUpdateConfirm(true), onCancel: () => respondToUpdateConfirm(false) }),
328
+ /* @__PURE__ */ jsx3(ConfirmDialog, { visible: !!infoMessage, title: infoMessage?.title ?? "", message: infoMessage?.message ?? "", confirmLabel: "OK", icon: "information-outline", rotation, onConfirm: () => setInfoMessage(null) })
218
329
  ] });
219
330
  }
220
- var styles2 = StyleSheet3.create({
331
+ var styles3 = StyleSheet4.create({
221
332
  content: {
222
333
  gap: 24,
223
334
  paddingVertical: 20
@@ -239,9 +350,6 @@ var styles2 = StyleSheet3.create({
239
350
  justifyContent: "center",
240
351
  width: 36
241
352
  },
242
- overlayBody: { textAlign: "center" },
243
- overlayButton: { width: 160 },
244
- overlayTitle: { fontWeight: "bold" },
245
353
  scrollArea: {
246
354
  marginBottom: 0
247
355
  },
@@ -291,89 +399,15 @@ var styles2 = StyleSheet3.create({
291
399
  import { useAutoPaperTheme as useAutoPaperTheme4 } from "@rific/auto-paper";
292
400
  import { Button as Button3, IconButton } from "@rific/feedback-press";
293
401
  import { useRotation as useRotation2 } from "@tastic/core";
294
- import { useState as useState2 } from "react";
402
+ import { useState as useState3 } from "react";
295
403
  import { ScrollView as ScrollView2, StyleSheet as StyleSheet5, View as View4 } from "react-native";
296
404
  import { Text as Text4 } from "react-native-paper";
297
-
298
- // src/ConfirmDialog.tsx
299
- import { useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
300
- import { Button as Button2 } from "@rific/feedback-press";
301
- import { toRotationStyle as toRotationStyle2 } from "@tastic/core";
302
- import { StyleSheet as StyleSheet4, View as View3 } from "react-native";
303
- import { Icon as Icon3, Portal as Portal2, Text as Text3 } from "react-native-paper";
304
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
305
- function ConfirmDialog({ visible, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, rotation = 0, icon = "help-circle-outline", destructive = false }) {
306
- const { dark, colors } = useAutoPaperTheme3();
307
- if (!visible) return null;
308
- const { fg, cardBg, cardBorder } = getOverlayCardColors(dark);
309
- const accent = destructive ? colors.danger : colors.primary;
310
- const onAccent = destructive ? colors.onDanger : colors.onPrimary;
311
- return /* @__PURE__ */ jsx3(Portal2, { children: /* @__PURE__ */ jsx3(View3, { style: [overlayStyles.overlay, styles3.overlay], children: /* @__PURE__ */ jsxs3(View3, { style: [overlayStyles.card, styles3.card, { backgroundColor: cardBg, borderColor: cardBorder }, toRotationStyle2(rotation)], children: [
312
- /* @__PURE__ */ jsx3(Icon3, { source: icon, size: 64, color: accent }),
313
- /* @__PURE__ */ jsx3(Text3, { variant: "headlineLarge", style: [styles3.title, { color: accent }], children: title }),
314
- /* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: [styles3.body, { color: fg }], children: message }),
315
- /* @__PURE__ */ jsxs3(View3, { style: styles3.actions, children: [
316
- /* @__PURE__ */ jsx3(Button2, { mode: "outlined", onPress: onCancel, style: styles3.actionButton, children: cancelLabel }),
317
- /* @__PURE__ */ jsx3(Button2, { mode: "contained", onPress: onConfirm, style: styles3.actionButton, buttonColor: accent, textColor: onAccent, children: confirmLabel })
318
- ] })
319
- ] }) }) });
320
- }
321
- var styles3 = StyleSheet4.create({
322
- // flexBasis (not flex: 1's own 0%) is each button's real floor: whenever both fit side by side,
323
- // flexGrow still splits any leftover width evenly beyond that floor, same visual result as the
324
- // old flex: 1 on a roomy screen - but a button can never be squeezed narrower than 128, only
325
- // wrap to its own row instead (see actions' flexWrap below). react-native-paper's Button
326
- // hardcodes numberOfLines={1} on its label with no override, so a button squeezed under its
327
- // label's natural width silently ellipsizes ("Cancel" -> "Can...") instead of wrapping - assumed
328
- // fixable by just giving the row more width, until confirmed live that these two-word labels
329
- // (Cancel/Reset/New Game) can still lose the truncation fight even with the extra room card's own
330
- // width fix below provides, on a narrow enough screen. flexBasis is the real fix: past a certain
331
- // width there's no side-by-side arrangement that fits both buttons at their natural label size,
332
- // so the layout has to give (stack) rather than the label (truncate).
333
- actionButton: {
334
- flexBasis: 128,
335
- flexGrow: 1
336
- },
337
- actions: {
338
- flexDirection: "row",
339
- flexWrap: "wrap",
340
- gap: 12,
341
- width: "100%"
342
- },
343
- body: {
344
- textAlign: "center"
345
- },
346
- // Additive on top of overlayCard's own shared `card` shape (borderRadius/borderWidth/gap/maxWidth/
347
- // padding) — claims its full allowance instead of just maxWidth alone, which only caps how wide the
348
- // card is allowed to get - without an explicit width, a flex-centered child sizes to its own content
349
- // instead of stretching, so the card (and therefore actions below) can end up narrower than the
350
- // screen actually has room for, needlessly forcing the action row to wrap sooner than it has to.
351
- card: {
352
- width: "100%"
353
- },
354
- // Additive on top of overlayCard's own shared `overlay` backdrop — gives the card room to breathe
355
- // from the screen edges now that it reliably stretches to fill its full width allowance (see
356
- // card's own width comment) - without this, width: '100%' would run the card edge-to-edge on a
357
- // screen narrower than maxWidth's own 360.
358
- overlay: {
359
- paddingHorizontal: 24
360
- },
361
- // textAlign is load-bearing, not decorative, once `title` wraps to more than one line - centered
362
- // within card's own alignItems: 'center' only centers the Text's block as a whole; each wrapped
363
- // line inside that block still defaults to left-aligned without this.
364
- title: {
365
- fontWeight: "bold",
366
- textAlign: "center"
367
- }
368
- });
369
-
370
- // src/BaseStatsScreen.tsx
371
405
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
372
406
  function BaseStatsScreen({ rotation: rotationOverride, title = "Achievements", titleVariant = "headlineSmall", onBack, insets, onReset, resetLabel = "Reset All Stats", resetConfirmTitle = "Reset Everything?", resetConfirmBody = "This permanently erases all stats and achievements. This cannot be undone.", fg: fgOverride, bg: bgOverride, children }) {
373
407
  const { dark, colors } = useAutoPaperTheme4();
374
408
  const ambientRotation = useRotation2();
375
409
  const rotation = rotationOverride ?? ambientRotation;
376
- const [confirmResetVisible, setConfirmResetVisible] = useState2(false);
410
+ const [confirmResetVisible, setConfirmResetVisible] = useState3(false);
377
411
  const showReset = !!onReset;
378
412
  const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
379
413
  const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
@@ -549,7 +583,7 @@ function TriggerGaugeHost(props) {
549
583
  }
550
584
 
551
585
  // src/useAutoAlign.ts
552
- import { useCallback, useEffect, useRef, useState as useState3 } from "react";
586
+ import { useCallback as useCallback2, useEffect, useRef as useRef2, useState as useState4 } from "react";
553
587
  import { useWindowDimensions as useWindowDimensions2 } from "react-native";
554
588
  var EDGE_MARGIN = 12;
555
589
  function rotateRect(x, y, width, height, rotation, windowWidth, windowHeight) {
@@ -562,18 +596,18 @@ function rotateRect(x, y, width, height, rotation, windowWidth, windowHeight) {
562
596
  return { x: cx + newDx - newWidth / 2, y: cy + newDy - newHeight / 2, width: newWidth, height: newHeight };
563
597
  }
564
598
  function useAutoAlign(open, contentWidth, contentHeight, rotation = 0) {
565
- const triggerRef = useRef(null);
599
+ const triggerRef = useRef2(null);
566
600
  const { width: windowWidth, height: windowHeight } = useWindowDimensions2();
567
- const [align, setAlign] = useState3("center");
568
- const [verticalAlign, setVerticalAlign] = useState3("below");
569
- const [maxHeight, setMaxHeight] = useState3(contentHeight);
570
- const [measured, setMeasured] = useState3(false);
571
- const [prevOpen, setPrevOpen] = useState3(open);
601
+ const [align, setAlign] = useState4("center");
602
+ const [verticalAlign, setVerticalAlign] = useState4("below");
603
+ const [maxHeight, setMaxHeight] = useState4(contentHeight);
604
+ const [measured, setMeasured] = useState4(false);
605
+ const [prevOpen, setPrevOpen] = useState4(open);
572
606
  if (open !== prevOpen) {
573
607
  setPrevOpen(open);
574
608
  if (open) setMeasured(false);
575
609
  }
576
- const measure = useCallback(() => {
610
+ const measure = useCallback2(() => {
577
611
  triggerRef.current?.measureInWindow((rawX, rawY, rawWidth, rawHeight) => {
578
612
  const { x, y, width: triggerWidth, height: triggerHeight } = rotateRect(rawX, rawY, rawWidth, rawHeight, rotation, windowWidth, windowHeight);
579
613
  const centerX = x + triggerWidth / 2;
@@ -1198,14 +1232,24 @@ var styles15 = StyleSheet17.create({
1198
1232
  }
1199
1233
  });
1200
1234
 
1235
+ // src/UpdateDialog.tsx
1236
+ import { useUpdater as useUpdater2 } from "@rific/updater";
1237
+ import { jsx as jsx19 } from "react/jsx-runtime";
1238
+ function UpdateDialog({ rotation = 0, autoCheck = true, autoPrompt = true, onError }) {
1239
+ const { manifest, onConfirm, respond } = useUpdateConfirmBridge();
1240
+ useUpdater2({ autoCheck, autoPrompt, onConfirm, onError });
1241
+ const date = manifest ? new Date(manifest.createdAt) : null;
1242
+ return /* @__PURE__ */ jsx19(ConfirmDialog, { visible: !!manifest, title: "Update Available", message: date ? `Released ${date.toLocaleDateString()} at ${date.toLocaleTimeString()}` : "", confirmLabel: "Restart", cancelLabel: "Later", icon: "update", rotation, onConfirm: () => respond(true), onCancel: () => respond(false) });
1243
+ }
1244
+
1201
1245
  // src/usePopoverHost.ts
1202
- import { useCallback as useCallback2, useState as useState4 } from "react";
1246
+ import { useCallback as useCallback3, useState as useState5 } from "react";
1203
1247
  function usePopoverHost() {
1204
- const [openId, setOpenId] = useState4(null);
1205
- const toggle = useCallback2((id) => {
1248
+ const [openId, setOpenId] = useState5(null);
1249
+ const toggle = useCallback3((id) => {
1206
1250
  setOpenId((prev) => prev === id ? null : id);
1207
1251
  }, []);
1208
- const close = useCallback2(() => setOpenId(null), []);
1252
+ const close = useCallback3(() => setOpenId(null), []);
1209
1253
  return { openId, toggle, close };
1210
1254
  }
1211
1255
  export {
@@ -1231,6 +1275,7 @@ export {
1231
1275
  StatRow,
1232
1276
  StatSection,
1233
1277
  TriggerGaugeHost,
1278
+ UpdateDialog,
1234
1279
  getLabeledDropdownContentHeight,
1235
1280
  getOverlayCardColors,
1236
1281
  loadSkiaWeb,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/hud",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
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",
@@ -73,8 +73,9 @@
73
73
  "@infinitetoken/eslint-config": "^0.2.0",
74
74
  "@infinitetoken/jest-config": "^0.3.0",
75
75
  "@infinitetoken/tsconfig": "^0.4.1",
76
- "@rific/auto-paper": "^0.10.3",
77
- "@rific/feedback-press": "^0.10.8",
76
+ "@rific/auto-paper": "^0.11.0",
77
+ "@rific/core": "^0.1.1",
78
+ "@rific/feedback-press": "^0.11.1",
78
79
  "@rific/updater": "^0.6.0",
79
80
  "@shopify/react-native-skia": "^2.6.2",
80
81
  "@tastic/core": "^0.4.1",
@@ -4,9 +4,10 @@ import { useUpdater } from '@rific/updater'
4
4
  import { toRotationStyle, useIsTouchPrimaryDevice, useRotation, type ViewRotation } from '@tastic/core'
5
5
  import { ReactNode, useContext, useState } from 'react'
6
6
  import { Platform, ScrollView, StyleSheet, View } from 'react-native'
7
- import { Icon, Portal, Text } from 'react-native-paper'
7
+ import { Icon, Text } from 'react-native-paper'
8
8
 
9
- import { getOverlayCardColors, overlayStyles } from './overlayCard'
9
+ import { ConfirmDialog } from './ConfirmDialog'
10
+ import { useUpdateConfirmBridge } from './useUpdateConfirmBridge'
10
11
 
11
12
  interface SettingIconProps {
12
13
  source: string
@@ -80,7 +81,7 @@ export interface BaseSettingsDialogProps {
80
81
  // backup flow, or anything else genuinely specific to it) renders those below Appearance, not by
81
82
  // forking this component.
82
83
  export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, children }: BaseSettingsDialogProps) {
83
- const { dark, colors } = useAutoPaperTheme()
84
+ const { colors } = useAutoPaperTheme()
84
85
  const ambientRotation = useRotation()
85
86
  const rotation = rotationOverride ?? ambientRotation
86
87
  // Also gates the Lock Orientation row below (alongside showLockOrientation itself) — true
@@ -105,13 +106,18 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
105
106
  // autoCheck: false — every consuming app's own root layout already runs the background check via
106
107
  // its own useUpdater() instance; a second instance with autoCheck's default (true) would set up a
107
108
  // second AppState listener and double every foreground-resume update check. This instance only
108
- // ever checks on an explicit tap of the button below.
109
+ // ever checks on an explicit tap of the button below. Deliberately its own useUpdater() instance
110
+ // rather than rendering UpdateDialog directly here, for the same reason — UpdateDialog owns a
111
+ // second AppState listener of its own.
112
+ const { manifest: updateManifest, onConfirm: onUpdateConfirm, respond: respondToUpdateConfirm } = useUpdateConfirmBridge()
109
113
  const { check, checking, updateReady } = useUpdater({
110
114
  autoCheck: false,
111
115
  autoPrompt: false,
116
+ onConfirm: onUpdateConfirm,
112
117
  onError: onUpdateError,
113
118
  onInfo: (title, message) => setInfoMessage({ title, message })
114
119
  })
120
+ const updateManifestDate = updateManifest ? new Date(updateManifest.createdAt) : null
115
121
 
116
122
  const showLockOrientation = isTouchPrimary && lockOrientation !== undefined && onLockOrientationChange !== undefined
117
123
  const showEdgeGuard = Platform.OS === 'ios' && deferBottomEdgeGestures !== undefined && onDeferBottomEdgeGestures !== undefined
@@ -120,8 +126,6 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
120
126
  const showAppearance = !hideAppearance
121
127
  const showUpdateCheck = Platform.OS !== 'web' && !hideUpdateCheck
122
128
 
123
- const { fg, cardBg, cardBorder } = getOverlayCardColors(dark)
124
-
125
129
  return (
126
130
  <>
127
131
  <Dialog visible={visible} onDismiss={onDismiss} style={[styles.dialog, toRotationStyle(rotation)]}>
@@ -261,24 +265,8 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
261
265
  </ScrollView>
262
266
  </Dialog.ScrollArea>
263
267
  </Dialog>
264
- {infoMessage && (
265
- <Portal>
266
- <View style={overlayStyles.overlay}>
267
- <View style={[overlayStyles.card, { backgroundColor: cardBg, borderColor: cardBorder }, toRotationStyle(rotation)]}>
268
- <Icon source='information-outline' size={64} color={colors.secondary} />
269
- <Text variant='headlineLarge' style={[styles.overlayTitle, { color: colors.secondary }]}>
270
- {infoMessage.title}
271
- </Text>
272
- <Text variant='bodyLarge' style={[styles.overlayBody, { color: fg }]}>
273
- {infoMessage.message}
274
- </Text>
275
- <Button mode='contained' onPress={() => setInfoMessage(null)} style={styles.overlayButton} buttonColor={colors.primary} textColor={colors.onPrimary}>
276
- OK
277
- </Button>
278
- </View>
279
- </View>
280
- </Portal>
281
- )}
268
+ <ConfirmDialog visible={!!updateManifest} title='Update Available' message={updateManifestDate ? `Released ${updateManifestDate.toLocaleDateString()} at ${updateManifestDate.toLocaleTimeString()}` : ''} confirmLabel='Restart' cancelLabel='Later' icon='update' rotation={rotation} onConfirm={() => respondToUpdateConfirm(true)} onCancel={() => respondToUpdateConfirm(false)} />
269
+ <ConfirmDialog visible={!!infoMessage} title={infoMessage?.title ?? ''} message={infoMessage?.message ?? ''} confirmLabel='OK' icon='information-outline' rotation={rotation} onConfirm={() => setInfoMessage(null)} />
282
270
  </>
283
271
  )
284
272
  }
@@ -305,9 +293,6 @@ const styles = StyleSheet.create({
305
293
  justifyContent: 'center',
306
294
  width: 36
307
295
  },
308
- overlayBody: { textAlign: 'center' },
309
- overlayButton: { width: 160 },
310
- overlayTitle: { fontWeight: 'bold' },
311
296
  scrollArea: {
312
297
  marginBottom: 0
313
298
  },