@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 +169 -11
- 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 +80 -9
- package/dist/index.d.ts +80 -9
- package/dist/index.js +417 -317
- package/dist/index.mjs +324 -296
- package/package.json +34 -7
- package/src/AchievementUnlockList.tsx +98 -0
- package/src/BaseSettingsDialog.tsx +179 -135
- package/src/ConfirmDialog.tsx +4 -43
- package/src/ContentGutter.tsx +12 -5
- package/src/RotatedFrame.tsx +71 -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 +3 -1
- package/src/overlayCard.ts +51 -4
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`
|
|
413
|
-
dependency, and
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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 };
|