@tastic/hud 0.8.5 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +425 -23
- package/dist/chunk-32U2OIRW.mjs +77 -0
- package/dist/guide/index.d.mts +118 -0
- package/dist/guide/index.d.ts +118 -0
- package/dist/guide/index.js +543 -0
- package/dist/guide/index.mjs +428 -0
- package/dist/index.d.mts +145 -16
- package/dist/index.d.ts +145 -16
- package/dist/index.js +612 -322
- package/dist/index.mjs +533 -334
- package/package.json +46 -11
- package/src/AchievementCatalogSection.tsx +60 -0
- package/src/AchievementUnlockList.tsx +98 -0
- package/src/ActivityStatSection.tsx +29 -0
- package/src/BaseSettingsDialog.tsx +32 -1
- package/src/ConfirmDialog.tsx +4 -43
- package/src/ContentGutter.tsx +12 -5
- package/src/ControlSchemePicker.tsx +11 -2
- package/src/InlineColorPicker.tsx +38 -11
- package/src/LabeledDropdown.tsx +5 -17
- package/src/PlayerSetupPanel.tsx +196 -0
- package/src/SectionedDropdown.tsx +18 -7
- package/src/StaggeredWord.tsx +114 -0
- package/src/guide/DirectionKeysHint.tsx +96 -0
- package/src/guide/GuideProvider.tsx +87 -0
- package/src/guide/HowToPlayDialog.tsx +224 -0
- package/src/guide/SeatDevicesHint.tsx +52 -0
- package/src/guide/SeatDiagram.tsx +72 -0
- package/src/guide/SwipeHint.tsx +115 -0
- package/src/guide/guideSlice.ts +131 -0
- package/src/guide/index.ts +14 -0
- package/src/guide/types.ts +16 -0
- package/src/index.ts +10 -3
- package/src/overlayCard.ts +51 -4
- package/src/useAutoAlign.ts +17 -1
- package/src/useQuitConfirmation.ts +61 -0
- package/src/useZoneClampedAlign.ts +135 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/guide/index.ts
|
|
31
|
+
var guide_exports = {};
|
|
32
|
+
__export(guide_exports, {
|
|
33
|
+
DirectionKeysHint: () => DirectionKeysHint,
|
|
34
|
+
GuideProvider: () => GuideProvider,
|
|
35
|
+
HowToPlayDialog: () => HowToPlayDialog,
|
|
36
|
+
SeatDevicesHint: () => SeatDevicesHint,
|
|
37
|
+
SeatDiagram: () => SeatDiagram,
|
|
38
|
+
SwipeHint: () => SwipeHint,
|
|
39
|
+
createGuideSlice: () => createGuideSlice,
|
|
40
|
+
createReturningPlayerMigrate: () => createReturningPlayerMigrate,
|
|
41
|
+
getGuideCardSize: () => getGuideCardSize,
|
|
42
|
+
useAutoShowGuide: () => useAutoShowGuide,
|
|
43
|
+
useGuide: () => useGuide
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(guide_exports);
|
|
46
|
+
|
|
47
|
+
// src/guide/DirectionKeysHint.tsx
|
|
48
|
+
var import_auto_paper = require("@rific/auto-paper");
|
|
49
|
+
var import_react_native = require("react-native");
|
|
50
|
+
var import_react_native_paper = require("react-native-paper");
|
|
51
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
52
|
+
var CAP_SIZE = 34;
|
|
53
|
+
var ARROWS = { up: "arrow-up", left: "arrow-left", down: "arrow-down", right: "arrow-right" };
|
|
54
|
+
function KeyCap({ label, icon, color }) {
|
|
55
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: [styles.cap, { borderColor: color }], children: label !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_paper.Text, { variant: "titleMedium", style: [styles.capLabel, { color }], children: label }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_paper.Icon, { source: icon, size: 20, color }) });
|
|
56
|
+
}
|
|
57
|
+
function DirectionKeysHint({ labels, axis = "both", color }) {
|
|
58
|
+
const { colors } = (0, import_auto_paper.useAutoPaperTheme)();
|
|
59
|
+
const tint = color ?? colors.primary;
|
|
60
|
+
if (axis === "vertical") {
|
|
61
|
+
const accessibilityLabel2 = labels?.up && labels.down ? `Keys ${labels.up} and ${labels.down}` : "Up and down arrow keys";
|
|
62
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native.View, { accessible: true, accessibilityLabel: accessibilityLabel2, style: styles.cluster, children: [
|
|
63
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeyCap, { label: labels?.up, icon: ARROWS.up, color: tint }),
|
|
64
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeyCap, { label: labels?.down, icon: ARROWS.down, color: tint })
|
|
65
|
+
] });
|
|
66
|
+
}
|
|
67
|
+
const accessibilityLabel = labels?.up && labels.left && labels.down && labels.right ? `Keys ${labels.up}, ${labels.left}, ${labels.down}, ${labels.right}` : "Arrow keys";
|
|
68
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native.View, { accessible: true, accessibilityLabel, style: styles.cluster, children: [
|
|
69
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeyCap, { label: labels?.up, icon: ARROWS.up, color: tint }),
|
|
70
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native.View, { style: styles.row, children: [
|
|
71
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeyCap, { label: labels?.left, icon: ARROWS.left, color: tint }),
|
|
72
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeyCap, { label: labels?.down, icon: ARROWS.down, color: tint }),
|
|
73
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeyCap, { label: labels?.right, icon: ARROWS.right, color: tint })
|
|
74
|
+
] })
|
|
75
|
+
] });
|
|
76
|
+
}
|
|
77
|
+
var styles = import_react_native.StyleSheet.create({
|
|
78
|
+
cap: {
|
|
79
|
+
alignItems: "center",
|
|
80
|
+
borderRadius: 8,
|
|
81
|
+
borderWidth: 2,
|
|
82
|
+
height: CAP_SIZE,
|
|
83
|
+
justifyContent: "center",
|
|
84
|
+
width: CAP_SIZE
|
|
85
|
+
},
|
|
86
|
+
capLabel: {
|
|
87
|
+
fontWeight: "bold"
|
|
88
|
+
},
|
|
89
|
+
cluster: {
|
|
90
|
+
alignItems: "center",
|
|
91
|
+
gap: 6
|
|
92
|
+
},
|
|
93
|
+
row: {
|
|
94
|
+
flexDirection: "row",
|
|
95
|
+
gap: 6
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// src/guide/GuideProvider.tsx
|
|
100
|
+
var import_react2 = require("react");
|
|
101
|
+
|
|
102
|
+
// src/guide/HowToPlayDialog.tsx
|
|
103
|
+
var import_auto_paper2 = require("@rific/auto-paper");
|
|
104
|
+
var import_feedback_press = require("@rific/feedback-press");
|
|
105
|
+
var import_react = require("react");
|
|
106
|
+
var import_react_native3 = require("react-native");
|
|
107
|
+
var import_react_native_paper2 = require("react-native-paper");
|
|
108
|
+
var import_react_native_reanimated = require("react-native-reanimated");
|
|
109
|
+
|
|
110
|
+
// src/overlayCard.ts
|
|
111
|
+
var import_react_native2 = require("react-native");
|
|
112
|
+
function getOverlayCardColors(dark) {
|
|
113
|
+
return {
|
|
114
|
+
fg: dark ? "#FFFFFF" : "#000000",
|
|
115
|
+
fgMuted: dark ? "rgba(255,255,255,0.4)" : "rgba(0,0,0,0.4)",
|
|
116
|
+
cardBg: dark ? "#111111" : "#F2F2F2",
|
|
117
|
+
cardBorder: dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)"
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
var overlayStyles = import_react_native2.StyleSheet.create({
|
|
121
|
+
card: {
|
|
122
|
+
alignItems: "center",
|
|
123
|
+
borderRadius: 20,
|
|
124
|
+
borderWidth: 1,
|
|
125
|
+
gap: 16,
|
|
126
|
+
maxWidth: 360,
|
|
127
|
+
padding: 32
|
|
128
|
+
},
|
|
129
|
+
overlay: {
|
|
130
|
+
alignItems: "center",
|
|
131
|
+
backgroundColor: "rgba(0,0,0,0.72)",
|
|
132
|
+
bottom: 0,
|
|
133
|
+
justifyContent: "center",
|
|
134
|
+
left: 0,
|
|
135
|
+
position: "absolute",
|
|
136
|
+
right: 0,
|
|
137
|
+
top: 0
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
var overlayActionStyles = import_react_native2.StyleSheet.create({
|
|
141
|
+
// minWidth (not flexBasis) is each button's real floor - whenever both fit side by side, flexGrow
|
|
142
|
+
// still splits any leftover width evenly beyond that floor, same visual result as a plain flex: 1
|
|
143
|
+
// on a roomy screen, but a button can never be squeezed narrower than 128, only wrap to its own
|
|
144
|
+
// row instead (see actions' flexWrap below). This used to be flexBasis: 128, which measured
|
|
145
|
+
// correctly on web but rendered every button roughly SQUARE on iOS - confirmed live (see the
|
|
146
|
+
// "Quit Match?" Cancel/Quit buttons and the single-button "OK" dialogs both coming out ~128pt
|
|
147
|
+
// tall instead of wide). Root cause: react-native-paper's Button renders its Surface as two
|
|
148
|
+
// nested views on iOS only (Surface.tsx's SurfaceIOS, for shadow rendering) - only a curated
|
|
149
|
+
// allowlist of style keys (flex/flexGrow/flexShrink/width/height/position/... - notably NOT
|
|
150
|
+
// flexBasis) reaches the outer view, which is the real flex item inside this row; every other
|
|
151
|
+
// key, flexBasis included, falls through to an inner view whose own unspecified flexDirection
|
|
152
|
+
// defaults to column. flexGrow: 1 landed on the (row-context) outer view and grew width
|
|
153
|
+
// correctly; flexBasis: 128 landed on the (column-context) inner view instead, where a flexBasis
|
|
154
|
+
// sets HEIGHT, not width. minWidth isn't axis-relative like flexBasis, so it constrains width
|
|
155
|
+
// correctly regardless of which of the two views it ends up on. react-native-paper's Button also
|
|
156
|
+
// hardcodes numberOfLines={1} on its label with no override, so a button squeezed under its
|
|
157
|
+
// label's natural width silently ellipsizes ("Cancel" -> "Can...") instead of wrapping - assumed
|
|
158
|
+
// fixable by just giving the row more width, until confirmed live that these two-word labels
|
|
159
|
+
// (Cancel/Reset/New Game) can still lose the truncation fight even with the extra room ConfirmDialog's own
|
|
160
|
+
// card width fix provides, on a narrow enough screen.
|
|
161
|
+
actionButton: {
|
|
162
|
+
flexGrow: 1,
|
|
163
|
+
minWidth: 128
|
|
164
|
+
},
|
|
165
|
+
// A lone confirm button (no cancel - BaseSettingsDialog's info-only "OK" dialogs, e.g. the
|
|
166
|
+
// update-check "no update found" message) has nothing left to share actions' width with, so
|
|
167
|
+
// actionButton's own flexGrow: 1 would stretch it across the entire row instead of reading as a
|
|
168
|
+
// normal button. Same 128 floor, no flexGrow, so it stays at that natural size instead of filling
|
|
169
|
+
// the card - centered by actions' own justifyContent below.
|
|
170
|
+
actionButtonSingle: {
|
|
171
|
+
minWidth: 128
|
|
172
|
+
},
|
|
173
|
+
actions: {
|
|
174
|
+
flexDirection: "row",
|
|
175
|
+
flexWrap: "wrap",
|
|
176
|
+
gap: 12,
|
|
177
|
+
justifyContent: "center",
|
|
178
|
+
width: "100%"
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// src/guide/HowToPlayDialog.tsx
|
|
183
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
184
|
+
var OVERLAY_GUTTER = 24;
|
|
185
|
+
var CARD_PADDING = 24;
|
|
186
|
+
var MAX_CARD_WIDTH = 360;
|
|
187
|
+
var MAX_CARD_WIDTH_TURNED = 520;
|
|
188
|
+
var ART_HEIGHT = 132;
|
|
189
|
+
var COMPACT_BELOW = 480;
|
|
190
|
+
var ART_HEIGHT_COMPACT = 88;
|
|
191
|
+
function getGuideCardSize(windowWidth, windowHeight, rotation) {
|
|
192
|
+
const turned = rotation === 90 || rotation === -90;
|
|
193
|
+
const availableWidth = (turned ? windowHeight : windowWidth) - OVERLAY_GUTTER * 2;
|
|
194
|
+
const availableHeight = (turned ? windowWidth : windowHeight) - OVERLAY_GUTTER * 2;
|
|
195
|
+
const width = Math.min(turned ? MAX_CARD_WIDTH_TURNED : MAX_CARD_WIDTH, availableWidth);
|
|
196
|
+
return {
|
|
197
|
+
width,
|
|
198
|
+
pageWidth: width - CARD_PADDING * 2,
|
|
199
|
+
maxHeight: availableHeight,
|
|
200
|
+
artHeight: availableHeight < COMPACT_BELOW ? ART_HEIGHT_COMPACT : ART_HEIGHT
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function HowToPlayCard({ steps, onFinish, onSkip, rotation = 0 }) {
|
|
204
|
+
const { dark, colors } = (0, import_auto_paper2.useAutoPaperTheme)();
|
|
205
|
+
const { width: windowWidth, height: windowHeight } = (0, import_react_native3.useWindowDimensions)();
|
|
206
|
+
const reducedMotion = (0, import_react_native_reanimated.useReducedMotion)();
|
|
207
|
+
const [index, setIndex] = (0, import_react.useState)(0);
|
|
208
|
+
const scrollRef = (0, import_react.useRef)(null);
|
|
209
|
+
(0, import_react.useEffect)(() => {
|
|
210
|
+
const subscription = import_react_native3.BackHandler.addEventListener("hardwareBackPress", () => {
|
|
211
|
+
onSkip();
|
|
212
|
+
return true;
|
|
213
|
+
});
|
|
214
|
+
return () => subscription.remove();
|
|
215
|
+
}, [onSkip]);
|
|
216
|
+
const { fg, cardBg, cardBorder, fgMuted } = getOverlayCardColors(dark);
|
|
217
|
+
const { width: cardWidth, pageWidth, maxHeight, artHeight } = getGuideCardSize(windowWidth, windowHeight, rotation);
|
|
218
|
+
const rotationStyle = rotation === 0 ? void 0 : { transform: [{ rotate: `${rotation}deg` }] };
|
|
219
|
+
const isFirst = index === 0;
|
|
220
|
+
const isLast = index === steps.length - 1;
|
|
221
|
+
const isOnly = steps.length === 1;
|
|
222
|
+
const artBackground = dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.05)";
|
|
223
|
+
const targetRef = (0, import_react.useRef)(null);
|
|
224
|
+
function goTo(next) {
|
|
225
|
+
targetRef.current = next;
|
|
226
|
+
setIndex(next);
|
|
227
|
+
scrollRef.current?.scrollTo({ x: next * pageWidth, animated: !reducedMotion });
|
|
228
|
+
}
|
|
229
|
+
function syncToOffset(offsetX) {
|
|
230
|
+
const page = Math.round(offsetX / pageWidth);
|
|
231
|
+
if (page < 0 || page >= steps.length) return;
|
|
232
|
+
if (targetRef.current !== null) {
|
|
233
|
+
if (page === targetRef.current) targetRef.current = null;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
setIndex(page);
|
|
237
|
+
}
|
|
238
|
+
function handleScroll(e) {
|
|
239
|
+
syncToOffset(e.nativeEvent.contentOffset.x);
|
|
240
|
+
}
|
|
241
|
+
function handleMomentumEnd(e) {
|
|
242
|
+
targetRef.current = null;
|
|
243
|
+
syncToOffset(e.nativeEvent.contentOffset.x);
|
|
244
|
+
}
|
|
245
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper2.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.View, { style: [overlayStyles.overlay, styles2.overlay], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.View, { accessibilityViewIsModal: true, style: [overlayStyles.card, styles2.card, { width: cardWidth, maxHeight, backgroundColor: cardBg, borderColor: cardBorder }, rotationStyle], children: [
|
|
246
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.ScrollView, { ref: scrollRef, horizontal: true, pagingEnabled: true, showsHorizontalScrollIndicator: false, onScroll: handleScroll, scrollEventThrottle: 16, onMomentumScrollEnd: handleMomentumEnd, style: [styles2.pager, { width: pageWidth }], children: steps.map((step, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.ScrollView, { nestedScrollEnabled: true, showsVerticalScrollIndicator: false, style: { width: pageWidth }, contentContainerStyle: styles2.page, children: [
|
|
247
|
+
step.art !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.View, { style: [styles2.art, { height: artHeight, backgroundColor: artBackground }], children: step.art }),
|
|
248
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper2.Text, { variant: "headlineSmall", style: [styles2.title, { color: fg }], children: step.title }),
|
|
249
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper2.Text, { variant: "bodyMedium", style: [styles2.body, { color: fg }], children: step.body })
|
|
250
|
+
] }, i)) }),
|
|
251
|
+
steps.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.View, { accessible: true, accessibilityLabel: `Step ${index + 1} of ${steps.length}`, style: styles2.dots, children: steps.map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.View, { style: [styles2.dot, { backgroundColor: i === index ? colors.primary : fgMuted }] }, i)) }),
|
|
252
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.View, { style: overlayActionStyles.actions, children: [
|
|
253
|
+
!isOnly && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_feedback_press.Button, { mode: "outlined", onPress: isFirst ? onSkip : () => goTo(index - 1), style: overlayActionStyles.actionButton, children: isFirst ? "Skip" : "Back" }),
|
|
254
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_feedback_press.Button, { mode: "contained", onPress: isLast ? onFinish : () => goTo(index + 1), style: isOnly ? overlayActionStyles.actionButtonSingle : overlayActionStyles.actionButton, buttonColor: colors.primary, textColor: colors.onPrimary, children: isLast ? "Ready" : "Next" })
|
|
255
|
+
] })
|
|
256
|
+
] }) }) });
|
|
257
|
+
}
|
|
258
|
+
function HowToPlayDialog({ visible, steps, ...rest }) {
|
|
259
|
+
if (!visible || steps.length === 0) return null;
|
|
260
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(HowToPlayCard, { steps, ...rest });
|
|
261
|
+
}
|
|
262
|
+
var styles2 = import_react_native3.StyleSheet.create({
|
|
263
|
+
art: {
|
|
264
|
+
alignItems: "center",
|
|
265
|
+
borderRadius: 12,
|
|
266
|
+
justifyContent: "center",
|
|
267
|
+
overflow: "hidden",
|
|
268
|
+
width: "100%"
|
|
269
|
+
},
|
|
270
|
+
body: {
|
|
271
|
+
textAlign: "center"
|
|
272
|
+
},
|
|
273
|
+
// Additive on top of overlayCard's own shared `card` shape — its default 32px padding is tighter
|
|
274
|
+
// here (a paged card wants more width for its art) and the shared 16px gap is right.
|
|
275
|
+
card: {
|
|
276
|
+
padding: CARD_PADDING
|
|
277
|
+
},
|
|
278
|
+
dot: {
|
|
279
|
+
borderRadius: 4,
|
|
280
|
+
height: 8,
|
|
281
|
+
width: 8
|
|
282
|
+
},
|
|
283
|
+
dots: {
|
|
284
|
+
flexDirection: "row",
|
|
285
|
+
gap: 8
|
|
286
|
+
},
|
|
287
|
+
overlay: {
|
|
288
|
+
paddingHorizontal: OVERLAY_GUTTER
|
|
289
|
+
},
|
|
290
|
+
page: {
|
|
291
|
+
alignItems: "center",
|
|
292
|
+
gap: 12
|
|
293
|
+
},
|
|
294
|
+
pager: {
|
|
295
|
+
flexShrink: 1
|
|
296
|
+
},
|
|
297
|
+
title: {
|
|
298
|
+
fontWeight: "bold",
|
|
299
|
+
textAlign: "center"
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// src/guide/GuideProvider.tsx
|
|
304
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
305
|
+
var noop = () => {
|
|
306
|
+
};
|
|
307
|
+
var GuideContext = (0, import_react2.createContext)({ isOpen: false, open: noop, close: noop, requestAutoShow: noop });
|
|
308
|
+
function GuideProvider({ steps, currentVersion, seenVersion, onSeen, rotation, children }) {
|
|
309
|
+
const [isOpen, setIsOpen] = (0, import_react2.useState)(false);
|
|
310
|
+
const autoShownRef = (0, import_react2.useRef)(false);
|
|
311
|
+
const unseen = seenVersion < currentVersion;
|
|
312
|
+
const open = (0, import_react2.useCallback)(() => setIsOpen(true), []);
|
|
313
|
+
const close = (0, import_react2.useCallback)(() => {
|
|
314
|
+
setIsOpen(false);
|
|
315
|
+
if (unseen) onSeen(currentVersion);
|
|
316
|
+
}, [unseen, onSeen, currentVersion]);
|
|
317
|
+
const requestAutoShow = (0, import_react2.useCallback)(() => {
|
|
318
|
+
if (!unseen || autoShownRef.current) return;
|
|
319
|
+
autoShownRef.current = true;
|
|
320
|
+
setIsOpen(true);
|
|
321
|
+
}, [unseen]);
|
|
322
|
+
const value = (0, import_react2.useMemo)(() => ({ isOpen, open, close, requestAutoShow }), [isOpen, open, close, requestAutoShow]);
|
|
323
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(GuideContext.Provider, { value, children: [
|
|
324
|
+
children,
|
|
325
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HowToPlayDialog, { visible: isOpen, steps, onFinish: close, onSkip: close, rotation })
|
|
326
|
+
] });
|
|
327
|
+
}
|
|
328
|
+
function useGuide() {
|
|
329
|
+
return (0, import_react2.useContext)(GuideContext);
|
|
330
|
+
}
|
|
331
|
+
function useAutoShowGuide(enabled = true) {
|
|
332
|
+
const { requestAutoShow } = useGuide();
|
|
333
|
+
(0, import_react2.useEffect)(() => {
|
|
334
|
+
if (enabled) requestAutoShow();
|
|
335
|
+
}, [enabled, requestAutoShow]);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/guide/guideSlice.ts
|
|
339
|
+
var REHYDRATE = "persist/REHYDRATE";
|
|
340
|
+
function createGuideSlice({ currentVersion, mountKey = "guide", persistKey = "root" }) {
|
|
341
|
+
const MARK_SEEN = `${mountKey}/markSeen`;
|
|
342
|
+
const initialState = { versionSeen: 0 };
|
|
343
|
+
function markSeen(version) {
|
|
344
|
+
return { type: MARK_SEEN, payload: version };
|
|
345
|
+
}
|
|
346
|
+
function reducer(state = initialState, action) {
|
|
347
|
+
if (action.type === REHYDRATE) {
|
|
348
|
+
if (action.key !== void 0 && action.key !== persistKey) return state;
|
|
349
|
+
const payload = action.payload;
|
|
350
|
+
if (!payload) return state;
|
|
351
|
+
const persisted = payload[mountKey];
|
|
352
|
+
if (typeof persisted?.versionSeen === "number") return { versionSeen: persisted.versionSeen };
|
|
353
|
+
return { versionSeen: currentVersion };
|
|
354
|
+
}
|
|
355
|
+
if (action.type === MARK_SEEN && typeof action.payload === "number") {
|
|
356
|
+
return { versionSeen: Math.max(state.versionSeen, action.payload) };
|
|
357
|
+
}
|
|
358
|
+
return state;
|
|
359
|
+
}
|
|
360
|
+
function selectVersionSeen(rootState) {
|
|
361
|
+
return rootState[mountKey]?.versionSeen ?? 0;
|
|
362
|
+
}
|
|
363
|
+
return { actions: { markSeen }, reducer, selectVersionSeen };
|
|
364
|
+
}
|
|
365
|
+
function createReturningPlayerMigrate(storage, legacyKeys) {
|
|
366
|
+
return async (state, version) => {
|
|
367
|
+
if (state !== void 0 || legacyKeys.length === 0) return state;
|
|
368
|
+
try {
|
|
369
|
+
const values = await Promise.all(legacyKeys.map((key) => storage.getItem(key)));
|
|
370
|
+
return values.some((value) => value !== null) ? { _persist: { version, rehydrated: false } } : void 0;
|
|
371
|
+
} catch {
|
|
372
|
+
return void 0;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/guide/SeatDevicesHint.tsx
|
|
378
|
+
var import_auto_paper3 = require("@rific/auto-paper");
|
|
379
|
+
var import_react_native4 = require("react-native");
|
|
380
|
+
var import_react_native_paper3 = require("react-native-paper");
|
|
381
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
382
|
+
var ICON = {
|
|
383
|
+
keyboard: { source: "keyboard-outline", size: 56 },
|
|
384
|
+
mouse: { source: "mouse-outline", size: 44 }
|
|
385
|
+
};
|
|
386
|
+
var ORDER = ["keyboard", "mouse"];
|
|
387
|
+
function SeatDevicesHint({ devices, seatColors }) {
|
|
388
|
+
const { colors } = (0, import_auto_paper3.useAutoPaperTheme)();
|
|
389
|
+
const tints = seatColors ?? [colors.primary, colors.secondary];
|
|
390
|
+
const accessibilityLabel = devices.map((device, i) => `Player ${i + 1} uses the ${device}`).join(", ");
|
|
391
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { accessible: true, accessibilityLabel, style: styles3.row, children: ORDER.map((device) => {
|
|
392
|
+
const seats = devices.flatMap((d, i) => d === device ? [i] : []);
|
|
393
|
+
if (seats.length === 0) return null;
|
|
394
|
+
const color = seats.length === 1 ? tints[seats[0]] ?? colors.onSurface : colors.onSurface;
|
|
395
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper3.Icon, { source: ICON[device].source, size: ICON[device].size, color }, device);
|
|
396
|
+
}) });
|
|
397
|
+
}
|
|
398
|
+
var styles3 = import_react_native4.StyleSheet.create({
|
|
399
|
+
row: {
|
|
400
|
+
alignItems: "center",
|
|
401
|
+
flexDirection: "row",
|
|
402
|
+
gap: 20
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// src/guide/SeatDiagram.tsx
|
|
407
|
+
var import_auto_paper4 = require("@rific/auto-paper");
|
|
408
|
+
var import_react_native5 = require("react-native");
|
|
409
|
+
var import_react_native_paper4 = require("react-native-paper");
|
|
410
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
411
|
+
var DEVICE_WIDTH = 88;
|
|
412
|
+
var DEVICE_HEIGHT = 120;
|
|
413
|
+
function SeatDiagram({ topColor, bottomColor, topLabel = "P2", bottomLabel = "P1" }) {
|
|
414
|
+
const { colors } = (0, import_auto_paper4.useAutoPaperTheme)();
|
|
415
|
+
const top = topColor ?? colors.secondary;
|
|
416
|
+
const bottom = bottomColor ?? colors.primary;
|
|
417
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.View, { accessible: true, accessibilityLabel: `${bottomLabel} plays the bottom half, ${topLabel} plays the top half`, style: [styles4.device, { borderColor: colors.onSurface }], children: [
|
|
418
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.View, { style: [styles4.half, { borderColor: top }], children: [
|
|
419
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.View, { style: [import_react_native5.StyleSheet.absoluteFill, styles4.tint, { backgroundColor: top }] }),
|
|
420
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native_paper4.Text, { variant: "titleMedium", style: [styles4.label, styles4.flipped, { color: top }], children: topLabel })
|
|
421
|
+
] }),
|
|
422
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.View, { style: [styles4.half, styles4.bottomHalf, { borderColor: bottom }], children: [
|
|
423
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.View, { style: [import_react_native5.StyleSheet.absoluteFill, styles4.tint, { backgroundColor: bottom }] }),
|
|
424
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native_paper4.Text, { variant: "titleMedium", style: [styles4.label, { color: bottom }], children: bottomLabel })
|
|
425
|
+
] })
|
|
426
|
+
] });
|
|
427
|
+
}
|
|
428
|
+
var styles4 = import_react_native5.StyleSheet.create({
|
|
429
|
+
bottomHalf: {
|
|
430
|
+
borderTopWidth: 0
|
|
431
|
+
},
|
|
432
|
+
device: {
|
|
433
|
+
borderRadius: 16,
|
|
434
|
+
borderWidth: 2,
|
|
435
|
+
height: DEVICE_HEIGHT,
|
|
436
|
+
overflow: "hidden",
|
|
437
|
+
width: DEVICE_WIDTH
|
|
438
|
+
},
|
|
439
|
+
flipped: {
|
|
440
|
+
transform: [{ rotate: "180deg" }]
|
|
441
|
+
},
|
|
442
|
+
half: {
|
|
443
|
+
alignItems: "center",
|
|
444
|
+
borderWidth: 2,
|
|
445
|
+
flex: 1,
|
|
446
|
+
justifyContent: "center"
|
|
447
|
+
},
|
|
448
|
+
label: {
|
|
449
|
+
fontWeight: "bold"
|
|
450
|
+
},
|
|
451
|
+
// A wash of the seat color behind its own label rather than an rgba() fill — the theme's colors
|
|
452
|
+
// aren't guaranteed to be 6-digit hex, so an opacity layer is the only alpha that always works.
|
|
453
|
+
tint: {
|
|
454
|
+
opacity: 0.2
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// src/guide/SwipeHint.tsx
|
|
459
|
+
var import_auto_paper5 = require("@rific/auto-paper");
|
|
460
|
+
var import_react3 = require("react");
|
|
461
|
+
var import_react_native6 = require("react-native");
|
|
462
|
+
var import_react_native_paper5 = require("react-native-paper");
|
|
463
|
+
var import_react_native_reanimated2 = __toESM(require("react-native-reanimated"));
|
|
464
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
465
|
+
var DOT_COUNT = 7;
|
|
466
|
+
var FINGER_SIZE = 28;
|
|
467
|
+
var MOUSE_SIZE = 30;
|
|
468
|
+
var SWEEP_MS = 900;
|
|
469
|
+
var PAUSE_MS = 500;
|
|
470
|
+
var ARROW_ICON = { up: "arrow-up", down: "arrow-down", left: "arrow-left", right: "arrow-right" };
|
|
471
|
+
function SwipeHint({ direction = "right", pointer = "finger", color, length = 96 }) {
|
|
472
|
+
const { colors } = (0, import_auto_paper5.useAutoPaperTheme)();
|
|
473
|
+
const tint = color ?? colors.primary;
|
|
474
|
+
const reducedMotion = (0, import_react_native_reanimated2.useReducedMotion)();
|
|
475
|
+
const progress = (0, import_react_native_reanimated2.useSharedValue)(0.5);
|
|
476
|
+
const horizontal = direction === "left" || direction === "right";
|
|
477
|
+
const sign = direction === "right" || direction === "down" ? 1 : -1;
|
|
478
|
+
(0, import_react3.useEffect)(() => {
|
|
479
|
+
if (reducedMotion) {
|
|
480
|
+
progress.value = 0.5;
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
progress.value = 0;
|
|
484
|
+
progress.value = (0, import_react_native_reanimated2.withRepeat)((0, import_react_native_reanimated2.withSequence)((0, import_react_native_reanimated2.withTiming)(1, { duration: SWEEP_MS, easing: import_react_native_reanimated2.Easing.out(import_react_native_reanimated2.Easing.cubic) }), (0, import_react_native_reanimated2.withTiming)(1, { duration: PAUSE_MS }), (0, import_react_native_reanimated2.withTiming)(0, { duration: 0 })), -1);
|
|
485
|
+
}, [reducedMotion]);
|
|
486
|
+
const fingerStyle = (0, import_react_native_reanimated2.useAnimatedStyle)(() => {
|
|
487
|
+
const travel = (progress.value - 0.5) * length * sign;
|
|
488
|
+
return {
|
|
489
|
+
opacity: reducedMotion ? 1 : Math.min(1, progress.value * 6, (1 - progress.value) * 6 + 1e-3),
|
|
490
|
+
transform: horizontal ? [{ translateX: travel }] : [{ translateY: travel }]
|
|
491
|
+
};
|
|
492
|
+
});
|
|
493
|
+
const axis = horizontal ? sign > 0 ? "row" : "row-reverse" : sign > 0 ? "column" : "column-reverse";
|
|
494
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { accessibilityLabel: `${pointer === "mouse" ? "Drag" : "Swipe"} ${direction}`, accessible: true, style: [styles5.path, { flexDirection: axis }], children: [
|
|
495
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.View, { style: [styles5.dots, { flexDirection: axis, [horizontal ? "width" : "height"]: length }], children: Array.from({ length: DOT_COUNT }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.View, { style: [styles5.dot, { backgroundColor: tint, opacity: 0.35 + 0.65 * i / (DOT_COUNT - 1) }] }, i)) }),
|
|
496
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_paper5.Icon, { source: ARROW_ICON[direction], size: 28, color: tint }),
|
|
497
|
+
pointer === "mouse" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_reanimated2.default.View, { style: [styles5.mouse, fingerStyle], children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_paper5.Icon, { source: "mouse", size: MOUSE_SIZE, color: tint }) }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_reanimated2.default.View, { style: [styles5.finger, { borderColor: tint }, fingerStyle] })
|
|
498
|
+
] });
|
|
499
|
+
}
|
|
500
|
+
var styles5 = import_react_native6.StyleSheet.create({
|
|
501
|
+
dot: {
|
|
502
|
+
borderRadius: 3,
|
|
503
|
+
height: 6,
|
|
504
|
+
width: 6
|
|
505
|
+
},
|
|
506
|
+
dots: {
|
|
507
|
+
alignItems: "center",
|
|
508
|
+
justifyContent: "space-between"
|
|
509
|
+
},
|
|
510
|
+
// Centered on the whole path (not on the dots row alone) by the container's own alignItems/
|
|
511
|
+
// justifyContent, then pushed along the axis by its own animated translate — absolute so it never
|
|
512
|
+
// takes space from the dots and arrowhead it's drawn over.
|
|
513
|
+
finger: {
|
|
514
|
+
borderRadius: FINGER_SIZE / 2,
|
|
515
|
+
borderWidth: 3,
|
|
516
|
+
height: FINGER_SIZE,
|
|
517
|
+
position: "absolute",
|
|
518
|
+
width: FINGER_SIZE
|
|
519
|
+
},
|
|
520
|
+
// Positioned exactly like `finger` (absolute, centered by the container, moved by the same animated
|
|
521
|
+
// translate); only what's drawn differs.
|
|
522
|
+
mouse: {
|
|
523
|
+
position: "absolute"
|
|
524
|
+
},
|
|
525
|
+
path: {
|
|
526
|
+
alignItems: "center",
|
|
527
|
+
justifyContent: "center"
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
531
|
+
0 && (module.exports = {
|
|
532
|
+
DirectionKeysHint,
|
|
533
|
+
GuideProvider,
|
|
534
|
+
HowToPlayDialog,
|
|
535
|
+
SeatDevicesHint,
|
|
536
|
+
SeatDiagram,
|
|
537
|
+
SwipeHint,
|
|
538
|
+
createGuideSlice,
|
|
539
|
+
createReturningPlayerMigrate,
|
|
540
|
+
getGuideCardSize,
|
|
541
|
+
useAutoShowGuide,
|
|
542
|
+
useGuide
|
|
543
|
+
});
|