@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.d.mts +12 -4
- package/dist/index.d.ts +12 -4
- package/dist/index.js +206 -160
- package/dist/index.mjs +185 -140
- package/package.json +4 -3
- package/src/BaseSettingsDialog.tsx +12 -27
- package/src/ConfirmDialog.tsx +38 -16
- package/src/UpdateDialog.tsx +28 -0
- package/src/index.ts +1 -0
- package/src/useUpdateConfirmBridge.ts +30 -0
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
|
|
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
|
|
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
|
|
227
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
115
228
|
function SettingIcon({ source, color, containerColor }) {
|
|
116
|
-
return /* @__PURE__ */
|
|
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 {
|
|
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] =
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
/* @__PURE__ */
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
/* @__PURE__ */
|
|
148
|
-
|
|
149
|
-
/* @__PURE__ */
|
|
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__ */
|
|
154
|
-
/* @__PURE__ */
|
|
155
|
-
/* @__PURE__ */
|
|
156
|
-
/* @__PURE__ */
|
|
157
|
-
/* @__PURE__ */
|
|
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__ */
|
|
161
|
-
showSound && /* @__PURE__ */
|
|
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:
|
|
285
|
+
style: styles3.toggleButtonInRow,
|
|
171
286
|
accessibilityLabel: `Sound ${soundSettings.enabled ? "on" : "off"}`,
|
|
172
|
-
children: /* @__PURE__ */
|
|
173
|
-
/* @__PURE__ */
|
|
174
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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:
|
|
302
|
+
style: styles3.toggleButtonInRow,
|
|
188
303
|
accessibilityLabel: `Haptics ${hapticSettings.vibrate ? "on" : "off"}`,
|
|
189
|
-
children: /* @__PURE__ */
|
|
190
|
-
/* @__PURE__ */
|
|
191
|
-
/* @__PURE__ */
|
|
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__ */
|
|
198
|
-
/* @__PURE__ */
|
|
199
|
-
/* @__PURE__ */
|
|
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__ */
|
|
203
|
-
/* @__PURE__ */
|
|
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__ */
|
|
323
|
+
/* @__PURE__ */ jsx3(Button2, { mode: "outlined", onPress: check, loading: checking, disabled: checking, children: "Check for Updates" })
|
|
209
324
|
] })
|
|
210
325
|
] }) })
|
|
211
326
|
] }),
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
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
|
|
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] =
|
|
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
|
|
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 =
|
|
599
|
+
const triggerRef = useRef2(null);
|
|
566
600
|
const { width: windowWidth, height: windowHeight } = useWindowDimensions2();
|
|
567
|
-
const [align, setAlign] =
|
|
568
|
-
const [verticalAlign, setVerticalAlign] =
|
|
569
|
-
const [maxHeight, setMaxHeight] =
|
|
570
|
-
const [measured, setMeasured] =
|
|
571
|
-
const [prevOpen, setPrevOpen] =
|
|
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 =
|
|
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
|
|
1246
|
+
import { useCallback as useCallback3, useState as useState5 } from "react";
|
|
1203
1247
|
function usePopoverHost() {
|
|
1204
|
-
const [openId, setOpenId] =
|
|
1205
|
-
const toggle =
|
|
1248
|
+
const [openId, setOpenId] = useState5(null);
|
|
1249
|
+
const toggle = useCallback3((id) => {
|
|
1206
1250
|
setOpenId((prev) => prev === id ? null : id);
|
|
1207
1251
|
}, []);
|
|
1208
|
-
const close =
|
|
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
|
+
"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.
|
|
77
|
-
"@rific/
|
|
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,
|
|
7
|
+
import { Icon, Text } from 'react-native-paper'
|
|
8
8
|
|
|
9
|
-
import {
|
|
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 {
|
|
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
|
-
{
|
|
265
|
-
|
|
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
|
},
|