@tastic/hud 0.8.3 → 0.8.4
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 +187 -160
- package/dist/index.mjs +166 -140
- package/package.json +4 -3
- package/src/BaseSettingsDialog.tsx +12 -27
- package/src/ConfirmDialog.tsx +9 -6
- 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,100 @@ 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
|
+
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: [
|
|
129
|
+
/* @__PURE__ */ jsx2(Icon2, { source: icon, size: 64, color: accent }),
|
|
130
|
+
/* @__PURE__ */ jsx2(Text2, { variant: "headlineLarge", style: [styles2.title, { color: accent }], children: title }),
|
|
131
|
+
/* @__PURE__ */ jsx2(Text2, { variant: "bodyLarge", style: [styles2.body, { color: fg }], children: message }),
|
|
132
|
+
/* @__PURE__ */ jsxs2(View2, { style: styles2.actions, children: [
|
|
133
|
+
cancelLabel !== void 0 && onCancel !== void 0 && /* @__PURE__ */ jsx2(Button, { mode: "outlined", onPress: onCancel, style: styles2.actionButton, children: cancelLabel }),
|
|
134
|
+
/* @__PURE__ */ jsx2(Button, { mode: "contained", onPress: onConfirm, style: styles2.actionButton, buttonColor: accent, textColor: onAccent, children: confirmLabel })
|
|
135
|
+
] })
|
|
136
|
+
] }) }) });
|
|
137
|
+
}
|
|
138
|
+
var styles2 = StyleSheet3.create({
|
|
139
|
+
// flexBasis (not flex: 1's own 0%) is each button's real floor: whenever both fit side by side,
|
|
140
|
+
// flexGrow still splits any leftover width evenly beyond that floor, same visual result as the
|
|
141
|
+
// old flex: 1 on a roomy screen - but a button can never be squeezed narrower than 128, only
|
|
142
|
+
// wrap to its own row instead (see actions' flexWrap below). react-native-paper's Button
|
|
143
|
+
// hardcodes numberOfLines={1} on its label with no override, so a button squeezed under its
|
|
144
|
+
// label's natural width silently ellipsizes ("Cancel" -> "Can...") instead of wrapping - assumed
|
|
145
|
+
// fixable by just giving the row more width, until confirmed live that these two-word labels
|
|
146
|
+
// (Cancel/Reset/New Game) can still lose the truncation fight even with the extra room card's own
|
|
147
|
+
// width fix below provides, on a narrow enough screen. flexBasis is the real fix: past a certain
|
|
148
|
+
// width there's no side-by-side arrangement that fits both buttons at their natural label size,
|
|
149
|
+
// so the layout has to give (stack) rather than the label (truncate).
|
|
150
|
+
actionButton: {
|
|
151
|
+
flexBasis: 128,
|
|
152
|
+
flexGrow: 1
|
|
153
|
+
},
|
|
154
|
+
actions: {
|
|
155
|
+
flexDirection: "row",
|
|
156
|
+
flexWrap: "wrap",
|
|
157
|
+
gap: 12,
|
|
158
|
+
width: "100%"
|
|
159
|
+
},
|
|
160
|
+
body: {
|
|
161
|
+
textAlign: "center"
|
|
162
|
+
},
|
|
163
|
+
// Additive on top of overlayCard's own shared `card` shape (borderRadius/borderWidth/gap/maxWidth/
|
|
164
|
+
// padding) — claims its full allowance instead of just maxWidth alone, which only caps how wide the
|
|
165
|
+
// card is allowed to get - without an explicit width, a flex-centered child sizes to its own content
|
|
166
|
+
// instead of stretching, so the card (and therefore actions below) can end up narrower than the
|
|
167
|
+
// screen actually has room for, needlessly forcing the action row to wrap sooner than it has to.
|
|
168
|
+
card: {
|
|
169
|
+
width: "100%"
|
|
170
|
+
},
|
|
171
|
+
// Additive on top of overlayCard's own shared `overlay` backdrop — gives the card room to breathe
|
|
172
|
+
// from the screen edges now that it reliably stretches to fill its full width allowance (see
|
|
173
|
+
// card's own width comment) - without this, width: '100%' would run the card edge-to-edge on a
|
|
174
|
+
// screen narrower than maxWidth's own 360.
|
|
175
|
+
overlay: {
|
|
176
|
+
paddingHorizontal: 24
|
|
177
|
+
},
|
|
178
|
+
// textAlign is load-bearing, not decorative, once `title` wraps to more than one line - centered
|
|
179
|
+
// within card's own alignItems: 'center' only centers the Text's block as a whole; each wrapped
|
|
180
|
+
// line inside that block still defaults to left-aligned without this.
|
|
181
|
+
title: {
|
|
182
|
+
fontWeight: "bold",
|
|
183
|
+
textAlign: "center"
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// src/useUpdateConfirmBridge.ts
|
|
188
|
+
import { useCallback, useRef, useState } from "react";
|
|
189
|
+
function useUpdateConfirmBridge() {
|
|
190
|
+
const [manifest, setManifest] = useState(null);
|
|
191
|
+
const resolveRef = useRef(null);
|
|
192
|
+
const onConfirm = useCallback(
|
|
193
|
+
(next) => new Promise((resolve) => {
|
|
194
|
+
resolveRef.current = resolve;
|
|
195
|
+
setManifest(next);
|
|
196
|
+
}),
|
|
197
|
+
[]
|
|
198
|
+
);
|
|
199
|
+
const respond = useCallback((confirmed) => {
|
|
200
|
+
setManifest(null);
|
|
201
|
+
resolveRef.current?.(confirmed);
|
|
202
|
+
resolveRef.current = null;
|
|
203
|
+
}, []);
|
|
204
|
+
return { manifest, onConfirm, respond };
|
|
205
|
+
}
|
|
206
|
+
|
|
113
207
|
// src/BaseSettingsDialog.tsx
|
|
114
|
-
import { Fragment, jsx as
|
|
208
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
115
209
|
function SettingIcon({ source, color, containerColor }) {
|
|
116
|
-
return /* @__PURE__ */
|
|
210
|
+
return /* @__PURE__ */ jsx3(View3, { style: [styles3.iconBadge, { backgroundColor: containerColor }], children: /* @__PURE__ */ jsx3(Icon3, { source, size: 18, color }) });
|
|
117
211
|
}
|
|
118
212
|
function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, children }) {
|
|
119
|
-
const {
|
|
213
|
+
const { colors } = useAutoPaperTheme3();
|
|
120
214
|
const ambientRotation = useRotation();
|
|
121
215
|
const rotation = rotationOverride ?? ambientRotation;
|
|
122
216
|
const isTouchPrimary = useIsTouchPrimaryDevice();
|
|
@@ -124,41 +218,43 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
|
|
|
124
218
|
const { settings: soundSettings, set: setSoundSettings } = useSoundSettings();
|
|
125
219
|
const sound = useContext(SoundContext);
|
|
126
220
|
const { forceShort: forceHapticFeedback } = useVibration();
|
|
127
|
-
const [infoMessage, setInfoMessage] =
|
|
221
|
+
const [infoMessage, setInfoMessage] = useState2(null);
|
|
222
|
+
const { manifest: updateManifest, onConfirm: onUpdateConfirm, respond: respondToUpdateConfirm } = useUpdateConfirmBridge();
|
|
128
223
|
const { check, checking, updateReady } = useUpdater({
|
|
129
224
|
autoCheck: false,
|
|
130
225
|
autoPrompt: false,
|
|
226
|
+
onConfirm: onUpdateConfirm,
|
|
131
227
|
onError: onUpdateError,
|
|
132
228
|
onInfo: (title, message) => setInfoMessage({ title, message })
|
|
133
229
|
});
|
|
230
|
+
const updateManifestDate = updateManifest ? new Date(updateManifest.createdAt) : null;
|
|
134
231
|
const showLockOrientation = isTouchPrimary && lockOrientation !== void 0 && onLockOrientationChange !== void 0;
|
|
135
232
|
const showEdgeGuard = Platform.OS === "ios" && deferBottomEdgeGestures !== void 0 && onDeferBottomEdgeGestures !== void 0;
|
|
136
233
|
const showHaptics = Platform.OS !== "web" && !hideHaptics;
|
|
137
234
|
const showSound = !hideSound;
|
|
138
235
|
const showAppearance = !hideAppearance;
|
|
139
236
|
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" })
|
|
237
|
+
return /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
238
|
+
/* @__PURE__ */ jsxs3(Dialog, { visible, onDismiss, style: [styles3.dialog, toRotationStyle2(rotation)], children: [
|
|
239
|
+
/* @__PURE__ */ jsx3(Dialog.Title, { children: "Settings" }),
|
|
240
|
+
/* @__PURE__ */ jsx3(Dialog.ScrollArea, { style: styles3.scrollArea, children: /* @__PURE__ */ jsxs3(ScrollView, { contentContainerStyle: styles3.content, showsVerticalScrollIndicator: false, children: [
|
|
241
|
+
/* @__PURE__ */ jsxs3(View3, { style: styles3.toggleGroup, children: [
|
|
242
|
+
showLockOrientation && /* @__PURE__ */ jsx3(TouchableRipple, { onPress: () => onLockOrientationChange(!lockOrientation), style: styles3.toggleButton, accessibilityLabel: `Lock orientation ${lockOrientation ? "on" : "off"}`, children: /* @__PURE__ */ jsxs3(View3, { style: styles3.toggleContent, children: [
|
|
243
|
+
/* @__PURE__ */ jsx3(SettingIcon, { source: lockOrientation ? "lock" : "lock-open-variant-outline", color: lockOrientation ? colors.secondary : colors.onSurfaceVariant, containerColor: lockOrientation ? colors.secondaryContainer : colors.surfaceVariant }),
|
|
244
|
+
/* @__PURE__ */ jsxs3(View3, { style: styles3.flexShrink, children: [
|
|
245
|
+
/* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Lock Orientation" }),
|
|
246
|
+
/* @__PURE__ */ jsx3(Text3, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Pins the current layout" })
|
|
151
247
|
] })
|
|
152
248
|
] }) }),
|
|
153
|
-
showEdgeGuard && /* @__PURE__ */
|
|
154
|
-
/* @__PURE__ */
|
|
155
|
-
/* @__PURE__ */
|
|
156
|
-
/* @__PURE__ */
|
|
157
|
-
/* @__PURE__ */
|
|
249
|
+
showEdgeGuard && /* @__PURE__ */ jsx3(TouchableRipple, { onPress: () => onDeferBottomEdgeGestures(!deferBottomEdgeGestures), style: styles3.toggleButton, accessibilityLabel: `Edge guard ${deferBottomEdgeGestures ? "on" : "off"}`, children: /* @__PURE__ */ jsxs3(View3, { style: styles3.toggleContent, children: [
|
|
250
|
+
/* @__PURE__ */ jsx3(SettingIcon, { source: deferBottomEdgeGestures ? "shield-check-outline" : "shield-off-outline", color: deferBottomEdgeGestures ? colors.secondary : colors.onSurfaceVariant, containerColor: deferBottomEdgeGestures ? colors.secondaryContainer : colors.surfaceVariant }),
|
|
251
|
+
/* @__PURE__ */ jsxs3(View3, { style: styles3.flexShrink, children: [
|
|
252
|
+
/* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Edge Guard" }),
|
|
253
|
+
/* @__PURE__ */ jsx3(Text3, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Requires a second swipe" })
|
|
158
254
|
] })
|
|
159
255
|
] }) }),
|
|
160
|
-
(showSound || showHaptics) && /* @__PURE__ */
|
|
161
|
-
showSound && /* @__PURE__ */
|
|
256
|
+
(showSound || showHaptics) && /* @__PURE__ */ jsxs3(View3, { style: styles3.toggleRow, children: [
|
|
257
|
+
showSound && /* @__PURE__ */ jsx3(
|
|
162
258
|
TouchableRipple,
|
|
163
259
|
{
|
|
164
260
|
soundDisabled: true,
|
|
@@ -167,15 +263,15 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
|
|
|
167
263
|
setSoundSettings({ enabled });
|
|
168
264
|
if (enabled) sound.selection?.();
|
|
169
265
|
},
|
|
170
|
-
style:
|
|
266
|
+
style: styles3.toggleButtonInRow,
|
|
171
267
|
accessibilityLabel: `Sound ${soundSettings.enabled ? "on" : "off"}`,
|
|
172
|
-
children: /* @__PURE__ */
|
|
173
|
-
/* @__PURE__ */
|
|
174
|
-
/* @__PURE__ */
|
|
268
|
+
children: /* @__PURE__ */ jsxs3(View3, { style: styles3.toggleContent, children: [
|
|
269
|
+
/* @__PURE__ */ jsx3(SettingIcon, { source: soundSettings.enabled ? "volume-high" : "volume-off", color: soundSettings.enabled ? colors.tertiary : colors.onSurfaceVariant, containerColor: soundSettings.enabled ? colors.tertiaryContainer : colors.surfaceVariant }),
|
|
270
|
+
/* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Sound" })
|
|
175
271
|
] })
|
|
176
272
|
}
|
|
177
273
|
),
|
|
178
|
-
showHaptics && /* @__PURE__ */
|
|
274
|
+
showHaptics && /* @__PURE__ */ jsx3(
|
|
179
275
|
TouchableRipple,
|
|
180
276
|
{
|
|
181
277
|
hapticDisabled: true,
|
|
@@ -184,40 +280,36 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
|
|
|
184
280
|
setHapticSettings({ vibrate });
|
|
185
281
|
if (vibrate) forceHapticFeedback();
|
|
186
282
|
},
|
|
187
|
-
style:
|
|
283
|
+
style: styles3.toggleButtonInRow,
|
|
188
284
|
accessibilityLabel: `Haptics ${hapticSettings.vibrate ? "on" : "off"}`,
|
|
189
|
-
children: /* @__PURE__ */
|
|
190
|
-
/* @__PURE__ */
|
|
191
|
-
/* @__PURE__ */
|
|
285
|
+
children: /* @__PURE__ */ jsxs3(View3, { style: styles3.toggleContent, children: [
|
|
286
|
+
/* @__PURE__ */ jsx3(SettingIcon, { source: hapticSettings.vibrate ? "vibrate" : "vibrate-off", color: hapticSettings.vibrate ? colors.tertiary : colors.onSurfaceVariant, containerColor: hapticSettings.vibrate ? colors.tertiaryContainer : colors.surfaceVariant }),
|
|
287
|
+
/* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Haptics" })
|
|
192
288
|
] })
|
|
193
289
|
}
|
|
194
290
|
)
|
|
195
291
|
] })
|
|
196
292
|
] }),
|
|
197
|
-
showAppearance && /* @__PURE__ */
|
|
198
|
-
/* @__PURE__ */
|
|
199
|
-
/* @__PURE__ */
|
|
293
|
+
showAppearance && /* @__PURE__ */ jsxs3(View3, { style: styles3.section, children: [
|
|
294
|
+
/* @__PURE__ */ jsx3(Text3, { variant: "labelMedium", style: [styles3.sectionLabel, { color: colors.onSurfaceVariant }], children: "APPEARANCE" }),
|
|
295
|
+
/* @__PURE__ */ jsx3(AutoAppearancePicker, { showLabels: false })
|
|
200
296
|
] }),
|
|
201
297
|
children,
|
|
202
|
-
showUpdateCheck && /* @__PURE__ */
|
|
203
|
-
/* @__PURE__ */
|
|
298
|
+
showUpdateCheck && /* @__PURE__ */ jsxs3(View3, { style: styles3.section, children: [
|
|
299
|
+
/* @__PURE__ */ jsxs3(Text3, { variant: "labelSmall", style: [styles3.sectionLabel, { color: colors.onSurfaceVariant }], children: [
|
|
204
300
|
"VERSION ",
|
|
205
301
|
version,
|
|
206
302
|
updateReady ? " \xB7 UPDATE READY" : ""
|
|
207
303
|
] }),
|
|
208
|
-
/* @__PURE__ */
|
|
304
|
+
/* @__PURE__ */ jsx3(Button2, { mode: "outlined", onPress: check, loading: checking, disabled: checking, children: "Check for Updates" })
|
|
209
305
|
] })
|
|
210
306
|
] }) })
|
|
211
307
|
] }),
|
|
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
|
-
] }) }) })
|
|
308
|
+
/* @__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) }),
|
|
309
|
+
/* @__PURE__ */ jsx3(ConfirmDialog, { visible: !!infoMessage, title: infoMessage?.title ?? "", message: infoMessage?.message ?? "", confirmLabel: "OK", icon: "information-outline", rotation, onConfirm: () => setInfoMessage(null) })
|
|
218
310
|
] });
|
|
219
311
|
}
|
|
220
|
-
var
|
|
312
|
+
var styles3 = StyleSheet4.create({
|
|
221
313
|
content: {
|
|
222
314
|
gap: 24,
|
|
223
315
|
paddingVertical: 20
|
|
@@ -239,9 +331,6 @@ var styles2 = StyleSheet3.create({
|
|
|
239
331
|
justifyContent: "center",
|
|
240
332
|
width: 36
|
|
241
333
|
},
|
|
242
|
-
overlayBody: { textAlign: "center" },
|
|
243
|
-
overlayButton: { width: 160 },
|
|
244
|
-
overlayTitle: { fontWeight: "bold" },
|
|
245
334
|
scrollArea: {
|
|
246
335
|
marginBottom: 0
|
|
247
336
|
},
|
|
@@ -291,89 +380,15 @@ var styles2 = StyleSheet3.create({
|
|
|
291
380
|
import { useAutoPaperTheme as useAutoPaperTheme4 } from "@rific/auto-paper";
|
|
292
381
|
import { Button as Button3, IconButton } from "@rific/feedback-press";
|
|
293
382
|
import { useRotation as useRotation2 } from "@tastic/core";
|
|
294
|
-
import { useState as
|
|
383
|
+
import { useState as useState3 } from "react";
|
|
295
384
|
import { ScrollView as ScrollView2, StyleSheet as StyleSheet5, View as View4 } from "react-native";
|
|
296
385
|
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
386
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
372
387
|
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
388
|
const { dark, colors } = useAutoPaperTheme4();
|
|
374
389
|
const ambientRotation = useRotation2();
|
|
375
390
|
const rotation = rotationOverride ?? ambientRotation;
|
|
376
|
-
const [confirmResetVisible, setConfirmResetVisible] =
|
|
391
|
+
const [confirmResetVisible, setConfirmResetVisible] = useState3(false);
|
|
377
392
|
const showReset = !!onReset;
|
|
378
393
|
const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
|
|
379
394
|
const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
|
|
@@ -549,7 +564,7 @@ function TriggerGaugeHost(props) {
|
|
|
549
564
|
}
|
|
550
565
|
|
|
551
566
|
// src/useAutoAlign.ts
|
|
552
|
-
import { useCallback, useEffect, useRef, useState as
|
|
567
|
+
import { useCallback as useCallback2, useEffect, useRef as useRef2, useState as useState4 } from "react";
|
|
553
568
|
import { useWindowDimensions as useWindowDimensions2 } from "react-native";
|
|
554
569
|
var EDGE_MARGIN = 12;
|
|
555
570
|
function rotateRect(x, y, width, height, rotation, windowWidth, windowHeight) {
|
|
@@ -562,18 +577,18 @@ function rotateRect(x, y, width, height, rotation, windowWidth, windowHeight) {
|
|
|
562
577
|
return { x: cx + newDx - newWidth / 2, y: cy + newDy - newHeight / 2, width: newWidth, height: newHeight };
|
|
563
578
|
}
|
|
564
579
|
function useAutoAlign(open, contentWidth, contentHeight, rotation = 0) {
|
|
565
|
-
const triggerRef =
|
|
580
|
+
const triggerRef = useRef2(null);
|
|
566
581
|
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] =
|
|
582
|
+
const [align, setAlign] = useState4("center");
|
|
583
|
+
const [verticalAlign, setVerticalAlign] = useState4("below");
|
|
584
|
+
const [maxHeight, setMaxHeight] = useState4(contentHeight);
|
|
585
|
+
const [measured, setMeasured] = useState4(false);
|
|
586
|
+
const [prevOpen, setPrevOpen] = useState4(open);
|
|
572
587
|
if (open !== prevOpen) {
|
|
573
588
|
setPrevOpen(open);
|
|
574
589
|
if (open) setMeasured(false);
|
|
575
590
|
}
|
|
576
|
-
const measure =
|
|
591
|
+
const measure = useCallback2(() => {
|
|
577
592
|
triggerRef.current?.measureInWindow((rawX, rawY, rawWidth, rawHeight) => {
|
|
578
593
|
const { x, y, width: triggerWidth, height: triggerHeight } = rotateRect(rawX, rawY, rawWidth, rawHeight, rotation, windowWidth, windowHeight);
|
|
579
594
|
const centerX = x + triggerWidth / 2;
|
|
@@ -1198,14 +1213,24 @@ var styles15 = StyleSheet17.create({
|
|
|
1198
1213
|
}
|
|
1199
1214
|
});
|
|
1200
1215
|
|
|
1216
|
+
// src/UpdateDialog.tsx
|
|
1217
|
+
import { useUpdater as useUpdater2 } from "@rific/updater";
|
|
1218
|
+
import { jsx as jsx19 } from "react/jsx-runtime";
|
|
1219
|
+
function UpdateDialog({ rotation = 0, autoCheck = true, autoPrompt = true, onError }) {
|
|
1220
|
+
const { manifest, onConfirm, respond } = useUpdateConfirmBridge();
|
|
1221
|
+
useUpdater2({ autoCheck, autoPrompt, onConfirm, onError });
|
|
1222
|
+
const date = manifest ? new Date(manifest.createdAt) : null;
|
|
1223
|
+
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) });
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1201
1226
|
// src/usePopoverHost.ts
|
|
1202
|
-
import { useCallback as
|
|
1227
|
+
import { useCallback as useCallback3, useState as useState5 } from "react";
|
|
1203
1228
|
function usePopoverHost() {
|
|
1204
|
-
const [openId, setOpenId] =
|
|
1205
|
-
const toggle =
|
|
1229
|
+
const [openId, setOpenId] = useState5(null);
|
|
1230
|
+
const toggle = useCallback3((id) => {
|
|
1206
1231
|
setOpenId((prev) => prev === id ? null : id);
|
|
1207
1232
|
}, []);
|
|
1208
|
-
const close =
|
|
1233
|
+
const close = useCallback3(() => setOpenId(null), []);
|
|
1209
1234
|
return { openId, toggle, close };
|
|
1210
1235
|
}
|
|
1211
1236
|
export {
|
|
@@ -1231,6 +1256,7 @@ export {
|
|
|
1231
1256
|
StatRow,
|
|
1232
1257
|
StatSection,
|
|
1233
1258
|
TriggerGaugeHost,
|
|
1259
|
+
UpdateDialog,
|
|
1234
1260
|
getLabeledDropdownContentHeight,
|
|
1235
1261
|
getOverlayCardColors,
|
|
1236
1262
|
loadSkiaWeb,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastic/hud",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
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
|
},
|
package/src/ConfirmDialog.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useAutoPaperTheme } from '@rific/auto-paper'
|
|
2
2
|
import { Button } from '@rific/feedback-press'
|
|
3
3
|
import { toRotationStyle, type ViewRotation } from '@tastic/core'
|
|
4
|
+
import { type ReactNode } from 'react'
|
|
4
5
|
import { StyleSheet, View } from 'react-native'
|
|
5
6
|
import { Icon, Portal, Text } from 'react-native-paper'
|
|
6
7
|
|
|
@@ -9,11 +10,11 @@ import { getOverlayCardColors, overlayStyles } from './overlayCard'
|
|
|
9
10
|
export interface ConfirmDialogProps {
|
|
10
11
|
visible: boolean
|
|
11
12
|
title: string
|
|
12
|
-
message:
|
|
13
|
+
message: ReactNode
|
|
13
14
|
confirmLabel: string
|
|
14
|
-
cancelLabel
|
|
15
|
+
cancelLabel?: string
|
|
15
16
|
onConfirm: () => void
|
|
16
|
-
onCancel
|
|
17
|
+
onCancel?: () => void
|
|
17
18
|
// Live physical-hold rotation (see @tastic/core's getViewRotation) - same convention as
|
|
18
19
|
// BaseSettingsDialog's own rotation prop; defaults to 0 for a caller with no per-player zone to
|
|
19
20
|
// match (a portrait-only or single-player game has nothing to stay consistent with).
|
|
@@ -54,9 +55,11 @@ export function ConfirmDialog({ visible, title, message, confirmLabel, cancelLab
|
|
|
54
55
|
{message}
|
|
55
56
|
</Text>
|
|
56
57
|
<View style={styles.actions}>
|
|
57
|
-
|
|
58
|
-
{
|
|
59
|
-
|
|
58
|
+
{cancelLabel !== undefined && onCancel !== undefined && (
|
|
59
|
+
<Button mode='outlined' onPress={onCancel} style={styles.actionButton}>
|
|
60
|
+
{cancelLabel}
|
|
61
|
+
</Button>
|
|
62
|
+
)}
|
|
60
63
|
<Button mode='contained' onPress={onConfirm} style={styles.actionButton} buttonColor={accent} textColor={onAccent}>
|
|
61
64
|
{confirmLabel}
|
|
62
65
|
</Button>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useUpdater } from '@rific/updater'
|
|
2
|
+
import { type ViewRotation } from '@tastic/core'
|
|
3
|
+
|
|
4
|
+
import { ConfirmDialog } from './ConfirmDialog'
|
|
5
|
+
import { useUpdateConfirmBridge } from './useUpdateConfirmBridge'
|
|
6
|
+
|
|
7
|
+
export interface UpdateDialogProps {
|
|
8
|
+
// Same live physical-hold rotation convention as ConfirmDialog/BaseSettingsDialog's own rotation
|
|
9
|
+
// prop - defaults to 0 for a caller with no per-player zone to match.
|
|
10
|
+
rotation?: ViewRotation
|
|
11
|
+
autoCheck?: boolean
|
|
12
|
+
autoPrompt?: boolean
|
|
13
|
+
onError?: (message: string) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Drop-in background updater: mount once (e.g. an app's root layout) and it owns its own
|
|
17
|
+
// useUpdater() instance end to end, rendering a themed ConfirmDialog instead of useUpdater's
|
|
18
|
+
// default Alert-based prompt whenever an update is found. BaseSettingsDialog's own manual "Check
|
|
19
|
+
// for Updates" button deliberately does NOT render this component - see its own comment on why
|
|
20
|
+
// that flow keeps a second, separate useUpdater() instance instead of reusing this one.
|
|
21
|
+
export function UpdateDialog({ rotation = 0, autoCheck = true, autoPrompt = true, onError }: UpdateDialogProps) {
|
|
22
|
+
const { manifest, onConfirm, respond } = useUpdateConfirmBridge()
|
|
23
|
+
useUpdater({ autoCheck, autoPrompt, onConfirm, onError })
|
|
24
|
+
|
|
25
|
+
const date = manifest ? new Date(manifest.createdAt) : null
|
|
26
|
+
|
|
27
|
+
return <ConfirmDialog visible={!!manifest} title='Update Available' message={date ? `Released ${date.toLocaleDateString()} at ${date.toLocaleTimeString()}` : ''} confirmLabel='Restart' cancelLabel='Later' icon='update' rotation={rotation} onConfirm={() => respond(true)} onCancel={() => respond(false)} />
|
|
28
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -28,5 +28,6 @@ export { StatSection } from './StatSection'
|
|
|
28
28
|
// TriggerGaugeHost is the only supported way to render this.
|
|
29
29
|
export type { TriggerGaugeProps } from './TriggerGauge'
|
|
30
30
|
export { default as TriggerGaugeHost } from './TriggerGaugeHost'
|
|
31
|
+
export { UpdateDialog, type UpdateDialogProps } from './UpdateDialog'
|
|
31
32
|
export { type PopoverAlign, type PopoverRotation, type PopoverVerticalAlign, useAutoAlign } from './useAutoAlign'
|
|
32
33
|
export { type PopoverHost, usePopoverHost } from './usePopoverHost'
|