@freegamestore/games 0.16.0 → 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.
Files changed (91) hide show
  1. package/README.md +61 -8
  2. package/TOKENS.md +132 -0
  3. package/dist/Confetti.d.ts +20 -0
  4. package/dist/Confetti.d.ts.map +1 -0
  5. package/dist/Confetti.js +76 -0
  6. package/dist/Confetti.js.map +1 -0
  7. package/dist/GameConfirm.d.ts +1 -1
  8. package/dist/GameConfirm.d.ts.map +1 -1
  9. package/dist/GameModal.d.ts +1 -1
  10. package/dist/GameModal.d.ts.map +1 -1
  11. package/dist/GameOverScreen.d.ts +1 -1
  12. package/dist/GameOverScreen.d.ts.map +1 -1
  13. package/dist/GameResultScreen.d.ts +46 -0
  14. package/dist/GameResultScreen.d.ts.map +1 -0
  15. package/dist/GameResultScreen.js +157 -0
  16. package/dist/GameResultScreen.js.map +1 -0
  17. package/dist/GameTextSizeToggle.d.ts +1 -1
  18. package/dist/GameTextSizeToggle.d.ts.map +1 -1
  19. package/dist/GameThemeToggle.d.ts +1 -1
  20. package/dist/GameThemeToggle.d.ts.map +1 -1
  21. package/dist/GameThemeToggle.js +43 -7
  22. package/dist/GameThemeToggle.js.map +1 -1
  23. package/dist/GameTopbar.d.ts +12 -7
  24. package/dist/GameTopbar.d.ts.map +1 -1
  25. package/dist/GameTopbar.js +54 -188
  26. package/dist/GameTopbar.js.map +1 -1
  27. package/dist/GameTopbar.module.css +201 -0
  28. package/dist/StartScreen.d.ts +48 -0
  29. package/dist/StartScreen.d.ts.map +1 -0
  30. package/dist/StartScreen.js +159 -0
  31. package/dist/StartScreen.js.map +1 -0
  32. package/dist/feel/ScorePopup.d.ts +25 -0
  33. package/dist/feel/ScorePopup.d.ts.map +1 -0
  34. package/dist/feel/ScorePopup.js +67 -0
  35. package/dist/feel/ScorePopup.js.map +1 -0
  36. package/dist/feel/easing.d.ts +14 -0
  37. package/dist/feel/easing.d.ts.map +1 -0
  38. package/dist/feel/easing.js +12 -0
  39. package/dist/feel/easing.js.map +1 -0
  40. package/dist/feel/haptics.d.ts +19 -0
  41. package/dist/feel/haptics.d.ts.map +1 -0
  42. package/dist/feel/haptics.js +24 -0
  43. package/dist/feel/haptics.js.map +1 -0
  44. package/dist/feel/index.d.ts +17 -0
  45. package/dist/feel/index.d.ts.map +1 -0
  46. package/dist/feel/index.js +17 -0
  47. package/dist/feel/index.js.map +1 -0
  48. package/dist/feel/particles.d.ts +46 -0
  49. package/dist/feel/particles.d.ts.map +1 -0
  50. package/dist/feel/particles.js +61 -0
  51. package/dist/feel/particles.js.map +1 -0
  52. package/dist/feel/reducedMotion.d.ts +6 -0
  53. package/dist/feel/reducedMotion.d.ts.map +1 -0
  54. package/dist/feel/reducedMotion.js +10 -0
  55. package/dist/feel/reducedMotion.js.map +1 -0
  56. package/dist/feel/shake.d.ts +6 -0
  57. package/dist/feel/shake.d.ts.map +1 -0
  58. package/dist/feel/shake.js +17 -0
  59. package/dist/feel/shake.js.map +1 -0
  60. package/dist/feel/tween.d.ts +9 -0
  61. package/dist/feel/tween.d.ts.map +1 -0
  62. package/dist/feel/tween.js +15 -0
  63. package/dist/feel/tween.js.map +1 -0
  64. package/dist/feel/useHaptics.d.ts +12 -0
  65. package/dist/feel/useHaptics.d.ts.map +1 -0
  66. package/dist/feel/useHaptics.js +16 -0
  67. package/dist/feel/useHaptics.js.map +1 -0
  68. package/dist/feel/useParticles.d.ts +18 -0
  69. package/dist/feel/useParticles.d.ts.map +1 -0
  70. package/dist/feel/useParticles.js +79 -0
  71. package/dist/feel/useParticles.js.map +1 -0
  72. package/dist/feel/useShake.d.ts +14 -0
  73. package/dist/feel/useShake.d.ts.map +1 -0
  74. package/dist/feel/useShake.js +17 -0
  75. package/dist/feel/useShake.js.map +1 -0
  76. package/dist/index.d.ts +5 -2
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +14 -2
  79. package/dist/index.js.map +1 -1
  80. package/dist/tokens.d.ts +112 -0
  81. package/dist/tokens.d.ts.map +1 -0
  82. package/dist/tokens.js +106 -0
  83. package/dist/tokens.js.map +1 -0
  84. package/dist/useGameSounds.d.ts.map +1 -1
  85. package/dist/useGameSounds.js +37 -15
  86. package/dist/useGameSounds.js.map +1 -1
  87. package/dist/useLeaderboard.d.ts +8 -1
  88. package/dist/useLeaderboard.d.ts.map +1 -1
  89. package/dist/useLeaderboard.js +21 -11
  90. package/dist/useLeaderboard.js.map +1 -1
  91. package/package.json +21 -6
