digitojs 1.0.1 → 1.2.1
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/CHANGELOG.md +15 -0
- package/README.md +129 -56
- package/dist/adapters/alpine.d.ts.map +1 -1
- package/dist/adapters/alpine.js +82 -58
- package/dist/adapters/alpine.js.map +1 -1
- package/dist/adapters/react.d.ts +9 -0
- package/dist/adapters/react.d.ts.map +1 -1
- package/dist/adapters/react.js +45 -4
- package/dist/adapters/react.js.map +1 -1
- package/dist/adapters/svelte.d.ts +21 -14
- package/dist/adapters/svelte.d.ts.map +1 -1
- package/dist/adapters/svelte.js +50 -4
- package/dist/adapters/svelte.js.map +1 -1
- package/dist/adapters/vanilla.d.ts +6 -0
- package/dist/adapters/vanilla.d.ts.map +1 -1
- package/dist/adapters/vanilla.js +48 -13
- package/dist/adapters/vanilla.js.map +1 -1
- package/dist/adapters/vue.d.ts +2 -0
- package/dist/adapters/vue.d.ts.map +1 -1
- package/dist/adapters/vue.js +35 -4
- package/dist/adapters/vue.js.map +1 -1
- package/dist/adapters/web-component.d.ts +9 -0
- package/dist/adapters/web-component.d.ts.map +1 -1
- package/dist/adapters/web-component.js +55 -12
- package/dist/adapters/web-component.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/machine.d.ts +3 -0
- package/dist/core/machine.d.ts.map +1 -1
- package/dist/core/machine.js +31 -3
- package/dist/core/machine.js.map +1 -1
- package/dist/core/timer.d.ts +8 -0
- package/dist/core/timer.d.ts.map +1 -1
- package/dist/core/timer.js +14 -0
- package/dist/core/timer.js.map +1 -1
- package/dist/core/types.d.ts +30 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/digito-wc.min.js +3 -3
- package/dist/digito-wc.min.js.map +3 -3
- package/dist/digito.min.js +2 -2
- package/dist/digito.min.js.map +3 -3
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/package.json +9 -3
- package/src/adapters/alpine.ts +75 -50
- package/src/adapters/react.tsx +50 -5
- package/src/adapters/svelte.ts +70 -17
- package/src/adapters/vanilla.ts +50 -15
- package/src/adapters/vue.ts +32 -3
- package/src/adapters/web-component.ts +53 -13
- package/src/core/index.ts +1 -1
- package/src/core/machine.ts +31 -3
- package/src/core/timer.ts +15 -0
- package/src/core/types.ts +30 -2
- package/src/index.ts +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.2.0] — 2026-03-14
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `Delete` key — clears the current slot value and keeps focus in place (distinct from Backspace, which moves back)
|
|
10
|
+
- `defaultValue` option — pre-fills slots on mount without triggering `onComplete` or `onChange`. Filtered through `type` and `pattern`; exposed as the `default-value` HTML attribute
|
|
11
|
+
- `readOnly` mode — blocks mutations while allowing focus and navigation; no opacity and cursor change unlike `disabled`; `aria-readonly="true"` on the hidden input; togglable at runtime via `setReadOnly(bool)`
|
|
12
|
+
- `data-complete`, `data-invalid`, `data-disabled`, `data-readonly` — boolean presence attributes on the wrapper, updated on every state change. Compatible with Tailwind `data-*` variants and CSS attribute selectors
|
|
13
|
+
|
|
14
|
+
## [1.0.1] — 2026-03-14
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Live demo URL updated to `https://digitojs.vercel.app` in README
|
|
19
|
+
|
|
5
20
|
## [1.0.0] — 2026-03-14
|
|
6
21
|
|
|
7
22
|
### Added
|
package/README.md
CHANGED
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
|
|
25
25
|
## Overview
|
|
26
26
|
|
|
27
|
-
Digito is a fully-featured, zero-dependency OTP input library for the web. It handles SMS autofill, password managers, smart paste, screen readers, countdown timers, and resend flows
|
|
27
|
+
Digito is a fully-featured, zero-dependency OTP input library for the web. It handles SMS autofill, password managers, smart paste, screen readers, countdown timers, and resend flows without requiring you to patch or work around any of them.
|
|
28
28
|
|
|
29
|
-
Most OTP libraries render one `<input>` per slot. This breaks native autofill, confuses screen readers, and creates complex focus-juggling logic. Digito instead renders **one transparent input** that captures all keyboard and paste events, with purely visual `<div>` elements mirroring the state. The browser sees a single real field
|
|
29
|
+
Most OTP libraries render one `<input>` per slot. This breaks native autofill, confuses screen readers, and creates complex focus-juggling logic. Digito instead renders **one transparent input** that captures all keyboard and paste events, with purely visual `<div>` elements mirroring the state. The browser sees a single real field, so everything works as expected.
|
|
30
30
|
|
|
31
31
|
The core is a **pure state machine** with no DOM or framework dependencies, wrapped by six independent adapters: Vanilla JS, React, Vue, Svelte, Alpine.js, and Web Component.
|
|
32
32
|
|
|
@@ -50,6 +50,9 @@ The core is a **pure state machine** with no DOM or framework dependencies, wrap
|
|
|
50
50
|
- **`onComplete` deferral** — fires after DOM sync; cancellable without clearing slot values
|
|
51
51
|
- **Native form support** — `name` option wires the hidden input into `<form>` / `FormData`
|
|
52
52
|
- **Fully accessible** — single ARIA-labelled input, `inputMode`, `autocomplete="one-time-code"`, all visual elements `aria-hidden`
|
|
53
|
+
- **`readOnly` mode** — field stays focusable and copyable but blocks all mutations; semantically distinct from `disabled`
|
|
54
|
+
- **`defaultValue`** — uncontrolled pre-fill applied once on mount without triggering `onComplete`
|
|
55
|
+
- **Data attribute state hooks** — `data-complete`, `data-invalid`, `data-disabled`, `data-readonly` set on the wrapper across all adapters for Tailwind `data-*` variant and plain CSS attribute styling
|
|
53
56
|
- **CDN-ready** — two IIFE bundles for no-build usage
|
|
54
57
|
|
|
55
58
|
---
|
|
@@ -60,23 +63,23 @@ The core is a **pure state machine** with no DOM or framework dependencies, wrap
|
|
|
60
63
|
|---|---|---|---|
|
|
61
64
|
| Pure headless state machine | ✅ | ✗ | ✗ |
|
|
62
65
|
| Web OTP API (SMS intercept) | ✅ | ✗ | ✗ |
|
|
63
|
-
| Built-in
|
|
64
|
-
| Built-in timer + resend | ✅ | ✗ | ✗ |
|
|
66
|
+
| Built-in timer and resend | ✅ | ✗ | ✗ |
|
|
65
67
|
| Masked mode | ✅ | ✗ | ✗ |
|
|
66
|
-
| Visual separators | ✅ | ✗ | ✗ |
|
|
67
68
|
| Programmatic API (`setError`, `setSuccess`, `reset`, `focus`) | ✅ | ✗ | ✗ |
|
|
68
69
|
| Haptic + sound feedback | ✅ | ✗ | ✗ |
|
|
69
70
|
| `blurOnComplete` (auto-advance) | ✅ | ✗ | ✗ |
|
|
70
71
|
| `onInvalidChar` callback | ✅ | ✗ | ✗ |
|
|
72
|
+
| `readOnly` mode (focusable, no mutations) | ✅ | ✗ | ✗ |
|
|
73
|
+
| Data attribute state hooks | ✅ | ✗ | ✗ |
|
|
71
74
|
| Vanilla JS | ✅ | ✗ | ✗ |
|
|
72
75
|
| Vue | ✅ | ✗ | ✗ |
|
|
73
76
|
| Svelte | ✅ | ✗ | ✗ |
|
|
74
77
|
| Alpine.js | ✅ | ✗ | ✗ |
|
|
75
78
|
| Web Component | ✅ | ✗ | ✗ |
|
|
79
|
+
| React | ✅ | ✅ | ✅ |
|
|
76
80
|
| Single hidden input | ✅ | ✅ | ✗ |
|
|
77
81
|
| Fake caret | ✅ | ✅ | ✗ |
|
|
78
82
|
| Password manager guard | ✅ | ✅ | ✗ |
|
|
79
|
-
| React | ✅ | ✅ | ✅ |
|
|
80
83
|
| Zero dependencies | ✅ | ✅ | ✗ |
|
|
81
84
|
| TypeScript | ✅ | ✅ | ✅ |
|
|
82
85
|
|
|
@@ -123,6 +126,8 @@ yarn add digitojs
|
|
|
123
126
|
|
|
124
127
|
Digito injects the slot inputs, styles, countdown badge, and resend button automatically. Nothing else to configure.
|
|
125
128
|
|
|
129
|
+
> **Note:** `verify(code)`, `sendOTP()`, and similar functions used throughout the examples are placeholder names, so replace them with your own API calls or application logic.
|
|
130
|
+
|
|
126
131
|
---
|
|
127
132
|
|
|
128
133
|
## Usage
|
|
@@ -140,6 +145,8 @@ Digito injects the slot inputs, styles, countdown badge, and resend button autom
|
|
|
140
145
|
| Native form submission | `name: 'otp_code'` |
|
|
141
146
|
| Async verification with lock | `setDisabled(true/false)` around API call |
|
|
142
147
|
| Auto-advance after entry | `blurOnComplete: true` |
|
|
148
|
+
| Pre-fill on mount (uncontrolled) | `defaultValue: '123456'` |
|
|
149
|
+
| Display-only / read-only field | `readOnly: true` |
|
|
143
150
|
|
|
144
151
|
---
|
|
145
152
|
|
|
@@ -152,6 +159,8 @@ Digito injects the slot inputs, styles, countdown badge, and resend button autom
|
|
|
152
159
|
data-type="numeric"
|
|
153
160
|
data-timer="60"
|
|
154
161
|
data-resend="30"
|
|
162
|
+
data-separator-after="2,4"
|
|
163
|
+
data-separator="—"
|
|
155
164
|
></div>
|
|
156
165
|
|
|
157
166
|
<script type="module">
|
|
@@ -172,7 +181,7 @@ Digito injects the slot inputs, styles, countdown badge, and resend button autom
|
|
|
172
181
|
</script>
|
|
173
182
|
```
|
|
174
183
|
|
|
175
|
-
**Custom timer UI** — pass `onTick` to suppress the built-in
|
|
184
|
+
**Custom timer UI** — pass `onTick` to suppress the built-in timer UI and drive your own:
|
|
176
185
|
|
|
177
186
|
```js
|
|
178
187
|
const [otp] = initDigito('.digito-wrapper', {
|
|
@@ -197,7 +206,7 @@ export function OTPInput() {
|
|
|
197
206
|
})
|
|
198
207
|
|
|
199
208
|
return (
|
|
200
|
-
<div style={{ position: 'relative', display: 'inline-flex', gap: 10 }}>
|
|
209
|
+
<div {...otp.wrapperProps} style={{ position: 'relative', display: 'inline-flex', gap: 10 }}>
|
|
201
210
|
<HiddenOTPInput {...otp.hiddenInputProps} />
|
|
202
211
|
|
|
203
212
|
{otp.slotValues.map((_, i) => {
|
|
@@ -241,7 +250,7 @@ const otp = useOTP({ length: 6, onComplete: (code) => verify(code) })
|
|
|
241
250
|
</script>
|
|
242
251
|
|
|
243
252
|
<template>
|
|
244
|
-
<div style="position: relative; display: inline-flex; gap: 10px">
|
|
253
|
+
<div v-bind="otp.wrapperAttrs.value" style="position: relative; display: inline-flex; gap: 10px">
|
|
245
254
|
<input
|
|
246
255
|
:ref="(el) => (otp.inputRef.value = el as HTMLInputElement)"
|
|
247
256
|
v-bind="otp.hiddenInputAttrs"
|
|
@@ -252,18 +261,22 @@ const otp = useOTP({ length: 6, onComplete: (code) => verify(code) })
|
|
|
252
261
|
@focus="otp.onFocus"
|
|
253
262
|
@blur="otp.onBlur"
|
|
254
263
|
/>
|
|
255
|
-
<
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
264
|
+
<template v-for="(char, i) in otp.slotValues.value" :key="i">
|
|
265
|
+
<span
|
|
266
|
+
v-if="otp.separatorAfter.value && i === otp.separatorAfter.value"
|
|
267
|
+
aria-hidden="true"
|
|
268
|
+
>{{ otp.separator.value }}</span>
|
|
269
|
+
<div
|
|
270
|
+
class="slot"
|
|
271
|
+
:class="{
|
|
272
|
+
'is-active': i === otp.activeSlot.value && otp.isFocused.value,
|
|
273
|
+
'is-filled': !!char,
|
|
274
|
+
'is-error': otp.hasError.value,
|
|
275
|
+
}"
|
|
276
|
+
>
|
|
277
|
+
{{ char || otp.placeholder }}
|
|
278
|
+
</div>
|
|
279
|
+
</template>
|
|
267
280
|
</div>
|
|
268
281
|
</template>
|
|
269
282
|
```
|
|
@@ -287,20 +300,23 @@ code.value = '' // resets the field reactively
|
|
|
287
300
|
const otp = useOTP({ length: 6, onComplete: (code) => verify(code) })
|
|
288
301
|
</script>
|
|
289
302
|
|
|
290
|
-
<div style="position: relative; display: inline-flex; gap: 10px">
|
|
303
|
+
<div {...$otp.wrapperAttrs} style="position: relative; display: inline-flex; gap: 10px">
|
|
291
304
|
<input
|
|
292
305
|
use:otp.action
|
|
293
306
|
style="position: absolute; inset: 0; opacity: 0; z-index: 1"
|
|
294
307
|
/>
|
|
295
308
|
|
|
296
309
|
{#each $otp.slotValues as char, i}
|
|
310
|
+
{#if $otp.separatorAfter && i === $otp.separatorAfter}
|
|
311
|
+
<span aria-hidden="true">{$otp.separator}</span>
|
|
312
|
+
{/if}
|
|
297
313
|
<div
|
|
298
314
|
class="slot"
|
|
299
315
|
class:is-active={i === $otp.activeSlot}
|
|
300
316
|
class:is-filled={!!char}
|
|
301
317
|
class:is-error={$otp.hasError}
|
|
302
318
|
>
|
|
303
|
-
{char}
|
|
319
|
+
{char || otp.placeholder}
|
|
304
320
|
</div>
|
|
305
321
|
{/each}
|
|
306
322
|
</div>
|
|
@@ -414,6 +430,7 @@ const otp = useOTP(options)
|
|
|
414
430
|
| Property | Type | Description |
|
|
415
431
|
|---|---|---|
|
|
416
432
|
| `hiddenInputProps` | `object` | Spread onto the `<input>` or use `<HiddenOTPInput>` |
|
|
433
|
+
| `wrapperProps` | `object` | Spread onto the wrapper `<div>` — carries `data-*` state attributes |
|
|
417
434
|
| `slotValues` | `string[]` | Current character per slot (`''` = empty) |
|
|
418
435
|
| `activeSlot` | `number` | Zero-based index of the focused slot |
|
|
419
436
|
| `isComplete` | `boolean` | All slots filled |
|
|
@@ -421,13 +438,17 @@ const otp = useOTP(options)
|
|
|
421
438
|
| `isDisabled` | `boolean` | Disabled state active |
|
|
422
439
|
| `isFocused` | `boolean` | Hidden input has browser focus |
|
|
423
440
|
| `timerSeconds` | `number` | Remaining countdown seconds |
|
|
441
|
+
| `separatorAfter` | `number \| number[]` | Separator position(s) for JSX rendering |
|
|
442
|
+
| `separator` | `string` | Separator character to render |
|
|
424
443
|
| `getSlotProps(i)` | `(number) => SlotRenderProps` | Full render metadata for slot `i` |
|
|
425
444
|
| `getCode()` | `() => string` | Joined code string |
|
|
426
445
|
| `reset()` | `() => void` | Clear all slots, restart timer |
|
|
427
446
|
| `setError(bool)` | `(boolean) => void` | Toggle error state |
|
|
428
|
-
| `setDisabled(bool)` | `(boolean) => void` | Toggle disabled state |
|
|
429
447
|
| `focus(i)` | `(number) => void` | Move focus to slot |
|
|
430
448
|
|
|
449
|
+
> **Note:** React does not expose a `setDisabled()` method. Pass `disabled` as an option prop instead — update it via your component's state (e.g. `const [isVerifying, setIsVerifying] = useState(false)` → `useOTP({ disabled: isVerifying })`).
|
|
450
|
+
|
|
451
|
+
|
|
431
452
|
**`SlotRenderProps`** (from `getSlotProps(i)`):
|
|
432
453
|
|
|
433
454
|
| Prop | Type | Description |
|
|
@@ -460,7 +481,8 @@ const otp = useOTP(options)
|
|
|
460
481
|
|
|
461
482
|
| Property | Type | Description |
|
|
462
483
|
|---|---|---|
|
|
463
|
-
| `hiddenInputAttrs` | `object` | Bind with `v-bind` |
|
|
484
|
+
| `hiddenInputAttrs` | `object` | Bind with `v-bind` on the hidden `<input>` |
|
|
485
|
+
| `wrapperAttrs` | `Ref<object>` | Bind with `v-bind` on the wrapper element — carries `data-*` state attributes |
|
|
464
486
|
| `inputRef` | `Ref<HTMLInputElement \| null>` | Bind with `:ref` |
|
|
465
487
|
| `slotValues` | `Ref<string[]>` | Current slot values |
|
|
466
488
|
| `activeSlot` | `Ref<number>` | Focused slot index |
|
|
@@ -472,6 +494,9 @@ const otp = useOTP(options)
|
|
|
472
494
|
| `isDisabled` | `Ref<boolean>` | Disabled state active |
|
|
473
495
|
| `masked` | `Ref<boolean>` | Masked mode active |
|
|
474
496
|
| `maskChar` | `Ref<string>` | Configured mask glyph |
|
|
497
|
+
| `separatorAfter` | `Ref<number \| number[]>` | Separator position(s) for template rendering |
|
|
498
|
+
| `separator` | `Ref<string>` | Separator character to render |
|
|
499
|
+
| `placeholder` | `string` | Placeholder character for empty slots |
|
|
475
500
|
| `onKeydown` | handler | Bind with `@keydown` |
|
|
476
501
|
| `onChange` | handler | Bind with `@input` |
|
|
477
502
|
| `onPaste` | handler | Bind with `@paste` |
|
|
@@ -499,12 +524,16 @@ const otp = useOTP(options)
|
|
|
499
524
|
|---|---|---|
|
|
500
525
|
| `subscribe` | Store | Subscribe to full OTP state |
|
|
501
526
|
| `action` | Svelte action | Use with `use:otp.action` on the hidden `<input>` |
|
|
527
|
+
| `wrapperAttrs` | `Readable<object>` | Spread with `{...$otp.wrapperAttrs}` on wrapper — carries `data-*` state attributes |
|
|
502
528
|
| `value` | Derived store | Joined code string |
|
|
503
529
|
| `isComplete` | Derived store | All slots filled |
|
|
504
530
|
| `hasError` | Derived store | Error state |
|
|
505
531
|
| `activeSlot` | Derived store | Focused slot index |
|
|
506
532
|
| `timerSeconds` | Writable store | Remaining countdown |
|
|
507
533
|
| `masked` | Writable store | Masked mode |
|
|
534
|
+
| `separatorAfter` | Writable store | Separator position(s) for template rendering |
|
|
535
|
+
| `separator` | Writable store | Separator character to render |
|
|
536
|
+
| `placeholder` | `string` | Placeholder character for empty slots |
|
|
508
537
|
| `getCode()` | `() => string` | Joined code |
|
|
509
538
|
| `reset()` | `() => void` | Clear and reset |
|
|
510
539
|
| `setError(bool)` | `(boolean) => void` | Toggle error |
|
|
@@ -535,6 +564,8 @@ otp.moveFocusTo(index)
|
|
|
535
564
|
otp.setError(bool)
|
|
536
565
|
otp.resetState()
|
|
537
566
|
otp.setDisabled(bool)
|
|
567
|
+
otp.setReadOnly(bool) // toggle readOnly at runtime
|
|
568
|
+
otp.clearSlot(slotIndex) // clear slot in place (Delete key semantics)
|
|
538
569
|
otp.cancelPendingComplete() // cancel onComplete without clearing slots
|
|
539
570
|
|
|
540
571
|
// Query
|
|
@@ -543,7 +574,7 @@ otp.getCode()
|
|
|
543
574
|
otp.getSnapshot()
|
|
544
575
|
otp.getState() // alias for getSnapshot() — Zustand-style
|
|
545
576
|
|
|
546
|
-
// Subscription (XState
|
|
577
|
+
// Subscription (XState style)
|
|
547
578
|
const unsub = otp.subscribe(state => render(state))
|
|
548
579
|
unsub()
|
|
549
580
|
```
|
|
@@ -567,7 +598,7 @@ timer.reset() // restore to totalSeconds without restarting
|
|
|
567
598
|
timer.restart() // reset + start
|
|
568
599
|
```
|
|
569
600
|
|
|
570
|
-
If `totalSeconds <= 0`, `onExpire` fires immediately on `start()`. `start()` is idempotent
|
|
601
|
+
If `totalSeconds <= 0`, `onExpire` fires immediately on `start()`. The `start()` method is idempotent, so calling it twice never double-ticks.
|
|
571
602
|
|
|
572
603
|
---
|
|
573
604
|
|
|
@@ -579,13 +610,27 @@ import { filterChar, filterString } from 'digitojs'
|
|
|
579
610
|
filterChar('A', 'numeric') // → '' (rejected)
|
|
580
611
|
filterChar('5', 'numeric') // → '5'
|
|
581
612
|
filterChar('A', 'alphanumeric') // → 'A'
|
|
582
|
-
filterChar('Z', 'any', /^[A-Z]$/)
|
|
613
|
+
filterChar('Z', 'any', /^[A-Z]$/) // → 'Z' (pattern overrides type)
|
|
583
614
|
|
|
584
615
|
filterString('84AB91', 'numeric') // → '8491'
|
|
585
616
|
```
|
|
586
617
|
|
|
587
618
|
---
|
|
588
619
|
|
|
620
|
+
### `formatCountdown(totalSeconds)` — Utility
|
|
621
|
+
|
|
622
|
+
Formats a second count as a `m:ss` string. Used internally by the built-in timer UI; exported for custom timer displays.
|
|
623
|
+
|
|
624
|
+
```ts
|
|
625
|
+
import { formatCountdown } from 'digitojs/core'
|
|
626
|
+
|
|
627
|
+
formatCountdown(65) // → "1:05"
|
|
628
|
+
formatCountdown(30) // → "0:30"
|
|
629
|
+
formatCountdown(9) // → "0:09"
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
---
|
|
633
|
+
|
|
589
634
|
## Configuration Options
|
|
590
635
|
|
|
591
636
|
All options are accepted by every adapter unless otherwise noted.
|
|
@@ -599,7 +644,7 @@ All options are accepted by every adapter unless otherwise noted.
|
|
|
599
644
|
| `onComplete` | `(code: string) => void` | — | Fired when all slots are filled |
|
|
600
645
|
| `onExpire` | `() => void` | — | Fired when countdown reaches zero |
|
|
601
646
|
| `onResend` | `() => void` | — | Fired when resend is triggered |
|
|
602
|
-
| `onTick` | `(remaining: number) => void` | — | Fired every second; suppresses built-in
|
|
647
|
+
| `onTick` | `(remaining: number) => void` | — | Fired every second; suppresses built-in timer UI (vanilla) |
|
|
603
648
|
| `onInvalidChar` | `(char: string, index: number) => void` | — | Fired when a typed character is rejected |
|
|
604
649
|
| `onChange` | `(code: string) => void` | — | Fired on every user interaction |
|
|
605
650
|
| `onFocus` | `() => void` | — | Fired when hidden input gains focus |
|
|
@@ -616,6 +661,8 @@ All options are accepted by every adapter unless otherwise noted.
|
|
|
616
661
|
| `separatorAfter` | `number \| number[]` | — | 1-based slot index/indices to insert a visual separator after |
|
|
617
662
|
| `separator` | `string` | `'—'` | Separator character to render |
|
|
618
663
|
| `disabled` | `boolean` | `false` | Disable all input on mount |
|
|
664
|
+
| `readOnly` | `boolean` | `false` | Block mutations while keeping the field focusable and copyable |
|
|
665
|
+
| `defaultValue` | `string` | — | Uncontrolled pre-fill applied once on mount; does not trigger `onComplete` |
|
|
619
666
|
| `haptic` | `boolean` | `true` | `navigator.vibrate(10)` on completion and error |
|
|
620
667
|
| `sound` | `boolean` | `false` | Play 880 Hz tone via Web Audio on completion |
|
|
621
668
|
|
|
@@ -630,27 +677,28 @@ Set on `.digito-wrapper` (vanilla) or `digito-input` (web component) to theme th
|
|
|
630
677
|
```css
|
|
631
678
|
.digito-wrapper {
|
|
632
679
|
/* Dimensions */
|
|
633
|
-
--digito-size: 56px;
|
|
634
|
-
--digito-gap: 12px;
|
|
635
|
-
--digito-radius: 10px;
|
|
636
|
-
--digito-font-size: 24px;
|
|
680
|
+
--digito-size: 56px; /* slot width + height */
|
|
681
|
+
--digito-gap: 12px; /* gap between slots */
|
|
682
|
+
--digito-radius: 10px; /* slot border radius */
|
|
683
|
+
--digito-font-size: 24px; /* digit font size */
|
|
637
684
|
|
|
638
685
|
/* Colors */
|
|
639
|
-
--digito-color: #0A0A0A;
|
|
640
|
-
--digito-bg: #FAFAFA;
|
|
641
|
-
--digito-bg-filled: #FFFFFF;
|
|
642
|
-
--digito-border-color: #E5E5E5;
|
|
643
|
-
--digito-active-color: #3D3D3D;
|
|
644
|
-
--digito-error-color: #FB2C36;
|
|
645
|
-
--digito-success-color: #00C950;
|
|
646
|
-
--digito-caret-color: #3D3D3D;
|
|
647
|
-
--digito-timer-color: #5C5C5C;
|
|
648
|
-
|
|
649
|
-
/* Placeholder &
|
|
650
|
-
--digito-placeholder-color: #D3D3D3;
|
|
651
|
-
--digito-placeholder-size: 16px;
|
|
652
|
-
--digito-separator-color: #A1A1A1;
|
|
653
|
-
--digito-separator-size: 18px;
|
|
686
|
+
--digito-color: #0A0A0A; /* digit text color */
|
|
687
|
+
--digito-bg: #FAFAFA; /* empty slot background */
|
|
688
|
+
--digito-bg-filled: #FFFFFF; /* filled slot background */
|
|
689
|
+
--digito-border-color: #E5E5E5; /* default slot border */
|
|
690
|
+
--digito-active-color: #3D3D3D; /* active border + ring */
|
|
691
|
+
--digito-error-color: #FB2C36; /* error border + ring */
|
|
692
|
+
--digito-success-color: #00C950; /* success border + ring */
|
|
693
|
+
--digito-caret-color: #3D3D3D; /* fake caret color */
|
|
694
|
+
--digito-timer-color: #5C5C5C; /* footer text */
|
|
695
|
+
|
|
696
|
+
/* Placeholder, separator & mask */
|
|
697
|
+
--digito-placeholder-color: #D3D3D3; /* placeholder glyph color */
|
|
698
|
+
--digito-placeholder-size: 16px; /* placeholder glyph size */
|
|
699
|
+
--digito-separator-color: #A1A1A1; /* separator text color */
|
|
700
|
+
--digito-separator-size: 18px; /* separator font size */
|
|
701
|
+
--digito-masked-size: 16px; /* mask glyph font size */
|
|
654
702
|
}
|
|
655
703
|
```
|
|
656
704
|
|
|
@@ -663,6 +711,7 @@ Set on `.digito-wrapper` (vanilla) or `digito-input` (web component) to theme th
|
|
|
663
711
|
| `.digito-slot.is-filled` | Slot contains a character |
|
|
664
712
|
| `.digito-slot.is-error` | Error state is active |
|
|
665
713
|
| `.digito-slot.is-success` | Success state is active |
|
|
714
|
+
| `.digito-slot.is-disabled` | Field is disabled |
|
|
666
715
|
| `.digito-caret` | The blinking caret inside the active empty slot |
|
|
667
716
|
| `.digito-timer` | The "Code expires in…" countdown row |
|
|
668
717
|
| `.digito-timer-badge` | The red pill countdown badge |
|
|
@@ -670,6 +719,28 @@ Set on `.digito-wrapper` (vanilla) or `digito-input` (web component) to theme th
|
|
|
670
719
|
| `.digito-resend-btn` | The resend chip button |
|
|
671
720
|
| `.digito-separator` | The visual separator between slot groups |
|
|
672
721
|
|
|
722
|
+
### Data Attribute State Hooks
|
|
723
|
+
|
|
724
|
+
All adapters set boolean presence attributes on the wrapper element that mirror the current field state, with no extra JS needed. It works with Tailwind `data-*` variants and plain CSS attribute selectors.
|
|
725
|
+
|
|
726
|
+
| Attribute | Set when |
|
|
727
|
+
|---|---|
|
|
728
|
+
| `data-complete` | All slots are filled |
|
|
729
|
+
| `data-invalid` | Error state is active |
|
|
730
|
+
| `data-disabled` | Field is disabled |
|
|
731
|
+
| `data-readonly` | Field is in read-only mode |
|
|
732
|
+
|
|
733
|
+
```css
|
|
734
|
+
/* Plain CSS */
|
|
735
|
+
.digito-wrapper[data-complete] { border-color: var(--digito-success-color); }
|
|
736
|
+
.digito-wrapper[data-invalid] { animation: shake 0.2s; }
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
```html
|
|
740
|
+
<!-- Tailwind -->
|
|
741
|
+
<div class="digito-wrapper data-[complete]:ring-green-500 data-[invalid]:ring-red-500"></div>
|
|
742
|
+
```
|
|
743
|
+
|
|
673
744
|
---
|
|
674
745
|
|
|
675
746
|
## Accessibility
|
|
@@ -677,14 +748,14 @@ Set on `.digito-wrapper` (vanilla) or `digito-input` (web component) to theme th
|
|
|
677
748
|
Digito is built with accessibility as a first-class concern:
|
|
678
749
|
|
|
679
750
|
- **Single ARIA-labelled input** — the hidden input carries `aria-label="Enter your N-digit code"` (or `N-character code` for non-numeric types). Screen readers announce one field, not six.
|
|
680
|
-
- **All visual elements `aria-hidden`** — slot divs, separators, caret, and timer
|
|
751
|
+
- **All visual elements `aria-hidden`** — slot divs, separators, caret, and timer UI are hidden from the accessibility tree.
|
|
681
752
|
- **`inputMode`** — set to `"numeric"` or `"text"` based on `type`, triggering the correct mobile keyboard.
|
|
682
753
|
- **`autocomplete="one-time-code"`** — enables native SMS autofill on iOS and Android.
|
|
683
754
|
- **Anti-interference** — `spellcheck="false"`, `autocorrect="off"`, `autocapitalize="off"` prevent browser UI from interfering.
|
|
684
755
|
- **`maxLength`** — constrains native input to `length`.
|
|
685
756
|
- **`type="password"` in masked mode** — triggers the OS password keyboard on mobile.
|
|
686
757
|
- **Native form integration** — the `name` option wires the hidden input into `<form>` and `FormData`, compatible with any form submission approach.
|
|
687
|
-
- **Keyboard navigation** — full keyboard support (`←`, `→`, `Backspace`, `Tab`). No mouse required.
|
|
758
|
+
- **Keyboard navigation** — full keyboard support (`←`, `→`, `Backspace`, `Delete`, `Tab`). No mouse required.
|
|
688
759
|
|
|
689
760
|
---
|
|
690
761
|
|
|
@@ -694,6 +765,7 @@ Digito is built with accessibility as a first-class concern:
|
|
|
694
765
|
|---|---|
|
|
695
766
|
| `0–9` / `a–z` / `A–Z` | Fill current slot and advance focus |
|
|
696
767
|
| `Backspace` | Clear current slot; step back if already empty |
|
|
768
|
+
| `Delete` | Clear current slot; focus stays in place |
|
|
697
769
|
| `←` | Move focus one slot left |
|
|
698
770
|
| `→` | Move focus one slot right |
|
|
699
771
|
| `Cmd/Ctrl+V` | Smart paste from cursor slot, wrapping if needed |
|
|
@@ -707,10 +779,10 @@ Digito is built with accessibility as a first-class concern:
|
|
|
707
779
|
|
|
708
780
|
| Browser | Support |
|
|
709
781
|
|---|---|
|
|
710
|
-
| Chrome / Edge |
|
|
711
|
-
| Firefox |
|
|
712
|
-
| Safari / iOS Safari |
|
|
713
|
-
| Android Chrome |
|
|
782
|
+
| Chrome / Edge | Full support including Web OTP API |
|
|
783
|
+
| Firefox | Full support |
|
|
784
|
+
| Safari / iOS Safari | Full support including SMS autofill |
|
|
785
|
+
| Android Chrome | Full support including Web OTP API |
|
|
714
786
|
|
|
715
787
|
**Frameworks (peer deps, all optional):**
|
|
716
788
|
|
|
@@ -732,7 +804,7 @@ Digito is built with accessibility as a first-class concern:
|
|
|
732
804
|
|
|
733
805
|
```
|
|
734
806
|
digitojs → Vanilla JS adapter + core utilities
|
|
735
|
-
digitojs/core → createDigito, createTimer, filterChar, filterString (no DOM)
|
|
807
|
+
digitojs/core → createDigito, createTimer, formatCountdown, filterChar, filterString (no DOM)
|
|
736
808
|
digitojs/react → useOTP hook + HiddenOTPInput + SlotRenderProps
|
|
737
809
|
digitojs/vue → useOTP composable
|
|
738
810
|
digitojs/svelte → useOTP store + action
|
|
@@ -744,10 +816,11 @@ All exports are fully typed. Core utilities are also available from the main ent
|
|
|
744
816
|
|
|
745
817
|
```ts
|
|
746
818
|
import { createDigito, createTimer, filterChar, filterString } from 'digitojs'
|
|
819
|
+
import { formatCountdown } from 'digitojs/core'
|
|
747
820
|
```
|
|
748
821
|
|
|
749
822
|
---
|
|
750
823
|
|
|
751
824
|
## License
|
|
752
825
|
|
|
753
|
-
MIT © [Olawale Balo](https://
|
|
826
|
+
MIT © [Olawale Balo](https://x.com/theolawalemi)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"alpine.d.ts","sourceRoot":"","sources":["../../src/adapters/alpine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;
|
|
1
|
+
{"version":3,"file":"alpine.d.ts","sourceRoot":"","sources":["../../src/adapters/alpine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAWH,yEAAyE;AACzE,KAAK,mBAAmB,GAAG;IACzB,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAA;IAClB,wEAAwE;IACxE,KAAK,EAAO,MAAM,CAAA;IAClB,qEAAqE;IACrE,SAAS,EAAG,MAAM,EAAE,CAAA;CACrB,CAAA;AAED,kEAAkE;AAClE,KAAK,wBAAwB,GAAG;IAC9B,iFAAiF;IACjF,QAAQ,EAAO,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;IACxC,qFAAqF;IACrF,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,KAAK,IAAI,CAAA;IAC7E,wFAAwF;IACxF,OAAO,EAAQ,CAAC,EAAE,EAAE,MAAM,IAAI,KAAK,IAAI,CAAA;IACvC,qFAAqF;IACrF,MAAM,EAAS,CAAC,EAAE,EAAE,MAAM,IAAI,KAAK,IAAI,CAAA;CACxC,CAAA;AAED,kFAAkF;AAClF,KAAK,YAAY,GAAG;IAClB,SAAS,EAAE,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,mBAAmB,EAAE,SAAS,EAAE,wBAAwB,KAAK;QAAE,OAAO,IAAI,IAAI,CAAA;KAAE,GAAG,IAAI,KACrH,IAAI,CAAA;CACV,CAAA;AAwCD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,YAAY,KAAG,IA0jBnD,CAAA"}
|