@crup/react-timer-hook 0.0.1-alpha.2 โ 0.0.1-alpha.4
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 +91 -200
- package/package.json +14 -6
package/README.md
CHANGED
|
@@ -1,79 +1,71 @@
|
|
|
1
1
|
# @crup/react-timer-hook
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Deterministic React timer primitives for countdowns, stopwatches, clocks, schedules, and many independent timers.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@crup/react-timer-hook?activeTab=versions)
|
|
6
|
+
[](https://www.npmjs.com/package/@crup/react-timer-hook)
|
|
7
|
+
[](https://github.com/crup/react-timer-hook/actions/workflows/ci.yml)
|
|
8
|
+
[](https://github.com/crup/react-timer-hook/actions/workflows/docs.yml)
|
|
9
|
+
[](https://github.com/crup/react-timer-hook/actions/workflows/size.yml)
|
|
10
|
+
[](./LICENSE)
|
|
11
|
+
[](./dist/index.d.ts)
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
๐ Docs: https://crup.github.io/react-timer-hook/
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
## Why it is different
|
|
16
|
+
|
|
17
|
+
Most timer libraries mix scheduling, lifecycle, formatting, and app behavior. This package keeps the core small:
|
|
18
|
+
|
|
19
|
+
- โฑ๏ธ `useTimer()` for one lifecycle.
|
|
20
|
+
- ๐งญ `useTimerGroup()` for many keyed lifecycles with one shared scheduler.
|
|
21
|
+
- ๐งฉ `durationParts()` for display-friendly duration math.
|
|
22
|
+
- ๐งผ No timezone, locale, or formatting opinions.
|
|
23
|
+
- ๐งช Built around React Strict Mode, rerenders, async callbacks, and cleanup.
|
|
24
|
+
- ๐ค AI-friendly docs via `llms.txt`, `llms-full.txt`, and a tiny local MCP docs utility.
|
|
10
25
|
|
|
11
26
|
## Install
|
|
12
27
|
|
|
28
|
+
Alpha is the only intended release channel until stable publishing is explicitly unlocked.
|
|
29
|
+
|
|
13
30
|
```sh
|
|
14
|
-
|
|
15
|
-
|
|
31
|
+
npm install @crup/react-timer-hook@alpha
|
|
32
|
+
pnpm add @crup/react-timer-hook@alpha
|
|
16
33
|
```
|
|
17
34
|
|
|
18
|
-
## Planned Public API
|
|
19
|
-
|
|
20
35
|
```ts
|
|
21
|
-
import { useTimer, useTimerGroup
|
|
36
|
+
import { durationParts, useTimer, useTimerGroup } from '@crup/react-timer-hook';
|
|
22
37
|
```
|
|
23
38
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- `useTimer()` for one timer lifecycle.
|
|
27
|
-
- `useTimerGroup()` for many keyed independent timer lifecycles.
|
|
28
|
-
- `durationParts()` as a pure numeric helper.
|
|
29
|
-
|
|
30
|
-
## Why This Shape
|
|
31
|
-
|
|
32
|
-
Most timer libraries mix three concerns:
|
|
33
|
-
|
|
34
|
-
- scheduling
|
|
35
|
-
- lifecycle state
|
|
36
|
-
- presentation formatting
|
|
37
|
-
|
|
38
|
-
This library should only own scheduling and lifecycle mechanics.
|
|
39
|
-
|
|
40
|
-
Consumers own:
|
|
41
|
-
|
|
42
|
-
- countdown math
|
|
43
|
-
- stopwatch display
|
|
44
|
-
- clock formatting
|
|
45
|
-
- timezone and locale behavior
|
|
46
|
-
- API polling behavior
|
|
47
|
-
- audio or notification side effects
|
|
39
|
+
## Quick examples
|
|
48
40
|
|
|
49
|
-
|
|
41
|
+
### Stopwatch
|
|
50
42
|
|
|
51
43
|
```tsx
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
});
|
|
44
|
+
import { useTimer } from '@crup/react-timer-hook';
|
|
45
|
+
|
|
46
|
+
export function Stopwatch() {
|
|
47
|
+
const timer = useTimer({ updateIntervalMs: 100 });
|
|
57
48
|
|
|
58
49
|
return (
|
|
59
50
|
<>
|
|
60
|
-
<
|
|
51
|
+
<output>{Math.floor(timer.elapsedMilliseconds / 1000)}s</output>
|
|
61
52
|
<button onClick={timer.start}>Start</button>
|
|
62
53
|
<button onClick={timer.pause}>Pause</button>
|
|
63
54
|
<button onClick={timer.resume}>Resume</button>
|
|
64
55
|
<button onClick={timer.restart}>Restart</button>
|
|
65
|
-
<button onClick={() => timer.reset()}>Reset</button>
|
|
66
56
|
</>
|
|
67
57
|
);
|
|
68
58
|
}
|
|
69
59
|
```
|
|
70
60
|
|
|
71
|
-
|
|
61
|
+
### Absolute countdown
|
|
72
62
|
|
|
73
|
-
Use
|
|
63
|
+
Use `now` for wall-clock deadlines from a server, auction, reservation, or job expiry.
|
|
74
64
|
|
|
75
65
|
```tsx
|
|
76
|
-
|
|
66
|
+
import { useTimer } from '@crup/react-timer-hook';
|
|
67
|
+
|
|
68
|
+
export function AuctionTimer({ auctionId, expiresAt }: {
|
|
77
69
|
auctionId: string;
|
|
78
70
|
expiresAt: number;
|
|
79
71
|
}) {
|
|
@@ -86,152 +78,53 @@ function AuctionTimer({ auctionId, expiresAt }: {
|
|
|
86
78
|
|
|
87
79
|
const remainingMs = Math.max(0, expiresAt - timer.now);
|
|
88
80
|
|
|
89
|
-
if (timer.isEnded)
|
|
90
|
-
return <span>Auction ended</span>;
|
|
91
|
-
}
|
|
92
|
-
|
|
81
|
+
if (timer.isEnded) return <span>Auction ended</span>;
|
|
93
82
|
return <span>{Math.ceil(remainingMs / 1000)}s left</span>;
|
|
94
83
|
}
|
|
95
84
|
```
|
|
96
85
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
## Pausable Duration Countdown
|
|
86
|
+
### Polling with early cancel
|
|
100
87
|
|
|
101
|
-
|
|
88
|
+
Schedules run while the timer is active. Slow async work is skipped by default with `overlap: 'skip'`.
|
|
102
89
|
|
|
103
90
|
```tsx
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
## Clock
|
|
120
|
-
|
|
121
|
-
```tsx
|
|
122
|
-
function Clock() {
|
|
123
|
-
const timer = useTimer({
|
|
124
|
-
autoStart: true,
|
|
125
|
-
updateIntervalMs: 1000,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
return <span>{new Date(timer.now).toLocaleTimeString()}</span>;
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
The hook does not format time. Use native `Intl`, `Date`, or your preferred date library.
|
|
133
|
-
|
|
134
|
-
## Schedules and Polling
|
|
135
|
-
|
|
136
|
-
Schedules are optional side effects that run while a timer is active. They are useful for polling, audio cues, analytics pings, or other app-owned side effects.
|
|
137
|
-
|
|
138
|
-
```tsx
|
|
139
|
-
function AuctionTimer({ auctionId, expiresAt }: Props) {
|
|
140
|
-
const timer = useTimer({
|
|
141
|
-
autoStart: true,
|
|
142
|
-
updateIntervalMs: 1000,
|
|
143
|
-
endWhen: snapshot => snapshot.now >= expiresAt,
|
|
144
|
-
onEnd: () => api.closeAuction(auctionId),
|
|
145
|
-
schedules: [
|
|
146
|
-
{
|
|
147
|
-
id: 'poll-auction',
|
|
148
|
-
everyMs: 5000,
|
|
149
|
-
overlap: 'skip',
|
|
150
|
-
callback: async (_snapshot, controls) => {
|
|
151
|
-
const auction = await api.getAuction(auctionId);
|
|
152
|
-
|
|
153
|
-
if (auction.status === 'sold') {
|
|
154
|
-
controls.cancel('sold');
|
|
155
|
-
}
|
|
156
|
-
},
|
|
91
|
+
const timer = useTimer({
|
|
92
|
+
autoStart: true,
|
|
93
|
+
updateIntervalMs: 1000,
|
|
94
|
+
endWhen: snapshot => snapshot.now >= expiresAt,
|
|
95
|
+
schedules: [
|
|
96
|
+
{
|
|
97
|
+
id: 'auction-poll',
|
|
98
|
+
everyMs: 5000,
|
|
99
|
+
overlap: 'skip',
|
|
100
|
+
callback: async (_snapshot, controls) => {
|
|
101
|
+
const auction = await api.getAuction(auctionId);
|
|
102
|
+
if (auction.status === 'sold') controls.cancel('sold');
|
|
157
103
|
},
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (timer.isCancelled) {
|
|
162
|
-
return <span>Auction closed</span>;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const remainingMs = Math.max(0, expiresAt - timer.now);
|
|
166
|
-
return <span>{Math.ceil(remainingMs / 1000)}s left</span>;
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
`overlap: 'skip'` is the default because it prevents slow async callbacks from piling up.
|
|
171
|
-
|
|
172
|
-
## Many Independent Timers
|
|
173
|
-
|
|
174
|
-
For lists where each item has its own pause, resume, cancel, and end lifecycle, use `useTimerGroup`.
|
|
175
|
-
|
|
176
|
-
```tsx
|
|
177
|
-
function AuctionList({ auctions }: { auctions: Auction[] }) {
|
|
178
|
-
const timers = useTimerGroup({
|
|
179
|
-
updateIntervalMs: 1000,
|
|
180
|
-
items: auctions.map(auction => ({
|
|
181
|
-
id: auction.id,
|
|
182
|
-
autoStart: true,
|
|
183
|
-
endWhen: snapshot => snapshot.now >= auction.expiresAt,
|
|
184
|
-
onEnd: () => api.closeAuction(auction.id),
|
|
185
|
-
})),
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
return (
|
|
189
|
-
<>
|
|
190
|
-
{auctions.map(auction => {
|
|
191
|
-
const timer = timers.get(auction.id);
|
|
192
|
-
const remainingMs = Math.max(0, auction.expiresAt - (timer?.now ?? timers.now));
|
|
193
|
-
|
|
194
|
-
return (
|
|
195
|
-
<AuctionRow
|
|
196
|
-
key={auction.id}
|
|
197
|
-
auction={auction}
|
|
198
|
-
remainingMs={remainingMs}
|
|
199
|
-
isPaused={timer?.isPaused ?? false}
|
|
200
|
-
isEnded={timer?.isEnded ?? false}
|
|
201
|
-
onPause={() => timers.pause(auction.id)}
|
|
202
|
-
onResume={() => timers.resume(auction.id)}
|
|
203
|
-
onCancel={() => timers.cancel(auction.id, 'sold')}
|
|
204
|
-
/>
|
|
205
|
-
);
|
|
206
|
-
})}
|
|
207
|
-
</>
|
|
208
|
-
);
|
|
209
|
-
}
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
});
|
|
210
107
|
```
|
|
211
108
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
## Debug Logs
|
|
109
|
+
### Many independent timers
|
|
215
110
|
|
|
216
|
-
|
|
111
|
+
Use `useTimerGroup()` when every row needs its own pause, resume, cancel, restart, schedules, or `onEnd`.
|
|
217
112
|
|
|
218
113
|
```tsx
|
|
219
|
-
const
|
|
220
|
-
autoStart: true,
|
|
114
|
+
const timers = useTimerGroup({
|
|
221
115
|
updateIntervalMs: 1000,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
116
|
+
items: auctions.map(auction => ({
|
|
117
|
+
id: auction.id,
|
|
118
|
+
autoStart: true,
|
|
119
|
+
endWhen: snapshot => snapshot.now >= auction.expiresAt,
|
|
120
|
+
onEnd: () => api.closeAuction(auction.id),
|
|
121
|
+
})),
|
|
225
122
|
});
|
|
226
123
|
```
|
|
227
124
|
|
|
228
|
-
|
|
125
|
+
## Bundle size
|
|
229
126
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
## Bundle Size
|
|
233
|
-
|
|
234
|
-
Current local build size:
|
|
127
|
+
Current local build:
|
|
235
128
|
|
|
236
129
|
| File | Raw | Gzip | Brotli |
|
|
237
130
|
| --- | ---: | ---: | ---: |
|
|
@@ -239,35 +132,33 @@ Current local build size:
|
|
|
239
132
|
| `dist/index.cjs` | 29.18 kB | 5.08 kB | 4.50 kB |
|
|
240
133
|
| `dist/index.d.ts` | 3.95 kB | 992 B | 888 B |
|
|
241
134
|
|
|
242
|
-
|
|
135
|
+
CI writes a size summary to the GitHub Actions UI and posts a bundle-size comment on pull requests.
|
|
136
|
+
|
|
137
|
+
## AI-friendly
|
|
138
|
+
|
|
139
|
+
Agents can start with:
|
|
140
|
+
|
|
141
|
+
- https://crup.github.io/react-timer-hook/llms.txt
|
|
142
|
+
- https://crup.github.io/react-timer-hook/llms-full.txt
|
|
143
|
+
|
|
144
|
+
Local helpers:
|
|
243
145
|
|
|
244
146
|
```sh
|
|
245
|
-
pnpm
|
|
147
|
+
pnpm ai:context
|
|
148
|
+
pnpm mcp:docs
|
|
246
149
|
```
|
|
247
150
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
##
|
|
251
|
-
|
|
252
|
-
-
|
|
253
|
-
-
|
|
254
|
-
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
-
|
|
259
|
-
-
|
|
260
|
-
-
|
|
261
|
-
|
|
262
|
-
See:
|
|
263
|
-
|
|
264
|
-
- [Requirements](./REQUIREMENTS.md)
|
|
265
|
-
- [API Specification](./docs/API.md)
|
|
266
|
-
- [Design Decisions](./docs/DECISIONS.md)
|
|
267
|
-
- [Recipes](./docs/RECIPES.md)
|
|
268
|
-
- [Branching and Commits](./docs/BRANCHING_AND_COMMITS.md)
|
|
269
|
-
- [Implementation Plan](./docs/IMPLEMENTATION.md)
|
|
270
|
-
- [Task Plan](./docs/TASKS.md)
|
|
271
|
-
- [OSS and GTM Plan](./docs/OSS_GTM.md)
|
|
272
|
-
- [Release and Docs Plan](./docs/RELEASE_AND_DOCS.md)
|
|
273
|
-
- [Agent Task Cards](./docs/AGENT_TASKS.md)
|
|
151
|
+
The MCP utility is repo-local and excluded from the published npm package.
|
|
152
|
+
|
|
153
|
+
## Release policy
|
|
154
|
+
|
|
155
|
+
- Published versions must stay `0.0.1-alpha.x` until stable release is explicitly unlocked.
|
|
156
|
+
- `@alpha` is the documented install tag right now.
|
|
157
|
+
- Npm requires a `latest` dist-tag, so the workflow keeps `latest` pointing at the current alpha until stable publishing is unlocked.
|
|
158
|
+
|
|
159
|
+
## Links
|
|
160
|
+
|
|
161
|
+
- ๐ Docs: https://crup.github.io/react-timer-hook/
|
|
162
|
+
- ๐ฆ npm: https://www.npmjs.com/package/@crup/react-timer-hook
|
|
163
|
+
- ๐งต Issues: https://github.com/crup/react-timer-hook/issues
|
|
164
|
+
- ๐ค Contributing: ./CONTRIBUTING.md
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crup/react-timer-hook",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.4",
|
|
4
4
|
"description": "Deterministic React timer lifecycle hooks for timers, schedules, and many independent timers.",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
],
|
|
20
20
|
"sideEffects": false,
|
|
21
21
|
"publishConfig": {
|
|
22
|
-
"access": "public"
|
|
22
|
+
"access": "public",
|
|
23
|
+
"tag": "alpha"
|
|
23
24
|
},
|
|
24
25
|
"engines": {
|
|
25
26
|
"node": ">=25.0.0"
|
|
@@ -29,7 +30,10 @@
|
|
|
29
30
|
"hook",
|
|
30
31
|
"timer",
|
|
31
32
|
"stopwatch",
|
|
32
|
-
"time"
|
|
33
|
+
"time",
|
|
34
|
+
"countdown",
|
|
35
|
+
"scheduler",
|
|
36
|
+
"react-hooks"
|
|
33
37
|
],
|
|
34
38
|
"author": "Rajender Joshi <connect@rajender.pro>",
|
|
35
39
|
"license": "MIT",
|
|
@@ -38,6 +42,7 @@
|
|
|
38
42
|
"react": ">=18.0.0"
|
|
39
43
|
},
|
|
40
44
|
"devDependencies": {
|
|
45
|
+
"@astrojs/starlight": "^0.38.2",
|
|
41
46
|
"@changesets/cli": "^2.29.7",
|
|
42
47
|
"@commitlint/cli": "^20.2.0",
|
|
43
48
|
"@commitlint/config-conventional": "^20.2.0",
|
|
@@ -46,20 +51,23 @@
|
|
|
46
51
|
"@types/react": "^19.2.7",
|
|
47
52
|
"@types/react-dom": "^19.2.3",
|
|
48
53
|
"@vitejs/plugin-react": "^5.1.2",
|
|
54
|
+
"astro": "^6.1.4",
|
|
49
55
|
"husky": "^9.1.7",
|
|
50
56
|
"jsdom": "^27.3.0",
|
|
51
57
|
"react": "^19.2.3",
|
|
52
58
|
"react-dom": "^19.2.3",
|
|
53
59
|
"tsup": "^8.5.1",
|
|
54
|
-
"typedoc": "^0.28.15",
|
|
55
60
|
"typescript": "^5.9.3",
|
|
56
61
|
"vitest": "^4.0.16"
|
|
57
62
|
},
|
|
58
63
|
"scripts": {
|
|
64
|
+
"ai:context": "node scripts/ai-context.mjs",
|
|
59
65
|
"build": "tsup",
|
|
60
66
|
"changeset": "changeset",
|
|
61
|
-
"docs:
|
|
62
|
-
"docs:
|
|
67
|
+
"docs:build": "astro build",
|
|
68
|
+
"docs:dev": "astro dev",
|
|
69
|
+
"docs:preview": "astro preview",
|
|
70
|
+
"mcp:docs": "node mcp/server.mjs",
|
|
63
71
|
"readme:check": "node scripts/check-readme.mjs",
|
|
64
72
|
"release": "changeset publish",
|
|
65
73
|
"size": "node scripts/size-report.mjs",
|