@nanahoshi/mona-ui 0.17.1 → 0.18.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.
@@ -7,13 +7,21 @@ import { isIterable } from 'rxjs/internal/util/isIterable';
7
7
 
8
8
  class AttributeBinderDirective {
9
9
  #element;
10
+ #previousKeys;
10
11
  #renderer;
11
12
  constructor() {
12
13
  this.#element = inject(ElementRef);
14
+ this.#previousKeys = new Set();
13
15
  this.#renderer = inject(Renderer2);
14
16
  this.attributes = input({}, { ...(ngDevMode ? { debugName: "attributes" } : /* istanbul ignore next */ {}), alias: "monaAttributeBinder" });
15
17
  effect(() => {
16
18
  const attributes = this.attributes();
19
+ const nextKeys = new Set(Object.keys(attributes));
20
+ for (const prevKey of this.#previousKeys) {
21
+ if (!nextKeys.has(prevKey)) {
22
+ this.#renderer.removeAttribute(this.#element.nativeElement, prevKey);
23
+ }
24
+ }
17
25
  for (const [key, value] of Object.entries(attributes)) {
18
26
  if (value == null || value === false) {
19
27
  this.#renderer.removeAttribute(this.#element.nativeElement, key);
@@ -23,6 +31,10 @@ class AttributeBinderDirective {
23
31
  this.#renderer.setAttribute(this.#element.nativeElement, key, attributeValue);
24
32
  }
25
33
  }
34
+ this.#previousKeys.clear();
35
+ for (const key of nextKeys) {
36
+ this.#previousKeys.add(key);
37
+ }
26
38
  });
27
39
  }
28
40
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AttributeBinderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
@@ -0,0 +1,817 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { inject, TemplateRef, Directive, computed, signal, viewChild, contentChild, input, output, model, effect, Component } from '@angular/core';
4
+ import { themeControlSurfaceClasses, AttributeBinderDirective } from '@nanahoshi/mona-ui/internal';
5
+ import { twMerge } from 'tailwind-merge';
6
+ import { cva } from 'class-variance-authority';
7
+
8
+ class OtpInputSeparatorTemplateDirective {
9
+ constructor() {
10
+ this.templateRef = inject((TemplateRef));
11
+ }
12
+ static ngTemplateContextGuard(_directive, _context) {
13
+ return true;
14
+ }
15
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: OtpInputSeparatorTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
16
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.6", type: OtpInputSeparatorTemplateDirective, isStandalone: true, selector: "ng-template[monaOtpInputSeparatorTemplate]", ngImport: i0 }); }
17
+ }
18
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: OtpInputSeparatorTemplateDirective, decorators: [{
19
+ type: Directive,
20
+ args: [{
21
+ selector: "ng-template[monaOtpInputSeparatorTemplate]"
22
+ }]
23
+ }] });
24
+
25
+ const otpInputHostThemeVariants = cva(`
26
+ relative inline-flex items-center gap-2
27
+ outline-none
28
+ transition-[color,box-shadow,border] duration-(--mona-motion-standard) ease-in-out
29
+ data-[disabled='true']:pointer-events-none
30
+ data-[disabled='true']:cursor-not-allowed
31
+ data-[readonly='true']:cursor-default
32
+ `);
33
+ const otpInputFieldThemeVariants = cva(`pointer-events-none absolute inset-0 size-full opacity-0 outline-none select-none -z-10`);
34
+ const otpInputSlotThemeVariants = cva(`
35
+ relative inline-flex items-center justify-center
36
+ text-center font-mono tabular-nums select-none
37
+ overflow-hidden
38
+ ${themeControlSurfaceClasses} text-foreground
39
+ border border-input-border shadow-(--shadow-control)
40
+ outline-none
41
+ transition-[color,box-shadow,border,background-color] duration-(--mona-motion-standard) ease-in-out
42
+
43
+ data-[disabled='true']:pointer-events-none
44
+ data-[disabled='true']:cursor-not-allowed
45
+ data-[disabled='true']:border-disabled-border
46
+ data-[disabled='true']:bg-disabled-background
47
+ data-[disabled='true']:text-disabled-foreground
48
+ data-[disabled='true']:shadow-none
49
+
50
+ data-[readonly='true']:cursor-default
51
+
52
+ data-[active='true']:border-focus-indicator
53
+ data-[active='true']:ring-2 data-[active='true']:ring-focus-indicator/35
54
+ data-[active='true']:z-10
55
+
56
+ data-[selected='true']:bg-primary/10
57
+
58
+ data-[placeholder='true']:text-muted-foreground
59
+
60
+ data-[invalid='true']:border-error
61
+ data-[invalid='true']:data-[active='true']:border-error
62
+ data-[invalid='true']:data-[active='true']:ring-2
63
+ data-[invalid='true']:data-[active='true']:ring-error/35
64
+ `, {
65
+ variants: {
66
+ rounded: {
67
+ full: "rounded-full",
68
+ large: "rounded-lg",
69
+ medium: "rounded-md",
70
+ none: "rounded-none",
71
+ small: "rounded-sm"
72
+ },
73
+ size: {
74
+ large: "h-10 w-10 min-w-10 text-md",
75
+ medium: "h-9 w-9 min-w-9 text-sm",
76
+ small: "h-8 w-8 min-w-8 text-xs"
77
+ }
78
+ },
79
+ defaultVariants: {
80
+ rounded: "medium",
81
+ size: "medium"
82
+ }
83
+ });
84
+
85
+ const RESERVED_INPUT_ATTRIBUTES = new Set([
86
+ "aria-hidden",
87
+ "aria-invalid",
88
+ "aria-required",
89
+ "class",
90
+ "disabled",
91
+ "maxlength",
92
+ "readonly",
93
+ "required",
94
+ "role",
95
+ "style",
96
+ "type",
97
+ "value"
98
+ ]);
99
+ const COMPONENT_MANAGED_ATTRIBUTES = new Set([
100
+ "aria-label",
101
+ "aria-labelledby",
102
+ "autocomplete",
103
+ "inputmode"
104
+ ]);
105
+ function findAttribute(attrs, name) {
106
+ const target = name.toLowerCase();
107
+ for (const [key, value] of Object.entries(attrs)) {
108
+ if (key.toLowerCase() === target) {
109
+ return value;
110
+ }
111
+ }
112
+ return undefined;
113
+ }
114
+ function findUsableAttribute(attrs, name) {
115
+ const value = findAttribute(attrs, name);
116
+ if (value == null || value === false) {
117
+ return undefined;
118
+ }
119
+ return value;
120
+ }
121
+ function toNonEmptyString(value) {
122
+ if (value == null || value === false) {
123
+ return null;
124
+ }
125
+ const text = String(value).trim();
126
+ return text.length > 0 ? text : null;
127
+ }
128
+ function normalizeLength(length) {
129
+ if (typeof length !== "number" || isNaN(length) || !isFinite(length) || length < 1) {
130
+ return 4;
131
+ }
132
+ return Math.floor(length);
133
+ }
134
+ function isValidCharacter(char, type, characterPattern) {
135
+ if (!char || char.length !== 1) {
136
+ return false;
137
+ }
138
+ if (characterPattern != null) {
139
+ const sanitized = new RegExp(characterPattern.source, characterPattern.flags.replace(/[gy]/g, ""));
140
+ return sanitized.test(char);
141
+ }
142
+ if (type === "number") {
143
+ return /^[0-9]$/.test(char);
144
+ }
145
+ return /^[a-zA-Z0-9]$/.test(char);
146
+ }
147
+ function filterCharacters(raw, type, characterPattern, maxLength) {
148
+ if (!raw) {
149
+ return "";
150
+ }
151
+ let result = "";
152
+ for (const char of raw) {
153
+ if (isValidCharacter(char, type, characterPattern)) {
154
+ result += char;
155
+ if (result.length >= maxLength) {
156
+ break;
157
+ }
158
+ }
159
+ }
160
+ return result;
161
+ }
162
+ function normalizeGroupLengths(groupLength, totalLength) {
163
+ if (totalLength <= 0) {
164
+ return [];
165
+ }
166
+ if (groupLength == null) {
167
+ return [totalLength];
168
+ }
169
+ if (typeof groupLength === "number") {
170
+ if (!isFinite(groupLength) || isNaN(groupLength) || groupLength <= 0) {
171
+ return [totalLength];
172
+ }
173
+ const size = Math.floor(groupLength);
174
+ if (size <= 0) {
175
+ return [totalLength];
176
+ }
177
+ const groups = [];
178
+ let remaining = totalLength;
179
+ while (remaining > 0) {
180
+ const g = Math.min(size, remaining);
181
+ groups.push(g);
182
+ remaining -= g;
183
+ }
184
+ return groups;
185
+ }
186
+ if (Array.isArray(groupLength)) {
187
+ const groups = [];
188
+ let remaining = totalLength;
189
+ for (const item of groupLength) {
190
+ if (typeof item === "number" && isFinite(item) && !isNaN(item) && item > 0) {
191
+ const size = Math.floor(item);
192
+ if (size > 0) {
193
+ const g = Math.min(size, remaining);
194
+ groups.push(g);
195
+ remaining -= g;
196
+ if (remaining <= 0) {
197
+ break;
198
+ }
199
+ }
200
+ }
201
+ }
202
+ if (remaining > 0) {
203
+ groups.push(remaining);
204
+ }
205
+ return groups.length > 0 ? groups : [totalLength];
206
+ }
207
+ return [totalLength];
208
+ }
209
+ function sanitizeInputAttributes(attrs) {
210
+ const sanitized = {};
211
+ for (const [key, val] of Object.entries(attrs)) {
212
+ const normalizedKey = key.toLowerCase();
213
+ if (!RESERVED_INPUT_ATTRIBUTES.has(normalizedKey) && !COMPONENT_MANAGED_ATTRIBUTES.has(normalizedKey)) {
214
+ sanitized[key] = val;
215
+ }
216
+ }
217
+ return sanitized;
218
+ }
219
+ function getSlotRoundedClasses(rounded, firstSlot, lastSlot, groupSize) {
220
+ if (groupSize <= 1) {
221
+ return "";
222
+ }
223
+ if (firstSlot) {
224
+ switch (rounded) {
225
+ case "none":
226
+ return "rounded-none";
227
+ case "small":
228
+ return "rounded-s-sm rounded-e-none";
229
+ case "medium":
230
+ return "rounded-s-md rounded-e-none";
231
+ case "large":
232
+ return "rounded-s-lg rounded-e-none";
233
+ case "full":
234
+ return "rounded-s-full rounded-e-none";
235
+ default:
236
+ return "rounded-s-md rounded-e-none";
237
+ }
238
+ }
239
+ if (lastSlot) {
240
+ switch (rounded) {
241
+ case "none":
242
+ return "rounded-none";
243
+ case "small":
244
+ return "rounded-e-sm rounded-s-none";
245
+ case "medium":
246
+ return "rounded-e-md rounded-s-none";
247
+ case "large":
248
+ return "rounded-e-lg rounded-s-none";
249
+ case "full":
250
+ return "rounded-e-full rounded-s-none";
251
+ default:
252
+ return "rounded-e-md rounded-s-none";
253
+ }
254
+ }
255
+ return "rounded-none";
256
+ }
257
+ function computeSlotClasses(options) {
258
+ const { firstSlot, groupSize, lastSlot, rounded, size, slotClass, spacing } = options;
259
+ const baseClasses = otpInputSlotThemeVariants({
260
+ rounded: spacing || groupSize === 1 ? rounded : "none",
261
+ size
262
+ });
263
+ let extraRounding = "";
264
+ if (!spacing) {
265
+ extraRounding = getSlotRoundedClasses(rounded, firstSlot, lastSlot, groupSize);
266
+ }
267
+ return twMerge(baseClasses, extraRounding, slotClass);
268
+ }
269
+
270
+ class OtpInputComponent {
271
+ constructor() {
272
+ this.activeSlotIndex = computed(() => {
273
+ if (!this.hasFocus()) {
274
+ return -1;
275
+ }
276
+ const totalLen = this.normalizedLength();
277
+ const start = this.logicalSelectionStart();
278
+ const end = this.logicalSelectionEnd();
279
+ if (start === end) {
280
+ if (start < totalLen) {
281
+ return start;
282
+ }
283
+ return Math.max(0, totalLen - 1);
284
+ }
285
+ return Math.min(start, totalLen - 1);
286
+ }, /* @ts-ignore */
287
+ ...(ngDevMode ? [{ debugName: "activeSlotIndex" }] : /* istanbul ignore next */ []));
288
+ this.classes = computed(() => {
289
+ const baseClasses = otpInputHostThemeVariants();
290
+ return twMerge(baseClasses, this.userClass());
291
+ }, /* @ts-ignore */
292
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
293
+ this.computedAriaLabel = computed(() => {
294
+ const labelledby = this.computedAriaLabelledby();
295
+ if (labelledby != null) {
296
+ return null;
297
+ }
298
+ const attrs = this.inputAttributes();
299
+ const rawAttrLabel = findUsableAttribute(attrs, "aria-label");
300
+ const customAttrLabel = toNonEmptyString(rawAttrLabel);
301
+ if (customAttrLabel != null) {
302
+ return customAttrLabel;
303
+ }
304
+ const fallback = toNonEmptyString(this.ariaLabel());
305
+ return fallback ?? "Verification code";
306
+ }, /* @ts-ignore */
307
+ ...(ngDevMode ? [{ debugName: "computedAriaLabel" }] : /* istanbul ignore next */ []));
308
+ this.computedAriaLabelledby = computed(() => {
309
+ const attrs = this.inputAttributes();
310
+ const raw = findUsableAttribute(attrs, "aria-labelledby");
311
+ return toNonEmptyString(raw);
312
+ }, /* @ts-ignore */
313
+ ...(ngDevMode ? [{ debugName: "computedAriaLabelledby" }] : /* istanbul ignore next */ []));
314
+ this.derivedAutocomplete = computed(() => {
315
+ const attrs = this.inputAttributes();
316
+ const autocomplete = findUsableAttribute(attrs, "autocomplete");
317
+ return autocomplete != null ? String(autocomplete) : "one-time-code";
318
+ }, /* @ts-ignore */
319
+ ...(ngDevMode ? [{ debugName: "derivedAutocomplete" }] : /* istanbul ignore next */ []));
320
+ this.derivedInputMode = computed(() => {
321
+ const attrs = this.inputAttributes();
322
+ const inputMode = findUsableAttribute(attrs, "inputmode");
323
+ if (inputMode != null) {
324
+ return String(inputMode);
325
+ }
326
+ return this.type() === "number" ? "numeric" : "text";
327
+ }, /* @ts-ignore */
328
+ ...(ngDevMode ? [{ debugName: "derivedInputMode" }] : /* istanbul ignore next */ []));
329
+ this.hasFocus = signal(false, /* @ts-ignore */
330
+ ...(ngDevMode ? [{ debugName: "hasFocus" }] : /* istanbul ignore next */ []));
331
+ this.inputClass = computed(() => otpInputFieldThemeVariants(), /* @ts-ignore */
332
+ ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
333
+ this.inputRef = viewChild.required("input", /* @ts-ignore */
334
+ ...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
335
+ this.invalidState = computed(() => this.touched() && (this.invalid() || (this.required() && this.value().length !== this.normalizedLength())), /* @ts-ignore */
336
+ ...(ngDevMode ? [{ debugName: "invalidState" }] : /* istanbul ignore next */ []));
337
+ this.isComplete = computed(() => this.value().length === this.normalizedLength(), /* @ts-ignore */
338
+ ...(ngDevMode ? [{ debugName: "isComplete" }] : /* istanbul ignore next */ []));
339
+ this.isComposing = signal(false, /* @ts-ignore */
340
+ ...(ngDevMode ? [{ debugName: "isComposing" }] : /* istanbul ignore next */ []));
341
+ this.logicalSelectionEnd = computed(() => {
342
+ const maxLogicalPosition = Math.min(this.value().length, this.normalizedLength());
343
+ const start = this.logicalSelectionStart();
344
+ const rawEnd = Math.max(0, Math.min(this.selectionEnd(), maxLogicalPosition));
345
+ return Math.max(start, rawEnd);
346
+ }, /* @ts-ignore */
347
+ ...(ngDevMode ? [{ debugName: "logicalSelectionEnd" }] : /* istanbul ignore next */ []));
348
+ this.logicalSelectionStart = computed(() => {
349
+ const maxLogicalPosition = Math.min(this.value().length, this.normalizedLength());
350
+ return Math.max(0, Math.min(this.selectionStart(), maxLogicalPosition));
351
+ }, /* @ts-ignore */
352
+ ...(ngDevMode ? [{ debugName: "logicalSelectionStart" }] : /* istanbul ignore next */ []));
353
+ this.nativeInputType = computed(() => (this.type() === "password" ? "password" : "text"), /* @ts-ignore */
354
+ ...(ngDevMode ? [{ debugName: "nativeInputType" }] : /* istanbul ignore next */ []));
355
+ this.normalizedLength = computed(() => normalizeLength(this.length()), /* @ts-ignore */
356
+ ...(ngDevMode ? [{ debugName: "normalizedLength" }] : /* istanbul ignore next */ []));
357
+ this.sanitizedAttributes = computed(() => sanitizeInputAttributes(this.inputAttributes()), /* @ts-ignore */
358
+ ...(ngDevMode ? [{ debugName: "sanitizedAttributes" }] : /* istanbul ignore next */ []));
359
+ this.selectionEnd = signal(0, /* @ts-ignore */
360
+ ...(ngDevMode ? [{ debugName: "selectionEnd" }] : /* istanbul ignore next */ []));
361
+ this.selectionStart = signal(0, /* @ts-ignore */
362
+ ...(ngDevMode ? [{ debugName: "selectionStart" }] : /* istanbul ignore next */ []));
363
+ this.separatorTemplate = contentChild(OtpInputSeparatorTemplateDirective, { ...(ngDevMode ? { debugName: "separatorTemplate" } : /* istanbul ignore next */ {}), read: TemplateRef });
364
+ this.visualGroups = computed(() => {
365
+ const totalLen = this.normalizedLength();
366
+ const groupSizes = normalizeGroupLengths(this.groupLength(), totalLen);
367
+ const val = this.value();
368
+ const activeIndex = this.activeSlotIndex();
369
+ const selStart = this.logicalSelectionStart();
370
+ const selEnd = this.logicalSelectionEnd();
371
+ const focused = this.hasFocus();
372
+ const inputType = this.type();
373
+ const placeholderStr = this.placeholder();
374
+ const placeholderChar = placeholderStr.length > 0 ? placeholderStr.charAt(0) : "";
375
+ const rounded = this.rounded();
376
+ const size = this.size();
377
+ const slotClass = this.slotClass();
378
+ const spacing = this.spacing();
379
+ const groups = [];
380
+ let slotCounter = 0;
381
+ for (let gIndex = 0; gIndex < groupSizes.length; gIndex++) {
382
+ const groupSize = groupSizes[gIndex];
383
+ const slots = [];
384
+ for (let s = 0; s < groupSize; s++) {
385
+ const slotIndex = slotCounter++;
386
+ const filled = slotIndex < val.length;
387
+ const character = filled ? val[slotIndex] : "";
388
+ const isPlaceholder = !filled && placeholderChar.length > 0;
389
+ let displayCharacter = "";
390
+ if (filled) {
391
+ displayCharacter = inputType === "password" ? "•" : character;
392
+ }
393
+ else if (isPlaceholder) {
394
+ displayCharacter = placeholderChar;
395
+ }
396
+ const active = focused && slotIndex === activeIndex;
397
+ const selected = focused && slotIndex >= selStart && slotIndex < selEnd;
398
+ const isFirstSlot = s === 0;
399
+ const isLastSlot = s === groupSize - 1;
400
+ const classes = computeSlotClasses({
401
+ firstSlot: isFirstSlot,
402
+ groupSize,
403
+ lastSlot: isLastSlot,
404
+ rounded,
405
+ size,
406
+ slotClass,
407
+ spacing
408
+ });
409
+ slots.push({
410
+ active,
411
+ classes,
412
+ displayCharacter,
413
+ filled,
414
+ index: slotIndex,
415
+ placeholder: isPlaceholder,
416
+ selected
417
+ });
418
+ }
419
+ groups.push({
420
+ index: gIndex,
421
+ slots
422
+ });
423
+ }
424
+ return groups;
425
+ }, /* @ts-ignore */
426
+ ...(ngDevMode ? [{ debugName: "visualGroups" }] : /* istanbul ignore next */ []));
427
+ /**
428
+ * @description Accessible name for the inner `<input>` element.
429
+ * @default "Verification code"
430
+ */
431
+ this.ariaLabel = input("Verification code", /* @ts-ignore */
432
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
433
+ /**
434
+ * @description Regular expression used to determine whether each individual OTP character is allowed.
435
+ * @default null
436
+ */
437
+ this.characterPattern = input(null, /* @ts-ignore */
438
+ ...(ngDevMode ? [{ debugName: "characterPattern" }] : /* istanbul ignore next */ []));
439
+ /**
440
+ * @description Emitted when a user interaction completes the verification code.
441
+ */
442
+ this.complete = output();
443
+ /**
444
+ * @description Renders the component with reduced visual emphasis and removes pointer interaction.
445
+ * When bound to a signal form field via `[formField]`, this is written by the `FormField` directive.
446
+ * @default false
447
+ */
448
+ this.disabled = input(false, /* @ts-ignore */
449
+ ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
450
+ /**
451
+ * @description Configures slot grouping lengths. Accepts a uniform number or an array of group sizes.
452
+ * @default null
453
+ */
454
+ this.groupLength = input(null, /* @ts-ignore */
455
+ ...(ngDevMode ? [{ debugName: "groupLength" }] : /* istanbul ignore next */ []));
456
+ /**
457
+ * @description Additional HTML attributes applied directly to the inner `<input>` element.
458
+ * @default {}
459
+ */
460
+ this.inputAttributes = input({}, /* @ts-ignore */
461
+ ...(ngDevMode ? [{ debugName: "inputAttributes" }] : /* istanbul ignore next */ []));
462
+ /**
463
+ * @description Emitted when the inner input loses focus.
464
+ */
465
+ this.inputBlur = output();
466
+ /**
467
+ * @description Emitted when the inner input gains focus.
468
+ */
469
+ this.inputFocus = output();
470
+ /**
471
+ * @description Marks the OTP input as invalid. When bound to a signal form field via `[formField]`,
472
+ * this is written by the `FormField` directive.
473
+ * @default false
474
+ */
475
+ this.invalid = input(false, /* @ts-ignore */
476
+ ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
477
+ /**
478
+ * @description Sets the total number of characters accepted by the OTP input and the number of visual slots.
479
+ * @default 4
480
+ */
481
+ this.length = input(4, /* @ts-ignore */
482
+ ...(ngDevMode ? [{ debugName: "length" }] : /* istanbul ignore next */ []));
483
+ /**
484
+ * @description Hint character displayed in empty visual slots.
485
+ * @default ""
486
+ */
487
+ this.placeholder = input("", /* @ts-ignore */
488
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
489
+ /**
490
+ * @description Prevents value changes while preserving the component's focusability and visual state.
491
+ * When bound to a signal form field via `[formField]`, this is written by the `FormField` directive.
492
+ * @default false
493
+ */
494
+ this.readonly = input(false, /* @ts-ignore */
495
+ ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
496
+ /**
497
+ * @description Marks the OTP input as required. Triggers invalid state when touched and incomplete.
498
+ * When bound to a signal form field via `[formField]`, this is written by the `FormField` directive.
499
+ * @default false
500
+ */
501
+ this.required = input(false, /* @ts-ignore */
502
+ ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
503
+ /**
504
+ * @description Border-radius preset applied to the OTP slots.
505
+ * @default "medium"
506
+ */
507
+ this.rounded = input("medium", /* @ts-ignore */
508
+ ...(ngDevMode ? [{ debugName: "rounded" }] : /* istanbul ignore next */ []));
509
+ /**
510
+ * @description Text character or string displayed between visual slot groups.
511
+ * @default ""
512
+ */
513
+ this.separator = input("", /* @ts-ignore */
514
+ ...(ngDevMode ? [{ debugName: "separator" }] : /* istanbul ignore next */ []));
515
+ /**
516
+ * @description Additional CSS classes applied to separator elements.
517
+ * @default ""
518
+ */
519
+ this.separatorClass = input("", /* @ts-ignore */
520
+ ...(ngDevMode ? [{ debugName: "separatorClass" }] : /* istanbul ignore next */ []));
521
+ /**
522
+ * @description Size preset controlling the dimensions and font size of the OTP slots.
523
+ * @default "medium"
524
+ */
525
+ this.size = input("medium", /* @ts-ignore */
526
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
527
+ /**
528
+ * @description Additional CSS classes applied to each visual slot element.
529
+ * @default ""
530
+ */
531
+ this.slotClass = input("", /* @ts-ignore */
532
+ ...(ngDevMode ? [{ debugName: "slotClass" }] : /* istanbul ignore next */ []));
533
+ /**
534
+ * @description Determines whether visual slots within a group have spacing between them or are joined together.
535
+ * @default true
536
+ */
537
+ this.spacing = input(true, /* @ts-ignore */
538
+ ...(ngDevMode ? [{ debugName: "spacing" }] : /* istanbul ignore next */ []));
539
+ /**
540
+ * @description Emitted when the OTP input is interacted with — on blur or user value change.
541
+ * The `FormField` directive listens to this to mark the field as touched.
542
+ */
543
+ this.touch = output();
544
+ /**
545
+ * @description Sets the touched state of the OTP input. When bound to a signal form field via `[formField]`,
546
+ * this is written by the `FormField` directive.
547
+ * @default false
548
+ */
549
+ this.touched = input(false, /* @ts-ignore */
550
+ ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
551
+ /**
552
+ * @description Sets the type of the OTP input ("text", "number", or "password").
553
+ * @default "text"
554
+ */
555
+ this.type = input("text", /* @ts-ignore */
556
+ ...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
557
+ /**
558
+ * @description Additional CSS classes merged onto the host element via `tailwind-merge`.
559
+ * @default ""
560
+ */
561
+ this.userClass = input("", { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: "class" });
562
+ /**
563
+ * @description Two-way bindable current value of the OTP input.
564
+ * @default ""
565
+ */
566
+ this.value = model("", /* @ts-ignore */
567
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
568
+ effect(() => {
569
+ const val = this.value();
570
+ const inputType = this.type();
571
+ const characterPattern = this.characterPattern();
572
+ const maxLen = this.normalizedLength();
573
+ const filtered = filterCharacters(val, inputType, characterPattern, maxLen);
574
+ if (filtered !== val) {
575
+ this.value.set(filtered);
576
+ }
577
+ });
578
+ }
579
+ blur() {
580
+ this.inputRef().nativeElement.blur();
581
+ }
582
+ focus(target) {
583
+ if (this.disabled()) {
584
+ return;
585
+ }
586
+ const el = this.inputRef().nativeElement;
587
+ if (typeof target === "object" && target !== null) {
588
+ el.focus(target);
589
+ this.syncSelection();
590
+ return;
591
+ }
592
+ el.focus();
593
+ const val = this.value();
594
+ const maxLen = this.normalizedLength();
595
+ if (typeof target === "number") {
596
+ const clamped = Math.max(0, Math.min(target, maxLen));
597
+ if (clamped < val.length) {
598
+ el.setSelectionRange(clamped, clamped + 1);
599
+ }
600
+ else {
601
+ const pos = Math.min(clamped, val.length);
602
+ el.setSelectionRange(pos, pos);
603
+ }
604
+ }
605
+ else {
606
+ if (val.length < maxLen) {
607
+ el.setSelectionRange(val.length, val.length);
608
+ }
609
+ else {
610
+ el.setSelectionRange(maxLen, maxLen);
611
+ }
612
+ }
613
+ this.syncSelection();
614
+ }
615
+ onBeforeInput(event) {
616
+ if (this.disabled() || this.readonly()) {
617
+ event.preventDefault();
618
+ return;
619
+ }
620
+ if (this.isComposing() || event.isComposing) {
621
+ return;
622
+ }
623
+ const inputType = event.inputType;
624
+ const inputEl = this.inputRef().nativeElement;
625
+ const start = inputEl.selectionStart ?? 0;
626
+ const end = inputEl.selectionEnd ?? 0;
627
+ const val = this.value();
628
+ const maxLen = this.normalizedLength();
629
+ if (inputType === "insertText") {
630
+ if (!event.data) {
631
+ return;
632
+ }
633
+ if (event.data.length === 1) {
634
+ if (!isValidCharacter(event.data, this.type(), this.characterPattern())) {
635
+ event.preventDefault();
636
+ return;
637
+ }
638
+ if (start >= maxLen && start === end) {
639
+ event.preventDefault();
640
+ return;
641
+ }
642
+ if (start === end && start < val.length) {
643
+ inputEl.setSelectionRange(start, start + 1);
644
+ }
645
+ }
646
+ else {
647
+ event.preventDefault();
648
+ this.applyUserInsertion(event.data, start, end);
649
+ }
650
+ }
651
+ else if (inputType === "insertReplacementText" || inputType === "insertFromPaste") {
652
+ if (event.data) {
653
+ event.preventDefault();
654
+ this.applyUserInsertion(event.data, start, end);
655
+ }
656
+ }
657
+ }
658
+ onBlur(event) {
659
+ this.hasFocus.set(false);
660
+ this.inputBlur.emit(event);
661
+ this.touch.emit();
662
+ }
663
+ onCompositionEnd(event) {
664
+ this.isComposing.set(false);
665
+ this.onInput(event);
666
+ }
667
+ onCompositionStart() {
668
+ this.isComposing.set(true);
669
+ }
670
+ onFocus(event) {
671
+ this.hasFocus.set(true);
672
+ this.syncSelection();
673
+ this.inputFocus.emit(event);
674
+ }
675
+ onInput(_event) {
676
+ if (this.disabled() || this.readonly()) {
677
+ const inputEl = this.inputRef().nativeElement;
678
+ inputEl.value = this.value();
679
+ return;
680
+ }
681
+ if (this.isComposing()) {
682
+ return;
683
+ }
684
+ const inputEl = this.inputRef().nativeElement;
685
+ const rawVal = inputEl.value;
686
+ const prevVal = this.value();
687
+ const nextVal = filterCharacters(rawVal, this.type(), this.characterPattern(), this.normalizedLength());
688
+ inputEl.value = nextVal;
689
+ const wasIncomplete = prevVal.length < this.normalizedLength();
690
+ this.value.set(nextVal);
691
+ this.touch.emit();
692
+ if (wasIncomplete && nextVal.length === this.normalizedLength()) {
693
+ this.complete.emit(nextVal);
694
+ }
695
+ this.syncSelection();
696
+ }
697
+ onKeyDown(event) {
698
+ if (this.disabled() || this.readonly() || this.isComposing() || event.isComposing) {
699
+ return;
700
+ }
701
+ if (event.ctrlKey || event.metaKey || event.altKey) {
702
+ return;
703
+ }
704
+ if (event.key.length !== 1) {
705
+ return;
706
+ }
707
+ const inputEl = this.inputRef().nativeElement;
708
+ const start = inputEl.selectionStart ?? 0;
709
+ const end = inputEl.selectionEnd ?? 0;
710
+ const val = this.value();
711
+ const maxLen = this.normalizedLength();
712
+ if (!isValidCharacter(event.key, this.type(), this.characterPattern())) {
713
+ event.preventDefault();
714
+ return;
715
+ }
716
+ if (start >= maxLen && start === end) {
717
+ event.preventDefault();
718
+ return;
719
+ }
720
+ if (start === end && start < val.length) {
721
+ inputEl.setSelectionRange(start, start + 1);
722
+ }
723
+ }
724
+ onKeyUp() {
725
+ this.syncSelection();
726
+ }
727
+ onPaste(event) {
728
+ if (this.disabled() || this.readonly()) {
729
+ event.preventDefault();
730
+ return;
731
+ }
732
+ const pastedText = event.clipboardData?.getData("text") ?? "";
733
+ if (!pastedText) {
734
+ return;
735
+ }
736
+ event.preventDefault();
737
+ const inputEl = this.inputRef().nativeElement;
738
+ const start = inputEl.selectionStart ?? 0;
739
+ const end = inputEl.selectionEnd ?? 0;
740
+ this.applyUserInsertion(pastedText, start, end);
741
+ }
742
+ onSelect() {
743
+ this.syncSelection();
744
+ }
745
+ onSlotPointerDown(event, index) {
746
+ if (this.disabled()) {
747
+ return;
748
+ }
749
+ event.preventDefault();
750
+ this.focus(index);
751
+ }
752
+ applyUserInsertion(text, selectionStart, selectionEnd) {
753
+ if (this.disabled() || this.readonly()) {
754
+ return;
755
+ }
756
+ const inputEl = this.inputRef().nativeElement;
757
+ const currentVal = this.value();
758
+ const maxLen = this.normalizedLength();
759
+ const filtered = filterCharacters(text, this.type(), this.characterPattern(), Number.MAX_SAFE_INTEGER);
760
+ if (filtered.length === 0 && selectionStart === selectionEnd) {
761
+ return;
762
+ }
763
+ const start = Math.max(0, Math.min(selectionStart, currentVal.length));
764
+ const end = Math.max(start, Math.min(selectionEnd, currentVal.length));
765
+ const before = currentVal.substring(0, start);
766
+ const after = currentVal.substring(end);
767
+ let combined = before + filtered + after;
768
+ if (combined.length > maxLen) {
769
+ combined = combined.substring(0, maxLen);
770
+ }
771
+ const newCaretPos = Math.min(start + filtered.length, maxLen);
772
+ const wasIncomplete = currentVal.length < maxLen;
773
+ inputEl.value = combined;
774
+ this.value.set(combined);
775
+ inputEl.setSelectionRange(newCaretPos, newCaretPos);
776
+ this.selectionStart.set(newCaretPos);
777
+ this.selectionEnd.set(newCaretPos);
778
+ this.touch.emit();
779
+ if (wasIncomplete && combined.length === maxLen) {
780
+ this.complete.emit(combined);
781
+ }
782
+ }
783
+ syncSelection() {
784
+ const el = this.inputRef().nativeElement;
785
+ this.selectionStart.set(el.selectionStart ?? 0);
786
+ this.selectionEnd.set(el.selectionEnd ?? 0);
787
+ }
788
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: OtpInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
789
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: OtpInputComponent, isStandalone: true, selector: "mona-otp-input", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, characterPattern: { classPropertyName: "characterPattern", publicName: "characterPattern", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, groupLength: { classPropertyName: "groupLength", publicName: "groupLength", isSignal: true, isRequired: false, transformFunction: null }, inputAttributes: { classPropertyName: "inputAttributes", publicName: "inputAttributes", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, length: { classPropertyName: "length", publicName: "length", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, separator: { classPropertyName: "separator", publicName: "separator", isSignal: true, isRequired: false, transformFunction: null }, separatorClass: { classPropertyName: "separatorClass", publicName: "separatorClass", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, slotClass: { classPropertyName: "slotClass", publicName: "slotClass", isSignal: true, isRequired: false, transformFunction: null }, spacing: { classPropertyName: "spacing", publicName: "spacing", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { complete: "complete", inputBlur: "inputBlur", inputFocus: "inputFocus", touch: "touch", value: "valueChange" }, host: { attributes: { "dir": "ltr" }, properties: { "attr.dir": "'ltr'", "attr.data-disabled": "disabled() || null", "attr.data-readonly": "readonly() || null", "attr.data-required": "required() || null", "attr.data-invalid": "invalidState() || null", "attr.data-complete": "isComplete() || null", "attr.data-focused": "hasFocus() || null", "class": "classes()" } }, queries: [{ propertyName: "separatorTemplate", first: true, predicate: OtpInputSeparatorTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["input"], descendants: true, isSignal: true }], ngImport: i0, template: "<input\n #input\n [class]=\"inputClass()\"\n tabindex=\"0\"\n [value]=\"value()\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [required]=\"required()\"\n [type]=\"nativeInputType()\"\n [attr.inputmode]=\"derivedInputMode()\"\n [attr.autocomplete]=\"derivedAutocomplete()\"\n [attr.autocapitalize]=\"'off'\"\n [attr.spellcheck]=\"'false'\"\n [attr.aria-label]=\"computedAriaLabel()\"\n [attr.aria-labelledby]=\"computedAriaLabelledby()\"\n [attr.aria-invalid]=\"invalidState() ? 'true' : null\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.maxlength]=\"normalizedLength()\"\n [monaAttributeBinder]=\"sanitizedAttributes()\"\n (beforeinput)=\"onBeforeInput($event)\"\n (keydown)=\"onKeyDown($event)\"\n (input)=\"onInput($event)\"\n (paste)=\"onPaste($event)\"\n (focus)=\"onFocus($event)\"\n (blur)=\"onBlur($event)\"\n (select)=\"onSelect()\"\n (keyup)=\"onKeyUp()\"\n (compositionstart)=\"onCompositionStart()\"\n (compositionend)=\"onCompositionEnd($event)\" />\n\n@for (group of visualGroups(); track group.index; let lastGroup = $last) {\n <div\n class=\"inline-flex items-center\"\n [class.gap-1.5]=\"spacing()\"\n [class.-space-x-px]=\"!spacing()\"\n [attr.data-group-index]=\"group.index\">\n @for (slot of group.slots; track slot.index) {\n <span\n class=\"otp-slot\"\n [class]=\"slot.classes\"\n [attr.data-slot-index]=\"slot.index\"\n [attr.data-active]=\"slot.active || null\"\n [attr.data-selected]=\"slot.selected || null\"\n [attr.data-filled]=\"slot.filled || null\"\n [attr.data-placeholder]=\"slot.placeholder || null\"\n [attr.data-disabled]=\"disabled() || null\"\n [attr.data-readonly]=\"readonly() || null\"\n [attr.data-invalid]=\"invalidState() || null\"\n (pointerdown)=\"onSlotPointerDown($event, slot.index)\"\n aria-hidden=\"true\">\n {{ slot.displayCharacter }}\n @if (slot.active && !slot.filled && hasFocus() && !disabled() && !readonly()) {\n <span\n class=\"pointer-events-none absolute inset-y-2 w-0.5 animate-pulse motion-reduce:animate-none bg-foreground\"\n aria-hidden=\"true\"></span>\n }\n </span>\n }\n </div>\n\n @if (!lastGroup && (separator() || separatorTemplate())) {\n <span\n class=\"inline-flex items-center justify-center text-muted-foreground select-none px-1\"\n [class]=\"separatorClass()\"\n aria-hidden=\"true\">\n @if (separatorTemplate(); as customTemplate) {\n <ng-container\n *ngTemplateOutlet=\"\n customTemplate;\n context: { $implicit: group.index, groupIndex: group.index }\n \"></ng-container>\n } @else {\n {{ separator() }}\n }\n </span>\n }\n}\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: AttributeBinderDirective, selector: "[monaAttributeBinder]", inputs: ["monaAttributeBinder"] }] }); }
790
+ }
791
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: OtpInputComponent, decorators: [{
792
+ type: Component,
793
+ args: [{ selector: "mona-otp-input", imports: [NgTemplateOutlet, AttributeBinderDirective], host: {
794
+ dir: "ltr",
795
+ "[attr.dir]": "'ltr'",
796
+ "[attr.data-disabled]": "disabled() || null",
797
+ "[attr.data-readonly]": "readonly() || null",
798
+ "[attr.data-required]": "required() || null",
799
+ "[attr.data-invalid]": "invalidState() || null",
800
+ "[attr.data-complete]": "isComplete() || null",
801
+ "[attr.data-focused]": "hasFocus() || null",
802
+ "[class]": "classes()"
803
+ }, template: "<input\n #input\n [class]=\"inputClass()\"\n tabindex=\"0\"\n [value]=\"value()\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [required]=\"required()\"\n [type]=\"nativeInputType()\"\n [attr.inputmode]=\"derivedInputMode()\"\n [attr.autocomplete]=\"derivedAutocomplete()\"\n [attr.autocapitalize]=\"'off'\"\n [attr.spellcheck]=\"'false'\"\n [attr.aria-label]=\"computedAriaLabel()\"\n [attr.aria-labelledby]=\"computedAriaLabelledby()\"\n [attr.aria-invalid]=\"invalidState() ? 'true' : null\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.maxlength]=\"normalizedLength()\"\n [monaAttributeBinder]=\"sanitizedAttributes()\"\n (beforeinput)=\"onBeforeInput($event)\"\n (keydown)=\"onKeyDown($event)\"\n (input)=\"onInput($event)\"\n (paste)=\"onPaste($event)\"\n (focus)=\"onFocus($event)\"\n (blur)=\"onBlur($event)\"\n (select)=\"onSelect()\"\n (keyup)=\"onKeyUp()\"\n (compositionstart)=\"onCompositionStart()\"\n (compositionend)=\"onCompositionEnd($event)\" />\n\n@for (group of visualGroups(); track group.index; let lastGroup = $last) {\n <div\n class=\"inline-flex items-center\"\n [class.gap-1.5]=\"spacing()\"\n [class.-space-x-px]=\"!spacing()\"\n [attr.data-group-index]=\"group.index\">\n @for (slot of group.slots; track slot.index) {\n <span\n class=\"otp-slot\"\n [class]=\"slot.classes\"\n [attr.data-slot-index]=\"slot.index\"\n [attr.data-active]=\"slot.active || null\"\n [attr.data-selected]=\"slot.selected || null\"\n [attr.data-filled]=\"slot.filled || null\"\n [attr.data-placeholder]=\"slot.placeholder || null\"\n [attr.data-disabled]=\"disabled() || null\"\n [attr.data-readonly]=\"readonly() || null\"\n [attr.data-invalid]=\"invalidState() || null\"\n (pointerdown)=\"onSlotPointerDown($event, slot.index)\"\n aria-hidden=\"true\">\n {{ slot.displayCharacter }}\n @if (slot.active && !slot.filled && hasFocus() && !disabled() && !readonly()) {\n <span\n class=\"pointer-events-none absolute inset-y-2 w-0.5 animate-pulse motion-reduce:animate-none bg-foreground\"\n aria-hidden=\"true\"></span>\n }\n </span>\n }\n </div>\n\n @if (!lastGroup && (separator() || separatorTemplate())) {\n <span\n class=\"inline-flex items-center justify-center text-muted-foreground select-none px-1\"\n [class]=\"separatorClass()\"\n aria-hidden=\"true\">\n @if (separatorTemplate(); as customTemplate) {\n <ng-container\n *ngTemplateOutlet=\"\n customTemplate;\n context: { $implicit: group.index, groupIndex: group.index }\n \"></ng-container>\n } @else {\n {{ separator() }}\n }\n </span>\n }\n}\n" }]
804
+ }], ctorParameters: () => [], propDecorators: { inputRef: [{ type: i0.ViewChild, args: ["input", { isSignal: true }] }], separatorTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OtpInputSeparatorTemplateDirective), { ...{
805
+ read: TemplateRef
806
+ }, isSignal: true }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], characterPattern: [{ type: i0.Input, args: [{ isSignal: true, alias: "characterPattern", required: false }] }], complete: [{ type: i0.Output, args: ["complete"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], groupLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupLength", required: false }] }], inputAttributes: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputAttributes", required: false }] }], inputBlur: [{ type: i0.Output, args: ["inputBlur"] }], inputFocus: [{ type: i0.Output, args: ["inputFocus"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], length: [{ type: i0.Input, args: [{ isSignal: true, alias: "length", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], separator: [{ type: i0.Input, args: [{ isSignal: true, alias: "separator", required: false }] }], separatorClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "separatorClass", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], slotClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "slotClass", required: false }] }], spacing: [{ type: i0.Input, args: [{ isSignal: true, alias: "spacing", required: false }] }], touch: [{ type: i0.Output, args: ["touch"] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }] } });
807
+
808
+ /*
809
+ * Public API Surface of @nanahoshi/mona-ui/otp-input
810
+ */
811
+
812
+ /**
813
+ * Generated bundle index. Do not edit.
814
+ */
815
+
816
+ export { OtpInputComponent, OtpInputSeparatorTemplateDirective };
817
+ //# sourceMappingURL=nanahoshi-mona-ui-otp-input.mjs.map
@@ -132,6 +132,7 @@ class SegmentedComponent {
132
132
  #lastAnimatedIndex;
133
133
  #lastOptions;
134
134
  #lastSize;
135
+ #observedOptionElements;
135
136
  #resizeObserver;
136
137
  constructor() {
137
138
  this.#destroyRef = inject(DestroyRef);
@@ -139,6 +140,7 @@ class SegmentedComponent {
139
140
  this.#lastAnimatedIndex = null;
140
141
  this.#lastOptions = null;
141
142
  this.#lastSize = null;
143
+ this.#observedOptionElements = [];
142
144
  this.#resizeObserver = null;
143
145
  this.containerClasses = computed(() => {
144
146
  const classes = segmentedContainerThemeVariants({ alignment: this.alignment(), rounded: this.rounded() });
@@ -265,7 +267,7 @@ class SegmentedComponent {
265
267
  afterRenderEffect({
266
268
  read: () => {
267
269
  this.selectedIndex();
268
- this.optionElements();
270
+ const optionElements = this.optionElements();
269
271
  this.alignment();
270
272
  this.rounded();
271
273
  this.animate();
@@ -276,12 +278,14 @@ class SegmentedComponent {
276
278
  const suppressTransition = isSizeChange || isOptionsChange;
277
279
  this.#lastSize = size;
278
280
  this.#lastOptions = options;
281
+ this.observeOptionElements(optionElements);
279
282
  this.updateIndicatorGeometry({ suppressTransition });
280
283
  }
281
284
  });
282
285
  afterNextRender({
283
286
  read: () => {
284
287
  this.setupResizeObserver();
288
+ this.scheduleSettledGeometryUpdate();
285
289
  }
286
290
  });
287
291
  this.#destroyRef.onDestroy(() => {
@@ -307,6 +311,39 @@ class SegmentedComponent {
307
311
  this.value.set(option.value);
308
312
  this.touch.emit();
309
313
  }
314
+ observeOptionElements(optionElements) {
315
+ if (!this.#resizeObserver) {
316
+ return;
317
+ }
318
+ const elements = optionElements.map(optionElement => optionElement.nativeElement);
319
+ for (const element of this.#observedOptionElements) {
320
+ if (!elements.includes(element)) {
321
+ this.#resizeObserver.unobserve(element);
322
+ }
323
+ }
324
+ for (const element of elements) {
325
+ if (!this.#observedOptionElements.includes(element)) {
326
+ this.#resizeObserver.observe(element);
327
+ }
328
+ }
329
+ this.#observedOptionElements = elements;
330
+ }
331
+ /**
332
+ * Guards against a stale indicator snapshot when the control mounts inside a container that is
333
+ * still transitioning in (e.g. a popup's enter animation): the host's own box may not change once
334
+ * that settles, so the ResizeObserver on it alone would never re-fire to correct the geometry.
335
+ */
336
+ scheduleSettledGeometryUpdate() {
337
+ const win = this.#hostElementRef.nativeElement.ownerDocument.defaultView;
338
+ if (!win) {
339
+ return;
340
+ }
341
+ win.requestAnimationFrame(() => {
342
+ win.requestAnimationFrame(() => {
343
+ this.updateIndicatorGeometry({ suppressTransition: true });
344
+ });
345
+ });
346
+ }
310
347
  setupResizeObserver() {
311
348
  if (typeof ResizeObserver === "undefined") {
312
349
  return;
@@ -315,6 +352,7 @@ class SegmentedComponent {
315
352
  this.updateIndicatorGeometry({ suppressTransition: true });
316
353
  });
317
354
  this.#resizeObserver.observe(this.#hostElementRef.nativeElement);
355
+ this.observeOptionElements(this.optionElements());
318
356
  }
319
357
  updateIndicatorGeometry(options) {
320
358
  const selectedIndex = this.selectedIndex();
@@ -0,0 +1,4 @@
1
+ {
2
+ "module": "../fesm2022/nanahoshi-mona-ui-otp-input.mjs",
3
+ "typings": "../types/nanahoshi-mona-ui-otp-input.d.ts"
4
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanahoshi/mona-ui",
3
- "version": "0.17.1",
3
+ "version": "0.18.1",
4
4
  "description": "A very light Angular UI framework",
5
5
  "author": "Nanahoshi",
6
6
  "repository": {
@@ -239,6 +239,10 @@
239
239
  "types": "./types/nanahoshi-mona-ui-numeric-text-box.d.ts",
240
240
  "default": "./fesm2022/nanahoshi-mona-ui-numeric-text-box.mjs"
241
241
  },
242
+ "./otp-input": {
243
+ "types": "./types/nanahoshi-mona-ui-otp-input.d.ts",
244
+ "default": "./fesm2022/nanahoshi-mona-ui-otp-input.mjs"
245
+ },
242
246
  "./pager": {
243
247
  "types": "./types/nanahoshi-mona-ui-pager.d.ts",
244
248
  "default": "./fesm2022/nanahoshi-mona-ui-pager.mjs"
@@ -0,0 +1,209 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { ElementRef, TemplateRef } from '@angular/core';
3
+ import { FormValueControl } from '@angular/forms/signals';
4
+ import { VariantInputs, AttributeConfig } from '@nanahoshi/mona-ui/internal';
5
+ import * as class_variance_authority_types from 'class-variance-authority/types';
6
+ import { VariantProps } from 'class-variance-authority';
7
+
8
+ type OtpInputType = "text" | "number" | "password";
9
+
10
+ declare const otpInputSlotThemeVariants: (props?: ({
11
+ rounded?: "full" | "large" | "medium" | "none" | "small" | null | undefined;
12
+ size?: "large" | "medium" | "small" | null | undefined;
13
+ } & class_variance_authority_types.ClassProp) | undefined) => string;
14
+ type OtpInputSlotVariantProps = VariantProps<typeof otpInputSlotThemeVariants>;
15
+ type OtpInputSlotVariantInput = VariantInputs<OtpInputSlotVariantProps>;
16
+ type OtpInputVariantProps = OtpInputSlotVariantProps;
17
+ type OtpInputVariantInput = OtpInputSlotVariantInput;
18
+
19
+ interface OtpSlotViewModel {
20
+ active: boolean;
21
+ classes: string;
22
+ displayCharacter: string;
23
+ filled: boolean;
24
+ index: number;
25
+ placeholder: boolean;
26
+ selected: boolean;
27
+ }
28
+ interface OtpGroupViewModel {
29
+ index: number;
30
+ slots: OtpSlotViewModel[];
31
+ }
32
+ declare class OtpInputComponent implements OtpInputVariantInput, FormValueControl<string> {
33
+ protected readonly activeSlotIndex: _angular_core.Signal<number>;
34
+ protected readonly classes: _angular_core.Signal<string>;
35
+ protected readonly computedAriaLabel: _angular_core.Signal<string | null>;
36
+ protected readonly computedAriaLabelledby: _angular_core.Signal<string | null>;
37
+ protected readonly derivedAutocomplete: _angular_core.Signal<string>;
38
+ protected readonly derivedInputMode: _angular_core.Signal<string>;
39
+ protected readonly hasFocus: _angular_core.WritableSignal<boolean>;
40
+ protected readonly inputClass: _angular_core.Signal<string>;
41
+ protected readonly inputRef: _angular_core.Signal<ElementRef<HTMLInputElement>>;
42
+ protected readonly invalidState: _angular_core.Signal<boolean>;
43
+ protected readonly isComplete: _angular_core.Signal<boolean>;
44
+ protected readonly isComposing: _angular_core.WritableSignal<boolean>;
45
+ protected readonly logicalSelectionEnd: _angular_core.Signal<number>;
46
+ protected readonly logicalSelectionStart: _angular_core.Signal<number>;
47
+ protected readonly nativeInputType: _angular_core.Signal<"text" | "password">;
48
+ protected readonly normalizedLength: _angular_core.Signal<number>;
49
+ protected readonly sanitizedAttributes: _angular_core.Signal<AttributeConfig>;
50
+ protected readonly selectionEnd: _angular_core.WritableSignal<number>;
51
+ protected readonly selectionStart: _angular_core.WritableSignal<number>;
52
+ protected readonly separatorTemplate: _angular_core.Signal<TemplateRef<any> | undefined>;
53
+ protected readonly visualGroups: _angular_core.Signal<OtpGroupViewModel[]>;
54
+ /**
55
+ * @description Accessible name for the inner `<input>` element.
56
+ * @default "Verification code"
57
+ */
58
+ readonly ariaLabel: _angular_core.InputSignal<string>;
59
+ /**
60
+ * @description Regular expression used to determine whether each individual OTP character is allowed.
61
+ * @default null
62
+ */
63
+ readonly characterPattern: _angular_core.InputSignal<RegExp | null>;
64
+ /**
65
+ * @description Emitted when a user interaction completes the verification code.
66
+ */
67
+ readonly complete: _angular_core.OutputEmitterRef<string>;
68
+ /**
69
+ * @description Renders the component with reduced visual emphasis and removes pointer interaction.
70
+ * When bound to a signal form field via `[formField]`, this is written by the `FormField` directive.
71
+ * @default false
72
+ */
73
+ readonly disabled: _angular_core.InputSignal<boolean>;
74
+ /**
75
+ * @description Configures slot grouping lengths. Accepts a uniform number or an array of group sizes.
76
+ * @default null
77
+ */
78
+ readonly groupLength: _angular_core.InputSignal<number | number[] | null>;
79
+ /**
80
+ * @description Additional HTML attributes applied directly to the inner `<input>` element.
81
+ * @default {}
82
+ */
83
+ readonly inputAttributes: _angular_core.InputSignal<AttributeConfig>;
84
+ /**
85
+ * @description Emitted when the inner input loses focus.
86
+ */
87
+ readonly inputBlur: _angular_core.OutputEmitterRef<FocusEvent>;
88
+ /**
89
+ * @description Emitted when the inner input gains focus.
90
+ */
91
+ readonly inputFocus: _angular_core.OutputEmitterRef<FocusEvent>;
92
+ /**
93
+ * @description Marks the OTP input as invalid. When bound to a signal form field via `[formField]`,
94
+ * this is written by the `FormField` directive.
95
+ * @default false
96
+ */
97
+ readonly invalid: _angular_core.InputSignal<boolean>;
98
+ /**
99
+ * @description Sets the total number of characters accepted by the OTP input and the number of visual slots.
100
+ * @default 4
101
+ */
102
+ readonly length: _angular_core.InputSignal<number>;
103
+ /**
104
+ * @description Hint character displayed in empty visual slots.
105
+ * @default ""
106
+ */
107
+ readonly placeholder: _angular_core.InputSignal<string>;
108
+ /**
109
+ * @description Prevents value changes while preserving the component's focusability and visual state.
110
+ * When bound to a signal form field via `[formField]`, this is written by the `FormField` directive.
111
+ * @default false
112
+ */
113
+ readonly readonly: _angular_core.InputSignal<boolean>;
114
+ /**
115
+ * @description Marks the OTP input as required. Triggers invalid state when touched and incomplete.
116
+ * When bound to a signal form field via `[formField]`, this is written by the `FormField` directive.
117
+ * @default false
118
+ */
119
+ readonly required: _angular_core.InputSignal<boolean>;
120
+ /**
121
+ * @description Border-radius preset applied to the OTP slots.
122
+ * @default "medium"
123
+ */
124
+ readonly rounded: _angular_core.InputSignal<"full" | "large" | "medium" | "none" | "small" | null | undefined>;
125
+ /**
126
+ * @description Text character or string displayed between visual slot groups.
127
+ * @default ""
128
+ */
129
+ readonly separator: _angular_core.InputSignal<string>;
130
+ /**
131
+ * @description Additional CSS classes applied to separator elements.
132
+ * @default ""
133
+ */
134
+ readonly separatorClass: _angular_core.InputSignal<string | string[]>;
135
+ /**
136
+ * @description Size preset controlling the dimensions and font size of the OTP slots.
137
+ * @default "medium"
138
+ */
139
+ readonly size: _angular_core.InputSignal<"large" | "medium" | "small" | null | undefined>;
140
+ /**
141
+ * @description Additional CSS classes applied to each visual slot element.
142
+ * @default ""
143
+ */
144
+ readonly slotClass: _angular_core.InputSignal<string | string[]>;
145
+ /**
146
+ * @description Determines whether visual slots within a group have spacing between them or are joined together.
147
+ * @default true
148
+ */
149
+ readonly spacing: _angular_core.InputSignal<boolean>;
150
+ /**
151
+ * @description Emitted when the OTP input is interacted with — on blur or user value change.
152
+ * The `FormField` directive listens to this to mark the field as touched.
153
+ */
154
+ readonly touch: _angular_core.OutputEmitterRef<void>;
155
+ /**
156
+ * @description Sets the touched state of the OTP input. When bound to a signal form field via `[formField]`,
157
+ * this is written by the `FormField` directive.
158
+ * @default false
159
+ */
160
+ readonly touched: _angular_core.InputSignal<boolean>;
161
+ /**
162
+ * @description Sets the type of the OTP input ("text", "number", or "password").
163
+ * @default "text"
164
+ */
165
+ readonly type: _angular_core.InputSignal<OtpInputType>;
166
+ /**
167
+ * @description Additional CSS classes merged onto the host element via `tailwind-merge`.
168
+ * @default ""
169
+ */
170
+ readonly userClass: _angular_core.InputSignal<string>;
171
+ /**
172
+ * @description Two-way bindable current value of the OTP input.
173
+ * @default ""
174
+ */
175
+ readonly value: _angular_core.ModelSignal<string>;
176
+ constructor();
177
+ blur(): void;
178
+ focus(options?: FocusOptions): void;
179
+ focus(index?: number): void;
180
+ protected onBeforeInput(event: InputEvent): void;
181
+ protected onBlur(event: FocusEvent): void;
182
+ protected onCompositionEnd(event: CompositionEvent): void;
183
+ protected onCompositionStart(): void;
184
+ protected onFocus(event: FocusEvent): void;
185
+ protected onInput(_event?: Event): void;
186
+ protected onKeyDown(event: KeyboardEvent): void;
187
+ protected onKeyUp(): void;
188
+ protected onPaste(event: ClipboardEvent): void;
189
+ protected onSelect(): void;
190
+ protected onSlotPointerDown(event: PointerEvent, index: number): void;
191
+ private applyUserInsertion;
192
+ private syncSelection;
193
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<OtpInputComponent, never>;
194
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<OtpInputComponent, "mona-otp-input", never, { "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "characterPattern": { "alias": "characterPattern"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "groupLength": { "alias": "groupLength"; "required": false; "isSignal": true; }; "inputAttributes": { "alias": "inputAttributes"; "required": false; "isSignal": true; }; "invalid": { "alias": "invalid"; "required": false; "isSignal": true; }; "length": { "alias": "length"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "readonly": { "alias": "readonly"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "rounded": { "alias": "rounded"; "required": false; "isSignal": true; }; "separator": { "alias": "separator"; "required": false; "isSignal": true; }; "separatorClass": { "alias": "separatorClass"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "slotClass": { "alias": "slotClass"; "required": false; "isSignal": true; }; "spacing": { "alias": "spacing"; "required": false; "isSignal": true; }; "touched": { "alias": "touched"; "required": false; "isSignal": true; }; "type": { "alias": "type"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "complete": "complete"; "inputBlur": "inputBlur"; "inputFocus": "inputFocus"; "touch": "touch"; "value": "valueChange"; }, ["separatorTemplate"], never, true, never>;
195
+ }
196
+
197
+ interface OtpInputSeparatorContext {
198
+ $implicit: number;
199
+ groupIndex: number;
200
+ }
201
+ declare class OtpInputSeparatorTemplateDirective {
202
+ readonly templateRef: TemplateRef<any>;
203
+ static ngTemplateContextGuard(_directive: OtpInputSeparatorTemplateDirective, _context: unknown): _context is OtpInputSeparatorContext;
204
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<OtpInputSeparatorTemplateDirective, never>;
205
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<OtpInputSeparatorTemplateDirective, "ng-template[monaOtpInputSeparatorTemplate]", never, {}, {}, never, never, true, never>;
206
+ }
207
+
208
+ export { OtpInputComponent, OtpInputSeparatorTemplateDirective };
209
+ export type { OtpInputSeparatorContext, OtpInputType, OtpInputVariantProps };
@@ -148,6 +148,13 @@ declare class SegmentedComponent<T extends SegmentedValue = SegmentedValue> impl
148
148
  protected createOptionContext(option: SegmentedOption<T>, index: number): SegmentedItemTemplateContext<T>;
149
149
  protected onOptionBlur(): void;
150
150
  protected onOptionChange(option: SegmentedOption<T>): void;
151
+ private observeOptionElements;
152
+ /**
153
+ * Guards against a stale indicator snapshot when the control mounts inside a container that is
154
+ * still transitioning in (e.g. a popup's enter animation): the host's own box may not change once
155
+ * that settles, so the ResizeObserver on it alone would never re-fire to correct the geometry.
156
+ */
157
+ private scheduleSettledGeometryUpdate;
151
158
  private setupResizeObserver;
152
159
  private updateIndicatorGeometry;
153
160
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<SegmentedComponent<any>, never>;