@tastic/hud 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -94,6 +94,43 @@ position can be nowhere near where the trigger actually is on screen, which is e
94
94
  rotation-awareness fixes: without it, a popover can pick an alignment for where its trigger *used to
95
95
  be* before rotating, clipping off the edge it's actually now near instead of the one it was near.
96
96
 
97
+ ### `BaseSettingsDialog` lays out in its own turned frame
98
+
99
+ The settings sheet doesn't rotate its card in place. It renders through one Paper `Portal` into a
100
+ `RotatedFrame` (below) turned by its `rotation`, sized to the turned footprint, with rotated safe-area
101
+ insets and its own `Portal.Host`, and the Dialog portals into that host. So at +/-90 the sheet's
102
+ `maxHeight: '90%'` is measured against the phone's short edge and the whole list scrolls, instead of
103
+ running off the screen with its top row (Lock Orientation) out of reach. The frame is pass-through and
104
+ an identity frame at 0, so nothing changes in portrait. Pass `rotation={0}` when the dialog already
105
+ sits under a `Portal.Host` inside a turned frame of your own.
106
+
107
+ This needs `@rific/auto-paper` 0.12.2 or later: the Dialog's own body now renders at the portal host,
108
+ and only 0.12.2+ mounts that host inside auto-paper's own contexts (blur, Reanimated, defaults).
109
+
110
+ ### `RotatedFrame`
111
+
112
+ The frame itself, exported for a screen or overlay that should follow the phone as one piece:
113
+
114
+ ```tsx
115
+ import { RotatedFrame } from '@tastic/hud'
116
+
117
+ <RotatedFrame portalHost rotatedSafeArea maxContentWidth={640}>
118
+ <StatsList />
119
+ </RotatedFrame>
120
+ ```
121
+
122
+ - `rotation`: the angle to turn by; defaults to the live `useRotation()`.
123
+ - `rotatedSafeArea`: re-provide the safe-area insets remapped onto the frame's own edges, for content
124
+ that reads `useSafeAreaInsets()` itself. Leave it off for content that rotates insets on its own.
125
+ - `portalHost`: mount a Paper `Portal.Host` inside the frame, so dialogs lay out and turn with it (tell
126
+ them `rotation={0}`). Must be static per call site: flipping it remounts the contents.
127
+ - `passThrough`: let touches fall through the frame's own boxes, for a frame laid over other content.
128
+ - `maxContentWidth`: cap the centred content column, in the wide (landscape) footprint only.
129
+
130
+ The tree is identical at every angle, so turning the phone never remounts what's inside. Tap-driven
131
+ content only: react-native-gesture-handler's deltas are untransformed, so never put a gesture-tracked
132
+ board inside it.
133
+
97
134
  ## The press-away pattern (read this before wiring it up)
98
135
 
99
136
  A single full-screen `PressAwayOverlay` works fine for a one-player screen. It does **not** work
@@ -409,9 +446,108 @@ return (
409
446
  )