@@ -0,0 +1,201 @@
1
+ /*
2
+ * GameTopbar styles. Each rule that used to be an inline style now lives here,
3
+ * so the topbar is styled in one place. Colours and the motion timing read the
4
+ * design tokens (TOKENS.md), each with a fallback, so the bar still renders
5
+ * when a game does not load GAME_TOKENS_CSS.
6
+ */
7
+
8
+ .bar {
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: space-between;
12
+ gap: 0.75rem;
13
+ padding: 0.25rem 0.75rem;
14
+ height: 2rem;
15
+ }
16
+
17
+ .group {
18
+ display: flex;
19
+ align-items: center;
20
+ gap: 0.25rem;
21
+ min-width: 0;
22
+ }
23
+
24
+ .groupEnd {
25
+ gap: 0.75rem;
26
+ }
27
+
28
+ .actions {
29
+ display: flex;
30
+ gap: 0.4rem;
31
+ align-items: center;
32
+ }
33
+
34
+ .title {
35
+ font-family: var(--font-body, "Manrope", system-ui, sans-serif);
36
+ font-weight: 600;
37
+ font-size: 0.8rem;
38
+ letter-spacing: -0.01em;
39
+ white-space: nowrap;
40
+ overflow: hidden;
41
+ text-overflow: ellipsis;
42
+ }
43
+
44
+ /* Icon buttons and the catalog link. Every control in the topbar uses this. */
45
+ .button {
46
+ display: flex;
47
+ align-items: center;
48
+ justify-content: center;
49
+ min-width: 2.75rem;
50
+ min-height: 2.75rem;
51
+ padding: 0;
52
+ background: none;
53
+ border: none;
54
+ cursor: pointer;
55
+ color: var(--muted, #999);
56
+ -webkit-tap-highlight-color: transparent;
57
+ transition: opacity var(--motion-duration, 80ms) var(--motion-easing, ease-out);
58
+ }
59
+
60
+ .link {
61
+ text-decoration: none;
62
+ }
63
+
64
+ /* Tones: the play/pause icon is ink, the sound icon is accent when sound is on */
65
+ .toneMuted {
66
+ color: var(--muted, #999);
67
+ }
68
+
69
+ .toneInk {
70
+ color: var(--ink, #f0f0f0);
71
+ }
72
+
73
+ .toneAccent {
74
+ color: var(--accent, #10b981);
75
+ }
76
+
77
+ /* A tap dips the control slightly, so it feels anchored on touch screens */
78
+ .button:active {
79
+ opacity: 0.55;
80
+ }
81
+
82
+ .button:focus-visible {
83
+ outline: 2px solid var(--accent, #10b981);
84
+ outline-offset: -2px;
85
+ border-radius: 0.4rem;
86
+ }
87
+
88
+ /* Score and stat values */
89
+ .stat {
90
+ text-align: right;
91
+ line-height: 1.05;
92
+ }
93
+
94
+ .statValue {
95
+ font-family: var(--font-body, "Manrope", system-ui, sans-serif);
96
+ font-weight: 800;
97
+ font-size: 0.85rem;
98
+ color: var(--ink);
99
+ font-variant-numeric: tabular-nums;
100
+ line-height: 1;
101
+ }
102
+
103
+ .statAccent {
104
+ color: var(--accent);
105
+ }
106
+
107
+ /* Label under a stat. 0.65rem keeps it legible in sunlight and for low vision. */
108
+ .statLabel {
109
+ font-size: 0.65rem;
110
+ font-weight: 700;
111
+ text-transform: uppercase;
112
+ letter-spacing: 0.06em;
113
+ color: var(--muted);
114
+ line-height: 1;
115
+ }
116
+
117
+ /* Rules and About overlays: a full-screen panel with a header and a scrolling body */
118
+ .overlay {
119
+ position: fixed;
120
+ inset: 0;
121
+ z-index: 9999;
122
+ display: flex;
123
+ flex-direction: column;
124
+ overflow: hidden;
125
+ background: var(--paper, #0f0f0f);
126
+ color: var(--ink, #f0f0f0);
127
+ }
128
+
129
+ .overlayHeader {
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: space-between;
133
+ flex-shrink: 0;
134
+ padding: 0.5rem 0.75rem;
135
+ border-bottom: 1px solid var(--line, #2a2a2a);
136
+ }
137
+
138
+ .overlayTitle {
139
+ font-family: var(--font-body, "Manrope", system-ui, sans-serif);
140
+ font-weight: 700;
141
+ font-size: 0.9rem;
142
+ }
143
+
144
+ .overlayBody {
145
+ flex: 1;
146
+ overflow-y: auto;
147
+ padding: 1rem;
148
+ font-family: var(--font-body, "Manrope", system-ui, sans-serif);
149
+ font-size: 0.9rem;
150
+ line-height: 1.6;
151
+ }
152
+
153
+ /* About panel */
154
+ .aboutHeading {
155
+ margin: 0 0 0.75rem;
156
+ font-family: var(--font-display, "Fraunces", Georgia, serif);
157
+ font-weight: 700;
158
+ font-size: 1.5rem;
159
+ }
160
+
161
+ .aboutExtra {
162
+ margin-bottom: 1rem;
163
+ }
164
+
165
+ .aboutGrid {
166
+ display: grid;
167
+ grid-template-columns: auto 1fr;
168
+ gap: 0.35rem 1rem;
169
+ margin: 0;
170
+ font-size: 0.85rem;
171
+ }
172
+
173
+ .aboutGrid dt {
174
+ color: var(--muted, #999);
175
+ }
176
+
177
+ .aboutGrid dd {
178
+ margin: 0;
179
+ font-variant-numeric: tabular-nums;
180
+ }
181
+
182
+ .code {
183
+ font-family: monospace;
184
+ }
185
+
186
+ .aboutNote {
187
+ margin-top: 1.25rem;
188
+ font-size: 0.8rem;
189
+ color: var(--muted, #999);
190
+ }
191
+
192
+ .aboutLink {
193
+ color: var(--accent, #10b981);
194
+ text-decoration: none;
195
+ }
196
+
197
+ @media (prefers-reduced-motion: reduce) {
198
+ .button {
199
+ transition: none;
200
+ }
201
+ }
@@ -0,0 +1,48 @@
1
+ import type * as React from 'react';
2
+ export interface StartScreenStep {
3
+ /** Short name of the step, shown in bold. */
4
+ heading: string;
5
+ /** One or two sentences of explanation. */
6
+ text: string;
7
+ }
8
+ export interface StartScreenProps {
9
+ /** The game's display name, set large in the display face. */
10
+ title: string;
11
+ /** One line under the title. */
12
+ tagline?: string;
13
+ /**
14
+ * Decorative art above the title, such as a mosaic or a logo. It is hidden
15
+ * from assistive technology, because the title already names the game.
16
+ */
17
+ artwork?: React.ReactNode;
18
+ /** Label for the main button. Default "Play". */
19
+ playLabel?: string;
20
+ /** Called when the player taps Play. */
21
+ onPlay: () => void;
22
+ /**
23
+ * Called when the player taps How to play. Use this when the game shows its
24
+ * own rules screen. When it is omitted, `howToPlay` steps open the SDK's overlay.
25
+ */
26
+ onHowToPlay?: () => void;
27
+ /**
28
+ * Numbered steps for the built-in How to play overlay. Used only when
29
+ * `onHowToPlay` is not given. Omit both to hide the button.
30
+ */
31
+ howToPlay?: readonly StartScreenStep[];
32
+ }
33
+ /**
34
+ * Styles for the start screen and its how-to-play list. The durations and
35
+ * easings come from the motion tokens, and everything stops under
36
+ * prefers-reduced-motion. Class names carry the `fgs-` prefix, so they cannot
37
+ * collide with a game's own styles.
38
+ */
39
+ export declare const START_SCREEN_CSS: string;
40
+ /**
41
+ * A game's start screen: artwork, title, tagline, then Play and How to play.
42
+ * Motion is CSS only, and it stops under prefers-reduced-motion.
43
+ *
44
+ * The screen does not define the brand tokens. It falls back to the #209
45
+ * light values, so a game that loads GAME_TOKENS_CSS gets the full theme.
46
+ */
47
+ export declare function StartScreen({ title, tagline, artwork, playLabel, onPlay, onHowToPlay, howToPlay, }: StartScreenProps): React.JSX.Element;
48
+ //# sourceMappingURL=StartScreen.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StartScreen.d.ts","sourceRoot":"","sources":["../src/StartScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAMpC,MAAM,WAAW,eAAe;IAC9B,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;CACxC;AAKD;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAuH5B,CAAC;AAwBF;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,OAAO,EACP,OAAO,EACP,SAAkB,EAClB,MAAM,EACN,WAAW,EACX,SAAS,GACV,EAAE,gBAAgB,qBAyClB"}
@@ -0,0 +1,159 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId, useState } from 'react';
3
+ import { GameButton } from './GameButton.js';
4
+ import { GameModal } from './GameModal.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 start screen and its how-to-play list. The durations and
10
+ * easings come from the motion tokens, and everything stops under
11
+ * prefers-reduced-motion. Class names carry the `fgs-` prefix, so they cannot
12
+ * collide with a game's own styles.
13
+ */
14
+ export const START_SCREEN_CSS = `
15
+ .fgs-start {
16
+ position: relative;
17
+ display: flex;
18
+ flex-direction: column;
19
+ align-items: center;
20
+ justify-content: center;
21
+ gap: clamp(0.75rem, 3svh, 1.5rem);
22
+ height: 100%;
23
+ padding: 1rem;
24
+ overflow: hidden;
25
+ background:
26
+ radial-gradient(120% 70% at 50% 0%, ${v('accent-soft', LIGHT_COLOURS.accentSoft)} 0%, transparent 65%),
27
+ ${v('paper', LIGHT_COLOURS.paper)};
28
+ }
29
+ .fgs-start-artwork { width: min(11rem, 52vw); }
30
+ .fgs-start-artwork-inner { animation: fgs-start-float 7s ease-in-out infinite; }
31
+ .fgs-start-card {
32
+ display: flex;
33
+ flex-direction: column;
34
+ align-items: center;
35
+ gap: 0.5rem;
36
+ width: min(20rem, 100%);
37
+ padding: 1.5rem 1.25rem 1.25rem;
38
+ border-radius: ${v('radius', SHAPE_TOKENS.radius)};
39
+ text-align: center;
40
+ background: ${v('panel', LIGHT_COLOURS.panel)};
41
+ border: 1px solid ${v('line', LIGHT_COLOURS.line)};
42
+ box-shadow:
43
+ inset 0 1px 0 rgb(255 255 255 / 0.4),
44
+ 0 1px 2px rgb(0 0 0 / 0.06),
45
+ 0 18px 32px -18px rgb(0 0 0 / 0.35);
46
+ }
47
+ .fgs-start-title {
48
+ margin: 0;
49
+ font-family: ${v('font-display', TYPOGRAPHY_TOKENS.fontDisplay)};
50
+ font-weight: 800;
51
+ font-optical-sizing: auto;
52
+ font-variation-settings: "opsz" 144;
53
+ font-size: ${v('text-display', TYPOGRAPHY_TOKENS.textDisplay)};
54
+ line-height: 0.95;
55
+ letter-spacing: -0.03em;
56
+ color: ${v('ink', LIGHT_COLOURS.ink)};
57
+ text-wrap: balance;
58
+ }
59
+ .fgs-start-tagline {
60
+ margin: 0 0 0.5rem;
61
+ max-width: 18rem;
62
+ font-size: 0.95rem;
63
+ line-height: 1.4;
64
+ color: ${v('muted', LIGHT_COLOURS.muted)};
65
+ }
66
+ .fgs-start-actions {
67
+ display: flex;
68
+ flex-direction: column;
69
+ gap: 0.5rem;
70
+ width: 100%;
71
+ margin-top: 0.25rem;
72
+ }
73
+ .fgs-start-enter {
74
+ animation: fgs-start-rise ${v('motion-slow', MOTION_TOKENS.slow)} cubic-bezier(0.2, 0.8, 0.2, 1) both;
75
+ animation-delay: var(--delay, 0ms);
76
+ }
77
+ @keyframes fgs-start-rise {
78
+ from { opacity: 0; transform: translateY(12px); }
79
+ to { opacity: 1; transform: none; }
80
+ }
81
+ @keyframes fgs-start-float {
82
+ 0%, 100% { transform: translateY(0) rotate(0deg); }
83
+ 50% { transform: translateY(-4px) rotate(-1deg); }
84
+ }
85
+ .fgs-how-steps {
86
+ display: flex;
87
+ flex-direction: column;
88
+ gap: 0.85rem;
89
+ margin: 0;
90
+ padding: 0;
91
+ list-style: none;
92
+ }
93
+ .fgs-how-steps li {
94
+ display: flex;
95
+ align-items: flex-start;
96
+ gap: 0.75rem;
97
+ animation: fgs-start-rise ${v('motion-base', MOTION_TOKENS.base)} ease-out both;
98
+ animation-delay: calc(var(--i, 0) * 60ms);
99
+ }
100
+ .fgs-how-num {
101
+ flex: none;
102
+ display: grid;
103
+ place-items: center;
104
+ width: 1.75rem;
105
+ height: 1.75rem;
106
+ border-radius: 0.5rem;
107
+ font-family: ${v('font-display', TYPOGRAPHY_TOKENS.fontDisplay)};
108
+ font-weight: 800;
109
+ color: #ffffff;
110
+ background: ${v('accent', LIGHT_COLOURS.accent)};
111
+ box-shadow:
112
+ inset 0 1px 0 rgb(255 255 255 / 0.35),
113
+ 0 2px 4px rgb(0 0 0 / 0.2);
114
+ }
115
+ .fgs-how-steps strong {
116
+ display: block;
117
+ font-weight: 700;
118
+ color: ${v('ink', LIGHT_COLOURS.ink)};
119
+ }
120
+ .fgs-how-steps p {
121
+ margin: 0.15rem 0 0;
122
+ font-size: 0.9rem;
123
+ line-height: 1.4;
124
+ color: ${v('muted', LIGHT_COLOURS.muted)};
125
+ }
126
+ @media (prefers-reduced-motion: reduce) {
127
+ .fgs-start-enter,
128
+ .fgs-start-artwork-inner,
129
+ .fgs-how-steps li {
130
+ animation: none;
131
+ }
132
+ }
133
+ `;
134
+ /** Entrance delay for one element, so the screen rises in a sequence. */
135
+ const delay = (ms) => ({ '--delay': `${ms}ms` });
136
+ /** The numbered steps of a game's rules. */
137
+ function HowToPlaySteps({ steps }) {
138
+ return (_jsx("ol", { className: "fgs-how-steps", children: steps.map((step, i) => (_jsxs("li", { style: { '--i': i }, children: [_jsx("span", { className: "fgs-how-num", "aria-hidden": "true", children: i + 1 }), _jsxs("div", { children: [_jsx("strong", { children: step.heading }), _jsx("p", { children: step.text })] })] }, step.heading))) }));
139
+ }
140
+ /**
141
+ * A game's start screen: artwork, title, tagline, then Play and How to play.
142
+ * Motion is CSS only, and it stops under prefers-reduced-motion.
143
+ *
144
+ * The screen does not define the brand tokens. It falls back to the #209
145
+ * light values, so a game that loads GAME_TOKENS_CSS gets the full theme.
146
+ */
147
+ export function StartScreen({ title, tagline, artwork, playLabel = 'Play', onPlay, onHowToPlay, howToPlay, }) {
148
+ const [helpOpen, setHelpOpen] = useState(false);
149
+ const titleId = useId();
150
+ const showsHelp = onHowToPlay !== undefined || howToPlay !== undefined;
151
+ const openHelp = () => {
152
+ if (onHowToPlay)
153
+ onHowToPlay();
154
+ else
155
+ setHelpOpen(true);
156
+ };
157
+ return (_jsxs("section", { className: "fgs-start", "aria-labelledby": titleId, children: [_jsx("style", { children: START_SCREEN_CSS }), artwork !== undefined && artwork !== null && (_jsx("div", { className: "fgs-start-artwork fgs-start-enter", style: delay(0), "aria-hidden": "true", children: _jsx("div", { className: "fgs-start-artwork-inner", children: artwork }) })), _jsxs("div", { className: "fgs-start-card fgs-start-enter", style: delay(120), children: [_jsx("h1", { id: titleId, className: "fgs-start-title", children: title }), tagline && _jsx("p", { className: "fgs-start-tagline", children: tagline }), _jsxs("div", { className: "fgs-start-actions", children: [_jsx(GameButton, { variant: "primary", size: "lg", block: true, onClick: onPlay, children: playLabel }), showsHelp && (_jsx(GameButton, { variant: "secondary", size: "md", block: true, onClick: openHelp, children: "How to play" }))] })] }), onHowToPlay === undefined && howToPlay !== undefined && (_jsx(GameModal, { open: helpOpen, onClose: () => setHelpOpen(false), title: "How to play", children: _jsx(HowToPlaySteps, { steps: howToPlay }) }))] }));
158
+ }
159
+ //# sourceMappingURL=StartScreen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StartScreen.js","sourceRoot":"","sources":["../src/StartScreen.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type * as React from 'react';
2
+ /** How long a pop-up stays on screen, in ms. */
3
+ export declare const SCORE_POP_MS = 700;
4
+ /**
5
+ * Styles for the pop-up. Its colour is the accent-ink token, so it follows the
6
+ * theme, with a light-theme fallback. The motion itself is set from script.
7
+ */
8
+ export declare const SCORE_POP_CSS = "\n.fgs-score-pop {\n position: absolute;\n z-index: 750;\n pointer-events: none;\n white-space: nowrap;\n font-family: var(--font-display, \"Fraunces\", Georgia, serif);\n font-size: var(--text-stat, 1.25rem);\n font-weight: 800;\n color: var(--accent-ink, #2563eb);\n text-shadow: 0 1px 2px rgb(0 0 0 / 0.55);\n will-change: transform, opacity;\n}\n";
9
+ export interface ScorePopupProps {
10
+ /** The points to show, as `+N`. */
11
+ points: number;
12
+ /** Horizontal centre, in the pixel space of its positioned ancestor. */
13
+ x: number;
14
+ /** Vertical centre, in the pixel space of its positioned ancestor. */
15
+ y: number;
16
+ /** Called once the pop-up has finished. The parent should remove it. */
17
+ onDone: () => void;
18
+ }
19
+ /**
20
+ * A floating `+N` that rises and fades from (x, y), then calls `onDone`.
21
+ * Under reduced motion it holds still for the same time, so the number is still
22
+ * seen and `onDone` still fires. Place it inside a `position: relative` container.
23
+ */
24
+ export declare function ScorePopup({ points, x, y, onDone }: ScorePopupProps): React.JSX.Element;
25
+ //# sourceMappingURL=ScorePopup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScorePopup.d.ts","sourceRoot":"","sources":["../../src/feel/ScorePopup.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAMpC,gDAAgD;AAChD,eAAO,MAAM,YAAY,MAAM,CAAC;AAKhC;;;GAGG;AACH,eAAO,MAAM,aAAa,6WAazB,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,mCAAmC;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,CAAC,EAAE,MAAM,CAAC;IACV,sEAAsE;IACtE,CAAC,EAAE,MAAM,CAAC;IACV,wEAAwE;IACxE,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,eAAe,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CA+CvF"}
@@ -0,0 +1,67 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { easeInQuad, easeOutQuad } from './easing.js';
4
+ import { prefersReducedMotion } from './reducedMotion.js';
5
+ import { tweenProgress, tweenValue } from './tween.js';
6
+ /** How long a pop-up stays on screen, in ms. */
7
+ export const SCORE_POP_MS = 700;
8
+ /** How far a pop-up rises before it fades, in px. */
9
+ const RISE_PX = 40;
10
+ /**
11
+ * Styles for the pop-up. Its colour is the accent-ink token, so it follows the
12
+ * theme, with a light-theme fallback. The motion itself is set from script.
13
+ */
14
+ export const SCORE_POP_CSS = `
15
+ .fgs-score-pop {
16
+ position: absolute;
17
+ z-index: 750;
18
+ pointer-events: none;
19
+ white-space: nowrap;
20
+ font-family: var(--font-display, "Fraunces", Georgia, serif);
21
+ font-size: var(--text-stat, 1.25rem);
22
+ font-weight: 800;
23
+ color: var(--accent-ink, #2563eb);
24
+ text-shadow: 0 1px 2px rgb(0 0 0 / 0.55);
25
+ will-change: transform, opacity;
26
+ }
27
+ `;
28
+ /**
29
+ * A floating `+N` that rises and fades from (x, y), then calls `onDone`.
30
+ * Under reduced motion it holds still for the same time, so the number is still
31
+ * seen and `onDone` still fires. Place it inside a `position: relative` container.
32
+ */
33
+ export function ScorePopup({ points, x, y, onDone }) {
34
+ const ref = useRef(null);
35
+ const onDoneRef = useRef(onDone);
36
+ useEffect(() => {
37
+ onDoneRef.current = onDone;
38
+ }, [onDone]);
39
+ useEffect(() => {
40
+ const element = ref.current;
41
+ if (!element)
42
+ return;
43
+ if (prefersReducedMotion()) {
44
+ const id = setTimeout(() => onDoneRef.current(), SCORE_POP_MS);
45
+ return () => clearTimeout(id);
46
+ }
47
+ let started = null;
48
+ let frame = 0;
49
+ const step = (now) => {
50
+ started ??= now;
51
+ const elapsed = now - started;
52
+ const rise = tweenValue(0, -RISE_PX, elapsed, SCORE_POP_MS, easeOutQuad);
53
+ element.style.transform = `translate(-50%, calc(-50% + ${rise}px))`;
54
+ element.style.opacity = String(tweenValue(1, 0, elapsed, SCORE_POP_MS, easeInQuad));
55
+ if (tweenProgress(elapsed, SCORE_POP_MS) < 1) {
56
+ frame = requestAnimationFrame(step);
57
+ }
58
+ else {
59
+ onDoneRef.current();
60
+ }
61
+ };
62
+ frame = requestAnimationFrame(step);
63
+ return () => cancelAnimationFrame(frame);
64
+ }, []);
65
+ return (_jsxs(_Fragment, { children: [_jsx("style", { children: SCORE_POP_CSS }), _jsx("span", { ref: ref, className: "fgs-score-pop", style: { left: x, top: y, transform: 'translate(-50%, -50%)' }, children: `+${points}` })] }));
66
+ }
67
+ //# sourceMappingURL=ScorePopup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScorePopup.js","sourceRoot":"","sources":["../../src/feel/ScorePopup.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEvD,gDAAgD;AAChD,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAC;AAEhC,qDAAqD;AACrD,MAAM,OAAO,GAAG,EAAE,CAAC;AAEnB;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;CAa5B,CAAC;AAaF;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAmB;IAClE,MAAM,GAAG,GAAG,MAAM,CAAkB,IAAI,CAAC,CAAC;IAC1C,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAEjC,SAAS,CAAC,GAAG,EAAE;QACb,SAAS,CAAC,OAAO,GAAG,MAAM,CAAC;IAC7B,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;QAC5B,IAAI,CAAC,OAAO;YAAE,OAAO;QAErB,IAAI,oBAAoB,EAAE,EAAE,CAAC;YAC3B,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC;YAC/D,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QAChC,CAAC;QAED,IAAI,OAAO,GAAkB,IAAI,CAAC;QAClC,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,EAAE;YAC3B,OAAO,KAAK,GAAG,CAAC;YAChB,MAAM,OAAO,GAAG,GAAG,GAAG,OAAO,CAAC;YAC9B,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC;YACzE,OAAO,CAAC,KAAK,CAAC,SAAS,GAAG,+BAA+B,IAAI,MAAM,CAAC;YACpE,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC,CAAC;YACpF,IAAI,aAAa,CAAC,OAAO,EAAE,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC7C,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;YACtC,CAAC;iBAAM,CAAC;gBACN,SAAS,CAAC,OAAO,EAAE,CAAC;YACtB,CAAC;QACH,CAAC,CAAC;QACF,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;QACpC,OAAO,GAAG,EAAE,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC;IAC3C,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,CACL,8BACE,0BAAQ,aAAa,GAAS,EAC9B,eACE,GAAG,EAAE,GAAG,EACR,SAAS,EAAC,eAAe,EACzB,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,SAAS,EAAE,uBAAuB,EAAE,YAE7D,IAAI,MAAM,EAAE,GACR,IACN,CACJ,CAAC;AACJ,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Easing curves. Each maps progress t in [0, 1] to an eased progress, with
3
+ * 0 → 0 and 1 → 1. Pure functions, so they can be used anywhere and tree-shaken.
4
+ */
5
+ export type Easing = (t: number) => number;
6
+ export declare const linear: Easing;
7
+ export declare const easeInQuad: Easing;
8
+ /** Starts fast and settles. The default for entrances and moves. */
9
+ export declare const easeOutQuad: Easing;
10
+ export declare const easeOutCubic: Easing;
11
+ export declare const easeInOutQuad: Easing;
12
+ /** Overshoots its target and settles back, a light spring. Use it for pops, not for long moves. */
13
+ export declare function easeOutBack(overshoot?: number): Easing;
14
+ //# sourceMappingURL=easing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"easing.d.ts","sourceRoot":"","sources":["../../src/feel/easing.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;AAE3C,eAAO,MAAM,MAAM,EAAE,MAAiB,CAAC;AAEvC,eAAO,MAAM,UAAU,EAAE,MAAqB,CAAC;AAE/C,oEAAoE;AACpE,eAAO,MAAM,WAAW,EAAE,MAAqC,CAAC;AAEhE,eAAO,MAAM,YAAY,EAAE,MAAgC,CAAC;AAE5D,eAAO,MAAM,aAAa,EAAE,MAAiE,CAAC;AAE9F,mGAAmG;AACnG,wBAAgB,WAAW,CAAC,SAAS,SAAU,GAAG,MAAM,CAGvD"}
@@ -0,0 +1,12 @@
1
+ export const linear = (t) => t;
2
+ export const easeInQuad = (t) => t * t;
3
+ /** Starts fast and settles. The default for entrances and moves. */
4
+ export const easeOutQuad = (t) => 1 - (1 - t) * (1 - t);
5
+ export const easeOutCubic = (t) => 1 - (1 - t) ** 3;
6
+ export const easeInOutQuad = (t) => (t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2);
7
+ /** Overshoots its target and settles back, a light spring. Use it for pops, not for long moves. */
8
+ export function easeOutBack(overshoot = 1.70158) {
9
+ const c3 = overshoot + 1;
10
+ return (t) => 1 + c3 * (t - 1) ** 3 + overshoot * (t - 1) ** 2;
11
+ }
12
+ //# sourceMappingURL=easing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"easing.js","sourceRoot":"","sources":["../../src/feel/easing.ts"],"names":[],"mappings":"AAMA,MAAM,CAAC,MAAM,MAAM,GAAW,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAEvC,MAAM,CAAC,MAAM,UAAU,GAAW,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;AAE/C,oEAAoE;AACpE,MAAM,CAAC,MAAM,WAAW,GAAW,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAEhE,MAAM,CAAC,MAAM,YAAY,GAAW,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;AAE5D,MAAM,CAAC,MAAM,aAAa,GAAW,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE9F,mGAAmG;AACnG,MAAM,UAAU,WAAW,CAAC,SAAS,GAAG,OAAO;IAC7C,MAAM,EAAE,GAAG,SAAS,GAAG,CAAC,CAAC;IACzB,OAAO,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;AACjE,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Haptic pulses through the Vibration API. Pure except for the one `vibrate` call,
3
+ * so the patterns can be checked on their own.
4
+ */
5
+ /** Milliseconds to vibrate, or alternating vibrate and pause durations. */
6
+ export type VibrationPattern = number | readonly number[];
7
+ /** Named pulses for common game events. Each stays short so it reads as feedback, not buzzing. */
8
+ export declare const HAPTICS: {
9
+ /** A match clears: a crisp double tick. */
10
+ readonly clear: readonly [12, 40, 18];
11
+ /** The game ends: a heavier, slower pulse. */
12
+ readonly gameOver: readonly [60, 80, 120];
13
+ };
14
+ /**
15
+ * Ask the device to vibrate. Returns false when the Vibration API is missing, which
16
+ * covers desktop browsers and iOS Safari. Nothing else happens in that case.
17
+ */
18
+ export declare function vibrate(pattern: VibrationPattern, nav?: Pick<Navigator, 'vibrate'> | undefined): boolean;
19
+ //# sourceMappingURL=haptics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"haptics.d.ts","sourceRoot":"","sources":["../../src/feel/haptics.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,2EAA2E;AAC3E,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;AAE1D,kGAAkG;AAClG,eAAO,MAAM,OAAO;IAClB,2CAA2C;;IAE3C,8CAA8C;;CAEK,CAAC;AAEtD;;;GAGG;AACH,wBAAgB,OAAO,CACrB,OAAO,EAAE,gBAAgB,EACzB,GAAG,GAAE,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,GAAG,SAA6B,GAC9D,OAAO,CAGT"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Haptic pulses through the Vibration API. Pure except for the one `vibrate` call,
3
+ * so the patterns can be checked on their own.
4
+ */
5
+ /** Named pulses for common game events. Each stays short so it reads as feedback, not buzzing. */
6
+ export const HAPTICS = {
7
+ /** A match clears: a crisp double tick. */
8
+ clear: [12, 40, 18],
9
+ /** The game ends: a heavier, slower pulse. */
10
+ gameOver: [60, 80, 120],
11
+ };
12
+ /**
13
+ * Ask the device to vibrate. Returns false when the Vibration API is missing, which
14
+ * covers desktop browsers and iOS Safari. Nothing else happens in that case.
15
+ */
16
+ export function vibrate(pattern, nav = globalNavigator()) {
17
+ if (typeof nav?.vibrate !== 'function')
18
+ return false;
19
+ return nav.vibrate(pattern);
20
+ }
21
+ function globalNavigator() {
22
+ return typeof navigator === 'undefined' ? undefined : navigator;
23
+ }
24
+ //# sourceMappingURL=haptics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"haptics.js","sourceRoot":"","sources":["../../src/feel/haptics.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,kGAAkG;AAClG,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,2CAA2C;IAC3C,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;IACnB,8CAA8C;IAC9C,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC;CAC4B,CAAC;AAEtD;;;GAGG;AACH,MAAM,UAAU,OAAO,CACrB,OAAyB,EACzB,MAA8C,eAAe,EAAE;IAE/D,IAAI,OAAO,GAAG,EAAE,OAAO,KAAK,UAAU;QAAE,OAAO,KAAK,CAAC;IACrD,OAAO,GAAG,CAAC,OAAO,CAAC,OAA4B,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,eAAe;IACtB,OAAO,OAAO,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;AAClE,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The feel kit: particles, tweens and easing, shake, haptics and score pop-ups.
3
+ * Every export is a separate module with no side effects on import, so a bundler
4
+ * keeps only what a game uses. Import it from `@freegamestore/games/feel`, or
5
+ * from the package root.
6
+ */
7
+ export { type Easing, easeInOutQuad, easeInQuad, easeOutBack, easeOutCubic, easeOutQuad, linear, } from './easing.js';
8
+ export { HAPTICS, type VibrationPattern, vibrate } from './haptics.js';
9
+ export { type BurstOptions, createBurst, GRAVITY, MAX_PARTICLES, type Particle, particleAlpha, stepParticles, } from './particles.js';
10
+ export { prefersReducedMotion } from './reducedMotion.js';
11
+ export { SCORE_POP_CSS, SCORE_POP_MS, ScorePopup, type ScorePopupProps } from './ScorePopup.js';
12
+ export { shakeKeyframes } from './shake.js';
13
+ export { tweenProgress, tweenValue } from './tween.js';
14
+ export { type Haptics, useHaptics } from './useHaptics.js';
15
+ export { type ParticleEmitter, useParticles } from './useParticles.js';
16
+ export { type ShakeOptions, useShake } from './useShake.js';
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/feel/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,KAAK,MAAM,EACX,aAAa,EACb,UAAU,EACV,WAAW,EACX,YAAY,EACZ,WAAW,EACX,MAAM,GACP,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,OAAO,EAAE,KAAK,gBAAgB,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EACL,KAAK,YAAY,EACjB,WAAW,EACX,OAAO,EACP,aAAa,EACb,KAAK,QAAQ,EACb,aAAa,EACb,aAAa,GACd,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAChG,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,KAAK,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,KAAK,eAAe,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,KAAK,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The feel kit: particles, tweens and easing, shake, haptics and score pop-ups.
3
+ * Every export is a separate module with no side effects on import, so a bundler
4
+ * keeps only what a game uses. Import it from `@freegamestore/games/feel`, or
5
+ * from the package root.
6
+ */
7
+ export { easeInOutQuad, easeInQuad, easeOutBack, easeOutCubic, easeOutQuad, linear, } from './easing.js';
8
+ export { HAPTICS, vibrate } from './haptics.js';
9
+ export { createBurst, GRAVITY, MAX_PARTICLES, particleAlpha, stepParticles, } from './particles.js';
10
+ export { prefersReducedMotion } from './reducedMotion.js';
11
+ export { SCORE_POP_CSS, SCORE_POP_MS, ScorePopup } from './ScorePopup.js';
12
+ export { shakeKeyframes } from './shake.js';
13
+ export { tweenProgress, tweenValue } from './tween.js';
14
+ export { useHaptics } from './useHaptics.js';
15
+ export { useParticles } from './useParticles.js';
16
+ export { useShake } from './useShake.js';
17
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/feel/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAEL,aAAa,EACb,UAAU,EACV,WAAW,EACX,YAAY,EACZ,WAAW,EACX,MAAM,GACP,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,OAAO,EAAyB,OAAO,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAEL,WAAW,EACX,OAAO,EACP,aAAa,EAEb,aAAa,EACb,aAAa,GACd,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAwB,MAAM,iBAAiB,CAAC;AAChG,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAgB,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAwB,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAqB,QAAQ,EAAE,MAAM,eAAe,CAAC"}