@ahrowe/ui 0.19.4 → 0.20.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/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/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/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/configProvider/configProvider.types.d.ts +4 -0
- 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/themeProvider/theme.types.d.ts +14 -0
- 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/index.d.ts +4 -0
- package/docs/CLAUDE.md +2 -0
- package/docs/ConfigProvider.md +2 -2
- package/docs/OtpInput.md +92 -0
- package/docs/Timer.md +141 -0
- package/package.json +1 -1
|
@@ -109,6 +109,20 @@ export interface ThemeVariables {
|
|
|
109
109
|
'--stepper-gap'?: string;
|
|
110
110
|
/** Text/check colour on a filled marker. Falls back to --text-on-primary. */
|
|
111
111
|
'--stepper-text-on-marker'?: string;
|
|
112
|
+
/** Timer ring track (unfilled) colour. Falls back to --background-accent-light. */
|
|
113
|
+
'--timer-track-color'?: string;
|
|
114
|
+
/** Timer ring progress (remaining time) colour. Falls back to --primary-color. */
|
|
115
|
+
'--timer-progress-color'?: string;
|
|
116
|
+
/** Timer ring colour once inside the urgent threshold. Falls back to --error-color. */
|
|
117
|
+
'--timer-urgent-color'?: string;
|
|
118
|
+
/** Timer ring diameter. Falls back to 160px (also settable per instance via the `size` prop). */
|
|
119
|
+
'--timer-size'?: string;
|
|
120
|
+
/** Timer ring stroke thickness. Falls back to 10px (also settable per instance via `strokeWidth`). */
|
|
121
|
+
'--timer-stroke-width'?: string;
|
|
122
|
+
/** Timer MM:SS readout text colour. Falls back to --text-color. */
|
|
123
|
+
'--timer-text-color'?: string;
|
|
124
|
+
/** Timer label (below the readout) text colour. Falls back to --text-dark. */
|
|
125
|
+
'--timer-label-color'?: string;
|
|
112
126
|
/** Width a column collapses to once it has no cards (see `collapseEmptyColumns`). Falls back to 140px. */
|
|
113
127
|
'--kanban-empty-column-width'?: string;
|
|
114
128
|
'--option-picker-padding'?: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DEFAULT_CHIME_DATA_URI = "data:audio/wav;base64,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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANACUAKwAKAMr/kv+R/9n/TwCxALwAVwCs/xb/8/5p/0YAFgFfAeYA3f/N/lP+wP7s/zwB9QGiAV4Azv7O/ff9Rf8RAWACcQImASb/g/0s/WH+jACEAjADIALY/4v9gfxZ/bD/TAK/AzAD2gD2/Rb8S/yN/q4B/wMyBBkCyP4H/F77Ov2qANgDAgV3A/r/aPyz+tr7Tf87A34F0AR7AUH9bfqS+rD9JQKJBfsFLAOQ/qP6jPn1+6EADgXSBucERQBk++74RvrE/gYEMQeABkECs/zW+M74svx1AvwGygdeBIH+Wvm795b6bwAmBpwIbQa2AIP6NPeg+BX+rATSCD0IKgNK/FX3A/eU+50CVQibCawFmv4w+O71HvkWABwHXAoHCE0Bx/mJ9ez2Q/0sBWAKAgo0BAr87PU19Vj6nQKRCWkLFAfa/ib3KfSR95f/7gcQDLEJCQIv+fDzLfVO/IUF1wvNC1wF8fuf9GbzAPl2Aq8KMg2TCEH/PvZv8vL18v6cCLMNaAvoAr/4bPJn8zn7tgUzDZoNogYA/HDznPGR9ycCqwvxDiUKzv979cTwRfQq/iQJQg8oDegDdvgB8ZzxBvq/BXQOZg8BCDf8YfLY7wz2sQGDDKQQyAuBANz0Ku+M8j79hAm6EO4OCAVU+LDv0u+4+KEFlQ8rEXgJlfx08SDudfQWATgNRxJ5DVYBZfSm7cvwM/y+CRkStxBEBlv4fu4L7lH3WgWWEOgSAgsZ/avwduzO8lYAxg3WEzIPTQIU9DnsBu8J+88JXBN9EpsHifhr7Uvs1PXuBHMRmRSdDML9B/Dd6hzxdP8tDk8V8hBkA+zz5+pB7cT5uAmAFD8UCQnf+HrslepF9FsELBI6FkYOjv6J71npYu9y/mwOrha1EpcE6/Oz6X/rZfh6CYQV+RWLClv5revu6KbypAO/EsgX+Q99/zPv7eek7VH9hA7yF3cU5QUS9J7ow+nx9hUJZBamFx8M/PkF61fn/PDKAisTQBmzEYsABe+b5uTrE/x0DhcZNBZLB2H0q+cR6Gn1iQggF0QZwA3A+oTq1eVI79ABbxOeGm8TtgH/7mblJ+q9+jwOGxroF8YI1vTc5m3m0vPaB7YXzxpsD6f7Kupq5JDttwCME+EbKxX9AiDvUeRw6FD53Q39GpEZUgpw9TLm2uQv8gcHJRhEHB8Rrvz36Rrj1+uB/4ETBh3iFlsEae9e48Pm0PdYDbsbLBvsCy72ruVb44LwFAZtGKEd1RLS/e3p5+Ef6jL+TRMKHpIYzgXZ747iJOVA9q4MUhyzHJEND/dR5fPh0O4BBY0Y4h6LFBL/CurT4G7ozPzzEuweNxpTB27w4+GV46P04QvDHCUePQ8P+BzlpeAd7dIDhRgFID0WagBP6uDfxuZS+3MSqR/MG+cIJ/Fe4Rni/fLzCg0dfx/uEC75EOV032vriQJVGAgh5xfXAbvqEd8r5cj5zRFBIFAdhgoD8gHhteBR8eYJLh29IJ4SaPor5WLev+koAf4X6SGHGVYDTetm3qDjMfgFEbMgvh4tDP/yy+Bq36PvuwgoHd4hSxS6+23lcd0c6LT/gBelIhgb5QQC7OLdKeKR9hoQ/CAUINgNGfS94Dze9+13B/oc3yLwFSP91+Wj3IXmL/7dFjwjlxx/BtvshN3I4Or0EA8eIVAhgw9P9dfgLN1P7BsGpBy9I4sXnv5m5vrb/+Sd/BcWrCMCHiEI1O1P3YHfQPPpDRghbSIrEZ72GeE+3LHqqwQoHHgkGBkoABrnd9uL4wD7LxX1I1Ufxwnr7kHdVd6Y8agM6iBrI80SA/iC4XLbHukpA4YbDSWUGr4B8ecb2y7iXfknFBYkjSBvCx/wXN1J3fTvTwuUIEckZBR7+RHiy9qb55oBwRp8JfsbXQPp6Ofa6uC39wITDySoIRQNbPGe3V3cWe7hCRggACXuFQP7xOJK2ivmAADaGcMlSx0BBf/p2trB3xL2whHgI6Misg7P8gfelNvK7GEIdx+TJWcXl/yb4/DZ0ORf/tMY4iWAHqYGMuv22rfecfRqEIojfCNHEEb0l97v2knr0waxHv8lyxg0/pLkvdmP47v8rhfaJZgfSQh+7Drbzt3Y8v0ODSMyJM4RzfVL33Da2+k7BckdRSYZGtb/qeWz2WniGPttFqklkSDlCeHtpdsH3Urxfg1qIsMkRRNg9yPgGNqD6JsDwRxiJkwbegHc5tDZYeF4+RUVUSVpIXkLWO823GTcy+/wC6MhLiWoFP34HOHn2UPn9wGbG1gmYhwcAyroFtp54N/3pxPSJB0i/wzg8Ozc59te7lcKuSBxJfQVn/o04t/ZHuZUAFoaJSZaHbkEjumD2rTfUvYnEi0krCJ2DnTyxd2R2wbttgivH40lJhdE/Gnj/9kX5bP+/xjLJTAeTAYH6xbbE9/T9JgQZCMVI9gPEvTA3mLbx+sSB4cegCU8GOf9uORG2jHkGv2PF0kl4x7UB5DsztuX3mbz/Q54IlcjJRG29dvfXNui6mwFQx1MJTMZhf8f5rXabOOM+w0WoiRxH0sJJ+6q3ELeDfJaDWwhcSNXEl33E+F925vpygPmG+8kCRobAZvnStvL4gv6exTXI9kfrwrI76jdFN7N8LILQSBjI24TA/ll4sbbtOguAnMabCS8GqQCJ+kF3FDinPjdEugiGyD9C2/xxd4P3qfvCgr6Hi0jZhSk+s/jN9zv550A7RjDI0sbHgTB6uTc++FB9zYR2SE0IDINGfMA4DHenu5kCJkdzyI9FT38TuXO3E3nGf9YF/UitBuFBWbs5N3N4f71iw+rICYgSw7D9Fbhe9617cUGIxxLIvIVyv3e5ovd0eam/bUVBCL1G9YGEu4E38jh1fTeDWEf7x9FD2j2xOLt3u3sLwWZGqAhghZI/33obN575kj8ChTyIBAcDwjB70Pg6uHJ8zQM/R2RHx8QBvhH5IbfSeynA/4Y0CDtFrMAJupv30zmAPtaEsEfAhwrCW/xnOE04tzyjwqCHAsf1xCY+dzlRODL6y8CVxfdHzAXCQLX65LgRebT+agQdB7MGyoKGvMO46biEfL0CPMaYB5rERv7f+cm4XLrywCnFckeTBdHA4zt0+Fm5sL4+A4MHW4bCQu+9JXkP+Np8WYHVRmPHdgRjfwu6SriQOt+//ETlR1BF2kEQO8v46/m0PdNDY0b6RrFC1b2Lub94+bw5wWpF5scIBLp/eTqT+M260r+ORJFHA0XbgXx8KTkH+cA96sL+xk+Gl0M4PfW59/kifB9BPMVhhs/Ei3/nuyS5FXrM/2DENsasRZSBpvyL+a351L2FgpYGG4Z0AxY+Yrp5OVT8CkDNxRRGjcSVgBZ7vHlm+s7/NEOWRkuFhUHO/TM53ToyvWRCKcWehgdDbz6ResK50Xw7gF5Ev8YBxJiARHwaecI7GT7KQ3DF4UVswfM9XnpVeln9R8H7BRlF0INCPwG7U7oXvDPALwQkxevEU4CwvH26J3sr/qMCxwWtxQtCEz3MetZ6iv1wwUrEzAWPw05/cfurumg8M//BA8PFjARGANp85fqV+0f+v8JZxTFE4AIuPjy7H7rFvWABGcR3RQUDUz+hfAn6wnx8P5UDXYUihC+AwL1R+w27rX5hQioErESrAgM+rjuwuwq9VkDpA9wE8IMQf898rfsmfEz/rALzBLAD0AEivYD7jjvcvkgB+IQfRGwCEb7f/Ai7mX1UALlDesRSAwUAOzzWe5P8pv9GwoUEdEOmwT+98jvW/BW+dQFGA8sEIwIYvxD8pvvyfVoAS4MUhCnC8MAjPUM8CrzKP2YCFEPwA3PBFv5k/Gc8WL5pARPDcAOQAhg/QL0K/FT9qIAggqmDuEKTQEd98vxKPTc/CsHhw2PDNsEnvpf8/rylvmSA4oLPA3NBzz+t/XO8gP3AADlCOwM+AmxAZn4k/NH9bf81gW6C0ELvwTE+yj1cvTx+aACzQmiCzMH9f5f94H02PeE/1sHJwvsCO4B//lh9YX2u/ydBOwJ1wl8BMv87fYA9nT60QEbCPYJdAaJ//f4QvbQ+C7/5QVaCb8HBAJK+zH33/fm/IIDIghVCBEEsf2o+KP3HPslAXcGPAiRBff/e/oM+On5AP+IBIoHdQbyAXr8//hU+Tj9hgJgBr0GfwN0/lb6Vvnq+58A5QR2BosEPgDp+9z5Ivv5/kYDuQUQBbcBiv3H+t/6sv2tAagEEwXIAhL/9fsW+9v8PwBoA6kEZQNeAD39rvt3/Bv/IQLsA5EDVgF6/of8f/xS/vcA/gJaA+0Biv+A/eD87f0HAAMC1wIhAlUAdf5+/ef9ZP8cASUC/QHNAEb/O/4w/hf/ZwBlAZUB7wDb//X+sP4f//b/uAAEAcEAJQCO/0r/bv/U/zkAaQBXAB8A7v/f/+7/";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';
|
|
3
|
+
export type TimerSlots = 'root' | 'ringWrapper' | 'ringSvg' | 'ringTrack' | 'ringProgress' | 'ripples' | 'ripple' | 'display' | 'time' | 'label' | 'controls' | 'addButton' | 'removeButton' | 'pauseButton' | 'cancelButton';
|
|
4
|
+
export type TimerStatus = 'idle' | 'running' | 'paused' | 'completed' | 'cancelled';
|
|
5
|
+
export interface TimerHandle {
|
|
6
|
+
/** Starts (or restarts, if idle/completed/cancelled) the countdown from its current duration. */
|
|
7
|
+
start: () => void;
|
|
8
|
+
/** Pauses a running timer. No-op if not running. */
|
|
9
|
+
pause: () => void;
|
|
10
|
+
/** Resumes a paused timer from where it left off. No-op if not paused. */
|
|
11
|
+
resume: () => void;
|
|
12
|
+
/** Adds seconds to the remaining time. Clamped to `maxSeconds` if set. */
|
|
13
|
+
addTime: (seconds: number) => void;
|
|
14
|
+
/** Removes seconds from the remaining time. Clamped to 0, which triggers completion. */
|
|
15
|
+
removeTime: (seconds: number) => void;
|
|
16
|
+
/** Stops the timer and resets it back to idle at its original starting duration. Does not fire onComplete. */
|
|
17
|
+
cancel: () => void;
|
|
18
|
+
/** Resets to idle at `durationSeconds` without starting. */
|
|
19
|
+
reset: () => void;
|
|
20
|
+
/** Current status snapshot. */
|
|
21
|
+
getStatus: () => TimerStatus;
|
|
22
|
+
/** Current remaining time in ms, read synchronously. */
|
|
23
|
+
getRemainingMs: () => number;
|
|
24
|
+
}
|
|
25
|
+
export interface TimerProps extends HtmlProps {
|
|
26
|
+
/** Starting duration in seconds. Changing this prop while mounted resets the timer to the new duration. */
|
|
27
|
+
durationSeconds: number;
|
|
28
|
+
/** Start ticking automatically on mount (and whenever `durationSeconds` changes). @default true */
|
|
29
|
+
autoStart?: boolean;
|
|
30
|
+
/** When `durationSeconds` changes after mount, restart from the new duration immediately (`true`) or only update the idle baseline (`false`). @default true */
|
|
31
|
+
resetOnDurationChange?: boolean;
|
|
32
|
+
/** Fires roughly every 200ms with the live remaining ms. */
|
|
33
|
+
onTick?: (remainingMs: number) => void;
|
|
34
|
+
/** Fires exactly once when the countdown reaches zero. */
|
|
35
|
+
onComplete?: () => void;
|
|
36
|
+
/** Fires when the timer transitions to running (initial start or resume). */
|
|
37
|
+
onStart?: () => void;
|
|
38
|
+
/** Fires when paused. */
|
|
39
|
+
onPause?: () => void;
|
|
40
|
+
/** Fires when resumed from pause. */
|
|
41
|
+
onResume?: () => void;
|
|
42
|
+
/** Fires when cancelled (via button or ref). */
|
|
43
|
+
onCancel?: () => void;
|
|
44
|
+
/** Fires whenever time is added, with the seconds added and the new total remaining seconds. */
|
|
45
|
+
onAddTime?: (secondsAdded: number, newRemainingSeconds: number) => void;
|
|
46
|
+
/** Fires whenever time is removed. */
|
|
47
|
+
onRemoveTime?: (secondsRemoved: number, newRemainingSeconds: number) => void;
|
|
48
|
+
/** Show the built-in control buttons (add/remove/pause-resume/cancel). @default true */
|
|
49
|
+
showControls?: boolean;
|
|
50
|
+
/** Individually hide the add-time button while keeping the others. */
|
|
51
|
+
hideAddButton?: boolean;
|
|
52
|
+
/** Individually hide the remove-time button while keeping the others. */
|
|
53
|
+
hideRemoveButton?: boolean;
|
|
54
|
+
/** Individually hide the pause/resume button while keeping the others. */
|
|
55
|
+
hidePauseButton?: boolean;
|
|
56
|
+
/** Individually hide the cancel button while keeping the others. */
|
|
57
|
+
hideCancelButton?: boolean;
|
|
58
|
+
/** Seconds added/removed per click of the add/remove buttons. @default 30 */
|
|
59
|
+
adjustSeconds?: number;
|
|
60
|
+
/** Optional cap on remaining time when adding. Unset = uncapped. */
|
|
61
|
+
maxSeconds?: number;
|
|
62
|
+
/** Label rendered under the time readout, e.g. "Boil pasta". */
|
|
63
|
+
label?: ReactNode;
|
|
64
|
+
/** Format the MM:SS (or H:MM:SS) readout. Defaults to a built-in formatter. */
|
|
65
|
+
formatTime?: (remainingMs: number) => ReactNode;
|
|
66
|
+
/** Diameter of the ring, any CSS length. Sets `--timer-size`. @default '160px' */
|
|
67
|
+
size?: string;
|
|
68
|
+
/** Ring stroke width in px. Sets `--timer-stroke-width`. @default 10 */
|
|
69
|
+
strokeWidth?: number;
|
|
70
|
+
/** Sweeps the depleting boundary counter-clockwise instead of the default clockwise (matching a clock face). @default false */
|
|
71
|
+
counterClockwise?: boolean;
|
|
72
|
+
/** Fraction of total duration (0-1) at which the ring switches to its urgent colour and the pulse (if enabled) kicks in. @default 0.1 */
|
|
73
|
+
urgentThreshold?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Floor, in seconds, for how much time the urgent state gets — whichever of this or
|
|
76
|
+
* `urgentThreshold` (as a fraction of the total duration) produces the larger window wins.
|
|
77
|
+
* Without this, a short timer's `urgentThreshold` fraction can resolve to under a second,
|
|
78
|
+
* making the urgent/critical state barely perceptible. @default 3
|
|
79
|
+
*/
|
|
80
|
+
urgentMinSeconds?: number;
|
|
81
|
+
/** Enables the heartbeat/pulse effect once inside `urgentThreshold`. @default true */
|
|
82
|
+
pulseOnUrgent?: boolean;
|
|
83
|
+
/** Shows a thin ring pulsing outward from the dial's own edge and fading out while running (a beacon/live-indicator-style halo). @default false */
|
|
84
|
+
showRunningRipples?: boolean;
|
|
85
|
+
/** Play a short built-in chime on completion. @default false */
|
|
86
|
+
playSoundOnComplete?: boolean;
|
|
87
|
+
/** Overrides the built-in sound with a consumer-supplied audio URL. */
|
|
88
|
+
soundUrl?: string;
|
|
89
|
+
/** Volume 0-1 for the completion sound. @default 0.5 */
|
|
90
|
+
soundVolume?: number;
|
|
91
|
+
/** Show a browser Notification on completion (permission requested lazily on first `start()`, fails silently if denied/unsupported). @default false */
|
|
92
|
+
notifyOnComplete?: boolean;
|
|
93
|
+
/** Notification title when `notifyOnComplete` is set. @default 'Timer done' */
|
|
94
|
+
notificationTitle?: string;
|
|
95
|
+
/** Notification body when `notifyOnComplete` is set. Falls back to `label` when it's a string. */
|
|
96
|
+
notificationBody?: string;
|
|
97
|
+
className?: string;
|
|
98
|
+
style?: CSSProperties;
|
|
99
|
+
classNames?: SlotClassNames<TimerSlots>;
|
|
100
|
+
styles?: SlotStyles<TimerSlots>;
|
|
101
|
+
}
|
|
@@ -78,6 +78,8 @@ export { default as NumberInput } from './common/numberInput';
|
|
|
78
78
|
export * from './common/numberInput';
|
|
79
79
|
export { default as OptionPicker } from './common/optionPicker';
|
|
80
80
|
export * from './common/optionPicker';
|
|
81
|
+
export { default as OtpInput } from './common/otpInput';
|
|
82
|
+
export * from './common/otpInput';
|
|
81
83
|
export { default as Overscroll } from './common/overscroll';
|
|
82
84
|
export * from './common/overscroll';
|
|
83
85
|
export { default as Popover } from './common/popover';
|
|
@@ -129,6 +131,8 @@ export { default as TimeInput } from './common/timeInput';
|
|
|
129
131
|
export * from './common/timeInput';
|
|
130
132
|
export { default as Timeline } from './common/timeline';
|
|
131
133
|
export * from './common/timeline';
|
|
134
|
+
export { default as Timer } from './common/timer';
|
|
135
|
+
export * from './common/timer';
|
|
132
136
|
export { default as ToastProvider } from './common/toast';
|
|
133
137
|
export * from './common/toast';
|
|
134
138
|
export { default as Tooltip } from './common/tooltip';
|
package/docs/CLAUDE.md
CHANGED
|
@@ -135,6 +135,7 @@ Slot keys per component are documented in each component's doc file below.
|
|
|
135
135
|
@Modal.md
|
|
136
136
|
@NumberInput.md
|
|
137
137
|
@OptionPicker.md
|
|
138
|
+
@OtpInput.md
|
|
138
139
|
@Overscroll.md
|
|
139
140
|
@Popover.md
|
|
140
141
|
@ProgressBar.md
|
|
@@ -159,6 +160,7 @@ Slot keys per component are documented in each component's doc file below.
|
|
|
159
160
|
@Tilt.md
|
|
160
161
|
@TimeInput.md
|
|
161
162
|
@Timeline.md
|
|
163
|
+
@Timer.md
|
|
162
164
|
@Toast.md
|
|
163
165
|
@Tooltip.md
|
|
164
166
|
@VirtualList.md
|
package/docs/ConfigProvider.md
CHANGED
|
@@ -60,8 +60,8 @@ So a global default only fills in props a given instance left out — any instan
|
|
|
60
60
|
|
|
61
61
|
Each entry is a `Partial<...Props>`, so any of that component's props can be defaulted:
|
|
62
62
|
|
|
63
|
-
- **Form inputs:** `Input` · `Textarea` · `NumberInput` · `Dropdown` · `InputDropdown` · `Checkbox` · `Switch` · `RadioGroup` · `DatePicker` · `OptionPicker`
|
|
64
|
-
- **Display:** `Button` · `ActionButtons` · `Badge` · `Chip` · `Card` · `SectionHeader` · `Skeleton` · `Accordion` · `Divider`
|
|
63
|
+
- **Form inputs:** `Input` · `Textarea` · `NumberInput` · `Dropdown` · `InputDropdown` · `Checkbox` · `Switch` · `RadioGroup` · `DatePicker` · `OptionPicker` · `OtpInput`
|
|
64
|
+
- **Display:** `Button` · `ActionButtons` · `Badge` · `Chip` · `Card` · `SectionHeader` · `Skeleton` · `Accordion` · `Divider` · `Timer`
|
|
65
65
|
- **Overlays:** `ConfirmModal`
|
|
66
66
|
|
|
67
67
|
```tsx
|
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/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`
|