digitojs 1.0.0 → 1.2.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/CHANGELOG.md +15 -0
- package/README.md +114 -41
- package/dist/adapters/alpine.d.ts.map +1 -1
- package/dist/adapters/alpine.js +65 -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 +16 -14
- package/dist/adapters/svelte.d.ts.map +1 -1
- package/dist/adapters/svelte.js +34 -4
- package/dist/adapters/svelte.js.map +1 -1
- package/dist/adapters/vanilla.d.ts.map +1 -1
- package/dist/adapters/vanilla.js +36 -12
- 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 +3 -0
- package/dist/adapters/web-component.d.ts.map +1 -1
- package/dist/adapters/web-component.js +37 -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 +2 -2
- package/dist/digito-wc.min.js.map +3 -3
- package/dist/digito.min.js +1 -1
- 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 +8 -2
- package/src/adapters/alpine.ts +58 -50
- package/src/adapters/react.tsx +50 -5
- package/src/adapters/svelte.ts +48 -16
- package/src/adapters/vanilla.ts +32 -14
- package/src/adapters/vue.ts +32 -3
- package/src/adapters/web-component.ts +35 -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 support — clears current slot, stays in place, no backward movement
|
|
10
|
+
- `defaultValue` option — uncontrolled pre-fill on mount, plain string, runs once, does not trigger `onComplete`
|
|
11
|
+
- `readOnly` mode — display and copy, no edit, visually and semantically distinct from `disabled`
|
|
12
|
+
- `data-complete`, `data-invalid`, `data-disabled`, `data-readonly` attributes on the wrapper element across all adapters — boolean presence attributes that mirror field state, compatible with Tailwind `data-*` variants and plain 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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<a href="https://
|
|
1
|
+
<a href="https://digitojs.vercel.app" target="_blank">
|
|
2
2
|
<img src="https://raw.githubusercontent.com/theolawalemi/digito/refs/heads/main/assets/banner.png" alt="Digito — Live Demo" width="100%" />
|
|
3
3
|
</a>
|
|
4
4
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
16
|
-
<a href="https://
|
|
16
|
+
<a href="https://digitojs.vercel.app"><img src="https://img.shields.io/badge/digitojs.vercel.app-0A0A0A?style=flat-square&logo=vercel&logoColor=white" alt="Live demo" /></a>
|
|
17
17
|
<a href="https://www.npmjs.com/package/digitojs"><img src="https://img.shields.io/npm/v/digitojs?style=flat-square&color=0A0A0A" alt="npm" /></a>
|
|
18
18
|
<a href="https://bundlephobia.com/package/digitojs"><img src="https://img.shields.io/bundlephobia/minzip/digitojs?style=flat-square&color=0A0A0A&label=gzip" /></a>
|
|
19
19
|
<img src="https://img.shields.io/badge/zero_dependencies-0A0A0A?style=flat-square" />
|
|
@@ -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
|
---
|
|
@@ -68,6 +71,8 @@ The core is a **pure state machine** with no DOM or framework dependencies, wrap
|
|
|
68
71
|
| Haptic + sound feedback | ✅ | ✗ | ✗ |
|
|
69
72
|
| `blurOnComplete` (auto-advance) | ✅ | ✗ | ✗ |
|
|
70
73
|
| `onInvalidChar` callback | ✅ | ✗ | ✗ |
|
|
74
|
+
| `readOnly` mode (focusable, no mutations) | ✅ | ✗ | ✗ |
|
|
75
|
+
| Data attribute state hooks | ✅ | ✗ | ✗ |
|
|
71
76
|
| Vanilla JS | ✅ | ✗ | ✗ |
|
|
72
77
|
| Vue | ✅ | ✗ | ✗ |
|
|
73
78
|
| Svelte | ✅ | ✗ | ✗ |
|
|
@@ -123,6 +128,8 @@ yarn add digitojs
|
|
|
123
128
|
|
|
124
129
|
Digito injects the slot inputs, styles, countdown badge, and resend button automatically. Nothing else to configure.
|
|
125
130
|
|
|
131
|
+
> **Note:** `verify(code)`, `sendOTP()`, and similar functions used throughout the examples are placeholder names — replace them with your own API calls or application logic.
|
|
132
|
+
|
|
126
133
|
---
|
|
127
134
|
|
|
128
135
|
## Usage
|
|
@@ -140,6 +147,8 @@ Digito injects the slot inputs, styles, countdown badge, and resend button autom
|
|
|
140
147
|
| Native form submission | `name: 'otp_code'` |
|
|
141
148
|
| Async verification with lock | `setDisabled(true/false)` around API call |
|
|
142
149
|
| Auto-advance after entry | `blurOnComplete: true` |
|
|
150
|
+
| Pre-fill on mount (uncontrolled) | `defaultValue: '123456'` |
|
|
151
|
+
| Display-only / read-only field | `readOnly: true` |
|
|
143
152
|
|
|
144
153
|
---
|
|
145
154
|
|
|
@@ -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
|
|
@@ -586,6 +617,20 @@ filterString('84AB91', 'numeric') // → '8491'
|
|
|
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.
|
|
@@ -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 — no extra JS needed. 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
|
|
@@ -684,7 +755,7 @@ Digito is built with accessibility as a first-class concern:
|
|
|
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 |
|
|
@@ -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,6 +816,7 @@ 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
|
---
|
|
@@ -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,IAyiBnD,CAAA"}
|
package/dist/adapters/alpine.js
CHANGED
|
@@ -23,17 +23,7 @@
|
|
|
23
23
|
* @author Olawale Balo — Product Designer + Design Engineer
|
|
24
24
|
* @license MIT
|
|
25
25
|
*/
|
|
26
|
-
import { createDigito, createTimer, filterString, } from '../core/index.js';
|
|
27
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
-
// HELPERS
|
|
29
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
-
function formatCountdown(totalSeconds) {
|
|
31
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
32
|
-
const seconds = totalSeconds % 60;
|
|
33
|
-
return minutes > 0
|
|
34
|
-
? `${minutes}:${String(seconds).padStart(2, '0')}`
|
|
35
|
-
: `0:${String(seconds).padStart(2, '0')}`;
|
|
36
|
-
}
|
|
26
|
+
import { createDigito, createTimer, filterString, formatCountdown, } from '../core/index.js';
|
|
37
27
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
38
28
|
// PLUGIN
|
|
39
29
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -68,10 +58,10 @@ export const DigitoAlpine = (Alpine) => {
|
|
|
68
58
|
console.error('[x-digito] failed to evaluate expression:', err);
|
|
69
59
|
options = {};
|
|
70
60
|
}
|
|
71
|
-
const { length = 6, type = 'numeric', timer: timerSecs = 0, disabled: initialDisabled = false, onComplete, onExpire, onResend, onTick: onTickProp, haptic = true, sound = false, pattern, pasteTransformer, onInvalidChar, onChange: onChangeProp, onFocus: onFocusProp, onBlur: onBlurProp, separatorAfter: rawSepAfter = 0, separator = '—', resendAfter: resendCooldown = 30, masked = false, maskChar = '\u25CF', autoFocus = true, name: inputName, placeholder = '', selectOnFocus = false, blurOnComplete = false, } = options;
|
|
61
|
+
const { length = 6, type = 'numeric', timer: timerSecs = 0, disabled: initialDisabled = false, onComplete, onExpire, onResend, onTick: onTickProp, haptic = true, sound = false, pattern, pasteTransformer, onInvalidChar, onChange: onChangeProp, onFocus: onFocusProp, onBlur: onBlurProp, separatorAfter: rawSepAfter = 0, separator = '—', resendAfter: resendCooldown = 30, masked = false, maskChar = '\u25CF', autoFocus = true, name: inputName, placeholder = '', selectOnFocus = false, blurOnComplete = false, defaultValue = '', readOnly: readOnlyOpt = false, } = options;
|
|
72
62
|
// Normalise separatorAfter to an array for consistent rendering
|
|
73
63
|
const separatorAfterPositions = Array.isArray(rawSepAfter) ? rawSepAfter : [rawSepAfter];
|
|
74
|
-
const digito = createDigito({ length, type, pattern, pasteTransformer, onInvalidChar, onComplete, onExpire, onResend, haptic, sound });
|
|
64
|
+
const digito = createDigito({ length, type, pattern, pasteTransformer, onInvalidChar, onComplete, onExpire, onResend, haptic, sound, readOnly: readOnlyOpt });
|
|
75
65
|
let isDisabled = initialDisabled;
|
|
76
66
|
let successState = false;
|
|
77
67
|
// ── Build DOM ─────────────────────────────────────────────────────────────
|
|
@@ -156,7 +146,19 @@ export const DigitoAlpine = (Alpine) => {
|
|
|
156
146
|
hiddenInputEl.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;opacity:0;border:none;outline:none;background:transparent;color:transparent;caret-color:transparent;z-index:1;cursor:text;font-size:1px';
|
|
157
147
|
if (inputName)
|
|
158
148
|
hiddenInputEl.name = inputName;
|
|
149
|
+
if (readOnlyOpt)
|
|
150
|
+
hiddenInputEl.setAttribute('aria-readonly', 'true');
|
|
159
151
|
wrapperEl.appendChild(hiddenInputEl);
|
|
152
|
+
// Apply defaultValue once on mount — no onComplete, no onChange
|
|
153
|
+
if (defaultValue) {
|
|
154
|
+
const filtered = filterString(defaultValue.slice(0, length), type, pattern);
|
|
155
|
+
if (filtered) {
|
|
156
|
+
for (let i = 0; i < filtered.length; i++)
|
|
157
|
+
digito.inputChar(i, filtered[i]);
|
|
158
|
+
digito.cancelPendingComplete();
|
|
159
|
+
hiddenInputEl.value = filtered;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
160
162
|
// ── Built-in timer + resend (mirrors vanilla adapter) ──────────────────────
|
|
161
163
|
let timerBadgeEl = null;
|
|
162
164
|
let resendActionBtn = null;
|
|
@@ -309,6 +311,10 @@ export const DigitoAlpine = (Alpine) => {
|
|
|
309
311
|
const newValue = slotValues.join('');
|
|
310
312
|
if (hiddenInputEl.value !== newValue)
|
|
311
313
|
hiddenInputEl.value = newValue;
|
|
314
|
+
wrapperEl.toggleAttribute('data-complete', digito.state.isComplete);
|
|
315
|
+
wrapperEl.toggleAttribute('data-invalid', digito.state.hasError);
|
|
316
|
+
wrapperEl.toggleAttribute('data-disabled', isDisabled);
|
|
317
|
+
wrapperEl.toggleAttribute('data-readonly', readOnlyOpt);
|
|
312
318
|
}
|
|
313
319
|
// ── Event handlers ─────────────────────────────────────────────────────────
|
|
314
320
|
hiddenInputEl.addEventListener('keydown', (e) => {
|
|
@@ -317,12 +323,23 @@ export const DigitoAlpine = (Alpine) => {
|
|
|
317
323
|
const pos = hiddenInputEl.selectionStart ?? 0;
|
|
318
324
|
if (e.key === 'Backspace') {
|
|
319
325
|
e.preventDefault();
|
|
326
|
+
if (readOnlyOpt)
|
|
327
|
+
return;
|
|
320
328
|
digito.deleteChar(pos);
|
|
321
329
|
syncSlotsToDOM();
|
|
322
330
|
onChangeProp?.(digito.getCode());
|
|
323
331
|
const next = digito.state.activeSlot;
|
|
324
332
|
requestAnimationFrame(() => hiddenInputEl.setSelectionRange(next, next));
|
|
325
333
|
}
|
|
334
|
+
else if (e.key === 'Delete') {
|
|
335
|
+
e.preventDefault();
|
|
336
|
+
if (readOnlyOpt)
|
|
337
|
+
return;
|
|
338
|
+
digito.clearSlot(pos);
|
|
339
|
+
syncSlotsToDOM();
|
|
340
|
+
onChangeProp?.(digito.getCode());
|
|
341
|
+
requestAnimationFrame(() => hiddenInputEl.setSelectionRange(pos, pos));
|
|
342
|
+
}
|
|
326
343
|
else if (e.key === 'ArrowLeft') {
|
|
327
344
|
e.preventDefault();
|
|
328
345
|
digito.moveFocusLeft(pos);
|
|
@@ -358,7 +375,7 @@ export const DigitoAlpine = (Alpine) => {
|
|
|
358
375
|
}
|
|
359
376
|
});
|
|
360
377
|
hiddenInputEl.addEventListener('input', () => {
|
|
361
|
-
if (isDisabled)
|
|
378
|
+
if (isDisabled || readOnlyOpt)
|
|
362
379
|
return;
|
|
363
380
|
const raw = hiddenInputEl.value;
|
|
364
381
|
if (!raw) {
|
|
@@ -384,7 +401,7 @@ export const DigitoAlpine = (Alpine) => {
|
|
|
384
401
|
}
|
|
385
402
|
});
|
|
386
403
|
hiddenInputEl.addEventListener('paste', (e) => {
|
|
387
|
-
if (isDisabled)
|
|
404
|
+
if (isDisabled || readOnlyOpt)
|
|
388
405
|
return;
|
|
389
406
|
e.preventDefault();
|
|
390
407
|
const text = e.clipboardData?.getData('text') ?? '';
|
|
@@ -449,51 +466,44 @@ export const DigitoAlpine = (Alpine) => {
|
|
|
449
466
|
hiddenInputEl.setSelectionRange(0, 0);
|
|
450
467
|
syncSlotsToDOM();
|
|
451
468
|
});
|
|
469
|
+
// ── Internal helpers (shared by public API methods below) ─────────────────
|
|
470
|
+
/** Tears down running timers and removes built-in footer elements from the DOM. */
|
|
471
|
+
function teardown() {
|
|
472
|
+
mainCountdown?.stop();
|
|
473
|
+
resendCountdown?.stop();
|
|
474
|
+
builtInFooterEl?.remove();
|
|
475
|
+
builtInResendRowEl?.remove();
|
|
476
|
+
}
|
|
477
|
+
/** Resets slot state, restarts timers, and restores focus — shared by reset() and resend(). */
|
|
478
|
+
function doReset() {
|
|
479
|
+
digito.resetState();
|
|
480
|
+
hiddenInputEl.value = '';
|
|
481
|
+
if (timerBadgeEl)
|
|
482
|
+
timerBadgeEl.textContent = formatCountdown(timerSecs);
|
|
483
|
+
if (builtInFooterEl)
|
|
484
|
+
builtInFooterEl.style.display = 'flex';
|
|
485
|
+
if (builtInResendRowEl)
|
|
486
|
+
builtInResendRowEl.classList.remove('is-visible');
|
|
487
|
+
resendCountdown?.stop();
|
|
488
|
+
mainCountdown?.restart();
|
|
489
|
+
if (!isDisabled)
|
|
490
|
+
hiddenInputEl.focus();
|
|
491
|
+
hiddenInputEl.setSelectionRange(0, 0);
|
|
492
|
+
syncSlotsToDOM();
|
|
493
|
+
}
|
|
494
|
+
// ── Public API on element ──────────────────────────────────────────────────
|
|
495
|
+
// Exposed on `el._digito` for programmatic control from Alpine components or
|
|
496
|
+
// external JavaScript. Mirrors the DigitoInstance interface from the vanilla adapter.
|
|
497
|
+
;
|
|
452
498
|
wrapperEl._digito = {
|
|
453
499
|
/** Returns the current joined code string (e.g. `"123456"`). */
|
|
454
500
|
getCode: () => digito.getCode(),
|
|
455
501
|
/** Stop timers and remove built-in footer elements. Call before removing the element. */
|
|
456
|
-
destroy: () =>
|
|
457
|
-
mainCountdown?.stop();
|
|
458
|
-
resendCountdown?.stop();
|
|
459
|
-
builtInFooterEl?.remove();
|
|
460
|
-
builtInResendRowEl?.remove();
|
|
461
|
-
},
|
|
502
|
+
destroy: () => teardown(),
|
|
462
503
|
/** Clear all slots, re-focus, reset to idle state, and restart the built-in timer. */
|
|
463
|
-
reset: () =>
|
|
464
|
-
digito.resetState();
|
|
465
|
-
hiddenInputEl.value = '';
|
|
466
|
-
if (timerBadgeEl)
|
|
467
|
-
timerBadgeEl.textContent = formatCountdown(timerSecs);
|
|
468
|
-
if (builtInFooterEl)
|
|
469
|
-
builtInFooterEl.style.display = 'flex';
|
|
470
|
-
if (builtInResendRowEl)
|
|
471
|
-
builtInResendRowEl.classList.remove('is-visible');
|
|
472
|
-
resendCountdown?.stop();
|
|
473
|
-
mainCountdown?.restart();
|
|
474
|
-
if (!isDisabled)
|
|
475
|
-
hiddenInputEl.focus();
|
|
476
|
-
hiddenInputEl.setSelectionRange(0, 0);
|
|
477
|
-
syncSlotsToDOM();
|
|
478
|
-
},
|
|
504
|
+
reset: () => doReset(),
|
|
479
505
|
/** Reset and fire the `onResend` callback. */
|
|
480
|
-
resend: () => {
|
|
481
|
-
digito.resetState();
|
|
482
|
-
hiddenInputEl.value = '';
|
|
483
|
-
if (timerBadgeEl)
|
|
484
|
-
timerBadgeEl.textContent = formatCountdown(timerSecs);
|
|
485
|
-
if (builtInFooterEl)
|
|
486
|
-
builtInFooterEl.style.display = 'flex';
|
|
487
|
-
if (builtInResendRowEl)
|
|
488
|
-
builtInResendRowEl.classList.remove('is-visible');
|
|
489
|
-
resendCountdown?.stop();
|
|
490
|
-
mainCountdown?.restart();
|
|
491
|
-
if (!isDisabled)
|
|
492
|
-
hiddenInputEl.focus();
|
|
493
|
-
hiddenInputEl.setSelectionRange(0, 0);
|
|
494
|
-
syncSlotsToDOM();
|
|
495
|
-
onResend?.();
|
|
496
|
-
},
|
|
506
|
+
resend: () => { doReset(); onResend?.(); },
|
|
497
507
|
/** Apply or clear the error state on all visual slots. */
|
|
498
508
|
setError: (isError) => {
|
|
499
509
|
if (isError)
|
|
@@ -548,10 +558,7 @@ export const DigitoAlpine = (Alpine) => {
|
|
|
548
558
|
return {
|
|
549
559
|
/** Alpine calls this when the component is destroyed. Stops timers and removes footer elements. */
|
|
550
560
|
cleanup() {
|
|
551
|
-
|
|
552
|
-
resendCountdown?.stop();
|
|
553
|
-
builtInFooterEl?.remove();
|
|
554
|
-
builtInResendRowEl?.remove();
|
|
561
|
+
teardown();
|
|
555
562
|
digito.resetState();
|
|
556
563
|
},
|
|
557
564
|
};
|