secret-sequence-react 1.0.0 → 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Diego Alonso
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,249 +1,250 @@
1
- <div align="center">
2
-
3
- # Secret Sequence React
4
-
5
- [![en](https://img.shields.io/badge/lang-en-red.svg)](./README.md)
6
- [![es](https://img.shields.io/badge/lang-es-yellow.svg)](./README_ES.md)
7
-
8
- ![GitHub stars](https://img.shields.io/github/stars/Diego0Alonso/secret-sequence)
9
- ![GitHub forks](https://img.shields.io/github/forks/Diego0Alonso/secret-sequence)
10
- ![GitHub issues](https://img.shields.io/github/issues/Diego0Alonso/secret-sequence)
11
- [![GitHub Sponsors](https://img.shields.io/badge/Sponsor-GitHub-pink?logo=github)](https://github.com/sponsors/Diego0Alonso)
12
-
13
- ![React](https://img.shields.io/badge/React-≥17-61DAFB?style=for-the-badge&logo=react)
14
- ![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue?style=for-the-badge&logo=typescript)
15
-
16
- > React hook & component for detecting directional sequences, key combos, and touch gestures — powered by [secret-sequence-core](../core/README.md).
17
-
18
- This package provides **React bindings** for the [Secret Sequence](../../README.md) monorepo.
19
- It wraps the core engine in a hook and a declarative component with full lifecycle management.
20
-
21
- </div>
22
-
23
- ---
24
-
25
- ## Installation
26
-
27
- ```bash
28
- npm install secret-sequence-react secret-sequence-core
29
- ````
30
-
31
- > `secret-sequence-core` is a peer dependency.
32
-
33
- ---
34
-
35
- ## Quick Start
36
-
37
- ### Hook — `useSecretSequence`
38
-
39
- ```tsx
40
- import { useSecretSequence } from "secret-sequence-react"
41
-
42
- function App() {
43
- const { progress } = useSecretSequence({
44
- sequences: [
45
- {
46
- id: "konami",
47
- sequence: ["up", "up", "down", "down", "left", "right", "left", "right"],
48
- onSuccess: () => alert("🎉 Konami Code activated!"),
49
- },
50
- ],
51
- enableTouch: true,
52
- touchOptions: { minDistance: 50, maxTime: 400 },
53
- })
54
-
55
- return <pre>{JSON.stringify(progress, null, 2)}</pre>
56
- }
57
- ```
58
-
59
- ---
60
-
61
- ### Component — `<SecretSequence />`
62
-
63
- #### Invisible Mode (effect only)
64
-
65
- ```tsx
66
- import { SecretSequence } from "secret-sequence-react"
67
-
68
- function App() {
69
- return (
70
- <SecretSequence
71
- sequences={[
72
- {
73
- id: "konami",
74
- sequence: ["up", "up", "down", "down", "left", "right", "left", "right"],
75
- onSuccess: () => console.log("🎉"),
76
- },
77
- ]}
78
- enableTouch={true}
79
- touchOptions={{ minDistance: 40 }}
80
- />
81
- )
82
- }
83
- ```
84
-
85
- #### With render prop
86
-
87
- ```tsx
88
- <SecretSequence
89
- sequences={[{ id: "code", sequence: ["up", "down"], onSuccess: fn }]}
90
- >
91
- {({ progress, reset }) => (
92
- <div>
93
- <p>Progress: {JSON.stringify(progress)}</p>
94
- <button onClick={reset}>Reset</button>
95
- </div>
96
- )}
97
- </SecretSequence>
98
- ```
99
-
100
- ---
101
-
102
- ### Stratagem-Style Input
103
-
104
- ```tsx
105
- const { progress } = useSecretSequence({
106
- sequences: [
107
- {
108
- id: "orbitalStrike",
109
- sequence: ["right", "right", "up"],
110
- onSuccess: () => deployStrike(),
111
- },
112
- ],
113
- timeout: 2000,
114
- })
115
- ```
116
-
117
- ---
118
-
119
- ### Key Combo Shortcut
120
-
121
- ```tsx
122
- const { progress } = useSecretSequence({
123
- sequences: [
124
- {
125
- id: "shortcut",
126
- sequence: [{ key: "k", ctrl: true }],
127
- onSuccess: () => console.log("Shortcut triggered"),
128
- },
129
- ],
130
- })
131
- ```
132
-
133
- ---
134
-
135
- ### Touch Gesture Support
136
-
137
- Swipe gestures on touch devices are automatically mapped to directional steps.
138
-
139
- ```tsx
140
- const { progress } = useSecretSequence({
141
- sequences: [
142
- {
143
- id: "swipe-pattern",
144
- sequence: ["up", "down", "left", "right"],
145
- onSuccess: () => alert("Swipe pattern detected!"),
146
- },
147
- ],
148
- enableTouch: true,
149
- touchOptions: {
150
- minDistance: 30,
151
- maxTime: 300,
152
- threshold: 1.5,
153
- },
154
- })
155
- ```
156
-
157
- ---
158
-
159
- ## API
160
-
161
- ### `useSecretSequence(options)`
162
-
163
- React hook that manages the engine lifecycle automatically.
164
-
165
- **Returns:** `{ progress, reset }`
166
-
167
- | Return | Type | Description |
168
- | ---------- | -------------------------- | ----------------------------- |
169
- | `progress` | `Record<string, number>` | Current progress per sequence |
170
- | `reset` | `() => void` | Reset all sequence progress |
171
-
172
- ---
173
-
174
- ### `<SecretSequence />` Component
175
-
176
- Declarative JSX wrapper over the `useSecretSequence` hook.
177
-
178
- | Prop | Type | Description |
179
- | ------------------ | ----------------------------------------------------------------------- | ---------------------------------------- |
180
- | `onProgressChange` | `(progress: Record<string, number>) => void` | Callback fired on progress changes |
181
- | `children` | `(state: { progress: Record<string, number>; reset: () => void }) => …` | Optional render prop for progress/reset |
182
- | *...hookOptions* | `UseSecretSequenceOptions` | All hook options (see below) |
183
-
184
- ---
185
-
186
- ### Options (shared by hook & component)
187
-
188
- | Option | Type | Default | Description |
189
- | -------------- | ------------------------ | ------- | ------------------------------------------------------ |
190
- | `sequences` | `SecretSequenceConfig[]` | — | Array of sequences to detect simultaneously |
191
- | `timeout` | `number` | `2000` | Milliseconds of inactivity before resetting progress |
192
- | `enabled` | `boolean` | `true` | Globally enable or disable detection |
193
- | `enableTouch` | `boolean` | `true` | Enable swipe gesture detection |
194
- | `ignoreInputs` | `boolean` | `true` | Ignore key events when focus is on input-like elements |
195
- | `touchOptions` | `TouchConfig` | — | Advanced touch configuration |
196
-
197
- ---
198
-
199
- ## Configuration Types
200
-
201
- ### `SecretSequenceConfig`
202
-
203
- | Property | Type | Description |
204
- | ----------- | --------------------- | ------------------------------------------- |
205
- | `id` | `string` *(optional)* | Unique identifier (defaults to array index) |
206
- | `sequence` | `SequenceStep[]` | Ordered steps to detect |
207
- | `onSuccess` | `() => void` | Fired when the sequence completes |
208
-
209
- ---
210
-
211
- ### Core Types
212
-
213
- ```ts
214
- type Direction = "up" | "down" | "left" | "right"
215
-
216
- type KeyCombo = {
217
- key: string
218
- ctrl?: boolean
219
- shift?: boolean
220
- alt?: boolean
221
- meta?: boolean
222
- }
223
-
224
- type SequenceStep = Direction | KeyCombo
225
- ```
226
-
227
- ---
228
-
229
- ### `TouchConfig`
230
-
231
- | Property | Type | Default | Description |
232
- | ------------- | -------- | ------- | ---------------------------------------------- |
233
- | `minDistance` | `number` | `30` | Minimum swipe distance (px) |
234
- | `maxTime` | `number` | `300` | Maximum swipe duration (ms) |
235
- | `threshold` | `number` | `1.5` | Axis dominance ratio to reject diagonal swipes |
236
-
237
- ---
238
-
239
- ## SSR Compatibility
240
-
241
- This package is safe to use in SSR environments such as Next.js or Remix.
242
-
243
- The underlying engine guards against accessing `window` during server rendering and only attaches event listeners in the browser.
244
-
245
- ---
246
-
247
- ## License
248
-
249
- MIT © Diego Alonso
1
+ <div align="center">
2
+
3
+ # Secret Sequence React
4
+
5
+ [![en](https://img.shields.io/badge/lang-en-red.svg)](./README.md)
6
+ [![es](https://img.shields.io/badge/lang-es-yellow.svg)](./README_ES.md)
7
+
8
+ ![GitHub stars](https://img.shields.io/github/stars/Diego0Alonso/secret-sequence)
9
+ ![GitHub forks](https://img.shields.io/github/forks/Diego0Alonso/secret-sequence)
10
+ ![GitHub issues](https://img.shields.io/github/issues/Diego0Alonso/secret-sequence)
11
+ [![GitHub Sponsors](https://img.shields.io/badge/Sponsor-GitHub-pink?logo=github)](https://github.com/sponsors/Diego0Alonso)
12
+
13
+ ![React](https://img.shields.io/badge/React-≥18-61DAFB?style=for-the-badge&logo=react)
14
+ ![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue?style=for-the-badge&logo=typescript)
15
+
16
+ > React hook & component for detecting directional sequences, key combos, and touch gestures — powered by [secret-sequence-core](../core/README.md).
17
+
18
+ This package provides **React bindings** for the [Secret Sequence](../../README.md) monorepo.
19
+ It wraps the core engine in a hook and a declarative component with full lifecycle management.
20
+
21
+ </div>
22
+
23
+ ---
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ npm install secret-sequence-react secret-sequence-core
29
+ ````
30
+
31
+ > `secret-sequence-core` is a peer dependency.
32
+
33
+ ---
34
+
35
+ ## Quick Start
36
+
37
+ ### Hook — `useSecretSequence`
38
+
39
+ ```tsx
40
+ import { useSecretSequence } from "secret-sequence-react"
41
+
42
+ function App() {
43
+ const { progress } = useSecretSequence({
44
+ sequences: [
45
+ {
46
+ id: "konami",
47
+ sequence: ["up", "up", "down", "down", "left", "right", "left", "right"],
48
+ onSuccess: () => alert("🎉 Konami Code activated!"),
49
+ },
50
+ ],
51
+ enableTouch: true,
52
+ touchOptions: { minDistance: 50, maxTime: 400 },
53
+ })
54
+
55
+ return <pre>{JSON.stringify(progress, null, 2)}</pre>
56
+ }
57
+ ```
58
+
59
+ ---
60
+
61
+ ### Component — `<SecretSequence />`
62
+
63
+ #### Invisible Mode (effect only)
64
+
65
+ ```tsx
66
+ import { SecretSequence } from "secret-sequence-react"
67
+
68
+ function App() {
69
+ return (
70
+ <SecretSequence
71
+ sequences={[
72
+ {
73
+ id: "konami",
74
+ sequence: ["up", "up", "down", "down", "left", "right", "left", "right"],
75
+ onSuccess: () => console.log("🎉"),
76
+ },
77
+ ]}
78
+ enableTouch={true}
79
+ touchOptions={{ minDistance: 40 }}
80
+ />
81
+ )
82
+ }
83
+ ```
84
+
85
+ #### With render prop
86
+
87
+ ```tsx
88
+ <SecretSequence
89
+ sequences={[{ id: "code", sequence: ["up", "down"], onSuccess: fn }]}
90
+ >
91
+ {({ progress, reset }) => (
92
+ <div>
93
+ <p>Progress: {JSON.stringify(progress)}</p>
94
+ <button onClick={reset}>Reset</button>
95
+ </div>
96
+ )}
97
+ </SecretSequence>
98
+ ```
99
+
100
+ ---
101
+
102
+ ### Stratagem-Style Input
103
+
104
+ ```tsx
105
+ const { progress } = useSecretSequence({
106
+ sequences: [
107
+ {
108
+ id: "orbitalStrike",
109
+ sequence: ["right", "right", "up"],
110
+ onSuccess: () => deployStrike(),
111
+ },
112
+ ],
113
+ timeout: 2000,
114
+ })
115
+ ```
116
+
117
+ ---
118
+
119
+ ### Key Combo Shortcut
120
+
121
+ ```tsx
122
+ const { progress } = useSecretSequence({
123
+ sequences: [
124
+ {
125
+ id: "shortcut",
126
+ sequence: [{ key: "k", ctrl: true }],
127
+ onSuccess: () => console.log("Shortcut triggered"),
128
+ },
129
+ ],
130
+ })
131
+ ```
132
+
133
+ ---
134
+
135
+ ### Touch Gesture Support
136
+
137
+ Swipe gestures on touch devices are automatically mapped to directional steps.
138
+
139
+ ```tsx
140
+ const { progress } = useSecretSequence({
141
+ sequences: [
142
+ {
143
+ id: "swipe-pattern",
144
+ sequence: ["up", "down", "left", "right"],
145
+ onSuccess: () => alert("Swipe pattern detected!"),
146
+ },
147
+ ],
148
+ enableTouch: true,
149
+ touchOptions: {
150
+ minDistance: 30,
151
+ maxTime: 300,
152
+ threshold: 1.5,
153
+ },
154
+ })
155
+ ```
156
+
157
+ ---
158
+
159
+ ## API
160
+
161
+ ### `useSecretSequence(options)`
162
+
163
+ React hook that manages the engine lifecycle automatically.
164
+
165
+ **Returns:** `{ progress, reset }`
166
+
167
+ | Return | Type | Description |
168
+ | ---------- | -------------------------- | ----------------------------- |
169
+ | `progress` | `Record<string, number>` | Current progress per sequence |
170
+ | `reset` | `() => void` | Reset all sequence progress |
171
+
172
+ ---
173
+
174
+ ### `<SecretSequence />` Component
175
+
176
+ Declarative JSX wrapper over the `useSecretSequence` hook.
177
+
178
+ | Prop | Type | Description |
179
+ | ------------------ | ----------------------------------------------------------------------- | ---------------------------------------- |
180
+ | `onProgressChange` | `(progress: Record<string, number>) => void` | Callback fired on progress changes |
181
+ | `children` | `(state: { progress: Record<string, number>; reset: () => void }) => …` | Optional render prop for progress/reset |
182
+ | *...hookOptions* | `UseSecretSequenceOptions` | All hook options (see below) |
183
+
184
+ ---
185
+
186
+ ### Options (shared by hook & component)
187
+
188
+ | Option | Type | Default | Description |
189
+ | -------------- | ------------------------ | ------- | ------------------------------------------------------ |
190
+ | `sequences` | `SecretSequenceConfig[]` | — | Array of sequences to detect simultaneously |
191
+ | `timeout` | `number` | `2000` | Milliseconds of inactivity before resetting progress |
192
+ | `enabled` | `boolean` | `true` | Globally enable or disable detection |
193
+ | `enableTouch` | `boolean` | `true` | Enable swipe gesture detection |
194
+ | `ignoreInputs` | `boolean` | `true` | Ignore key events when focus is on input-like elements |
195
+ | `touchOptions` | `TouchConfig` | — | Advanced touch configuration |
196
+ | `target` | `EventTarget` | `window` | Element to attach listeners to (e.g. a scoped container) |
197
+
198
+ ---
199
+
200
+ ## Configuration Types
201
+
202
+ ### `SecretSequenceConfig`
203
+
204
+ | Property | Type | Description |
205
+ | ----------- | --------------------- | ------------------------------------------- |
206
+ | `id` | `string` *(optional)* | Unique identifier (defaults to array index) |
207
+ | `sequence` | `SequenceStep[]` | Ordered steps to detect |
208
+ | `onSuccess` | `() => void` | Fired when the sequence completes |
209
+
210
+ ---
211
+
212
+ ### Core Types
213
+
214
+ ```ts
215
+ type Direction = "up" | "down" | "left" | "right"
216
+
217
+ type KeyCombo = {
218
+ key: string
219
+ ctrl?: boolean
220
+ shift?: boolean
221
+ alt?: boolean
222
+ meta?: boolean
223
+ }
224
+
225
+ type SequenceStep = Direction | KeyCombo
226
+ ```
227
+
228
+ ---
229
+
230
+ ### `TouchConfig`
231
+
232
+ | Property | Type | Default | Description |
233
+ | ------------- | -------- | ------- | ---------------------------------------------- |
234
+ | `minDistance` | `number` | `30` | Minimum swipe distance (px) |
235
+ | `maxTime` | `number` | `300` | Maximum swipe duration (ms) |
236
+ | `threshold` | `number` | `1.5` | Axis dominance ratio to reject diagonal swipes |
237
+
238
+ ---
239
+
240
+ ## SSR Compatibility
241
+
242
+ This package is safe to use in SSR environments such as Next.js or Remix.
243
+
244
+ The underlying engine guards against accessing `window` during server rendering and only attaches event listeners in the browser.
245
+
246
+ ---
247
+
248
+ ## License
249
+
250
+ MIT © Diego Alonso
package/dist/index.d.mts CHANGED
@@ -18,6 +18,8 @@ interface UseSecretSequenceOptions {
18
18
  ignoreInputs?: boolean;
19
19
  /** Configuración de sensibilidad para gestos táctiles */
20
20
  touchOptions?: TouchConfig;
21
+ /** Elemento al que adjuntar los listeners. Default: `window` */
22
+ target?: EventTarget;
21
23
  }
22
24
  /**
23
25
  * Valor retornado por el hook useSecretSequence.
package/dist/index.d.ts CHANGED
@@ -18,6 +18,8 @@ interface UseSecretSequenceOptions {
18
18
  ignoreInputs?: boolean;
19
19
  /** Configuración de sensibilidad para gestos táctiles */
20
20
  touchOptions?: TouchConfig;
21
+ /** Elemento al que adjuntar los listeners. Default: `window` */
22
+ target?: EventTarget;
21
23
  }
22
24
  /**
23
25
  * Valor retornado por el hook useSecretSequence.
package/dist/index.js CHANGED
@@ -28,6 +28,14 @@ module.exports = __toCommonJS(index_exports);
28
28
  // src/useSecretSequence.ts
29
29
  var import_react = require("react");
30
30
  var import_secret_sequence_core = require("secret-sequence-core");
31
+ function isSameProgress(a, b) {
32
+ const aKeys = Object.keys(a);
33
+ if (aKeys.length !== Object.keys(b).length) return false;
34
+ for (const key of aKeys) {
35
+ if (a[key] !== b[key]) return false;
36
+ }
37
+ return true;
38
+ }
31
39
  function useSecretSequence(options) {
32
40
  const {
33
41
  sequences,
@@ -35,7 +43,8 @@ function useSecretSequence(options) {
35
43
  enabled = true,
36
44
  enableTouch = true,
37
45
  ignoreInputs = true,
38
- touchOptions
46
+ touchOptions,
47
+ target
39
48
  } = options;
40
49
  const [progress, setProgress] = (0, import_react.useState)({});
41
50
  const engineRef = (0, import_react.useRef)(null);
@@ -47,19 +56,33 @@ function useSecretSequence(options) {
47
56
  sequences.map(({ onSuccess, ...rest }) => rest)
48
57
  );
49
58
  const touchOptionsKey = JSON.stringify(touchOptions);
59
+ const stableSequences = (0, import_react.useMemo)(
60
+ () => sequencesRef.current.map((seq, i) => ({
61
+ ...seq,
62
+ onSuccess: () => sequencesRef.current[i]?.onSuccess()
63
+ })),
64
+ // Se recrea solo cuando cambian teclas/id/orden/cantidad, no el callback.
65
+ // eslint-disable-next-line react-hooks/exhaustive-deps
66
+ [sequencesKey]
67
+ );
68
+ const stableSequencesRef = (0, import_react.useRef)(stableSequences);
69
+ stableSequencesRef.current = stableSequences;
70
+ const syncProgress = (0, import_react.useCallback)(() => {
71
+ const engine = engineRef.current;
72
+ if (!engine) return;
73
+ const next = engine.getProgressMap();
74
+ setProgress((prev) => isSameProgress(prev, next) ? prev : next);
75
+ }, []);
50
76
  (0, import_react.useEffect)(() => {
51
77
  const engineOptions = {
52
- sequences: sequencesRef.current,
78
+ sequences: stableSequencesRef.current,
53
79
  timeout,
54
80
  enabled,
55
81
  enableTouch,
56
82
  ignoreInputs,
57
83
  touchOptions: touchOptionsRef.current,
58
- onProgress: (_id, _step) => {
59
- if (engineRef.current) {
60
- setProgress(engineRef.current.getProgressMap());
61
- }
62
- }
84
+ target,
85
+ onProgress: syncProgress
63
86
  };
64
87
  const engine = new import_secret_sequence_core.SecretSequenceEngine(engineOptions);
65
88
  engineRef.current = engine;
@@ -69,20 +92,16 @@ function useSecretSequence(options) {
69
92
  engine.destroy();
70
93
  engineRef.current = null;
71
94
  };
72
- }, [timeout, enabled, enableTouch, ignoreInputs]);
95
+ }, [timeout, enabled, enableTouch, ignoreInputs, target, syncProgress]);
73
96
  (0, import_react.useEffect)(() => {
74
97
  if (!engineRef.current) return;
75
98
  engineRef.current.setOptions({
76
- sequences: sequencesRef.current,
99
+ sequences: stableSequencesRef.current,
77
100
  touchOptions: touchOptionsRef.current,
78
- onProgress: () => {
79
- if (engineRef.current) {
80
- setProgress(engineRef.current.getProgressMap());
81
- }
82
- }
101
+ onProgress: syncProgress
83
102
  });
84
103
  setProgress(engineRef.current.getProgressMap());
85
- }, [sequencesKey, touchOptionsKey]);
104
+ }, [sequencesKey, touchOptionsKey, syncProgress]);
86
105
  const reset = (0, import_react.useCallback)(() => {
87
106
  if (engineRef.current) {
88
107
  engineRef.current.reset();
package/dist/index.mjs CHANGED
@@ -1,8 +1,16 @@
1
1
  // src/useSecretSequence.ts
2
- import { useEffect, useRef, useState, useCallback } from "react";
2
+ import { useEffect, useMemo, useRef, useState, useCallback } from "react";
3
3
  import {
4
4
  SecretSequenceEngine
5
5
  } from "secret-sequence-core";
6
+ function isSameProgress(a, b) {
7
+ const aKeys = Object.keys(a);
8
+ if (aKeys.length !== Object.keys(b).length) return false;
9
+ for (const key of aKeys) {
10
+ if (a[key] !== b[key]) return false;
11
+ }
12
+ return true;
13
+ }
6
14
  function useSecretSequence(options) {
7
15
  const {
8
16
  sequences,
@@ -10,7 +18,8 @@ function useSecretSequence(options) {
10
18
  enabled = true,
11
19
  enableTouch = true,
12
20
  ignoreInputs = true,
13
- touchOptions
21
+ touchOptions,
22
+ target
14
23
  } = options;
15
24
  const [progress, setProgress] = useState({});
16
25
  const engineRef = useRef(null);
@@ -22,19 +31,33 @@ function useSecretSequence(options) {
22
31
  sequences.map(({ onSuccess, ...rest }) => rest)
23
32
  );
24
33
  const touchOptionsKey = JSON.stringify(touchOptions);
34
+ const stableSequences = useMemo(
35
+ () => sequencesRef.current.map((seq, i) => ({
36
+ ...seq,
37
+ onSuccess: () => sequencesRef.current[i]?.onSuccess()
38
+ })),
39
+ // Se recrea solo cuando cambian teclas/id/orden/cantidad, no el callback.
40
+ // eslint-disable-next-line react-hooks/exhaustive-deps
41
+ [sequencesKey]
42
+ );
43
+ const stableSequencesRef = useRef(stableSequences);
44
+ stableSequencesRef.current = stableSequences;
45
+ const syncProgress = useCallback(() => {
46
+ const engine = engineRef.current;
47
+ if (!engine) return;
48
+ const next = engine.getProgressMap();
49
+ setProgress((prev) => isSameProgress(prev, next) ? prev : next);
50
+ }, []);
25
51
  useEffect(() => {
26
52
  const engineOptions = {
27
- sequences: sequencesRef.current,
53
+ sequences: stableSequencesRef.current,
28
54
  timeout,
29
55
  enabled,
30
56
  enableTouch,
31
57
  ignoreInputs,
32
58
  touchOptions: touchOptionsRef.current,
33
- onProgress: (_id, _step) => {
34
- if (engineRef.current) {
35
- setProgress(engineRef.current.getProgressMap());
36
- }
37
- }
59
+ target,
60
+ onProgress: syncProgress
38
61
  };
39
62
  const engine = new SecretSequenceEngine(engineOptions);
40
63
  engineRef.current = engine;
@@ -44,20 +67,16 @@ function useSecretSequence(options) {
44
67
  engine.destroy();
45
68
  engineRef.current = null;
46
69
  };
47
- }, [timeout, enabled, enableTouch, ignoreInputs]);
70
+ }, [timeout, enabled, enableTouch, ignoreInputs, target, syncProgress]);
48
71
  useEffect(() => {
49
72
  if (!engineRef.current) return;
50
73
  engineRef.current.setOptions({
51
- sequences: sequencesRef.current,
74
+ sequences: stableSequencesRef.current,
52
75
  touchOptions: touchOptionsRef.current,
53
- onProgress: () => {
54
- if (engineRef.current) {
55
- setProgress(engineRef.current.getProgressMap());
56
- }
57
- }
76
+ onProgress: syncProgress
58
77
  });
59
78
  setProgress(engineRef.current.getProgressMap());
60
- }, [sequencesKey, touchOptionsKey]);
79
+ }, [sequencesKey, touchOptionsKey, syncProgress]);
61
80
  const reset = useCallback(() => {
62
81
  if (engineRef.current) {
63
82
  engineRef.current.reset();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secret-sequence-react",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "React hook & component wrapper for secret-sequence-core — detect directional sequences, key combos, and touch gestures",
5
5
  "keywords": [
6
6
  "react",
@@ -22,9 +22,14 @@
22
22
  "license": "MIT",
23
23
  "repository": {
24
24
  "type": "git",
25
- "url": "git+https://github.com/diego0alonso/secret-sequence.git",
25
+ "url": "git+https://github.com/Diego0Alonso/secret-sequence.git",
26
26
  "directory": "packages/react"
27
27
  },
28
+ "homepage": "https://github.com/Diego0Alonso/secret-sequence/tree/main/packages/react#readme",
29
+ "bugs": "https://github.com/Diego0Alonso/secret-sequence/issues",
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
28
33
  "main": "dist/index.js",
29
34
  "module": "dist/index.mjs",
30
35
  "types": "dist/index.d.ts",
@@ -39,19 +44,24 @@
39
44
  "files": [
40
45
  "dist"
41
46
  ],
42
- "scripts": {
43
- "build": "tsup",
44
- "prepublishOnly": "npm run build"
45
- },
46
47
  "peerDependencies": {
47
- "react": ">=17.0.0",
48
+ "react": ">=18.0.0",
48
49
  "secret-sequence-core": "^2.0.0"
49
50
  },
50
51
  "devDependencies": {
52
+ "@testing-library/jest-dom": "^6.9.1",
53
+ "@testing-library/react": "^16.3.2",
51
54
  "@types/react": "^19.0.0",
55
+ "jsdom": "^28.1.0",
52
56
  "react": "^19.0.0",
53
- "secret-sequence-core": "^2.0.0",
54
57
  "tsup": "^8.0.0",
55
- "typescript": "^5.0.0"
58
+ "typescript": "^5.0.0",
59
+ "vitest": "^4.0.18",
60
+ "secret-sequence-core": "^2.1.0"
61
+ },
62
+ "scripts": {
63
+ "build": "tsup",
64
+ "test": "vitest run",
65
+ "typecheck": "tsc --noEmit -p tsconfig.json"
56
66
  }
57
67
  }