@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.
Files changed (2) hide show
  1. package/README.md +91 -200
  2. package/package.json +14 -6
package/README.md CHANGED
@@ -1,79 +1,71 @@
1
1
  # @crup/react-timer-hook
2
2
 
3
- A small React hook library for deterministic timer lifecycles.
3
+ > Deterministic React timer primitives for countdowns, stopwatches, clocks, schedules, and many independent timers.
4
4
 
5
- This package is planned as a replacement for the previous `react-timer-hook` API. It intentionally does not ship formatting, timezone conversion, `ampm` fields, or countdown/stopwatch/clock mode enums. It gives you raw time data and lifecycle controls so your app can decide what the timer means.
5
+ [![npm alpha](https://img.shields.io/npm/v/%40crup%2Freact-timer-hook/alpha?label=npm%20alpha&color=00b894)](https://www.npmjs.com/package/@crup/react-timer-hook?activeTab=versions)
6
+ [![npm downloads](https://img.shields.io/npm/dm/%40crup%2Freact-timer-hook?color=0f766e)](https://www.npmjs.com/package/@crup/react-timer-hook)
7
+ [![CI](https://github.com/crup/react-timer-hook/actions/workflows/ci.yml/badge.svg)](https://github.com/crup/react-timer-hook/actions/workflows/ci.yml)
8
+ [![Docs](https://github.com/crup/react-timer-hook/actions/workflows/docs.yml/badge.svg)](https://github.com/crup/react-timer-hook/actions/workflows/docs.yml)
9
+ [![Size](https://github.com/crup/react-timer-hook/actions/workflows/size.yml/badge.svg)](https://github.com/crup/react-timer-hook/actions/workflows/size.yml)
10
+ [![license](https://img.shields.io/npm/l/%40crup%2Freact-timer-hook?color=111827)](./LICENSE)
11
+ [![types](https://img.shields.io/npm/types/%40crup%2Freact-timer-hook?color=2563eb)](./dist/index.d.ts)
6
12
 
7
- ## Status
13
+ ๐Ÿ“š Docs: https://crup.github.io/react-timer-hook/
8
14
 
9
- Alpha-ready v1 implementation. The package is intended to be published under the `@crup` npm scope.
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
- pnpm add @crup/react-timer-hook
15
- npm install @crup/react-timer-hook
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, durationParts } from '@crup/react-timer-hook';
36
+ import { durationParts, useTimer, useTimerGroup } from '@crup/react-timer-hook';
22
37
  ```
23
38
 
24
- V1 should expose only:
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
- ## Single Timer
41
+ ### Stopwatch
50
42
 
51
43
  ```tsx
52
- function Stopwatch() {
53
- const timer = useTimer({
54
- autoStart: false,
55
- updateIntervalMs: 100,
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
- <span>{Math.floor(timer.elapsedMilliseconds / 1000)}s</span>
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
- ## Absolute Deadline Countdown
61
+ ### Absolute countdown
72
62
 
73
- Use this for auctions, server deadlines, reservations, or any timer where the end timestamp comes from outside the UI.
63
+ Use `now` for wall-clock deadlines from a server, auction, reservation, or job expiry.
74
64
 
75
65
  ```tsx
76
- function AuctionTimer({ auctionId, expiresAt }: {
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
- For absolute deadlines, `pause()` pauses the local timer lifecycle and schedules, but it does not change the external server deadline. On `resume()`, the next `now` value catches up to wall time.
98
-
99
- ## Pausable Duration Countdown
86
+ ### Polling with early cancel
100
87
 
101
- Use this when pausing should freeze the remaining duration.
88
+ Schedules run while the timer is active. Slow async work is skipped by default with `overlap: 'skip'`.
102
89
 
103
90
  ```tsx
104
- function BreakTimer() {
105
- const durationMs = 5 * 60 * 1000;
106
-
107
- const timer = useTimer({
108
- autoStart: true,
109
- updateIntervalMs: 1000,
110
- endWhen: snapshot => snapshot.elapsedMilliseconds >= durationMs,
111
- });
112
-
113
- const remainingMs = Math.max(0, durationMs - timer.elapsedMilliseconds);
114
-
115
- return <span>{Math.ceil(remainingMs / 1000)}s left</span>;
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
- `useTimerGroup()` should use one scheduler internally, not one timeout loop per item.
213
-
214
- ## Debug Logs
109
+ ### Many independent timers
215
110
 
216
- Debug logging is planned for v1, but it is opt-in.
111
+ Use `useTimerGroup()` when every row needs its own pause, resume, cancel, restart, schedules, or `onEnd`.
217
112
 
218
113
  ```tsx
219
- const timer = useTimer({
220
- autoStart: true,
114
+ const timers = useTimerGroup({
221
115
  updateIntervalMs: 1000,
222
- debug: event => {
223
- console.debug('[timer]', event);
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
- No logs should be emitted by default.
125
+ ## Bundle size
229
126
 
230
- Debug events should be semantic, for example `timer:start`, `timer:tick`, `scheduler:start`, `schedule:skip`, and `timer:end`. The library should not expose raw `setTimeout` handles.
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
- Run this after `pnpm build`:
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 size
147
+ pnpm ai:context
148
+ pnpm mcp:docs
246
149
  ```
247
150
 
248
- CI compares PR bundle size against `main` and writes a size summary to the workflow output.
249
-
250
- ## Implementation Notes
251
-
252
- - Use recursive `setTimeout`, not `setInterval`.
253
- - Never schedule timers during render.
254
- - Use `Date.now()` for wall-clock `now`.
255
- - Use `performance.now()` internally for active elapsed duration, with a `Date.now()` fallback.
256
- - Keep controls stable for React dependency arrays.
257
- - Keep latest callbacks and options in refs so rerenders do not restart the scheduler unnecessarily.
258
- - Guard async work with generation IDs.
259
- - Clean up on unmount.
260
- - Test with fake timers and React Strict Mode.
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.2",
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:api": "typedoc --out docs-api src/index.ts",
62
- "docs:build": "pnpm docs:api",
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",