410
447
  ```
411
448
 
412
- `AchievementCatalogSection` is the only place in this package with a `@tastic/achievements`
413
- dependency, and even there it's type-only (`AchievementCatalogRow`) — never a value/runtime
414
- import. See Peer Dependencies below.
449
+ `AchievementCatalogSection` and `AchievementUnlockList` (below) are the only places in this package
450
+ with a `@tastic/achievements` dependency, and both are type-only (`AchievementCatalogRow`,
451
+ `AchievementTier`) — never a value/runtime import. See Peer Dependencies below.
452
+
453
+ ## Achievement unlocks on a result card (`AchievementUnlockList`)
454
+
455
+ The achievements a match just unlocked, for a game-over or round-over card: one row per
456
+ achievement, each with its owner's avatar (the seat's `ProfileChip`, or a plain avatar in the seat's
457
+ color for a guest), a tier-colored icon badge, and the title. Rows are listed seat by seat. Renders
458
+ nothing when no seat unlocked anything, so it can sit in the card unconditionally.
459
+
460
+ ```tsx
461
+ import { ACHIEVEMENT_TIER_COLORS } from '@tastic/achievements'
462
+ import { AchievementUnlockList } from '@tastic/hud'
463
+
464
+ <AchievementUnlockList
465
+ unlocks={seatUnlocks} // Partial<Record<Seat, { id, icon, title, tier }[]>>
466
+ seats={[1, 2]}
467
+ owners={{ 1: { profile: p1Profile, color: p1Color }, 2: { profile: null, color: p2Color } }}
468
+ tierColors={ACHIEVEMENT_TIER_COLORS} // a prop, so @tastic/achievements stays type-only here
469
+ />
470
+ ```
471
+
472
+ Optional `fg` (title color, defaults from the theme) and `style` (the list container).
473
+
474
+ For muted profile colors, mount `@tastic/profile`'s `ProfileColorProvider`: the `ProfileChip`s here
475
+ pick it up. A guest seat's avatar draws the `color` you pass as-is, so pass it already muted.
476
+
477
+ ## How-to-play guide (`@tastic/hud/guide`)
478
+
479
+ A short, skippable, paged "how to play" card that shows once on a fresh install and can be replayed on
480
+ demand from Settings. Import it from the **`@tastic/hud/guide` subpath**, not the main barrel: it's a
481
+ standalone build that pulls in only `react`, `react-native`, `react-native-paper`,
482
+ `react-native-reanimated`, `@rific/auto-paper` and `@rific/feedback-press` — none of Skia,
483
+ `@tastic/core`, `@tastic/profile`, `@tastic/split-screen`, `@tastic/achievements` or `@rific/updater`
484
+ (all marked optional in `peerDependenciesMeta`), so an app that has none of them can use it. A test
485
+ (`src/__tests__/guideImports.test.ts`) fails if any file under `src/guide/` ever imports one.
486
+
487
+ ```tsx
488
+ import { createGuideSlice, GuideProvider, SeatDiagram, SwipeHint, useAutoShowGuide, useGuide } from '@tastic/hud/guide'
489
+
490
+ // 1. A persisted "seen" version — its own slice, mounted under `guide` in your root reducer.
491
+ const guide = createGuideSlice({ currentVersion: 1 }) // { actions.markSeen, reducer, selectVersionSeen }
492
+
493
+ // 2. Once, near the root, wrapping your screens (inside PersistGate + your theme/Paper providers).
494
+ // `rotation` is your game's live fake-landscape rotation (useRotation / getViewRotation).
495
+ <GuideProvider
496
+ steps={[{ title: 'Steer Your Cycle', body: 'Swipe up, down, left or right.', art: <SwipeHint direction='up' /> }]}
497
+ currentVersion={1}
498
+ seenVersion={useSelector(guide.selectVersionSeen)}
499
+ onSeen={(v) => dispatch(guide.actions.markSeen(v))}
500
+ rotation={rotation}
501
+ >
502
+ <Screens />
503
+ </GuideProvider>
504
+
505
+ // 3. On the Home screen, and nowhere else: opens it once per session for a player who hasn't seen it.
506
+ useAutoShowGuide()
507
+
508
+ // 4. Replay from Settings — BaseSettingsDialog renders a "How to Play" row when this is passed.
509
+ const { open } = useGuide()
510
+ <BaseSettingsDialog {...props} onShowHowToPlay={open} />
511
+ ```
512
+
513
+ - **Existing players are grandfathered.** On a fresh install redux-persist's `REHYDRATE` has no payload;
514
+ an existing player has a payload that simply predates the `guide` key. `createGuideSlice` stamps the
515
+ second with `currentVersion` instead of `0`, so an OTA update never greets them with a tutorial (they
516
+ can still replay it). Only finishing or skipping writes; a replay never does. Bump `currentVersion`
517
+ only for a real controls/rules change — everyone sees it again.
518
+ - **If your last shipped build predates your redux-persist store, you also need the migrate.** Those
519
+ players have no root store yet, which looks exactly like a fresh install. Pass the AsyncStorage keys the
520
+ shipped build wrote (read them from the last shipped commit, not today's source):
521
+
522
+ ```ts
523
+ const persistConfig = {
524
+ key: 'root',
525
+ storage: AsyncStorage,
526
+ migrate: createReturningPlayerMigrate(AsyncStorage, ['pong.settings', 'pong.stats', 'pong.achievements'])
527
+ }
528
+ ```
529
+
530
+ No root store but any of those keys present means "played an earlier build": that launch rehydrates
531
+ with an empty payload, which the slice grandfathers. An existing root store passes through untouched.
532
+ - **Home only.** Call `useAutoShowGuide()` from the Home screen — that placement is what keeps it from
533
+ firing over a live match or a two-seat loadout, where a single centered card can't be read right-way-up
534
+ by both players. Pass `false` to hold it back (e.g. while a deep link is being handled).
535
+ - **Leave the row off during a live match.** Omit `onShowHowToPlay` on any screen where Settings pauses or
536
+ overlays a running game.
537
+ - **Content is the app's.** A `GuideStep` is `{ title, body, art? }`; only the app knows its own controls
538
+ and win condition. Shared illustrations for the arcade games: `SwipeHint` (animated finger sweeping
539
+ toward an arrowhead; `pointer='mouse'` for a desktop seat that plays by dragging), `DirectionKeysHint`
540
+ (an inverted-T of key caps, its keyboard counterpart; pass the seat's real keys as `labels`, arrow
541
+ icons when omitted, `axis='vertical'` for up/down-only games), `SeatDiagram` (a phone split into two
542
+ seat-colored halves) and `SeatDevicesHint` (its desktop counterpart: a keyboard and/or mouse, each in
543
+ the color of the seat using it). All take their colors from the theme's primary/secondary by default. Give every
544
+ card an illustration: the pager sizes all pages to the tallest, so a card without one leaves a gap.
545
+ - **Fleet copy conventions.** Title Case titles ("Last One Riding Wins"), one-word buttons. The action
546
+ row is ConfirmDialog's (`overlayActionStyles`): outlined Skip (first card only) or Back on the left,
547
+ contained Next on the right, `Ready` on the last card. None of that is configurable: every game gets the identical
548
+ dialog and supplies only its cards (Jay, 2026-09-24: "only their content should be different"). Short bodies,
549
+ and only say what's true in every mode ("your half" is only true in two-player).
550
+ - **Not `onboarding`.** LightCycles and Snake already use that word for their 3-2-1-GO countdown.
415
551
 
416
552
  ## Install
417
553
 
@@ -423,13 +559,35 @@ self-reading rotation behavior on these four components (defaulting to `@tastic/
423
559
  npm install @tastic/hud
