digitojs 1.0.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +21 -0
  3. package/README.md +753 -0
  4. package/dist/adapters/alpine.d.ts +71 -0
  5. package/dist/adapters/alpine.d.ts.map +1 -0
  6. package/dist/adapters/alpine.js +560 -0
  7. package/dist/adapters/alpine.js.map +1 -0
  8. package/dist/adapters/react.d.ts +223 -0
  9. package/dist/adapters/react.d.ts.map +1 -0
  10. package/dist/adapters/react.js +337 -0
  11. package/dist/adapters/react.js.map +1 -0
  12. package/dist/adapters/svelte.d.ts +139 -0
  13. package/dist/adapters/svelte.d.ts.map +1 -0
  14. package/dist/adapters/svelte.js +295 -0
  15. package/dist/adapters/svelte.js.map +1 -0
  16. package/dist/adapters/vanilla.d.ts +110 -0
  17. package/dist/adapters/vanilla.d.ts.map +1 -0
  18. package/dist/adapters/vanilla.js +650 -0
  19. package/dist/adapters/vanilla.js.map +1 -0
  20. package/dist/adapters/vue.d.ts +163 -0
  21. package/dist/adapters/vue.d.ts.map +1 -0
  22. package/dist/adapters/vue.js +298 -0
  23. package/dist/adapters/vue.js.map +1 -0
  24. package/dist/adapters/web-component.d.ts +192 -0
  25. package/dist/adapters/web-component.d.ts.map +1 -0
  26. package/dist/adapters/web-component.js +832 -0
  27. package/dist/adapters/web-component.js.map +1 -0
  28. package/dist/core/feedback.d.ts +26 -0
  29. package/dist/core/feedback.d.ts.map +1 -0
  30. package/dist/core/feedback.js +47 -0
  31. package/dist/core/feedback.js.map +1 -0
  32. package/dist/core/filter.d.ts +24 -0
  33. package/dist/core/filter.d.ts.map +1 -0
  34. package/dist/core/filter.js +47 -0
  35. package/dist/core/filter.js.map +1 -0
  36. package/dist/core/index.d.ts +16 -0
  37. package/dist/core/index.d.ts.map +1 -0
  38. package/dist/core/index.js +15 -0
  39. package/dist/core/index.js.map +1 -0
  40. package/dist/core/machine.d.ts +67 -0
  41. package/dist/core/machine.d.ts.map +1 -0
  42. package/dist/core/machine.js +328 -0
  43. package/dist/core/machine.js.map +1 -0
  44. package/dist/core/timer.d.ts +24 -0
  45. package/dist/core/timer.d.ts.map +1 -0
  46. package/dist/core/timer.js +67 -0
  47. package/dist/core/timer.js.map +1 -0
  48. package/dist/core/types.d.ts +162 -0
  49. package/dist/core/types.d.ts.map +1 -0
  50. package/dist/core/types.js +10 -0
  51. package/dist/core/types.js.map +1 -0
  52. package/dist/digito-wc.min.js +254 -0
  53. package/dist/digito-wc.min.js.map +7 -0
  54. package/dist/digito.min.js +91 -0
  55. package/dist/digito.min.js.map +7 -0
  56. package/dist/index.d.ts +18 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +25 -0
  59. package/dist/index.js.map +1 -0
  60. package/package.json +109 -0
  61. package/src/adapters/alpine.ts +666 -0
  62. package/src/adapters/react.tsx +603 -0
  63. package/src/adapters/svelte.ts +444 -0
  64. package/src/adapters/vanilla.ts +810 -0
  65. package/src/adapters/vue.ts +462 -0
  66. package/src/adapters/web-component.ts +858 -0
  67. package/src/core/feedback.ts +44 -0
  68. package/src/core/filter.ts +48 -0
  69. package/src/core/index.ts +16 -0
  70. package/src/core/machine.ts +373 -0
  71. package/src/core/timer.ts +75 -0
  72. package/src/core/types.ts +167 -0
  73. package/src/index.ts +51 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * digito/alpine
