@ahrowe/ui 0.19.4 → 0.21.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/dist/esm/common/actionIcon/actionIcon.mjs +1 -1
- package/dist/esm/common/actionIcon/actionIcon.mjs.map +1 -1
- package/dist/esm/common/alert/alert.mjs +1 -1
- package/dist/esm/common/alert/alert.mjs.map +1 -1
- package/dist/esm/common/alert/alert.types.mjs.map +1 -1
- package/dist/esm/common/avatar/avatar.mjs +1 -1
- package/dist/esm/common/avatar/avatar.mjs.map +1 -1
- package/dist/esm/common/breadcrumb/breadcrumb.mjs +1 -1
- package/dist/esm/common/breadcrumb/breadcrumb.mjs.map +1 -1
- package/dist/esm/common/button/button.mjs +1 -1
- package/dist/esm/common/button/button.mjs.map +1 -1
- package/dist/esm/common/button/button.types.mjs.map +1 -1
- package/dist/esm/common/emptyState/emptyState.mjs +1 -1
- package/dist/esm/common/emptyState/emptyState.mjs.map +1 -1
- package/dist/esm/common/fab/fab.mjs +1 -1
- package/dist/esm/common/fab/fab.mjs.map +1 -1
- package/dist/esm/common/otpInput/otpInput.mjs +2 -0
- package/dist/esm/common/otpInput/otpInput.mjs.map +1 -0
- package/dist/esm/common/otpInput/otpInput.module.mjs +2 -0
- package/dist/esm/common/otpInput/otpInput.module.mjs.map +1 -0
- package/dist/esm/common/otpInput/otpInput.types.mjs +2 -0
- package/dist/esm/common/otpInput/otpInput.types.mjs.map +1 -0
- package/dist/esm/common/rating/rating.mjs +1 -1
- package/dist/esm/common/rating/rating.mjs.map +1 -1
- package/dist/esm/common/sectionHeader/sectionHeader.mjs +1 -1
- package/dist/esm/common/sectionHeader/sectionHeader.mjs.map +1 -1
- package/dist/esm/common/sectionHeader/sectionHeader.types.mjs.map +1 -1
- package/dist/esm/common/splitButton/splitButton.mjs +1 -1
- package/dist/esm/common/splitButton/splitButton.mjs.map +1 -1
- package/dist/esm/common/stepper/stepper.mjs +1 -1
- package/dist/esm/common/stepper/stepper.mjs.map +1 -1
- package/dist/esm/common/stepper/stepper.module.mjs.map +1 -1
- package/dist/esm/common/stepper/stepper.types.mjs.map +1 -1
- package/dist/esm/common/tabHeader/tabHeader.mjs +1 -1
- package/dist/esm/common/tabHeader/tabHeader.mjs.map +1 -1
- package/dist/esm/common/timeline/timeline.mjs +1 -1
- package/dist/esm/common/timeline/timeline.mjs.map +1 -1
- package/dist/esm/common/timeline/timeline.module.mjs +1 -1
- package/dist/esm/common/timeline/timeline.module.mjs.map +1 -1
- package/dist/esm/common/timeline/timeline.types.mjs.map +1 -1
- package/dist/esm/common/timer/timer.chime.mjs +2 -0
- package/dist/esm/common/timer/timer.chime.mjs.map +1 -0
- package/dist/esm/common/timer/timer.mjs +2 -0
- package/dist/esm/common/timer/timer.mjs.map +1 -0
- package/dist/esm/common/timer/timer.module.mjs +2 -0
- package/dist/esm/common/timer/timer.module.mjs.map +1 -0
- package/dist/esm/common/utils/renderIcon.mjs +2 -0
- package/dist/esm/common/utils/renderIcon.mjs.map +1 -0
- package/dist/esm/index.mjs +1 -1
- package/dist/index.cjs +6 -6
- package/dist/index.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types/package/common/actionButtons/actionButtons.types.d.ts +3 -3
- package/dist/types/package/common/actionIcon/actionIcon.types.d.ts +2 -2
- package/dist/types/package/common/alert/alert.types.d.ts +2 -2
- package/dist/types/package/common/avatar/avatar.types.d.ts +2 -2
- package/dist/types/package/common/breadcrumb/breadcrumb.types.d.ts +2 -2
- package/dist/types/package/common/button/button.types.d.ts +2 -2
- package/dist/types/package/common/configProvider/configProvider.types.d.ts +4 -0
- package/dist/types/package/common/emptyState/emptyState.types.d.ts +2 -2
- package/dist/types/package/common/fab/fab.types.d.ts +2 -2
- package/dist/types/package/common/otpInput/index.d.ts +2 -0
- package/dist/types/package/common/otpInput/otpInput.d.ts +4 -0
- package/dist/types/package/common/otpInput/otpInput.types.d.ts +49 -0
- package/dist/types/package/common/rating/rating.types.d.ts +2 -2
- package/dist/types/package/common/sectionHeader/sectionHeader.types.d.ts +2 -2
- package/dist/types/package/common/splitButton/splitButton.types.d.ts +2 -2
- package/dist/types/package/common/stepper/stepper.types.d.ts +3 -3
- package/dist/types/package/common/tabHeader/tabHeader.types.d.ts +2 -2
- package/dist/types/package/common/themeProvider/theme.types.d.ts +14 -0
- package/dist/types/package/common/timeline/timeline.types.d.ts +3 -3
- package/dist/types/package/common/timer/index.d.ts +2 -0
- package/dist/types/package/common/timer/timer.chime.d.ts +1 -0
- package/dist/types/package/common/timer/timer.d.ts +3 -0
- package/dist/types/package/common/timer/timer.types.d.ts +101 -0
- package/dist/types/package/common/types/actions.types.d.ts +2 -2
- package/dist/types/package/common/types/icon.types.d.ts +17 -0
- package/dist/types/package/common/utils/renderIcon.d.ts +29 -0
- package/dist/types/package/index.d.ts +4 -0
- package/docs/ActionIcon.md +1 -1
- package/docs/Alert.md +1 -1
- package/docs/Avatar.md +1 -1
- package/docs/Breadcrumb.md +1 -1
- package/docs/Button.md +1 -1
- package/docs/CLAUDE.md +24 -0
- package/docs/ConfigProvider.md +2 -2
- package/docs/EmptyState.md +1 -1
- package/docs/OtpInput.md +92 -0
- package/docs/Rating.md +1 -1
- package/docs/SectionHeader.md +2 -2
- package/docs/Stepper.md +1 -1
- package/docs/TabHeader.md +1 -1
- package/docs/Timeline.md +1 -1
- package/docs/Timer.md +141 -0
- package/package.json +1 -1
package/docs/OtpInput.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# OtpInput
|
|
2
|
+
|
|
3
|
+
**When to use:** A one-time-passcode / verification-code input, rendered as one box per character. Typing a character auto-advances to the next box, Backspace on an empty box steps back to the previous one, arrow keys move freely between boxes, and pasting a full code (or an SMS autofill suggestion) fills every box at once instead of just the focused one.
|
|
4
|
+
|
|
5
|
+
**Keywords:** otp, 2fa, mfa, verification code, sms code, passcode, pin code, two-factor
|
|
6
|
+
|
|
7
|
+
**Import:** `import { OtpInput, OtpInputCharset } from '@ahrowe/ui'`
|
|
8
|
+
|
|
9
|
+
**Requires:** `<div id="bodyEnd"></div>` in your HTML (the error message renders via `Tooltip`, which portals).
|
|
10
|
+
|
|
11
|
+
**Charset:** `OtpInputCharset.Numeric` (default) | `OtpInputCharset.Alphanumeric`
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { useState } from 'react';
|
|
15
|
+
import { OtpInput, OtpInputCharset } from '@ahrowe/ui';
|
|
16
|
+
|
|
17
|
+
// Basic — 6 numeric boxes, controlled
|
|
18
|
+
const [code, setCode] = useState('');
|
|
19
|
+
<OtpInput value={code} onChange={setCode} />
|
|
20
|
+
|
|
21
|
+
// React once the code is fully entered — e.g. auto-submit for verification
|
|
22
|
+
<OtpInput
|
|
23
|
+
onComplete={(code) => verifyCode(code)}
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
// Labeled and required
|
|
27
|
+
<OtpInput label="Verification code" isRequired autoFocus />
|
|
28
|
+
|
|
29
|
+
// Fewer/more boxes, and a non-numeric charset (e.g. a backup/recovery code)
|
|
30
|
+
<OtpInput length={4} charset={OtpInputCharset.Alphanumeric} value={code} onChange={setCode} />
|
|
31
|
+
|
|
32
|
+
// Masked, like a password field
|
|
33
|
+
<OtpInput masked value={code} onChange={setCode} />
|
|
34
|
+
|
|
35
|
+
// Visually split into groups, e.g. 3 + 3
|
|
36
|
+
<OtpInput groupAfter={[3]} value={code} onChange={setCode} />
|
|
37
|
+
|
|
38
|
+
// Manual error state (e.g. after a failed verification call)
|
|
39
|
+
<OtpInput
|
|
40
|
+
value={code}
|
|
41
|
+
onChange={setCode}
|
|
42
|
+
isValid={!wasRejected}
|
|
43
|
+
errorMessage="That code didn't match. Try again."
|
|
44
|
+
/>
|
|
45
|
+
|
|
46
|
+
// Auto-clear on error — once isValid turns false, the wrong code and error message stay
|
|
47
|
+
// visible briefly, then every box clears and refocuses the first one for a clean retry
|
|
48
|
+
<OtpInput
|
|
49
|
+
value={code}
|
|
50
|
+
onChange={setCode}
|
|
51
|
+
onComplete={verifyCode}
|
|
52
|
+
isValid={!wasRejected}
|
|
53
|
+
errorMessage="That code didn't match. Try again."
|
|
54
|
+
clearOnError
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
// With FormValidator
|
|
58
|
+
import { FormValidator, Validators } from '@ahrowe/ui';
|
|
59
|
+
const codeValidator = new FormValidator('', [Validators.required(), Validators.minLength(6)]);
|
|
60
|
+
<OtpInput formValidator={codeValidator} />
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Interaction:** typing a character fills the focused box and jumps to the next one; on the last box, it blurs instead. Backspace on a filled box just clears it; on an already-empty box it clears and focuses the previous one. Left/Right arrow keys move focus without changing values. Pasting (or an SMS autofill suggestion landing in one box) distributes the pasted text across boxes starting from wherever it lands. Focusing a box selects its content, so typing immediately overwrites it rather than requiring a manual clear first.
|
|
64
|
+
|
|
65
|
+
**Error display:** like `Input`/`RadioGroup`, the boxes get a red border once errored, and the message itself appears in an error `Tooltip` on focus or hover rather than sitting inline under the boxes all the time. With a `formValidator`, this only shows once the field has been *touched* (focus has left the group) **and** has an error — so a required-field error doesn't flash before the user has even had a chance to type. With manual `isValid`/`errorMessage`, there's no touched-gating (the consumer already controls when to set `isValid={false}`, e.g. only after a failed verification call). Either way, the error persists across renders until whatever's driving it — `isValid`, or the `formValidator`'s own error state — actually changes; `OtpInput` never silently clears it on its own. See `clearOnError` below for the one built-in exception.
|
|
66
|
+
|
|
67
|
+
**`onComplete`:** fires once per genuinely new completion — i.e. the code going from fewer than `length` characters to exactly `length`. Editing individual boxes of an already-complete code (retyping a digit that was wrong) does **not** refire it on every keystroke, since the code never drops below `length` in between; it fires again only after the code is cleared (manually, or via `clearOnError` below) and refilled.
|
|
68
|
+
|
|
69
|
+
**Handling a failed verification (`clearOnError`):** when the code the user entered turns out to be wrong, don't leave it sitting there for them to correct digit-by-digit — set `clearOnError` so that once `isValid`/`formValidator` newly reports invalid, the boxes shake briefly (respecting `prefers-reduced-motion`) and, after `clearErrorDelay` (default `900`ms — long enough to read the error message first), every box clears and focus returns to the first one, ready for a clean retry. This is the standard pattern used by most OTP UIs (a wrong SMS/authenticator code almost always means the user misread the whole code, not mistyped one digit). Off by default, since the consumer's own error handling (e.g. a form-level retry flow) may prefer to leave the value in place instead.
|
|
70
|
+
|
|
71
|
+
**Key props:**
|
|
72
|
+
|
|
73
|
+
| Prop | Type | Description |
|
|
74
|
+
|------|------|-------------|
|
|
75
|
+
| `length` | `number` | Number of boxes (default `6`) |
|
|
76
|
+
| `value` | `string` | Controlled code, e.g. `"123456"` |
|
|
77
|
+
| `onChange` | `(value: string) => void` | Fires on every change with the current (possibly partial) code |
|
|
78
|
+
| `onComplete` | `(value: string) => void` | Fires once the code reaches `length` characters |
|
|
79
|
+
| `formValidator` | `FormValidator` | Connects to form validation |
|
|
80
|
+
| `charset` | `OtpInputCharset` | Restricts allowed characters and picks the mobile keyboard (default `Numeric`) |
|
|
81
|
+
| `masked` | `boolean` | Renders each box's value as a dot, like a password field |
|
|
82
|
+
| `groupAfter` | `number[]` | 1-based box indices after which a small visual gap is drawn, e.g. `[3]` for 3 + 3 |
|
|
83
|
+
| `label` | `string` | Label above the boxes |
|
|
84
|
+
| `isRequired` | `boolean` | Shows a required mark (`*`) next to the label |
|
|
85
|
+
| `isValid` | `boolean` | Manual valid state (default `true`) |
|
|
86
|
+
| `errorMessage` | `string` | Manual error message, shown in an error `Tooltip` on focus/hover (falls back to `formValidator`'s current error) |
|
|
87
|
+
| `clearOnError` | `boolean` | Once an error newly appears, shake the boxes and, after `clearErrorDelay`, clear them and refocus the first one (default `false`) |
|
|
88
|
+
| `clearErrorDelay` | `number` | Delay in ms before `clearOnError` clears the boxes (default `900`) |
|
|
89
|
+
| `disabled` | `boolean` | |
|
|
90
|
+
| `autoFocus` | `boolean` | Focuses the first empty box (or the first box) on mount |
|
|
91
|
+
|
|
92
|
+
**Slots:** `root` `label` `inputs` `input` `separator`
|
package/docs/Rating.md
CHANGED
|
@@ -55,7 +55,7 @@ import { faHeart } from '@fortawesome/free-solid-svg-icons';
|
|
|
55
55
|
| `allowClear` | `boolean` | Clicking the currently-selected star resets the value to `0` (default `true`) |
|
|
56
56
|
| `readOnly` | `boolean` | Display only, not focusable or interactive |
|
|
57
57
|
| `disabled` | `boolean` | Dims the control and disables interaction |
|
|
58
|
-
| `icon` | `IconDefinition` | Overrides the default star icon |
|
|
58
|
+
| `icon` | `IconDefinition \| ReactElement` | Overrides the default star icon — FontAwesome icon or any React element |
|
|
59
59
|
| `aria-label` | `string` | Accessible label (default `'Rating'`) |
|
|
60
60
|
|
|
61
61
|
**Accessibility:** renders `role="slider"` with `aria-valuemin`/`aria-valuemax`/`aria-valuenow`/`aria-valuetext`, matching `Slider`'s pattern.
|
package/docs/SectionHeader.md
CHANGED
|
@@ -62,7 +62,7 @@ import { faFolder, faPen, faTrash } from '@fortawesome/free-solid-svg-icons';
|
|
|
62
62
|
|------|------|-------------|
|
|
63
63
|
| `title` | `ReactNode` | Main heading |
|
|
64
64
|
| `subtitle` | `ReactNode` | Secondary text below the title |
|
|
65
|
-
| `icon` | `IconDefinition` |
|
|
65
|
+
| `icon` | `IconDefinition \| ReactElement` | Icon shown on the left (FontAwesome icon or any React element); ignored when `thumbnail` is set |
|
|
66
66
|
| `thumbnail` | `string \| ReactNode` | URL → background image; any other ReactNode → rendered inside the leading circle |
|
|
67
67
|
| `left` | `ReactNode` | Leftmost content of the trailing block (title stays fixed to the left of it) |
|
|
68
68
|
| `center` | `ReactNode` | Center content of the trailing block, e.g. a search box or tabs |
|
|
@@ -76,7 +76,7 @@ import { faFolder, faPen, faTrash } from '@fortawesome/free-solid-svg-icons';
|
|
|
76
76
|
```ts
|
|
77
77
|
interface SectionHeaderAction {
|
|
78
78
|
title?: string;
|
|
79
|
-
icon: IconDefinition;
|
|
79
|
+
icon: IconDefinition | ReactElement;
|
|
80
80
|
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
|
81
81
|
}
|
|
82
82
|
```
|
package/docs/Stepper.md
CHANGED
|
@@ -69,7 +69,7 @@ const steps = [
|
|
|
69
69
|
| `label` | `ReactNode` | Primary step label |
|
|
70
70
|
| `description` | `ReactNode` | Secondary line under the label |
|
|
71
71
|
| `content` | `ReactNode` | Collapsible body under the label (vertical only) |
|
|
72
|
-
| `icon` | `IconDefinition` |
|
|
72
|
+
| `icon` | `IconDefinition \| ReactElement` | Icon in the marker, replacing the number — FontAwesome icon or any React element |
|
|
73
73
|
| `error` | `boolean` | Recolours the marker with `--stepper-error-color`; suppresses the checkmark |
|
|
74
74
|
| `optional` | `boolean \| ReactNode` | `true` → an "Optional" hint; a node → custom hint text |
|
|
75
75
|
| `disabled` | `boolean` | Never clickable, dimmed |
|
package/docs/TabHeader.md
CHANGED
|
@@ -56,7 +56,7 @@ function Example() {
|
|
|
56
56
|
| Field | Type | Description |
|
|
57
57
|
|-------|------|-------------|
|
|
58
58
|
| `id` | `string` | Unique identifier, matched against `currentTab` |
|
|
59
|
-
| `icon` | `IconDefinition` | FontAwesome icon (always visible, incl. mobile) |
|
|
59
|
+
| `icon` | `IconDefinition \| ReactElement` | FontAwesome icon or any React element (always visible, incl. mobile) |
|
|
60
60
|
| `title` | `string` | Optional title next to the icon |
|
|
61
61
|
| `subtitle` | `string` | Optional subtitle beneath the title |
|
|
62
62
|
| `disabled` | `boolean` | Dim the tab and prevent selection |
|
package/docs/Timeline.md
CHANGED
|
@@ -87,7 +87,7 @@ function AuditLog() {
|
|
|
87
87
|
| `title` | `ReactNode` | Primary label for the entry |
|
|
88
88
|
| `description` | `ReactNode` | Secondary line under the title |
|
|
89
89
|
| `timestamp` | `ReactNode` | Rendered next to the title, e.g. a date or relative time ("2 hours ago") |
|
|
90
|
-
| `icon` | `IconDefinition` |
|
|
90
|
+
| `icon` | `IconDefinition \| ReactElement` | Icon shown in the marker instead of a plain dot — FontAwesome icon or any React element |
|
|
91
91
|
| `status` | `TimelineItemStatus` | Semantic marker colour (default `Default`) |
|
|
92
92
|
| `content` | `ReactNode` | Extra content rendered under the description, e.g. a diff, an attachment, an actor avatar |
|
|
93
93
|
| `onClick` | `(event: MouseEvent<HTMLDivElement>) => void` | Makes the whole entry clickable, rendering it as an accessible button (`role="button"`, focusable, click and Enter both fire it). Omit to render a plain, non-interactive row |
|
package/docs/Timer.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Timer
|
|
2
|
+
|
|
3
|
+
**When to use:** A countdown timer with a depleting ring, add/remove-time and pause/cancel controls, and an imperative ref API. Built for step-by-step flows where a wait needs its own clock — a recipe step ("simmer for 9 minutes"), a rest interval, a checkout hold, a break timer. Each instance owns its own state, so several can run independently at once (e.g. one per recipe step).
|
|
4
|
+
|
|
5
|
+
**Keywords:** countdown, cooking timer, stopwatch, recipe, kitchen timer, ring progress
|
|
6
|
+
|
|
7
|
+
**Import:** `import { Timer } from '@ahrowe/ui'`
|
|
8
|
+
**Types:** `import type { TimerProps, TimerHandle, TimerStatus } from '@ahrowe/ui'`
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { Timer } from '@ahrowe/ui';
|
|
12
|
+
|
|
13
|
+
// Basic countdown — starts automatically, built-in controls shown
|
|
14
|
+
<Timer durationSeconds={9 * 60} />
|
|
15
|
+
|
|
16
|
+
// With a label inside the ring
|
|
17
|
+
<Timer durationSeconds={9 * 60} label="Simmer the sauce" />
|
|
18
|
+
|
|
19
|
+
// Doesn't start until start() is called (see ref example below)
|
|
20
|
+
<Timer durationSeconds={60} autoStart={false} />
|
|
21
|
+
|
|
22
|
+
// Display only — no built-in controls
|
|
23
|
+
<Timer durationSeconds={45} showControls={false} />
|
|
24
|
+
|
|
25
|
+
// Only some controls — keep add/remove, hide pause and cancel
|
|
26
|
+
<Timer durationSeconds={45} hidePauseButton hideCancelButton adjustSeconds={15} />
|
|
27
|
+
|
|
28
|
+
// Smaller ring, thinner stroke
|
|
29
|
+
<Timer durationSeconds={30} size="100px" strokeWidth={6} />
|
|
30
|
+
|
|
31
|
+
// Depletes counter-clockwise instead of the default clockwise sweep
|
|
32
|
+
<Timer durationSeconds={30} counterClockwise />
|
|
33
|
+
|
|
34
|
+
// Urgent colour + heartbeat pulse kicks in earlier (last 25% instead of the default 10%)
|
|
35
|
+
<Timer durationSeconds={60} urgentThreshold={0.25} />
|
|
36
|
+
|
|
37
|
+
// Ambient sonar-style ripples expanding from the center while running
|
|
38
|
+
<Timer durationSeconds={10 * 60} showRunningRipples label="Meditating" />
|
|
39
|
+
|
|
40
|
+
// Completion sound and a browser Notification (both opt-in, both default off)
|
|
41
|
+
<Timer
|
|
42
|
+
durationSeconds={5 * 60}
|
|
43
|
+
playSoundOnComplete
|
|
44
|
+
notifyOnComplete
|
|
45
|
+
notificationTitle="Timer done"
|
|
46
|
+
notificationBody="The pasta is ready"
|
|
47
|
+
/>
|
|
48
|
+
|
|
49
|
+
// Imperative control via ref — for a fully custom UI, or driving start()/pause() from
|
|
50
|
+
// elsewhere on the page
|
|
51
|
+
import { useRef } from 'react';
|
|
52
|
+
import type { TimerHandle } from '@ahrowe/ui';
|
|
53
|
+
|
|
54
|
+
function Example() {
|
|
55
|
+
const ref = useRef<TimerHandle>(null);
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
<Timer ref={ref} durationSeconds={60} showControls={false} autoStart={false} />
|
|
59
|
+
<button onClick={() => ref.current?.start()}>Start</button>
|
|
60
|
+
<button onClick={() => ref.current?.addTime(30)}>+30s</button>
|
|
61
|
+
</>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Not a controlled value:** `Timer` isn't a controlled numeric input like `Slider` — there's no `remainingSeconds` prop to drive from outside. `durationSeconds` seeds the countdown; from there it owns the ticking internally, exposing state via callbacks (`onTick`, `onComplete`, …) and the `TimerHandle` ref rather than a value a consumer sets on every tick. Changing `durationSeconds` after mount resets the timer to the new duration (see `resetOnDurationChange` below).
|
|
67
|
+
|
|
68
|
+
**Direction:** the ring depletes clockwise by default, starting from 12 o'clock. Set `counterClockwise` to sweep the other way instead.
|
|
69
|
+
|
|
70
|
+
**Controls:** the built-in row has remove-time / pause-resume / add-time, plus a cancel button underneath. `showControls={false}` hides the whole block; the individual `hide*Button` props hide one control while keeping the rest (e.g. a fixed-duration display-only step keeps pause/cancel but drops add/remove). The pause button doubles as a play/restart button whenever there's nothing to pause — titled "Start" for an idle timer that hasn't run yet, or "Restart" once completed or cancelled — so there's always a way to (re)start the countdown from the built-in controls alone, without wiring up a separate button via the ref. Add-time also stays available after completion and, if clicked, un-completes and resumes the countdown (the "oops, give it 2 more minutes" case).
|
|
71
|
+
|
|
72
|
+
**Urgency and the heartbeat pulse:** once remaining time drops to `urgentThreshold` (default the last 10% of the original duration) the ring switches to `--timer-urgent-color` and, if `pulseOnUrgent` is set (default `true`), the ring gently scales in a breathing "heartbeat" — accelerating from a 1s to a 0.6s cycle in the final 5 seconds. Disable it with `pulseOnUrgent={false}` if the motion isn't wanted.
|
|
73
|
+
|
|
74
|
+
`urgentThreshold`'s 10%-of-duration fraction gets vanishingly small on a short timer (10% of 5 seconds is half a second — barely long enough to register). `urgentMinSeconds` (default `3`) sets a floor in absolute seconds: the urgent state uses whichever of the two — the fraction or the floor — produces the larger window. It has no effect on longer timers, where the fraction alone already exceeds it.
|
|
75
|
+
|
|
76
|
+
**Running ripples:** `showRunningRipples` (default `false`) shows a thin ring pulsing outward from the dial's own edge and fading out — a beacon/live-indicator-style halo, similar to a map's live-location dot. It starts right at the ring's boundary and only grows slightly beyond it, so it never overlaps the time readout or the progress ring itself. A new one spawns about once a second while `status === 'running'`, each animating through its own 3-second cycle independently, so several are visible overlapping at once. Pausing (or completing/cancelling) only stops *new* ones from spawning — whichever are already mid-flight keep animating through to a normal, faded finish rather than disappearing mid-cycle. Purely decorative: colour comes from `--timer-ripple-color` (falls back to `--timer-progress-color`/`--primary-color`), and it's disabled entirely under `prefers-reduced-motion: reduce`.
|
|
77
|
+
|
|
78
|
+
**Sound and notifications (both opt-in, both default `false`):** `playSoundOnComplete` plays a short built-in chime on completion (override with your own clip via `soundUrl`); playback failures (autoplay policy, unsupported browser) are swallowed silently, never thrown. `notifyOnComplete` shows a browser `Notification` on completion — permission is requested lazily on the first `start()` (a user gesture, as browsers require), and a denied or unsupported Notification API fails completely silently, with no console warning. Note: `autoStart` firing on page load with no prior user interaction may itself have its permission prompt silently blocked by the browser — that's a platform limitation, not something `Timer` can work around.
|
|
79
|
+
|
|
80
|
+
**Key props:**
|
|
81
|
+
|
|
82
|
+
| Prop | Type | Description |
|
|
83
|
+
|------|------|-------------|
|
|
84
|
+
| `durationSeconds` | `number` | Starting duration in seconds (required) |
|
|
85
|
+
| `autoStart` | `boolean` | Start ticking automatically on mount and on `durationSeconds` change (default `true`) |
|
|
86
|
+
| `resetOnDurationChange` | `boolean` | A `durationSeconds` change restarts the countdown (`true`, default) or only updates the idle baseline (`false`) |
|
|
87
|
+
| `onTick` | `(remainingMs: number) => void` | Fires roughly every 200ms while running |
|
|
88
|
+
| `onComplete` | `() => void` | Fires exactly once when it reaches zero |
|
|
89
|
+
| `onStart` / `onPause` / `onResume` / `onCancel` | `() => void` | Lifecycle callbacks |
|
|
90
|
+
| `onAddTime` / `onRemoveTime` | `(seconds, newRemainingSeconds) => void` | Fires when time is adjusted, by button or ref |
|
|
91
|
+
| `showControls` | `boolean` | Show the built-in control row (default `true`) |
|
|
92
|
+
| `hideAddButton` / `hideRemoveButton` / `hidePauseButton` / `hideCancelButton` | `boolean` | Hide one control while keeping the rest |
|
|
93
|
+
| `adjustSeconds` | `number` | Seconds added/removed per click (default `30`) |
|
|
94
|
+
| `maxSeconds` | `number` | Optional cap on remaining time when adding |
|
|
95
|
+
| `label` | `ReactNode` | Text under the time readout, inside the ring |
|
|
96
|
+
| `formatTime` | `(remainingMs: number) => ReactNode` | Overrides the default `MM:SS` / `H:MM:SS` formatter |
|
|
97
|
+
| `size` | `string` | Ring diameter, any CSS length (default `'160px'`) |
|
|
98
|
+
| `strokeWidth` | `number` | Ring stroke width in px (default `10`) |
|
|
99
|
+
| `counterClockwise` | `boolean` | Depletes counter-clockwise instead of the default clockwise sweep (default `false`) |
|
|
100
|
+
| `urgentThreshold` | `number` | Fraction (0–1) of the original duration at which the urgent colour/pulse kicks in (default `0.1`) |
|
|
101
|
+
| `urgentMinSeconds` | `number` | Floor, in seconds, for the urgent window — whichever of this or `urgentThreshold`'s fraction is larger wins (default `3`) |
|
|
102
|
+
| `pulseOnUrgent` | `boolean` | Enables the heartbeat pulse once urgent (default `true`) |
|
|
103
|
+
| `showRunningRipples` | `boolean` | Shows a beacon-style halo pulsing outward from the dial's edge while running (default `false`) |
|
|
104
|
+
| `playSoundOnComplete` | `boolean` | Play the built-in (or `soundUrl`) chime on completion (default `false`) |
|
|
105
|
+
| `soundUrl` | `string` | Overrides the built-in completion sound |
|
|
106
|
+
| `soundVolume` | `number` | Completion sound volume, 0–1 (default `0.5`) |
|
|
107
|
+
| `notifyOnComplete` | `boolean` | Show a browser Notification on completion (default `false`) |
|
|
108
|
+
| `notificationTitle` / `notificationBody` | `string` | Notification text (body falls back to `label` when it's a string) |
|
|
109
|
+
|
|
110
|
+
**`TimerHandle`** (via `ref`):
|
|
111
|
+
|
|
112
|
+
| Method | Description |
|
|
113
|
+
|--------|-------------|
|
|
114
|
+
| `start()` | Starts (or restarts, if idle/completed/cancelled) from the current duration |
|
|
115
|
+
| `pause()` | Pauses a running timer |
|
|
116
|
+
| `resume()` | Resumes a paused timer from where it left off |
|
|
117
|
+
| `addTime(seconds)` | Adds seconds to the remaining time, clamped to `maxSeconds` if set |
|
|
118
|
+
| `removeTime(seconds)` | Removes seconds, clamped to 0 (which completes the timer) |
|
|
119
|
+
| `cancel()` | Stops and resets to idle at the original duration; does not fire `onComplete` |
|
|
120
|
+
| `reset()` | Resets to idle at `durationSeconds` |
|
|
121
|
+
| `getStatus()` | Returns the current `TimerStatus` synchronously |
|
|
122
|
+
| `getRemainingMs()` | Returns the current remaining ms synchronously |
|
|
123
|
+
|
|
124
|
+
**`TimerStatus`:** `'idle'` \| `'running'` \| `'paused'` \| `'completed'` \| `'cancelled'`
|
|
125
|
+
|
|
126
|
+
**Theming:** override these CSS variables theme-wide via `ThemeProvider` or per instance via `style`; each falls back to a built-in default:
|
|
127
|
+
|
|
128
|
+
| Variable | Falls back to |
|
|
129
|
+
|----------|---------------|
|
|
130
|
+
| `--timer-track-color` | `var(--background-accent-light)` |
|
|
131
|
+
| `--timer-progress-color` | `var(--primary-color)` |
|
|
132
|
+
| `--timer-urgent-color` | `var(--error-color)` |
|
|
133
|
+
| `--timer-ripple-color` | `var(--timer-progress-color)` (which itself falls back to `var(--primary-color)`) |
|
|
134
|
+
| `--timer-size` | `160px` (also settable per instance via `size`) |
|
|
135
|
+
| `--timer-stroke-width` | `10px` (also settable per instance via `strokeWidth`) |
|
|
136
|
+
| `--timer-text-color` | `var(--text-color)` |
|
|
137
|
+
| `--timer-label-color` | `var(--text-dark)` |
|
|
138
|
+
|
|
139
|
+
**Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ Timer: { adjustSeconds: 60, playSoundOnComplete: true } }}`. See [ConfigProvider.md](ConfigProvider.md).
|
|
140
|
+
|
|
141
|
+
**Slots:** `root` `ringWrapper` `ringSvg` `ringTrack` `ringProgress` `ripples` `ripple` `display` `time` `label` `controls` `addButton` `removeButton` `pauseButton` `cancelButton`
|