424
560
  ```
425
561
 
562
+ ## ContentGutter
563
+
564
+ Caps a play area at `maxContentWidth` and splits the leftover width into two equal gutters
565
+ (`leftGutter`/`rightGutter` decorations). The width it clamps against is `@tastic/core`'s
566
+ `useRotatedWindowDimensions()`, so under a fake-landscape ancestor (`FakeLandscapeView`: OS window
567
+ stays portrait, the screen is only visually rotated) it caps against the swapped, post-rotation width
568
+ instead of the never-changing raw window width. It is an exact identity with `useWindowDimensions()`
569
+ when no rotation applies.
570
+
426
571
  ## Peer dependencies
427
572
 
428
- `react`, `react-native`, `react-native-paper` (`Icon`, `IconButton`, `Text`), `@rific/auto-paper`
429
- (`defaultColors`, `getContrastColor`, `getBlendedColor`, `SeedColor`), `@rific/feedback-press`
430
- (`IconButton`, `TouchableRipple`), [`@tastic/core`](https://github.com/jayrdeaton/react-native-game-core)
431
- (>=0.1.0 — `useRotation()`, see Rotation above),
432
- [`@tastic/achievements`](https://github.com/jayrdeaton/react-native-game-achievements) (>=0.1.2 —
433
- `AchievementCatalogRow`, in `AchievementCatalogSection.tsx` only, and type-only: no value/runtime
434
- import, see Achievements catalog above) — none of these are bundled, so use whatever versions your
435
- app already has.
573
+ None of these are bundled, so use whatever versions your app already has. Six are marked `optional` in
574
+ `peerDependenciesMeta` only so that an app using just the `@tastic/hud/guide` subpath (Hangman) can
575
+ install hud without them. **npm does not install optional peers for you**, so an app that imports the
576
+ main `@tastic/hud` entry must list every peer that entry loads in its own `dependencies`, including
577
+ the four optional ones marked "main entry" below. Without them, Metro fails to bundle with "Unable to
578
+ resolve module".
579
+
580
+ | Peer | Floor | Needed by |
581
+ | --- | --- | --- |
582
+ | `react`, `react-native` | >=19.0.0, >=0.76.0 | every entry |
583
+ | `react-native-paper` | >=5.0.0 | main entry, `./guide` |
584
+ | `react-native-reanimated` | >=3.0.0 | main entry (`StaggeredWord`), `./guide` |
585
+ | `@rific/auto-paper` | >=0.12.2 | main entry (`BaseSettingsDialog` needs the nested `Portal.Host` first shipped in 0.12.2), `./guide` |
586
+ | `@rific/feedback-press` | >=0.10.2 | main entry (`useSoundSettings` first shipped in 0.10.2), `./guide` |
587
+ | `react-native-safe-area-context` | >=5.0.0 | main entry (`useZoneClampedAlign`) |
588
+ | `@tastic/core` | >=0.12.0, optional | main entry: `useRotation()` and most dialogs; `RotatedFrame` and `BaseSettingsDialog` need `FakeLandscapeView`'s `rotation`/`passThrough`, first shipped in 0.12.0 |
589
+ | `@rific/updater` | >=0.4.0, optional | main entry: `BaseSettingsDialog`, `UpdateDialog` |
590
+ | `@tastic/profile` | >=0.4.0, optional | main entry: `PlayerSetupPanel`, `AchievementUnlockList` (`ProfileChip`) |
591
+ | `@tastic/split-screen` | >=0.5.0, optional | main entry: `useZoneClampedAlign` |
592
+ | `@shopify/react-native-skia` | >=1.5.0, optional | `TriggerGaugeHost`'s gauge (lazy-loaded behind `SkiaGate`) and `./skia-gate` |
593
+ | `@tastic/achievements` | >=0.2.0, optional | `AchievementCatalogSection`, `AchievementUnlockList`, type-only (`AchievementCatalogRow`, first exported in 0.2.0; `AchievementTier`) |
@@ -0,0 +1,77 @@
1
+ // src/overlayCard.ts
2
+ import { StyleSheet } from "react-native";
3
+ function getOverlayCardColors(dark) {
4
+ return {
5
+ fg: dark ? "#FFFFFF" : "#000000",
6
+ fgMuted: dark ? "rgba(255,255,255,0.4)" : "rgba(0,0,0,0.4)",
7
+ cardBg: dark ? "#111111" : "#F2F2F2",
8
+ cardBorder: dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)"
9
+ };
10
+ }
11
+ var overlayStyles = StyleSheet.create({
12
+ card: {
13
+ alignItems: "center",
14
+ borderRadius: 20,
15
+ borderWidth: 1,
16
+ gap: 16,
17
+ maxWidth: 360,
18
+ padding: 32
19
+ },
20
+ overlay: {
21
+ alignItems: "center",
22
+ backgroundColor: "rgba(0,0,0,0.72)",
23
+ bottom: 0,
24
+ justifyContent: "center",
25
+ left: 0,
26
+ position: "absolute",
27
+ right: 0,
28
+ top: 0
29
+ }
30
+ });
31
+ var overlayActionStyles = StyleSheet.create({
32
+ // minWidth (not flexBasis) is each button's real floor - whenever both fit side by side, flexGrow
33
+ // still splits any leftover width evenly beyond that floor, same visual result as a plain flex: 1
34
+ // on a roomy screen, but a button can never be squeezed narrower than 128, only wrap to its own
35
+ // row instead (see actions' flexWrap below). This used to be flexBasis: 128, which measured
36
+ // correctly on web but rendered every button roughly SQUARE on iOS - confirmed live (see the
37
+ // "Quit Match?" Cancel/Quit buttons and the single-button "OK" dialogs both coming out ~128pt
38
+ // tall instead of wide). Root cause: react-native-paper's Button renders its Surface as two
39
+ // nested views on iOS only (Surface.tsx's SurfaceIOS, for shadow rendering) - only a curated
40
+ // allowlist of style keys (flex/flexGrow/flexShrink/width/height/position/... - notably NOT
41
+ // flexBasis) reaches the outer view, which is the real flex item inside this row; every other
42
+ // key, flexBasis included, falls through to an inner view whose own unspecified flexDirection
43
+ // defaults to column. flexGrow: 1 landed on the (row-context) outer view and grew width
44
+ // correctly; flexBasis: 128 landed on the (column-context) inner view instead, where a flexBasis
45
+ // sets HEIGHT, not width. minWidth isn't axis-relative like flexBasis, so it constrains width
46
+ // correctly regardless of which of the two views it ends up on. react-native-paper's Button also
47
+ // hardcodes numberOfLines={1} on its label with no override, so a button squeezed under its
48
+ // label's natural width silently ellipsizes ("Cancel" -> "Can...") instead of wrapping - assumed
49
+ // fixable by just giving the row more width, until confirmed live that these two-word labels
50
+ // (Cancel/Reset/New Game) can still lose the truncation fight even with the extra room ConfirmDialog's own
51
+ // card width fix provides, on a narrow enough screen.
52
+ actionButton: {
53
+ flexGrow: 1,
54
+ minWidth: 128
55
+ },
56
+ // A lone confirm button (no cancel - BaseSettingsDialog's info-only "OK" dialogs, e.g. the
57
+ // update-check "no update found" message) has nothing left to share actions' width with, so
58
+ // actionButton's own flexGrow: 1 would stretch it across the entire row instead of reading as a
59
+ // normal button. Same 128 floor, no flexGrow, so it stays at that natural size instead of filling
60
+ // the card - centered by actions' own justifyContent below.
61
+ actionButtonSingle: {
62
+ minWidth: 128
63
+ },
64
+ actions: {
65
+ flexDirection: "row",
66
+ flexWrap: "wrap",
67
+ gap: 12,
68
+ justifyContent: "center",
69
+ width: "100%"
70
+ }
71
+ });
72
+
73
+ export {
74
+ getOverlayCardColors,
75
+ overlayStyles,
76
+ overlayActionStyles
77
+ };
@@ -0,0 +1,118 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface DirectionKeyLabels {
5
+ up: string;
6
+ left: string;
7
+ down: string;
8
+ right: string;
9
+ }
10
+ type DirectionKeysAxis = 'both' | 'vertical';
11
+ interface DirectionKeysHintProps {
12
+ labels?: Partial<DirectionKeyLabels>;
13
+ axis?: DirectionKeysAxis;
14
+ color?: string;
15
+ }
16
+ declare function DirectionKeysHint({ labels, axis, color }: DirectionKeysHintProps): react.JSX.Element;
17
+
18
+ interface GuideStep {
19
+ title: string;
20
+ body: string;
21
+ art?: ReactNode;
22
+ }
23
+ type GuideRotation = 0 | 90 | 180 | -90;
24
+
25
+ interface GuideContextValue {
26
+ isOpen: boolean;
27
+ open: () => void;
28
+ close: () => void;
29
+ requestAutoShow: () => void;
30
+ }
31
+ interface GuideProviderProps {
32
+ steps: GuideStep[];
33
+ currentVersion: number;
34
+ seenVersion: number;
35
+ onSeen: (version: number) => void;
36
+ rotation?: GuideRotation;
37
+ children?: ReactNode;
38
+ }
39
+ declare function GuideProvider({ steps, currentVersion, seenVersion, onSeen, rotation, children }: GuideProviderProps): react.JSX.Element;
40
+ declare function useGuide(): GuideContextValue;
41
+ declare function useAutoShowGuide(enabled?: boolean): void;
42
+
43
+ interface GuideState {
44
+ versionSeen: number;
45
+ }
46
+ type GuideAction = {
47
+ type: string;
48
+ payload?: unknown;
49
+ key?: string;
50
+ };
51
+ interface GuideSliceOptions {
52
+ currentVersion: number;
53
+ mountKey?: string;
54
+ persistKey?: string;
55
+ }
56
+ interface GuideSlice {
57
+ actions: {
58
+ markSeen: (version: number) => {
59
+ type: string;
60
+ payload: number;
61
+ };
62
+ };
63
+ reducer: (state: GuideState | undefined, action: GuideAction) => GuideState;
64
+ selectVersionSeen: (rootState: object) => number;
65
+ }
66
+ declare function createGuideSlice({ currentVersion, mountKey, persistKey }: GuideSliceOptions): GuideSlice;
67
+ interface GuideStorageReader {
68
+ getItem: (key: string) => Promise<string | null>;
69
+ }
70
+ type GuidePersistedState = {
71
+ _persist: {
72
+ version: number;
73
+ rehydrated: boolean;
74
+ };
75
+ } | undefined;
76
+ declare function createReturningPlayerMigrate(storage: GuideStorageReader, legacyKeys: readonly string[]): (state: GuidePersistedState, version: number) => Promise<GuidePersistedState>;
77
+
78
+ declare function getGuideCardSize(windowWidth: number, windowHeight: number, rotation: GuideRotation): {
79
+ width: number;
80
+ pageWidth: number;
81
+ maxHeight: number;
82
+ artHeight: number;
83
+ };
84
+ interface HowToPlayDialogProps {
85
+ visible: boolean;
86
+ steps: GuideStep[];
87
+ onFinish: () => void;
88
+ onSkip: () => void;
89
+ rotation?: GuideRotation;
90
+ }
91
+ declare function HowToPlayDialog({ visible, steps, ...rest }: HowToPlayDialogProps): react.JSX.Element | null;
92
+
93
+ type SeatDevice = 'keyboard' | 'mouse';
94
+ interface SeatDevicesHintProps {
95
+ devices: readonly SeatDevice[];
96
+ seatColors?: readonly string[];
97
+ }
98
+ declare function SeatDevicesHint({ devices, seatColors }: SeatDevicesHintProps): react.JSX.Element;
99
+
100
+ interface SeatDiagramProps {
101
+ topColor?: string;
102
+ bottomColor?: string;
103
+ topLabel?: string;
104
+ bottomLabel?: string;
105
+ }
106
+ declare function SeatDiagram({ topColor, bottomColor, topLabel, bottomLabel }: SeatDiagramProps): react.JSX.Element;
107
+
108
+ type SwipeDirection = 'up' | 'down' | 'left' | 'right';
109
+ type SwipePointer = 'finger' | 'mouse';
110
+ interface SwipeHintProps {
111
+ direction?: SwipeDirection;
112
+ pointer?: SwipePointer;
113
+ color?: string;
114
+ length?: number;
115
+ }
116
+ declare function SwipeHint({ direction, pointer, color, length }: SwipeHintProps): react.JSX.Element;
117
+
118
+ export { type DirectionKeyLabels, type DirectionKeysAxis, DirectionKeysHint, type DirectionKeysHintProps, type GuideAction, type GuidePersistedState, GuideProvider, type GuideProviderProps, type GuideRotation, type GuideSlice, type GuideSliceOptions, type GuideState, type GuideStep, type GuideStorageReader, HowToPlayDialog, type HowToPlayDialogProps, type SeatDevice, SeatDevicesHint, type SeatDevicesHintProps, SeatDiagram, type SeatDiagramProps, type SwipeDirection, SwipeHint, type SwipeHintProps, type SwipePointer, createGuideSlice, createReturningPlayerMigrate, getGuideCardSize, useAutoShowGuide, useGuide };
@@ -0,0 +1,118 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface DirectionKeyLabels {
5
+ up: string;
6
+ left: string;
7
+ down: string;
8
+ right: string;
9
+ }
10
+ type DirectionKeysAxis = 'both' | 'vertical';
11
+ interface DirectionKeysHintProps {
12
+ labels?: Partial<DirectionKeyLabels>;
13
+ axis?: DirectionKeysAxis;
14
+ color?: string;
15
+ }
16
+ declare function DirectionKeysHint({ labels, axis, color }: DirectionKeysHintProps): react.JSX.Element;
17
+
18
+ interface GuideStep {
19
+ title: string;
20
+ body: string;
21
+ art?: ReactNode;
22
+ }
23
+ type GuideRotation = 0 | 90 | 180 | -90;
24
+
25
+ interface GuideContextValue {
26
+ isOpen: boolean;
27
+ open: () => void;
28
+ close: () => void;
29
+ requestAutoShow: () => void;
30
+ }
31
+ interface GuideProviderProps {
32
+ steps: GuideStep[];
33
+ currentVersion: number;
34
+ seenVersion: number;
35
+ onSeen: (version: number) => void;
36
+ rotation?: GuideRotation;
37
+ children?: ReactNode;
38
+ }
39
+ declare function GuideProvider({ steps, currentVersion, seenVersion, onSeen, rotation, children }: GuideProviderProps): react.JSX.Element;
40
+ declare function useGuide(): GuideContextValue;
41
+ declare function useAutoShowGuide(enabled?: boolean): void;
42
+
43
+ interface GuideState {
44
+ versionSeen: number;
45
+ }
46
+ type GuideAction = {
47
+ type: string;
48
+ payload?: unknown;
49
+ key?: string;
50
+ };
51
+ interface GuideSliceOptions {
52
+ currentVersion: number;
53
+ mountKey?: string;
54
+ persistKey?: string;
55
+ }
56
+ interface GuideSlice {
57
+ actions: {
58
+ markSeen: (version: number) => {
59
+ type: string;
60
+ payload: number;
61
+ };
62
+ };
63
+ reducer: (state: GuideState | undefined, action: GuideAction) => GuideState;
64
+ selectVersionSeen: (rootState: object) => number;
65
+ }
66
+ declare function createGuideSlice({ currentVersion, mountKey, persistKey }: GuideSliceOptions): GuideSlice;
67
+ interface GuideStorageReader {
68
+ getItem: (key: string) => Promise<string | null>;
69
+ }
70
+ type GuidePersistedState = {
71
+ _persist: {
72
+ version: number;
73
+ rehydrated: boolean;
74
+ };
75
+ } | undefined;
76
+ declare function createReturningPlayerMigrate(storage: GuideStorageReader, legacyKeys: readonly string[]): (state: GuidePersistedState, version: number) => Promise<GuidePersistedState>;
77
+
78
+ declare function getGuideCardSize(windowWidth: number, windowHeight: number, rotation: GuideRotation): {
79
+ width: number;
80
+ pageWidth: number;
81
+ maxHeight: number;
82
+ artHeight: number;
83
+ };
84
+ interface HowToPlayDialogProps {
85
+ visible: boolean;
86
+ steps: GuideStep[];
87
+ onFinish: () => void;
88
+ onSkip: () => void;
89
+ rotation?: GuideRotation;
90
+ }
91
+ declare function HowToPlayDialog({ visible, steps, ...rest }: HowToPlayDialogProps): react.JSX.Element | null;
92
+
93
+ type SeatDevice = 'keyboard' | 'mouse';
94
+ interface SeatDevicesHintProps {
95
+ devices: readonly SeatDevice[];
96
+ seatColors?: readonly string[];
97
+ }
98
+ declare function SeatDevicesHint({ devices, seatColors }: SeatDevicesHintProps): react.JSX.Element;
99
+
100
+ interface SeatDiagramProps {
101
+ topColor?: string;
102
+ bottomColor?: string;
103
+ topLabel?: string;
104
+ bottomLabel?: string;
105
+ }
106
+ declare function SeatDiagram({ topColor, bottomColor, topLabel, bottomLabel }: SeatDiagramProps): react.JSX.Element;
107
+
108
+ type SwipeDirection = 'up' | 'down' | 'left' | 'right';
109
+ type SwipePointer = 'finger' | 'mouse';
110
+ interface SwipeHintProps {
111
+ direction?: SwipeDirection;
112
+ pointer?: SwipePointer;
113
+ color?: string;
114
+ length?: number;
115
+ }
116
+ declare function SwipeHint({ direction, pointer, color, length }: SwipeHintProps): react.JSX.Element;
117
+
118
+ export { type DirectionKeyLabels, type DirectionKeysAxis, DirectionKeysHint, type DirectionKeysHintProps, type GuideAction, type GuidePersistedState, GuideProvider, type GuideProviderProps, type GuideRotation, type GuideSlice, type GuideSliceOptions, type GuideState, type GuideStep, type GuideStorageReader, HowToPlayDialog, type HowToPlayDialogProps, type SeatDevice, SeatDevicesHint, type SeatDevicesHintProps, SeatDiagram, type SeatDiagramProps, type SwipeDirection, SwipeHint, type SwipeHintProps, type SwipePointer, createGuideSlice, createReturningPlayerMigrate, getGuideCardSize, useAutoShowGuide, useGuide };