3
+ * ─────────────────────────────────────────────────────────────────────────────
4
+ * Alpine.js adapter — x-digito directive (single hidden-input architecture)
5
+ *
6
+ * Usage:
7
+ * import Alpine from 'alpinejs'
8
+ * import { DigitoAlpine } from 'digito/alpine'
9
+ * Alpine.plugin(DigitoAlpine)
10
+ * Alpine.start()
11
+ *
12
+ * <div x-digito="{ length: 6, type: 'numeric', timer: 60 }"></div>
13
+ *
14
+ * // With separator:
15
+ * <div x-digito="{ length: 6, separatorAfter: 2 }"></div>
16
+ *
17
+ * // Access public API:
18
+ * el._digito.setDisabled(true)
19
+ * el._digito.setError(true)
20
+ * el._digito.reset()
21
+ * el._digito.getCode()
22
+ *
23
+ * @author Olawale Balo — Product Designer + Design Engineer
24
+ * @license MIT
25
+ */
26
+ /** Shape of the data object Alpine passes to every directive handler. */
27
+ type AlpineDirectiveData = {
28
+ /** The raw expression string from `x-digito="{ ... }"`. */
29
+ expression: string;
30
+ /** Directive value segment, e.g. `x-digito:value`. Unused by Digito. */
31
+ value: string;
32
+ /** Modifier segments, e.g. `x-digito.modifier`. Unused by Digito. */
33
+ modifiers: string[];
34
+ };
35
+ /** Utility functions Alpine passes to every directive handler. */
36
+ type AlpineDirectiveUtilities = {
37
+ /** Evaluate an Alpine expression in the component scope and return its value. */
38
+ evaluate: (expr: string) => unknown;
39
+ /** Return a function that evaluates `expr` reactively via Alpine's effect system. */
40
+ evaluateLater: (expr: string) => (callback: (value: unknown) => void) => void;
41
+ /** Register a teardown function called when the component or directive is destroyed. */
42
+ cleanup: (fn: () => void) => void;
43
+ /** Run `fn` reactively; re-runs whenever its Alpine reactive dependencies change. */
44
+ effect: (fn: () => void) => void;
45
+ };
46
+ /** Minimal interface for the Alpine.js instance — only the parts Digito needs. */
47
+ type AlpinePlugin = {
48
+ directive: (name: string, handler: (el: HTMLElement, data: AlpineDirectiveData, utilities: AlpineDirectiveUtilities) => {
49
+ cleanup(): void;
50
+ } | void) => void;
51
+ };
52
+ /**
53
+ * Alpine.js plugin that registers the `x-digito` directive.
54
+ *
55
+ * Install once before `Alpine.start()`:
56
+ * ```js
57
+ * import Alpine from 'alpinejs'
58
+ * import { DigitoAlpine } from 'digito/alpine'
59
+ * Alpine.plugin(DigitoAlpine)
60
+ * Alpine.start()
61
+ * ```
62
+ *
63
+ * The directive accepts the same options as `DigitoOptions` plus `separatorAfter`,
64
+ * `separator`, `masked`, and `maskChar`. Options are evaluated via Alpine's
65
+ * `evaluate()` so reactive expressions and Alpine `$data` references work.
66
+ *
67
+ * @param Alpine - The Alpine.js global passed automatically by `Alpine.plugin()`.
68
+ */
69
+ export declare const DigitoAlpine: (Alpine: AlpinePlugin) => void;
70
+ export {};
71
+ //# sourceMappingURL=alpine.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,560 @@
1
+ /**
2
+ * digito/alpine
3
+ * ─────────────────────────────────────────────────────────────────────────────
4
+ * Alpine.js adapter — x-digito directive (single hidden-input architecture)
5
+ *
6
+ * Usage:
7
+ * import Alpine from 'alpinejs'
8
+ * import { DigitoAlpine } from 'digito/alpine'
9
+ * Alpine.plugin(DigitoAlpine)
10
+ * Alpine.start()
11
+ *
12
+ * <div x-digito="{ length: 6, type: 'numeric', timer: 60 }"></div>
13
+ *
14
+ * // With separator:
15
+ * <div x-digito="{ length: 6, separatorAfter: 2 }"></div>
16
+ *
17
+ * // Access public API:
18
+ * el._digito.setDisabled(true)
19
+ * el._digito.setError(true)
20
+ * el._digito.reset()
21
+ * el._digito.getCode()
22
+ *
23
+ * @author Olawale Balo — Product Designer + Design Engineer
24
+ * @license MIT
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
+ }
37
+ // ─────────────────────────────────────────────────────────────────────────────
38
+ // PLUGIN
39
+ // ─────────────────────────────────────────────────────────────────────────────
40
+ /**
41
+ * Alpine.js plugin that registers the `x-digito` directive.
42
+ *
43
+ * Install once before `Alpine.start()`:
44
+ * ```js
45
+ * import Alpine from 'alpinejs'
46
+ * import { DigitoAlpine } from 'digito/alpine'
47
+ * Alpine.plugin(DigitoAlpine)
48
+ * Alpine.start()
49
+ * ```
50
+ *
51
+ * The directive accepts the same options as `DigitoOptions` plus `separatorAfter`,
52
+ * `separator`, `masked`, and `maskChar`. Options are evaluated via Alpine's
53
+ * `evaluate()` so reactive expressions and Alpine `$data` references work.
54
+ *
55
+ * @param Alpine - The Alpine.js global passed automatically by `Alpine.plugin()`.
56
+ */
57
+ export const DigitoAlpine = (Alpine) => {
58
+ Alpine.directive('digito', (wrapperEl, { expression }, { evaluate }) => {
59
+ let options;
60
+ try {
61
+ options = (expression ? evaluate(expression) : {});
62
+ if (!options || typeof options !== 'object') {
63
+ console.error('[x-digito] expression did not return a plain object. Got:', options);
64
+ options = {};
65
+ }
66
+ }
67
+ catch (err) {
68
+ console.error('[x-digito] failed to evaluate expression:', err);
69
+ options = {};
70
+ }
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;
72
+ // Normalise separatorAfter to an array for consistent rendering
73
+ const separatorAfterPositions = Array.isArray(rawSepAfter) ? rawSepAfter : [rawSepAfter];
74
+ const digito = createDigito({ length, type, pattern, pasteTransformer, onInvalidChar, onComplete, onExpire, onResend, haptic, sound });
75
+ let isDisabled = initialDisabled;
76
+ let successState = false;
77
+ // ── Build DOM ─────────────────────────────────────────────────────────────
78
+ while (wrapperEl.firstChild)
79
+ wrapperEl.removeChild(wrapperEl.firstChild);
80
+ wrapperEl.style.cssText = 'position:relative;display:inline-flex;gap:var(--digito-gap,12px);align-items:center;flex-wrap:wrap';
81
+ const slotEls = [];
82
+ const caretEls = [];
83
+ for (let i = 0; i < length; i++) {
84
+ const slotEl = document.createElement('div');
85
+ slotEl.style.cssText = [
86
+ `width:var(--digito-size,56px)`,
87
+ `height:var(--digito-size,56px)`,
88
+ `border:1px solid var(--digito-border-color,#E5E5E5)`,
89
+ `border-radius:var(--digito-radius,10px)`,
90
+ `font-size:var(--digito-font-size,24px)`,
91
+ `font-weight:600`,
92
+ `display:flex`,
93
+ `align-items:center`,
94
+ `justify-content:center`,
95
+ `background:var(--digito-bg,#FAFAFA)`,
96
+ `color:var(--digito-color,#0A0A0A)`,
97
+ `position:relative`,
98
+ `cursor:text`,
99
+ `transition:border-color 150ms ease,box-shadow 150ms ease`,
100
+ `font-family:inherit`,
101
+ `user-select:none`,
102
+ ].join(';');
103
+ slotEl.setAttribute('aria-hidden', 'true');
104
+ const caretEl = document.createElement('div');
105
+ caretEl.style.cssText = 'position:absolute;width:2px;height:52%;background:var(--digito-caret-color,#3D3D3D);border-radius:1px;animation:digito-alpine-blink 1s step-start infinite;pointer-events:none;display:none';
106
+ slotEl.appendChild(caretEl);
107
+ caretEls.push(caretEl);
108
+ slotEls.push(slotEl);
109
+ wrapperEl.appendChild(slotEl);
110
+ // Separator — purely decorative, aria-hidden, no effect on value
111
+ if (separatorAfterPositions.some(pos => pos > 0 && i === pos - 1)) {
112
+ const sepEl = document.createElement('div');
113
+ sepEl.setAttribute('aria-hidden', 'true');
114
+ sepEl.style.cssText = [
115
+ `display:flex`,
116
+ `align-items:center`,
117
+ `justify-content:center`,
118
+ `color:var(--digito-separator-color,#A1A1A1)`,
119
+ `font-size:var(--digito-separator-size,18px)`,
120
+ `font-weight:400`,
121
+ `user-select:none`,
122
+ `flex-shrink:0`,
123
+ ].join(';');
124
+ sepEl.textContent = separator;
125
+ wrapperEl.appendChild(sepEl);
126
+ }
127
+ }
128
+ // Inject styles once — caret keyframes + timer/resend classes matching vanilla
129
+ if (!document.getElementById('digito-alpine-styles')) {
130
+ const s = document.createElement('style');
131
+ s.id = 'digito-alpine-styles';
132
+ s.textContent = [
133
+ '@keyframes digito-alpine-blink{0%,100%{opacity:1}50%{opacity:0}}',
134
+ '.digito-timer{display:flex;align-items:center;gap:8px;font-size:14px;padding:20px 0 0}',
135
+ '.digito-timer-label{color:var(--digito-timer-color,#5C5C5C);font-size:14px}',
136
+ '.digito-timer-badge{display:inline-flex;align-items:center;background:color-mix(in srgb,var(--digito-error-color,#FB2C36) 10%,transparent);color:var(--digito-error-color,#FB2C36);font-weight:500;font-size:14px;padding:2px 10px;border-radius:99px;height:24px}',
137
+ '.digito-resend{display:none;align-items:center;gap:8px;font-size:14px;color:var(--digito-timer-color,#5C5C5C);padding:20px 0 0}',
138
+ '.digito-resend.is-visible{display:flex}',
139
+ '.digito-resend-btn{display:inline-flex;align-items:center;background:#E8E8E8;border:none;padding:2px 10px;border-radius:99px;color:#0A0A0A;font-weight:500;font-size:14px;transition:background 150ms ease;cursor:pointer;height:24px}',
140
+ '.digito-resend-btn:hover{background:#E5E5E5}',
141
+ '.digito-resend-btn:disabled{color:#A1A1A1;cursor:not-allowed;background:#F5F5F5}',
142
+ ].join('');
143
+ document.head.appendChild(s);
144
+ }
145
+ // Hidden real input
146
+ const hiddenInputEl = document.createElement('input');
147
+ hiddenInputEl.type = masked ? 'password' : 'text';
148
+ hiddenInputEl.inputMode = type === 'numeric' ? 'numeric' : 'text';
149
+ hiddenInputEl.autocomplete = 'one-time-code';
150
+ hiddenInputEl.maxLength = length;
151
+ hiddenInputEl.disabled = isDisabled;
152
+ hiddenInputEl.setAttribute('aria-label', `Enter your ${length}-${type === 'numeric' ? 'digit' : 'character'} code`);
153
+ hiddenInputEl.setAttribute('spellcheck', 'false');
154
+ hiddenInputEl.setAttribute('autocorrect', 'off');
155
+ hiddenInputEl.setAttribute('autocapitalize', 'off');
156
+ 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
+ if (inputName)
158
+ hiddenInputEl.name = inputName;
159
+ wrapperEl.appendChild(hiddenInputEl);
160
+ // ── Built-in timer + resend (mirrors vanilla adapter) ──────────────────────
161
+ let timerBadgeEl = null;
162
+ let resendActionBtn = null;
163
+ let mainCountdown = null;
164
+ let resendCountdown = null;
165
+ let builtInFooterEl = null;
166
+ let builtInResendRowEl = null;
167
+ if (timerSecs > 0) {
168
+ const shouldUseBuiltInFooter = !onTickProp;
169
+ if (shouldUseBuiltInFooter) {
170
+ builtInFooterEl = document.createElement('div');
171
+ builtInFooterEl.className = 'digito-timer';
172
+ const expiresLabel = document.createElement('span');
173
+ expiresLabel.className = 'digito-timer-label';
174
+ expiresLabel.textContent = 'Code expires in';
175
+ timerBadgeEl = document.createElement('span');
176
+ timerBadgeEl.className = 'digito-timer-badge';
177
+ timerBadgeEl.textContent = formatCountdown(timerSecs);
178
+ builtInFooterEl.appendChild(expiresLabel);
179
+ builtInFooterEl.appendChild(timerBadgeEl);
180
+ wrapperEl.insertAdjacentElement('afterend', builtInFooterEl);
181
+ builtInResendRowEl = document.createElement('div');
182
+ builtInResendRowEl.className = 'digito-resend';
183
+ const didntReceiveLabel = document.createElement('span');
184
+ didntReceiveLabel.textContent = 'Didn\u2019t receive the code?';
185
+ resendActionBtn = document.createElement('button');
186
+ resendActionBtn.className = 'digito-resend-btn';
187
+ resendActionBtn.textContent = 'Resend';
188
+ resendActionBtn.type = 'button';
189
+ builtInResendRowEl.appendChild(didntReceiveLabel);
190
+ builtInResendRowEl.appendChild(resendActionBtn);
191
+ builtInFooterEl.insertAdjacentElement('afterend', builtInResendRowEl);
192
+ }
193
+ mainCountdown = createTimer({
194
+ totalSeconds: timerSecs,
195
+ onTick: (remaining) => {
196
+ if (timerBadgeEl)
197
+ timerBadgeEl.textContent = formatCountdown(remaining);
198
+ onTickProp?.(remaining);
199
+ },
200
+ onExpire: () => {
201
+ if (builtInFooterEl)
202
+ builtInFooterEl.style.display = 'none';
203
+ if (builtInResendRowEl)
204
+ builtInResendRowEl.classList.add('is-visible');
205
+ onExpire?.();
206
+ },
207
+ });
208
+ mainCountdown.start();
209
+ if (shouldUseBuiltInFooter && resendActionBtn) {
210
+ resendActionBtn.addEventListener('click', () => {
211
+ if (!resendActionBtn || !timerBadgeEl || !builtInFooterEl || !builtInResendRowEl)
212
+ return;
213
+ builtInResendRowEl.classList.remove('is-visible');
214
+ builtInFooterEl.style.display = 'flex';
215
+ timerBadgeEl.textContent = formatCountdown(resendCooldown);
216
+ resendCountdown?.stop();
217
+ resendCountdown = createTimer({
218
+ totalSeconds: resendCooldown,
219
+ onTick: (r) => { if (timerBadgeEl)
220
+ timerBadgeEl.textContent = formatCountdown(r); },
221
+ onExpire: () => {
222
+ if (builtInFooterEl)
223
+ builtInFooterEl.style.display = 'none';
224
+ if (builtInResendRowEl)
225
+ builtInResendRowEl.classList.add('is-visible');
226
+ },
227
+ });
228
+ resendCountdown.start();
229
+ onResend?.();
230
+ });
231
+ }
232
+ }
233
+ // ── DOM sync ───────────────────────────────────────────────────────────────
234
+ /**
235
+ * Reconcile every visual slot div with the current core state.
236
+ * Called after every user action (input, keydown, paste, focus, click).
237
+ *
238
+ * The Alpine adapter uses inline styles exclusively — no shared stylesheet
239
+ * is injected, so each mounted element is fully self-contained. State
240
+ * priority order for slot appearance: disabled > error > complete > active > default.
241
+ *
242
+ * Font size and color are also set inline here to support placeholder
243
+ * character styling (`--digito-placeholder-size` / `--digito-placeholder-color`),
244
+ * since CSS `:not(.is-filled)` selectors are unavailable without a stylesheet.
245
+ */
246
+ function syncSlotsToDOM() {
247
+ const { slotValues, activeSlot, hasError } = digito.state;
248
+ const focused = document.activeElement === hiddenInputEl;
249
+ slotEls.forEach((slotEl, i) => {
250
+ const char = slotValues[i] ?? '';
251
+ const isActive = i === activeSlot && focused;
252
+ const isFilled = char.length === 1;
253
+ let textNode = slotEl.childNodes[1];
254
+ if (!textNode || textNode.nodeType !== Node.TEXT_NODE) {
255
+ textNode = document.createTextNode('');
256
+ slotEl.appendChild(textNode);
257
+ }
258
+ textNode.nodeValue = masked && char ? maskChar : char || placeholder;
259
+ slotEl.style.fontSize = isFilled
260
+ ? (masked ? 'var(--digito-masked-size,16px)' : 'var(--digito-font-size,24px)')
261
+ : 'var(--digito-placeholder-size,16px)';
262
+ slotEl.style.color = isFilled
263
+ ? 'var(--digito-color,#0A0A0A)'
264
+ : 'var(--digito-placeholder-color,#D3D3D3)';
265
+ const activeColor = 'var(--digito-active-color,#3D3D3D)';
266
+ const errorColor = 'var(--digito-error-color,#FB2C36)';
267
+ const successColor = 'var(--digito-success-color,#00C950)';
268
+ if (isDisabled) {
269
+ slotEl.style.opacity = '0.45';
270
+ slotEl.style.cursor = 'not-allowed';
271
+ slotEl.style.pointerEvents = 'none';
272
+ slotEl.style.borderColor = 'var(--digito-border-color,#E5E5E5)';
273
+ slotEl.style.boxShadow = 'none';
274
+ }
275
+ else if (hasError) {
276
+ slotEl.style.opacity = '';
277
+ slotEl.style.cursor = 'text';
278
+ slotEl.style.pointerEvents = '';
279
+ slotEl.style.borderColor = errorColor;
280
+ slotEl.style.boxShadow = `0 0 0 3px color-mix(in srgb,${errorColor} 12%,transparent)`;
281
+ }
282
+ else if (successState) {
283
+ slotEl.style.opacity = '';
284
+ slotEl.style.cursor = 'text';
285
+ slotEl.style.pointerEvents = '';
286
+ slotEl.style.borderColor = successColor;
287
+ slotEl.style.boxShadow = `0 0 0 3px color-mix(in srgb,${successColor} 12%,transparent)`;
288
+ }
289
+ else if (isActive) {
290
+ slotEl.style.opacity = '';
291
+ slotEl.style.cursor = 'text';
292
+ slotEl.style.pointerEvents = '';
293
+ slotEl.style.borderColor = activeColor;
294
+ slotEl.style.boxShadow = `0 0 0 3px color-mix(in srgb,${activeColor} 10%,transparent)`;
295
+ slotEl.style.background = 'var(--digito-bg-filled,#FFFFFF)';
296
+ }
297
+ else {
298
+ slotEl.style.opacity = '';
299
+ slotEl.style.cursor = 'text';
300
+ slotEl.style.pointerEvents = '';
301
+ slotEl.style.borderColor = 'var(--digito-border-color,#E5E5E5)';
302
+ slotEl.style.boxShadow = 'none';
303
+ slotEl.style.background = isFilled ? 'var(--digito-bg-filled,#FFFFFF)' : 'var(--digito-bg,#FAFAFA)';
304
+ }
305
+ caretEls[i].style.display = isActive && !isFilled && !isDisabled ? 'block' : 'none';
306
+ });
307
+ // Only update value when it actually differs — assigning the same string
308
+ // resets selectionStart/End in some browsers, clobbering the cursor.
309
+ const newValue = slotValues.join('');
310
+ if (hiddenInputEl.value !== newValue)
311
+ hiddenInputEl.value = newValue;
312
+ }
313
+ // ── Event handlers ─────────────────────────────────────────────────────────
314
+ hiddenInputEl.addEventListener('keydown', (e) => {
315
+ if (isDisabled)
316
+ return;
317
+ const pos = hiddenInputEl.selectionStart ?? 0;
318
+ if (e.key === 'Backspace') {
319
+ e.preventDefault();
320
+ digito.deleteChar(pos);
321
+ syncSlotsToDOM();
322
+ onChangeProp?.(digito.getCode());
323
+ const next = digito.state.activeSlot;
324
+ requestAnimationFrame(() => hiddenInputEl.setSelectionRange(next, next));
325
+ }
326
+ else if (e.key === 'ArrowLeft') {
327
+ e.preventDefault();
328
+ digito.moveFocusLeft(pos);
329
+ syncSlotsToDOM();
330
+ const next = digito.state.activeSlot;
331
+ requestAnimationFrame(() => hiddenInputEl.setSelectionRange(next, next));
332
+ }
333
+ else if (e.key === 'ArrowRight') {
334
+ e.preventDefault();
335
+ digito.moveFocusRight(pos);
336
+ syncSlotsToDOM();
337
+ const next = digito.state.activeSlot;
338
+ requestAnimationFrame(() => hiddenInputEl.setSelectionRange(next, next));
339
+ }
340
+ else if (e.key === 'Tab') {
341
+ if (e.shiftKey) {
342
+ if (pos === 0)
343
+ return;
344
+ e.preventDefault();
345
+ digito.moveFocusLeft(pos);
346
+ }
347
+ else {
348
+ if (!digito.state.slotValues[pos])
349
+ return;
350
+ if (pos >= length - 1)
351
+ return;
352
+ e.preventDefault();
353
+ digito.moveFocusRight(pos);
354
+ }
355
+ syncSlotsToDOM();
356
+ const next = digito.state.activeSlot;
357
+ requestAnimationFrame(() => hiddenInputEl.setSelectionRange(next, next));
358
+ }
359
+ });
360
+ hiddenInputEl.addEventListener('input', () => {
361
+ if (isDisabled)
362
+ return;
363
+ const raw = hiddenInputEl.value;
364
+ if (!raw) {
365
+ digito.resetState();
366
+ hiddenInputEl.value = '';
367
+ hiddenInputEl.setSelectionRange(0, 0);
368
+ syncSlotsToDOM();
369
+ onChangeProp?.('');
370
+ return;
371
+ }
372
+ const valid = filterString(raw, type, pattern).slice(0, length);
373
+ digito.resetState();
374
+ for (let i = 0; i < valid.length; i++)
375
+ digito.inputChar(i, valid[i]);
376
+ const next = Math.min(valid.length, length - 1);
377
+ hiddenInputEl.value = valid;
378
+ hiddenInputEl.setSelectionRange(next, next);
379
+ digito.moveFocusTo(next);
380
+ syncSlotsToDOM();
381
+ onChangeProp?.(digito.getCode());
382
+ if (blurOnComplete && digito.state.isComplete) {
383
+ requestAnimationFrame(() => hiddenInputEl.blur());
384
+ }
385
+ });
386
+ hiddenInputEl.addEventListener('paste', (e) => {
387
+ if (isDisabled)
388
+ return;
389
+ e.preventDefault();
390
+ const text = e.clipboardData?.getData('text') ?? '';
391
+ const pos = hiddenInputEl.selectionStart ?? 0;
392
+ digito.pasteString(pos, text);
393
+ const { slotValues, activeSlot } = digito.state;
394
+ hiddenInputEl.value = slotValues.join('');
395
+ hiddenInputEl.setSelectionRange(activeSlot, activeSlot);
396
+ syncSlotsToDOM();
397
+ onChangeProp?.(digito.getCode());
398
+ if (blurOnComplete && digito.state.isComplete) {
399
+ requestAnimationFrame(() => hiddenInputEl.blur());
400
+ }
401
+ });
402
+ hiddenInputEl.addEventListener('focus', () => {
403
+ onFocusProp?.();
404
+ requestAnimationFrame(() => {
405
+ const pos = digito.state.activeSlot;
406
+ const char = digito.state.slotValues[pos];
407
+ if (selectOnFocus && char) {
408
+ hiddenInputEl.setSelectionRange(pos, pos + 1);
409
+ }
410
+ else {
411
+ hiddenInputEl.setSelectionRange(pos, pos);
412
+ }
413
+ syncSlotsToDOM();
414
+ });
415
+ });
416
+ hiddenInputEl.addEventListener('blur', () => {
417
+ onBlurProp?.();
418
+ slotEls.forEach(s => { s.style.borderColor = 'var(--digito-border-color,#E5E5E5)'; s.style.boxShadow = 'none'; });
419
+ caretEls.forEach(c => { c.style.display = 'none'; });
420
+ });
421
+ hiddenInputEl.addEventListener('click', (e) => {
422
+ if (isDisabled)
423
+ return;
424
+ // click fires after the browser places cursor (always 0 due to font-size:1px).
425
+ // Coordinate hit-test determines which slot was visually clicked, then
426
+ // setSelectionRange overrides the browser's placement.
427
+ let rawSlot = slotEls.length - 1;
428
+ for (let i = 0; i < slotEls.length; i++) {
429
+ if (e.clientX <= slotEls[i].getBoundingClientRect().right) {
430
+ rawSlot = i;
431
+ break;
432
+ }
433
+ }
434
+ // Clamp to filled count so the visual active slot matches the actual cursor position.
435
+ const clickedSlot = Math.min(rawSlot, hiddenInputEl.value.length);
436
+ digito.moveFocusTo(clickedSlot);
437
+ const char = digito.state.slotValues[clickedSlot];
438
+ if (selectOnFocus && char) {
439
+ hiddenInputEl.setSelectionRange(clickedSlot, clickedSlot + 1);
440
+ }
441
+ else {
442
+ hiddenInputEl.setSelectionRange(clickedSlot, clickedSlot);
443
+ }
444
+ syncSlotsToDOM();
445
+ });
446
+ requestAnimationFrame(() => {
447
+ if (!isDisabled && autoFocus)
448
+ hiddenInputEl.focus();
449
+ hiddenInputEl.setSelectionRange(0, 0);
450
+ syncSlotsToDOM();
451
+ });
452
+ wrapperEl._digito = {
453
+ /** Returns the current joined code string (e.g. `"123456"`). */
454
+ getCode: () => digito.getCode(),
455
+ /** 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
+ },
462
+ /** 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
+ },
479
+ /** 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
+ },
497
+ /** Apply or clear the error state on all visual slots. */
498
+ setError: (isError) => {
499
+ if (isError)
500
+ successState = false;
501
+ digito.setError(isError);
502
+ syncSlotsToDOM();
503
+ },
504
+ /** Apply or clear the success state. On success, stops the timer and hides the footer. */
505
+ setSuccess: (isSuccess) => {
506
+ successState = isSuccess;
507
+ if (isSuccess) {
508
+ digito.setError(false);
509
+ mainCountdown?.stop();
510
+ resendCountdown?.stop();
511
+ if (builtInFooterEl)
512
+ builtInFooterEl.style.display = 'none';
513
+ if (builtInResendRowEl)
514
+ builtInResendRowEl.style.display = 'none';
515
+ }
516
+ syncSlotsToDOM();
517
+ },
518
+ /**
519
+ * Enable or disable the input at runtime.
520
+ * When disabled, all keyboard input, paste events, and click-to-focus
521
+ * are silently ignored. Re-enabling automatically restores focus.
522
+ */
523
+ setDisabled: (value) => {
524
+ isDisabled = value;
525
+ digito.setDisabled(value);
526
+ hiddenInputEl.disabled = value;
527
+ syncSlotsToDOM();
528
+ if (!value) {
529
+ requestAnimationFrame(() => {
530
+ hiddenInputEl.focus();
531
+ hiddenInputEl.setSelectionRange(digito.state.activeSlot, digito.state.activeSlot);
532
+ });
533
+ }
534
+ },
535
+ /**
536
+ * Programmatically move focus to a specific slot index.
537
+ * Focuses the hidden input and positions the cursor at `slotIndex`.
538
+ */
539
+ focus: (slotIndex) => {
540
+ if (isDisabled)
541
+ return;
542
+ digito.moveFocusTo(slotIndex);
543
+ hiddenInputEl.focus();
544
+ hiddenInputEl.setSelectionRange(slotIndex, slotIndex);
545
+ syncSlotsToDOM();
546
+ },
547
+ };
548
+ return {
549
+ /** Alpine calls this when the component is destroyed. Stops timers and removes footer elements. */
550
+ cleanup() {
551
+ mainCountdown?.stop();
552
+ resendCountdown?.stop();
553
+ builtInFooterEl?.remove();
554
+ builtInResendRowEl?.remove();
555
+ digito.resetState();
556
+ },
557
+ };
558
+ });
559
+ };
560
+ //# sourceMappingURL=alpine.js.map