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.
Files changed (59) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +129 -56
  3. package/dist/adapters/alpine.d.ts.map +1 -1
  4. package/dist/adapters/alpine.js +82 -58
  5. package/dist/adapters/alpine.js.map +1 -1
  6. package/dist/adapters/react.d.ts +9 -0
  7. package/dist/adapters/react.d.ts.map +1 -1
  8. package/dist/adapters/react.js +45 -4
  9. package/dist/adapters/react.js.map +1 -1
  10. package/dist/adapters/svelte.d.ts +21 -14
  11. package/dist/adapters/svelte.d.ts.map +1 -1
  12. package/dist/adapters/svelte.js +50 -4
  13. package/dist/adapters/svelte.js.map +1 -1
  14. package/dist/adapters/vanilla.d.ts +6 -0
  15. package/dist/adapters/vanilla.d.ts.map +1 -1
  16. package/dist/adapters/vanilla.js +48 -13
  17. package/dist/adapters/vanilla.js.map +1 -1
  18. package/dist/adapters/vue.d.ts +2 -0
  19. package/dist/adapters/vue.d.ts.map +1 -1
  20. package/dist/adapters/vue.js +35 -4
  21. package/dist/adapters/vue.js.map +1 -1
  22. package/dist/adapters/web-component.d.ts +9 -0
  23. package/dist/adapters/web-component.d.ts.map +1 -1
  24. package/dist/adapters/web-component.js +55 -12
  25. package/dist/adapters/web-component.js.map +1 -1
  26. package/dist/core/index.d.ts +1 -1
  27. package/dist/core/index.d.ts.map +1 -1
  28. package/dist/core/index.js +1 -1
  29. package/dist/core/index.js.map +1 -1
  30. package/dist/core/machine.d.ts +3 -0
  31. package/dist/core/machine.d.ts.map +1 -1
  32. package/dist/core/machine.js +31 -3
  33. package/dist/core/machine.js.map +1 -1
  34. package/dist/core/timer.d.ts +8 -0
  35. package/dist/core/timer.d.ts.map +1 -1
  36. package/dist/core/timer.js +14 -0
  37. package/dist/core/timer.js.map +1 -1
  38. package/dist/core/types.d.ts +30 -2
  39. package/dist/core/types.d.ts.map +1 -1
  40. package/dist/digito-wc.min.js +3 -3
  41. package/dist/digito-wc.min.js.map +3 -3
  42. package/dist/digito.min.js +2 -2
  43. package/dist/digito.min.js.map +3 -3
  44. package/dist/index.d.ts +0 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +0 -1
  47. package/dist/index.js.map +1 -1
  48. package/package.json +9 -3
  49. package/src/adapters/alpine.ts +75 -50
  50. package/src/adapters/react.tsx +50 -5
  51. package/src/adapters/svelte.ts +70 -17
  52. package/src/adapters/vanilla.ts +50 -15
  53. package/src/adapters/vue.ts +32 -3
  54. package/src/adapters/web-component.ts +53 -13
  55. package/src/core/index.ts +1 -1
  56. package/src/core/machine.ts +31 -3
  57. package/src/core/timer.ts +15 -0
  58. package/src/core/types.ts +30 -2
  59. 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 without requiring you to patch or workaround any of them.
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 everything works as expected.
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 styles | ✅ | ✗ | ✗ |
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 footer and drive your own:
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
- <div
256
- v-for="(char, i) in otp.slotValues.value"
257
- :key="i"
258
- class="slot"
259
- :class="{
260
- 'is-active': i === otp.activeSlot.value && otp.isFocused.value,
261
- 'is-filled': !!char,
262
- 'is-error': otp.hasError.value,
263
- }"
264
- >
265
- {{ char }}
266
- </div>
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/Zustand-style)
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 calling it twice never double-ticks.
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]$/) // → 'Z' (pattern overrides type)
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 footer (vanilla) |
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; /* slot width + height */
634
- --digito-gap: 12px; /* gap between slots */
635
- --digito-radius: 10px; /* slot border radius */
636
- --digito-font-size: 24px; /* digit font size */
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; /* digit text color */
640
- --digito-bg: #FAFAFA; /* empty slot background */
641
- --digito-bg-filled: #FFFFFF; /* filled slot background */
642
- --digito-border-color: #E5E5E5; /* default slot border */
643
- --digito-active-color: #3D3D3D; /* active border + ring */
644
- --digito-error-color: #FB2C36; /* error border + ring */
645
- --digito-success-color: #00C950; /* success border + ring */
646
- --digito-caret-color: #3D3D3D; /* fake caret color */
647
- --digito-timer-color: #5C5C5C; /* footer text */
648
-
649
- /* Placeholder & separator */
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 footer are hidden from the accessibility tree.
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 | Full support including Web OTP API |
711
- | Firefox | Full support |
712
- | Safari / iOS Safari | Full support including SMS autofill |
713
- | Android Chrome | Full support including Web OTP API |
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://github.com/theolawalemi)
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;AAUH,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;AAoDD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,YAAY,KAAG,IAshBnD,CAAA"}
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"}