@freegamestore/games 0.15.1 → 0.18.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 +61 -8
- package/TOKENS.md +132 -0
- package/dist/Confetti.d.ts +20 -0
- package/dist/Confetti.d.ts.map +1 -0
- package/dist/Confetti.js +76 -0
- package/dist/Confetti.js.map +1 -0
- package/dist/GameConfirm.d.ts +1 -1
- package/dist/GameConfirm.d.ts.map +1 -1
- package/dist/GameModal.d.ts +1 -1
- package/dist/GameModal.d.ts.map +1 -1
- package/dist/GameOverScreen.d.ts +1 -1
- package/dist/GameOverScreen.d.ts.map +1 -1
- package/dist/GameResultScreen.d.ts +46 -0
- package/dist/GameResultScreen.d.ts.map +1 -0
- package/dist/GameResultScreen.js +157 -0
- package/dist/GameResultScreen.js.map +1 -0
- package/dist/GameTextSizeToggle.d.ts +1 -1
- package/dist/GameTextSizeToggle.d.ts.map +1 -1
- package/dist/GameThemeToggle.d.ts +1 -1
- package/dist/GameThemeToggle.d.ts.map +1 -1
- package/dist/GameThemeToggle.js +43 -7
- package/dist/GameThemeToggle.js.map +1 -1
- package/dist/GameTopbar.d.ts +19 -7
- package/dist/GameTopbar.d.ts.map +1 -1
- package/dist/GameTopbar.js +113 -168
- package/dist/GameTopbar.js.map +1 -1
- package/dist/GameTopbar.module.css +201 -0
- package/dist/StartScreen.d.ts +48 -0
- package/dist/StartScreen.d.ts.map +1 -0
- package/dist/StartScreen.js +159 -0
- package/dist/StartScreen.js.map +1 -0
- package/dist/feel/ScorePopup.d.ts +25 -0
- package/dist/feel/ScorePopup.d.ts.map +1 -0
- package/dist/feel/ScorePopup.js +67 -0
- package/dist/feel/ScorePopup.js.map +1 -0
- package/dist/feel/easing.d.ts +14 -0
- package/dist/feel/easing.d.ts.map +1 -0
- package/dist/feel/easing.js +12 -0
- package/dist/feel/easing.js.map +1 -0
- package/dist/feel/haptics.d.ts +19 -0
- package/dist/feel/haptics.d.ts.map +1 -0
- package/dist/feel/haptics.js +24 -0
- package/dist/feel/haptics.js.map +1 -0
- package/dist/feel/index.d.ts +17 -0
- package/dist/feel/index.d.ts.map +1 -0
- package/dist/feel/index.js +17 -0
- package/dist/feel/index.js.map +1 -0
- package/dist/feel/particles.d.ts +46 -0
- package/dist/feel/particles.d.ts.map +1 -0
- package/dist/feel/particles.js +61 -0
- package/dist/feel/particles.js.map +1 -0
- package/dist/feel/reducedMotion.d.ts +6 -0
- package/dist/feel/reducedMotion.d.ts.map +1 -0
- package/dist/feel/reducedMotion.js +10 -0
- package/dist/feel/reducedMotion.js.map +1 -0
- package/dist/feel/shake.d.ts +6 -0
- package/dist/feel/shake.d.ts.map +1 -0
- package/dist/feel/shake.js +17 -0
- package/dist/feel/shake.js.map +1 -0
- package/dist/feel/tween.d.ts +9 -0
- package/dist/feel/tween.d.ts.map +1 -0
- package/dist/feel/tween.js +15 -0
- package/dist/feel/tween.js.map +1 -0
- package/dist/feel/useHaptics.d.ts +12 -0
- package/dist/feel/useHaptics.d.ts.map +1 -0
- package/dist/feel/useHaptics.js +16 -0
- package/dist/feel/useHaptics.js.map +1 -0
- package/dist/feel/useParticles.d.ts +18 -0
- package/dist/feel/useParticles.d.ts.map +1 -0
- package/dist/feel/useParticles.js +79 -0
- package/dist/feel/useParticles.js.map +1 -0
- package/dist/feel/useShake.d.ts +14 -0
- package/dist/feel/useShake.d.ts.map +1 -0
- package/dist/feel/useShake.js +17 -0
- package/dist/feel/useShake.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -2
- package/dist/index.js.map +1 -1
- package/dist/tokens.d.ts +112 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +106 -0
- package/dist/tokens.js.map +1 -0
- package/dist/useGameSounds.d.ts.map +1 -1
- package/dist/useGameSounds.js +37 -15
- package/dist/useGameSounds.js.map +1 -1
- package/dist/useLeaderboard.d.ts +8 -1
- package/dist/useLeaderboard.d.ts.map +1 -1
- package/dist/useLeaderboard.js +21 -11
- package/dist/useLeaderboard.js.map +1 -1
- package/package.json +21 -6
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Shared React UI primitives for games on **freegamestore.online**. Brand-consistent layout, scroll-free viewport lock, touch-friendly controls, and synthesized sound effects.
|
|
4
4
|
|
|
5
|
-
**Version:** 0.
|
|
5
|
+
**Version:** 0.18.0
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -21,7 +21,7 @@ import { GameShell, GameTopbar } from '@freegamestore/games'
|
|
|
21
21
|
|
|
22
22
|
export default function App() {
|
|
23
23
|
return (
|
|
24
|
-
<GameShell topbar={<GameTopbar title="Chess"
|
|
24
|
+
<GameShell topbar={<GameTopbar title="Chess" />}>
|
|
25
25
|
{/* your game canvas / DOM */}
|
|
26
26
|
</GameShell>
|
|
27
27
|
)
|
|
@@ -35,7 +35,7 @@ export default function App() {
|
|
|
35
35
|
Root layout wrapper. Locks the game to `100svh`, prevents document scroll, disables text selection and touch callout. Every game wraps its content in this.
|
|
36
36
|
|
|
37
37
|
```tsx
|
|
38
|
-
<GameShell topbar={<GameTopbar title="Tetris"
|
|
38
|
+
<GameShell topbar={<GameTopbar title="Tetris" />}>
|
|
39
39
|
<MyGame />
|
|
40
40
|
</GameShell>
|
|
41
41
|
```
|
|
@@ -50,8 +50,11 @@ Root layout wrapper. Locks the game to `100svh`, prevents document scroll, disab
|
|
|
50
50
|
The single allowed topbar. Same font, padding, and color tokens across every game.
|
|
51
51
|
|
|
52
52
|
```tsx
|
|
53
|
-
// Simple: just
|
|
54
|
-
<GameTopbar title="Tetris"
|
|
53
|
+
// Simple: just the title. The score pill is hidden by default (#348)
|
|
54
|
+
<GameTopbar title="Tetris" />
|
|
55
|
+
|
|
56
|
+
// Opt in to the live score pill
|
|
57
|
+
<GameTopbar title="Tetris" score={42} showScore />
|
|
55
58
|
|
|
56
59
|
// Custom stats + play/pause controls
|
|
57
60
|
<GameTopbar
|
|
@@ -71,8 +74,9 @@ The single allowed topbar. Same font, padding, and color tokens across every gam
|
|
|
71
74
|
| Prop | Type | Description |
|
|
72
75
|
|------|------|-------------|
|
|
73
76
|
| `title` | `string?` | Game name, left side |
|
|
74
|
-
| `score` | `number?` |
|
|
75
|
-
| `
|
|
77
|
+
| `score` | `number?` | Live score, shown only when `showScore` is set |
|
|
78
|
+
| `showScore` | `boolean?` | Show the live score pill. Off by default (#348) |
|
|
79
|
+
| `stats` | `GameTopbarStat[]?` | Custom stat lineup (lives, level, time) |
|
|
76
80
|
| `actions` | `ReactNode?` | Right-side buttons (max 2) |
|
|
77
81
|
| `rules` | `ReactNode?` | Game instructions — shows info icon, opens overlay on tap |
|
|
78
82
|
| `onPlayPause` | `() => void?` | Renders play/pause icon button |
|
|
@@ -84,7 +88,7 @@ The single allowed topbar. Same font, padding, and color tokens across every gam
|
|
|
84
88
|
Sign-in / avatar widget for the topbar `actions` slot. Shows "Sign in" when signed out, avatar + dropdown when signed in.
|
|
85
89
|
|
|
86
90
|
```tsx
|
|
87
|
-
<GameTopbar title="Chess"
|
|
91
|
+
<GameTopbar title="Chess" actions={<GameAuth />} />
|
|
88
92
|
```
|
|
89
93
|
|
|
90
94
|
No props — uses `useAuth` internally.
|
|
@@ -149,6 +153,48 @@ Standard game-over overlay with score, high score, and play again button.
|
|
|
149
153
|
| `onPlayAgain` | `() => void` | Play again handler |
|
|
150
154
|
| `children` | `ReactNode?` | Extra content below the score |
|
|
151
155
|
|
|
156
|
+
|
|
157
|
+
### GameResultScreen
|
|
158
|
+
|
|
159
|
+
An animated result screen for a finished level or game. A win shows falling confetti and an accent title. A loss shows the card alone. The action is focused when the screen opens. Use `GameOverScreen` for a plain game-over card. `GameResultScreen` is the animated version.
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
<GameResultScreen
|
|
163
|
+
tone={won ? 'win' : 'lose'}
|
|
164
|
+
eyebrow="Level 4"
|
|
165
|
+
title={won ? 'Level 4 cleared!' : 'Game over'}
|
|
166
|
+
message={won ? undefined : 'Your tray filled up.'}
|
|
167
|
+
stats={[
|
|
168
|
+
{ label: 'Score', value: score.toLocaleString() },
|
|
169
|
+
{ label: 'Moves', value: moves },
|
|
170
|
+
]}
|
|
171
|
+
badge={newBest ? 'New best!' : undefined}
|
|
172
|
+
action={won ? { label: 'Next level', onClick: next } : { label: 'Play again', onClick: restart }}
|
|
173
|
+
>
|
|
174
|
+
<Leaderboard {...leaderboard} />
|
|
175
|
+
</GameResultScreen>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
| Prop | Type | Description |
|
|
179
|
+
|------|------|-------------|
|
|
180
|
+
| `tone` | `'win' \| 'lose'` | `win` shows confetti and the accent title colour |
|
|
181
|
+
| `eyebrow` | `string?` | Small line above the title, such as the level name |
|
|
182
|
+
| `title` | `string` | The headline |
|
|
183
|
+
| `message` | `string?` | One line under the title |
|
|
184
|
+
| `stats` | `{ label, value }[]?` | Stat rows in a two-column grid. `value` may be a number |
|
|
185
|
+
| `badge` | `ReactNode?` | Highlight line under the stats, such as "New best!" |
|
|
186
|
+
| `action` | `{ label, onClick }` | The primary button. It is focused when the screen opens |
|
|
187
|
+
| `children` | `ReactNode?` | Extra content between the badge and the action |
|
|
188
|
+
|
|
189
|
+
The screen covers the viewport and has `role="dialog"`. Motion is CSS only and
|
|
190
|
+
stops under `prefers-reduced-motion`, and the confetti is hidden for players who
|
|
191
|
+
ask for less motion. Its colours and motion read the design tokens from
|
|
192
|
+
[TOKENS.md](./TOKENS.md), with light-theme fallbacks, so it works without
|
|
193
|
+
`GAME_TOKENS_CSS`.
|
|
194
|
+
|
|
195
|
+
`Confetti` is exported on its own, for a game that wants the celebration
|
|
196
|
+
somewhere else.
|
|
197
|
+
|
|
152
198
|
### GameConfirm
|
|
153
199
|
|
|
154
200
|
Confirm/cancel dialog built on GameModal.
|
|
@@ -234,6 +280,13 @@ const { topScores, recentScores, submitScore, loading, refresh } = useLeaderboar
|
|
|
234
280
|
const { ok, rank } = await submitScore(1200)
|
|
235
281
|
```
|
|
236
282
|
|
|
283
|
+
Pass `period` (a UTC day, `YYYY-MM-DD`) for a per-day board, such as a daily puzzle. Omit it for the all-time board:
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
287
|
+
const { topScores, submitScore } = useLeaderboard('tile-match', { period: today })
|
|
288
|
+
```
|
|
289
|
+
|
|
237
290
|
### useGameSounds
|
|
238
291
|
|
|
239
292
|
Synthesized sound effects via Web Audio API. Zero audio files. All sounds auto-respect the mute toggle.
|
package/TOKENS.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Design tokens
|
|
2
|
+
|
|
3
|
+
`@freegamestore/games` defines the brand, typography, shape and motion tokens
|
|
4
|
+
that every FreeGameStore game uses. They are lifted from the Mosaic Rush pilot,
|
|
5
|
+
which is the first game built on them. The source is `src/tokens.ts`. This file
|
|
6
|
+
is the reference.
|
|
7
|
+
|
|
8
|
+
## Where the values live
|
|
9
|
+
|
|
10
|
+
| Layer | Where | What it does |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| Definitions | `src/tokens.ts` | Exports the values as TypeScript constants and builds `GAME_TOKENS_CSS`. |
|
|
13
|
+
| Consumption | SDK components | Read `var(--paper)` and the other tokens inline, with a fallback for each. |
|
|
14
|
+
| Injection | Each game | Adds `GAME_TOKENS_CSS` to its stylesheet. Nothing is injected on import, because the package is `sideEffects: false`. |
|
|
15
|
+
|
|
16
|
+
Before this package defined them, the brand colours (`--paper`, `--ink`,
|
|
17
|
+
`--accent`, `--panel`, `--line`, `--muted`) were defined only in each game's own
|
|
18
|
+
`index.css`. Components in the SDK used them with fallbacks, so a game that
|
|
19
|
+
did not define them still rendered. Defining them here gives every game the
|
|
20
|
+
same values.
|
|
21
|
+
|
|
22
|
+
## Using the tokens
|
|
23
|
+
|
|
24
|
+
Add the stylesheet once, at the top of the game:
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { GAME_TOKENS_CSS } from '@freegamestore/games';
|
|
28
|
+
|
|
29
|
+
// In the root component:
|
|
30
|
+
<style>{GAME_TOKENS_CSS}</style>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or copy `GAME_TOKENS_CSS` into `index.css`, which is what Mosaic Rush does.
|
|
34
|
+
Then use the tokens in your own CSS:
|
|
35
|
+
|
|
36
|
+
```css
|
|
37
|
+
.card {
|
|
38
|
+
background: var(--panel);
|
|
39
|
+
border: 1px solid var(--line);
|
|
40
|
+
border-radius: var(--radius);
|
|
41
|
+
font-family: var(--font-display);
|
|
42
|
+
transition: transform var(--motion-fast) var(--motion-ease-out);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The dark theme applies under `data-theme="dark"` on `<html>`.
|
|
47
|
+
`GameThemeToggle` sets that attribute. The theme follows the system preference
|
|
48
|
+
until the player picks one.
|
|
49
|
+
|
|
50
|
+
## Colours
|
|
51
|
+
|
|
52
|
+
Light values are the defaults on `:root`. The dark values override only the
|
|
53
|
+
tokens that differ, under `:root[data-theme="dark"]`.
|
|
54
|
+
|
|
55
|
+
| Token | Light | Dark | Use |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| `--paper` | `#ffffff` | `#0f0f0f` | Page and board background. |
|
|
58
|
+
| `--ink` | `#1a1a1a` | `#f5f5f5` | Primary text. |
|
|
59
|
+
| `--muted` | `#6b7280` | `#9ca3af` | Secondary text and captions. |
|
|
60
|
+
| `--line` | `#e5e7eb` | `#2d2d2d` | Hairline borders on empty surfaces. |
|
|
61
|
+
| `--line-strong` | `#d1d5db` | `#404040` | Borders on buttons and toggles. |
|
|
62
|
+
| `--panel` | `#f9fafb` | `#1a1a1a` | Raised cards and overlays. |
|
|
63
|
+
| `--glass` | `rgba(255, 255, 255, 0.8)` | `rgba(15, 15, 15, 0.8)` | Frosted overlay surfaces. |
|
|
64
|
+
| `--dock` | `#ffffff` | `#1a1a1a` | Bottom dock. |
|
|
65
|
+
| `--success` | `#16a34a` | `#22c55e` | Success status. |
|
|
66
|
+
| `--warning` | `#d97706` | `#fbbf24` | Warning status. |
|
|
67
|
+
| `--error` | `#dc2626` | `#ef4444` | Error status. |
|
|
68
|
+
| `--accent` | `#2563eb` | `#2563eb` | Primary actions and highlights. The same in both themes. |
|
|
69
|
+
| `--accent-soft` | `#eff6ff` | `#1a2e26` | Quiet fill behind accent text, such as badges. |
|
|
70
|
+
| `--accent-ink` | `#2563eb` | `#60a5fa` | Accent used as text. Lighter in dark mode so it passes AA. |
|
|
71
|
+
|
|
72
|
+
Ink, muted and accent-ink each meet WCAG AA (4.5:1) on `--paper` in both themes.
|
|
73
|
+
`src/tokens.test.ts` checks this.
|
|
74
|
+
|
|
75
|
+
Use `--accent` for fills, such as buttons, and `--accent-ink` for accent text.
|
|
76
|
+
`--accent` on a dark surface does not pass AA as text.
|
|
77
|
+
|
|
78
|
+
## Typography
|
|
79
|
+
|
|
80
|
+
Only display headings and score values use the display face. Body text keeps the
|
|
81
|
+
platform's default sizes.
|
|
82
|
+
|
|
83
|
+
| Token | Value | Use |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `--font-display` | `"Fraunces", Georgia, serif` | Titles and score values. |
|
|
86
|
+
| `--font-body` | `"Manrope", system-ui, sans-serif` | Everything else. |
|
|
87
|
+
| `--text-stat` | `1.25rem` | Score and stat values. |
|
|
88
|
+
| `--text-title` | `clamp(2rem, 9vw, 2.6rem)` | Overlay titles. |
|
|
89
|
+
| `--text-display` | `clamp(2.25rem, 11vw, 3.25rem)` | The start-screen title. |
|
|
90
|
+
|
|
91
|
+
The brand fonts (Manrope and Fraunces) are the only ones allowed. The compliance
|
|
92
|
+
suite checks for them.
|
|
93
|
+
|
|
94
|
+
## Shape
|
|
95
|
+
|
|
96
|
+
| Token | Value | Use |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `--radius` | `1.25rem` | Corner radius for cards and overlays. |
|
|
99
|
+
|
|
100
|
+
## Motion
|
|
101
|
+
|
|
102
|
+
Every animation reads its duration and easing from these tokens, so one change
|
|
103
|
+
affects the whole game.
|
|
104
|
+
|
|
105
|
+
| Token | Value | Use |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `--motion-fast` | `120ms` | Presses and pops. |
|
|
108
|
+
| `--motion-base` | `240ms` | Standard transitions. |
|
|
109
|
+
| `--motion-slow` | `420ms` | Larger entrances and overlays. |
|
|
110
|
+
| `--motion-ease-out` | `cubic-bezier(0.2, 0.9, 0.3, 1)` | Default ease-out. |
|
|
111
|
+
| `--motion-ease-spring` | `cubic-bezier(0.2, 0.9, 0.3, 1.3)` | Overshooting pops. |
|
|
112
|
+
| `--motion-duration` | `var(--motion-fast)`, so `120ms` | Default duration for a control transition, such as a press. Reduced motion sets it to `0ms`. |
|
|
113
|
+
| `--motion-easing` | `var(--motion-ease-out)` | Default easing for a control transition. |
|
|
114
|
+
|
|
115
|
+
### Reduced motion
|
|
116
|
+
|
|
117
|
+
Under `prefers-reduced-motion: reduce`, the three durations are set to `0ms`.
|
|
118
|
+
Animations finish at once and only the final state shows. The easings do not
|
|
119
|
+
change.
|
|
120
|
+
|
|
121
|
+
Animations that depend on a duration then finish instantly. Any animation that
|
|
122
|
+
is pure decoration should also be disabled explicitly, as Mosaic Rush does for
|
|
123
|
+
its confetti and float effects.
|
|
124
|
+
|
|
125
|
+
## Compatibility
|
|
126
|
+
|
|
127
|
+
- Adding these tokens changes no existing export. The SDK components keep their
|
|
128
|
+
fallbacks.
|
|
129
|
+
- Games that already define `--paper` and the others in their own `index.css`
|
|
130
|
+
keep working as long as their own stylesheet loads after the tokens, so its declarations win.
|
|
131
|
+
- The store-site aliases from Mosaic Rush (`--bg`, `--surface`, `--border`) are
|
|
132
|
+
not part of this package.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type * as React from 'react';
|
|
2
|
+
/** The default palette: the four status-and-accent colours, following the theme. */
|
|
3
|
+
export declare const CONFETTI_COLOURS: readonly string[];
|
|
4
|
+
/**
|
|
5
|
+
* Styles for the falling pieces. Each piece's duration and delay are set
|
|
6
|
+
* inline. Under prefers-reduced-motion the layer is hidden, because the
|
|
7
|
+
* falling pieces are decoration only.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CONFETTI_CSS = "\n.fgs-confetti {\n position: absolute;\n inset: 0;\n z-index: 0;\n pointer-events: none;\n overflow: hidden;\n}\n.fgs-confetti-piece {\n position: absolute;\n top: -12px;\n left: var(--x);\n width: 0.5rem;\n height: 0.8rem;\n border-radius: 2px;\n background: var(--colour);\n animation: fgs-confetti-fall var(--duration, 1.6s) cubic-bezier(0.3, 0, 0.7, 1) var(--delay, 0ms) both;\n}\n.fgs-confetti-piece:nth-child(3n) {\n width: 0.6rem;\n height: 0.6rem;\n border-radius: 999px;\n}\n@keyframes fgs-confetti-fall {\n from { transform: translate3d(0, 0, 0) rotate(0deg); opacity: 1; }\n 80% { opacity: 1; }\n to { transform: translate3d(var(--drift, 0px), 620px, 0) rotate(var(--spin, 360deg)); opacity: 0; }\n}\n@media (prefers-reduced-motion: reduce) {\n .fgs-confetti {\n display: none;\n }\n}\n";
|
|
10
|
+
export interface ConfettiProps {
|
|
11
|
+
/** Colours cycled across the pieces. Default: the accent and the status colours. */
|
|
12
|
+
colours?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Falling confetti for a win. Each piece's position, drift, spin and timing
|
|
16
|
+
* come from its index, so the render is the same every time. Renders nothing
|
|
17
|
+
* under prefers-reduced-motion.
|
|
18
|
+
*/
|
|
19
|
+
export declare function Confetti({ colours }: ConfettiProps): React.JSX.Element | null;
|
|
20
|
+
//# sourceMappingURL=Confetti.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Confetti.d.ts","sourceRoot":"","sources":["../src/Confetti.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AASpC,oFAAoF;AACpF,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAK7C,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,YAAY,2zBAiCxB,CAAC;AASF,MAAM,WAAW,aAAa;IAC5B,oFAAoF;IACpF,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,EAAE,OAA0B,EAAE,EAAE,aAAa,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,CAuBhG"}
|
package/dist/Confetti.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { LIGHT_COLOURS } from './tokens.js';
|
|
3
|
+
/** Number of falling pieces. */
|
|
4
|
+
const PIECES = 30;
|
|
5
|
+
/** A CSS variable with a fallback taken from the #209 light tokens. */
|
|
6
|
+
const v = (name, fallback) => `var(--${name}, ${fallback})`;
|
|
7
|
+
/** The default palette: the four status-and-accent colours, following the theme. */
|
|
8
|
+
export const CONFETTI_COLOURS = [
|
|
9
|
+
v('accent', LIGHT_COLOURS.accent),
|
|
10
|
+
v('success', LIGHT_COLOURS.success),
|
|
11
|
+
v('warning', LIGHT_COLOURS.warning),
|
|
12
|
+
v('error', LIGHT_COLOURS.error),
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Styles for the falling pieces. Each piece's duration and delay are set
|
|
16
|
+
* inline. Under prefers-reduced-motion the layer is hidden, because the
|
|
17
|
+
* falling pieces are decoration only.
|
|
18
|
+
*/
|
|
19
|
+
export const CONFETTI_CSS = `
|
|
20
|
+
.fgs-confetti {
|
|
21
|
+
position: absolute;
|
|
22
|
+
inset: 0;
|
|
23
|
+
z-index: 0;
|
|
24
|
+
pointer-events: none;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
}
|
|
27
|
+
.fgs-confetti-piece {
|
|
28
|
+
position: absolute;
|
|
29
|
+
top: -12px;
|
|
30
|
+
left: var(--x);
|
|
31
|
+
width: 0.5rem;
|
|
32
|
+
height: 0.8rem;
|
|
33
|
+
border-radius: 2px;
|
|
34
|
+
background: var(--colour);
|
|
35
|
+
animation: fgs-confetti-fall var(--duration, 1.6s) cubic-bezier(0.3, 0, 0.7, 1) var(--delay, 0ms) both;
|
|
36
|
+
}
|
|
37
|
+
.fgs-confetti-piece:nth-child(3n) {
|
|
38
|
+
width: 0.6rem;
|
|
39
|
+
height: 0.6rem;
|
|
40
|
+
border-radius: 999px;
|
|
41
|
+
}
|
|
42
|
+
@keyframes fgs-confetti-fall {
|
|
43
|
+
from { transform: translate3d(0, 0, 0) rotate(0deg); opacity: 1; }
|
|
44
|
+
80% { opacity: 1; }
|
|
45
|
+
to { transform: translate3d(var(--drift, 0px), 620px, 0) rotate(var(--spin, 360deg)); opacity: 0; }
|
|
46
|
+
}
|
|
47
|
+
@media (prefers-reduced-motion: reduce) {
|
|
48
|
+
.fgs-confetti {
|
|
49
|
+
display: none;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
/** True when the player has asked for less motion. False on the server, where there is no window. */
|
|
54
|
+
function prefersReducedMotion() {
|
|
55
|
+
return typeof window !== 'undefined' && typeof window.matchMedia === 'function'
|
|
56
|
+
? window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
57
|
+
: false;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Falling confetti for a win. Each piece's position, drift, spin and timing
|
|
61
|
+
* come from its index, so the render is the same every time. Renders nothing
|
|
62
|
+
* under prefers-reduced-motion.
|
|
63
|
+
*/
|
|
64
|
+
export function Confetti({ colours = CONFETTI_COLOURS }) {
|
|
65
|
+
if (prefersReducedMotion())
|
|
66
|
+
return null;
|
|
67
|
+
return (_jsxs("div", { className: "fgs-confetti", "aria-hidden": "true", children: [_jsx("style", { children: CONFETTI_CSS }), Array.from({ length: PIECES }, (_, i) => (_jsx("span", { className: "fgs-confetti-piece", style: {
|
|
68
|
+
'--x': `${(i * 37) % 100}%`,
|
|
69
|
+
'--drift': `${((i * 53) % 41) - 20}px`,
|
|
70
|
+
'--spin': `${((i * 97) % 720) - 360}deg`,
|
|
71
|
+
'--delay': `${(i % 7) * 70}ms`,
|
|
72
|
+
'--duration': `${1500 + (i % 5) * 200}ms`,
|
|
73
|
+
'--colour': colours[i % colours.length],
|
|
74
|
+
} }, i)))] }));
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=Confetti.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Confetti.js","sourceRoot":"","sources":["../src/Confetti.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,gCAAgC;AAChC,MAAM,MAAM,GAAG,EAAE,CAAC;AAElB,uEAAuE;AACvE,MAAM,CAAC,GAAG,CAAC,IAAY,EAAE,QAAgB,EAAE,EAAE,CAAC,SAAS,IAAI,KAAK,QAAQ,GAAG,CAAC;AAE5E,oFAAoF;AACpF,MAAM,CAAC,MAAM,gBAAgB,GAAsB;IACjD,CAAC,CAAC,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC;IACjC,CAAC,CAAC,SAAS,EAAE,aAAa,CAAC,OAAO,CAAC;IACnC,CAAC,CAAC,SAAS,EAAE,aAAa,CAAC,OAAO,CAAC;IACnC,CAAC,CAAC,OAAO,EAAE,aAAa,CAAC,KAAK,CAAC;CAChC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiC3B,CAAC;AAEF,qGAAqG;AACrG,SAAS,oBAAoB;IAC3B,OAAO,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU;QAC7E,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,kCAAkC,CAAC,CAAC,OAAO;QAC/D,CAAC,CAAC,KAAK,CAAC;AACZ,CAAC;AAOD;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,EAAE,OAAO,GAAG,gBAAgB,EAAiB;IACpE,IAAI,oBAAoB,EAAE;QAAE,OAAO,IAAI,CAAC;IACxC,OAAO,CACL,eAAK,SAAS,EAAC,cAAc,iBAAa,MAAM,aAC9C,0BAAQ,YAAY,GAAS,EAC5B,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CACxC,eAEE,SAAS,EAAC,oBAAoB,EAC9B,KAAK,EACH;oBACE,KAAK,EAAE,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,GAAG,GAAG;oBAC3B,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,IAAI;oBACtC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,KAAK;oBACxC,SAAS,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI;oBAC9B,YAAY,EAAE,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,IAAI;oBACzC,UAAU,EAAE,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;iBACjB,IAVrB,CAAC,CAYN,CACH,CAAC,IACE,CACP,CAAC;AACJ,CAAC"}
|
package/dist/GameConfirm.d.ts
CHANGED
|
@@ -12,5 +12,5 @@ export interface GameConfirmProps {
|
|
|
12
12
|
* Confirm/cancel dialog for games. Built on GameModal.
|
|
13
13
|
* Use for: quit game, restart, delete save, etc.
|
|
14
14
|
*/
|
|
15
|
-
export declare function GameConfirm({ open, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, variant, }: GameConfirmProps): import("react
|
|
15
|
+
export declare function GameConfirm({ open, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, variant, }: GameConfirmProps): import("react").JSX.Element;
|
|
16
16
|
//# sourceMappingURL=GameConfirm.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GameConfirm.d.ts","sourceRoot":"","sources":["../src/GameConfirm.tsx"],"names":[],"mappings":"AAGA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,KAAK,EACL,OAAO,EACP,YAAwB,EACxB,WAAsB,EACtB,SAAS,EACT,QAAQ,EACR,OAAmB,GACpB,EAAE,gBAAgB
|
|
1
|
+
{"version":3,"file":"GameConfirm.d.ts","sourceRoot":"","sources":["../src/GameConfirm.tsx"],"names":[],"mappings":"AAGA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,KAAK,EACL,OAAO,EACP,YAAwB,EACxB,WAAsB,EACtB,SAAS,EACT,QAAQ,EACR,OAAmB,GACpB,EAAE,gBAAgB,+BA4BlB"}
|
package/dist/GameModal.d.ts
CHANGED
|
@@ -10,5 +10,5 @@ export interface GameModalProps {
|
|
|
10
10
|
* Closes on backdrop click or Escape key.
|
|
11
11
|
* Styled with platform design tokens for consistency across all games.
|
|
12
12
|
*/
|
|
13
|
-
export declare function GameModal({ open, onClose, title, children }: GameModalProps): import("react
|
|
13
|
+
export declare function GameModal({ open, onClose, title, children }: GameModalProps): import("react").JSX.Element | null;
|
|
14
14
|
//# sourceMappingURL=GameModal.d.ts.map
|
package/dist/GameModal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GameModal.d.ts","sourceRoot":"","sources":["../src/GameModal.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAa,MAAM,OAAO,CAAC;AAElD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"GameModal.d.ts","sourceRoot":"","sources":["../src/GameModal.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAa,MAAM,OAAO,CAAC;AAElD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,cAAc,sCAiF3E"}
|
package/dist/GameOverScreen.d.ts
CHANGED
|
@@ -14,5 +14,5 @@ export interface GameOverScreenProps {
|
|
|
14
14
|
* play again button, and optional extra content.
|
|
15
15
|
* Renders as a centered overlay on top of the game board.
|
|
16
16
|
*/
|
|
17
|
-
export declare function GameOverScreen({ score, highScore, onPlayAgain, children }: GameOverScreenProps): import("react
|
|
17
|
+
export declare function GameOverScreen({ score, highScore, onPlayAgain, children }: GameOverScreenProps): import("react").JSX.Element;
|
|
18
18
|
//# sourceMappingURL=GameOverScreen.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GameOverScreen.d.ts","sourceRoot":"","sources":["../src/GameOverScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,MAAM,WAAW,mBAAmB;IAClC,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,EAAE,mBAAmB
|
|
1
|
+
{"version":3,"file":"GameOverScreen.d.ts","sourceRoot":"","sources":["../src/GameOverScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,MAAM,WAAW,mBAAmB;IAClC,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,EAAE,mBAAmB,+BA+E9F"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type * as React from 'react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
export interface GameResultStat {
|
|
4
|
+
/** Short label, shown small and uppercase. */
|
|
5
|
+
label: string;
|
|
6
|
+
/** The value, shown large in the display face. */
|
|
7
|
+
value: string | number;
|
|
8
|
+
}
|
|
9
|
+
export interface GameResultScreenProps {
|
|
10
|
+
/** 'win' shows confetti and the accent title colour. 'lose' shows the card without confetti. */
|
|
11
|
+
tone: 'win' | 'lose';
|
|
12
|
+
/** Small line above the title, e.g. the level name. */
|
|
13
|
+
eyebrow?: string;
|
|
14
|
+
/** The headline, set large in the display face. */
|
|
15
|
+
title: string;
|
|
16
|
+
/** One line under the title. */
|
|
17
|
+
message?: string;
|
|
18
|
+
/** Stat rows shown in a two-column grid. Omit or pass [] for none. */
|
|
19
|
+
stats?: readonly GameResultStat[];
|
|
20
|
+
/** A highlight line under the stats, such as "New best!". */
|
|
21
|
+
badge?: ReactNode;
|
|
22
|
+
/** The primary action. It is focused when the screen opens. */
|
|
23
|
+
action: {
|
|
24
|
+
label: string;
|
|
25
|
+
onClick: () => void;
|
|
26
|
+
};
|
|
27
|
+
/** Extra content between the badge and the action, such as a leaderboard submit button. */
|
|
28
|
+
children?: ReactNode;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Styles for the result screen. Durations and easings come from the motion
|
|
32
|
+
* tokens. Everything stops under prefers-reduced-motion. Class names carry the
|
|
33
|
+
* `fgs-` prefix, so they cannot collide with a game's own styles.
|
|
34
|
+
*/
|
|
35
|
+
export declare const RESULT_SCREEN_CSS: string;
|
|
36
|
+
/**
|
|
37
|
+
* An animated result screen for a finished level or game. It is a modal dialog
|
|
38
|
+
* with a title, optional stats and badge, optional extra content, and one
|
|
39
|
+
* action. A win adds falling confetti. The action is focused when the screen
|
|
40
|
+
* opens, so keyboard and screen-reader users land on the next step.
|
|
41
|
+
*
|
|
42
|
+
* It covers the viewport. Motion is CSS only, and it stops under
|
|
43
|
+
* prefers-reduced-motion.
|
|
44
|
+
*/
|
|
45
|
+
export declare function GameResultScreen({ tone, eyebrow, title, message, stats, badge, action, children, }: GameResultScreenProps): React.JSX.Element;
|
|
46
|
+
//# sourceMappingURL=GameResultScreen.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GameResultScreen.d.ts","sourceRoot":"","sources":["../src/GameResultScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAMvC,MAAM,WAAW,cAAc;IAC7B,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,qBAAqB;IACpC,gGAAgG;IAChG,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;IACrB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,KAAK,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAClC,6DAA6D;IAC7D,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,+DAA+D;IAC/D,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;IAC/C,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAKD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,QA8H7B,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,OAAO,EACP,KAAK,EACL,OAAO,EACP,KAAU,EACV,KAAK,EACL,MAAM,EACN,QAAQ,GACT,EAAE,qBAAqB,qBA+CvB"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useRef } from 'react';
|
|
3
|
+
import { Confetti } from './Confetti.js';
|
|
4
|
+
import { GameButton } from './GameButton.js';
|
|
5
|
+
import { LIGHT_COLOURS, MOTION_TOKENS, SHAPE_TOKENS, TYPOGRAPHY_TOKENS } from './tokens.js';
|
|
6
|
+
/** A CSS variable with a fallback taken from the #209 light tokens, so the screen renders without GAME_TOKENS_CSS. */
|
|
7
|
+
const v = (name, fallback) => `var(--${name}, ${fallback})`;
|
|
8
|
+
/**
|
|
9
|
+
* Styles for the result screen. Durations and easings come from the motion
|
|
10
|
+
* tokens. Everything stops under prefers-reduced-motion. Class names carry the
|
|
11
|
+
* `fgs-` prefix, so they cannot collide with a game's own styles.
|
|
12
|
+
*/
|
|
13
|
+
export const RESULT_SCREEN_CSS = `
|
|
14
|
+
.fgs-result-overlay {
|
|
15
|
+
position: fixed;
|
|
16
|
+
inset: 0;
|
|
17
|
+
z-index: 800;
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
padding: 1rem;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
background: color-mix(in srgb, ${v('paper', LIGHT_COLOURS.paper)} 72%, transparent);
|
|
24
|
+
backdrop-filter: blur(5px);
|
|
25
|
+
animation: fgs-result-fade ${v('motion-base', MOTION_TOKENS.base)} ease-out both;
|
|
26
|
+
}
|
|
27
|
+
.fgs-result-card {
|
|
28
|
+
position: relative;
|
|
29
|
+
z-index: 1;
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: 0.5rem;
|
|
34
|
+
width: min(20rem, 100%);
|
|
35
|
+
padding: 1.25rem 1.1rem 1.1rem;
|
|
36
|
+
border-radius: ${v('radius', SHAPE_TOKENS.radius)};
|
|
37
|
+
text-align: center;
|
|
38
|
+
background: ${v('panel', LIGHT_COLOURS.panel)};
|
|
39
|
+
border: 1px solid ${v('line', LIGHT_COLOURS.line)};
|
|
40
|
+
box-shadow:
|
|
41
|
+
inset 0 1px 0 rgb(255 255 255 / 0.4),
|
|
42
|
+
0 1px 2px rgb(0 0 0 / 0.06),
|
|
43
|
+
0 22px 40px -20px rgb(0 0 0 / 0.45);
|
|
44
|
+
animation: fgs-result-enter ${v('motion-slow', MOTION_TOKENS.slow)} cubic-bezier(0.2, 0.9, 0.3, 1.15) both;
|
|
45
|
+
}
|
|
46
|
+
.fgs-result-eyebrow {
|
|
47
|
+
margin: 0;
|
|
48
|
+
font-size: 0.72rem;
|
|
49
|
+
font-weight: 700;
|
|
50
|
+
letter-spacing: 0.12em;
|
|
51
|
+
text-transform: uppercase;
|
|
52
|
+
color: ${v('muted', LIGHT_COLOURS.muted)};
|
|
53
|
+
}
|
|
54
|
+
.fgs-result-title {
|
|
55
|
+
margin: 0;
|
|
56
|
+
font-family: ${v('font-display', TYPOGRAPHY_TOKENS.fontDisplay)};
|
|
57
|
+
font-weight: 800;
|
|
58
|
+
font-optical-sizing: auto;
|
|
59
|
+
font-variation-settings: "opsz" 144;
|
|
60
|
+
font-size: ${v('text-title', TYPOGRAPHY_TOKENS.textTitle)};
|
|
61
|
+
line-height: 1;
|
|
62
|
+
letter-spacing: -0.02em;
|
|
63
|
+
color: ${v('ink', LIGHT_COLOURS.ink)};
|
|
64
|
+
text-wrap: balance;
|
|
65
|
+
animation: fgs-result-enter ${v('motion-slow', MOTION_TOKENS.slow)} cubic-bezier(0.2, 0.9, 0.3, 1.2) 120ms both;
|
|
66
|
+
}
|
|
67
|
+
.fgs-result-win .fgs-result-title {
|
|
68
|
+
color: ${v('accent-ink', LIGHT_COLOURS.accentInk)};
|
|
69
|
+
}
|
|
70
|
+
.fgs-result-message {
|
|
71
|
+
margin: 0;
|
|
72
|
+
font-size: 0.9rem;
|
|
73
|
+
color: ${v('muted', LIGHT_COLOURS.muted)};
|
|
74
|
+
}
|
|
75
|
+
.fgs-result-stats {
|
|
76
|
+
display: grid;
|
|
77
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
78
|
+
gap: 0.5rem;
|
|
79
|
+
width: 100%;
|
|
80
|
+
margin: 0.5rem 0 0;
|
|
81
|
+
}
|
|
82
|
+
.fgs-result-stat {
|
|
83
|
+
padding: 0.5rem 0.4rem;
|
|
84
|
+
border-radius: 0.75rem;
|
|
85
|
+
background: ${v('paper', LIGHT_COLOURS.paper)};
|
|
86
|
+
border: 1px solid ${v('line', LIGHT_COLOURS.line)};
|
|
87
|
+
box-shadow: inset 0 -2px 0 rgb(0 0 0 / 0.04);
|
|
88
|
+
animation: fgs-result-enter ${v('motion-base', MOTION_TOKENS.base)} ease-out both;
|
|
89
|
+
animation-delay: calc(260ms + var(--i, 0) * 70ms);
|
|
90
|
+
}
|
|
91
|
+
.fgs-result-stat:last-child:nth-child(odd) {
|
|
92
|
+
grid-column: 1 / -1;
|
|
93
|
+
}
|
|
94
|
+
.fgs-result-stat dt {
|
|
95
|
+
font-size: 0.68rem;
|
|
96
|
+
font-weight: 700;
|
|
97
|
+
letter-spacing: 0.08em;
|
|
98
|
+
text-transform: uppercase;
|
|
99
|
+
color: ${v('muted', LIGHT_COLOURS.muted)};
|
|
100
|
+
}
|
|
101
|
+
.fgs-result-stat dd {
|
|
102
|
+
margin: 0.15rem 0 0;
|
|
103
|
+
font-family: ${v('font-display', TYPOGRAPHY_TOKENS.fontDisplay)};
|
|
104
|
+
font-weight: 800;
|
|
105
|
+
font-size: ${v('text-stat', TYPOGRAPHY_TOKENS.textStat)};
|
|
106
|
+
color: ${v('ink', LIGHT_COLOURS.ink)};
|
|
107
|
+
font-variant-numeric: tabular-nums;
|
|
108
|
+
}
|
|
109
|
+
.fgs-result-badge {
|
|
110
|
+
margin: 0.25rem 0 0;
|
|
111
|
+
font-size: 0.85rem;
|
|
112
|
+
font-weight: 700;
|
|
113
|
+
color: ${v('accent-ink', LIGHT_COLOURS.accentInk)};
|
|
114
|
+
animation: fgs-result-enter ${v('motion-base', MOTION_TOKENS.base)} ease-out 560ms both;
|
|
115
|
+
}
|
|
116
|
+
.fgs-result-actions {
|
|
117
|
+
width: 100%;
|
|
118
|
+
margin-top: 0.5rem;
|
|
119
|
+
animation: fgs-result-enter ${v('motion-base', MOTION_TOKENS.base)} ease-out 480ms both;
|
|
120
|
+
}
|
|
121
|
+
@keyframes fgs-result-fade {
|
|
122
|
+
from { opacity: 0; }
|
|
123
|
+
to { opacity: 1; }
|
|
124
|
+
}
|
|
125
|
+
@keyframes fgs-result-enter {
|
|
126
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
127
|
+
to { opacity: 1; transform: none; }
|
|
128
|
+
}
|
|
129
|
+
@media (prefers-reduced-motion: reduce) {
|
|
130
|
+
.fgs-result-overlay,
|
|
131
|
+
.fgs-result-card,
|
|
132
|
+
.fgs-result-title,
|
|
133
|
+
.fgs-result-stat,
|
|
134
|
+
.fgs-result-badge,
|
|
135
|
+
.fgs-result-actions {
|
|
136
|
+
animation: none;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
`;
|
|
140
|
+
/**
|
|
141
|
+
* An animated result screen for a finished level or game. It is a modal dialog
|
|
142
|
+
* with a title, optional stats and badge, optional extra content, and one
|
|
143
|
+
* action. A win adds falling confetti. The action is focused when the screen
|
|
144
|
+
* opens, so keyboard and screen-reader users land on the next step.
|
|
145
|
+
*
|
|
146
|
+
* It covers the viewport. Motion is CSS only, and it stops under
|
|
147
|
+
* prefers-reduced-motion.
|
|
148
|
+
*/
|
|
149
|
+
export function GameResultScreen({ tone, eyebrow, title, message, stats = [], badge, action, children, }) {
|
|
150
|
+
const titleId = useId();
|
|
151
|
+
const cardRef = useRef(null);
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
cardRef.current?.querySelector('.fgs-result-actions button')?.focus();
|
|
154
|
+
}, []);
|
|
155
|
+
return (_jsxs("div", { className: `fgs-result-overlay fgs-result-${tone}`, role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, children: [_jsx("style", { children: RESULT_SCREEN_CSS }), tone === 'win' && _jsx(Confetti, {}), _jsxs("div", { className: "fgs-result-card", ref: cardRef, children: [eyebrow && _jsx("p", { className: "fgs-result-eyebrow", children: eyebrow }), _jsx("h2", { id: titleId, className: "fgs-result-title", children: title }), message && _jsx("p", { className: "fgs-result-message", children: message }), stats.length > 0 && (_jsx("dl", { className: "fgs-result-stats", children: stats.map((stat, i) => (_jsxs("div", { className: "fgs-result-stat", style: { '--i': i }, children: [_jsx("dt", { children: stat.label }), _jsx("dd", { children: stat.value })] }, stat.label))) })), badge ? _jsx("div", { className: "fgs-result-badge", children: badge }) : null, children, _jsx("div", { className: "fgs-result-actions", children: _jsx(GameButton, { variant: "primary", size: "lg", block: true, onClick: action.onClick, children: action.label }) })] })] }));
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=GameResultScreen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GameResultScreen.js","sourceRoot":"","sources":["../src/GameResultScreen.tsx"],"names":[],"mappings":"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"}
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Compact text-size toggle for game topbars. 28px square, shows A/A+/A-.
|
|
3
3
|
* Designed to fit in GameTopbar's `actions` slot alongside GameThemeToggle.
|
|
4
4
|
*/
|
|
5
|
-
export declare function GameTextSizeToggle(): import("react
|
|
5
|
+
export declare function GameTextSizeToggle(): import("react").JSX.Element;
|
|
6
6
|
//# sourceMappingURL=GameTextSizeToggle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GameTextSizeToggle.d.ts","sourceRoot":"","sources":["../src/GameTextSizeToggle.tsx"],"names":[],"mappings":"AAsBA;;;GAGG;AACH,wBAAgB,kBAAkB,
|
|
1
|
+
{"version":3,"file":"GameTextSizeToggle.d.ts","sourceRoot":"","sources":["../src/GameTextSizeToggle.tsx"],"names":[],"mappings":"AAsBA;;;GAGG;AACH,wBAAgB,kBAAkB,gCA0CjC"}
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Compact theme toggle for game topbars. Sun/moon icon, 28px square.
|
|
3
3
|
* Designed to fit in GameTopbar's `actions` slot.
|
|
4
4
|
*/
|
|
5
|
-
export declare function GameThemeToggle(): import("react
|
|
5
|
+
export declare function GameThemeToggle(): import("react").JSX.Element;
|
|
6
6
|
//# sourceMappingURL=GameThemeToggle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GameThemeToggle.d.ts","sourceRoot":"","sources":["../src/GameThemeToggle.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"GameThemeToggle.d.ts","sourceRoot":"","sources":["../src/GameThemeToggle.tsx"],"names":[],"mappings":"AAiDA;;;GAGG;AACH,wBAAgB,eAAe,gCAwF9B"}
|