@cdevhub/ngx-tw 0.2.0 → 0.3.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 (144) hide show
  1. package/README.md +264 -49
  2. package/fesm2022/cdevhub-ngx-tw-accordion.mjs +11 -7
  3. package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -1
  4. package/fesm2022/cdevhub-ngx-tw-alert.mjs +43 -29
  5. package/fesm2022/cdevhub-ngx-tw-alert.mjs.map +1 -1
  6. package/fesm2022/cdevhub-ngx-tw-aspect-ratio.mjs +103 -0
  7. package/fesm2022/cdevhub-ngx-tw-aspect-ratio.mjs.map +1 -0
  8. package/fesm2022/cdevhub-ngx-tw-avatar.mjs +56 -31
  9. package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -1
  10. package/fesm2022/cdevhub-ngx-tw-badge.mjs +61 -35
  11. package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -1
  12. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +154 -93
  13. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -1
  14. package/fesm2022/cdevhub-ngx-tw-button.mjs +35 -22
  15. package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -1
  16. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +3 -3
  17. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -1
  18. package/fesm2022/cdevhub-ngx-tw-calendar.mjs +416 -216
  19. package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -1
  20. package/fesm2022/cdevhub-ngx-tw-card.mjs +33 -24
  21. package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -1
  22. package/fesm2022/cdevhub-ngx-tw-carousel.mjs +152 -81
  23. package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -1
  24. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +99 -47
  25. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -1
  26. package/fesm2022/cdevhub-ngx-tw-code-block.mjs +42 -29
  27. package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -1
  28. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +46 -29
  29. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -1
  30. package/fesm2022/cdevhub-ngx-tw-combobox.mjs +205 -110
  31. package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -1
  32. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +141 -74
  33. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -1
  34. package/fesm2022/cdevhub-ngx-tw-core.mjs +16 -11
  35. package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -1
  36. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +395 -149
  37. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -1
  38. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +305 -150
  39. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -1
  40. package/fesm2022/cdevhub-ngx-tw-dialog.mjs +56 -43
  41. package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -1
  42. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +51 -33
  43. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -1
  44. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs +905 -0
  45. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs.map +1 -0
  46. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +49 -26
  47. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -1
  48. package/fesm2022/cdevhub-ngx-tw-form-field.mjs +133 -74
  49. package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -1
  50. package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs +2 -2
  51. package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs.map +1 -1
  52. package/fesm2022/cdevhub-ngx-tw-icon.mjs +28 -17
  53. package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -1
  54. package/fesm2022/cdevhub-ngx-tw-input.mjs +37 -21
  55. package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -1
  56. package/fesm2022/cdevhub-ngx-tw-item.mjs +33 -24
  57. package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -1
  58. package/fesm2022/cdevhub-ngx-tw-menu.mjs +63 -48
  59. package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -1
  60. package/fesm2022/cdevhub-ngx-tw-number-input.mjs +622 -0
  61. package/fesm2022/cdevhub-ngx-tw-number-input.mjs.map +1 -0
  62. package/fesm2022/cdevhub-ngx-tw-paginator.mjs +142 -72
  63. package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -1
  64. package/fesm2022/cdevhub-ngx-tw-popover.mjs +101 -46
  65. package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -1
  66. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +57 -30
  67. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -1
  68. package/fesm2022/cdevhub-ngx-tw-radio.mjs +108 -57
  69. package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -1
  70. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +46 -26
  71. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -1
  72. package/fesm2022/cdevhub-ngx-tw-select.mjs +227 -122
  73. package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -1
  74. package/fesm2022/cdevhub-ngx-tw-separator.mjs +21 -12
  75. package/fesm2022/cdevhub-ngx-tw-separator.mjs.map +1 -1
  76. package/fesm2022/cdevhub-ngx-tw-sheet.mjs +58 -44
  77. package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -1
  78. package/fesm2022/cdevhub-ngx-tw-skeleton.mjs +25 -14
  79. package/fesm2022/cdevhub-ngx-tw-skeleton.mjs.map +1 -1
  80. package/fesm2022/cdevhub-ngx-tw-slider.mjs +155 -79
  81. package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -1
  82. package/fesm2022/cdevhub-ngx-tw-sort.mjs +47 -25
  83. package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -1
  84. package/fesm2022/cdevhub-ngx-tw-spinner.mjs +29 -16
  85. package/fesm2022/cdevhub-ngx-tw-spinner.mjs.map +1 -1
  86. package/fesm2022/cdevhub-ngx-tw-split.mjs +72 -42
  87. package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -1
  88. package/fesm2022/cdevhub-ngx-tw-stat.mjs +107 -64
  89. package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -1
  90. package/fesm2022/cdevhub-ngx-tw-stepper.mjs +113 -47
  91. package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -1
  92. package/fesm2022/cdevhub-ngx-tw-switch.mjs +61 -32
  93. package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -1
  94. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +88 -42
  95. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -1
  96. package/fesm2022/cdevhub-ngx-tw-table.mjs +266 -118
  97. package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -1
  98. package/fesm2022/cdevhub-ngx-tw-tabs.mjs +102 -52
  99. package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -1
  100. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs +798 -0
  101. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs.map +1 -0
  102. package/fesm2022/cdevhub-ngx-tw-textarea.mjs +16 -8
  103. package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -1
  104. package/fesm2022/cdevhub-ngx-tw-theme.mjs +22 -14
  105. package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -1
  106. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +243 -95
  107. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -1
  108. package/fesm2022/cdevhub-ngx-tw-timeline.mjs +149 -82
  109. package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -1
  110. package/fesm2022/cdevhub-ngx-tw-toast.mjs +100 -55
  111. package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -1
  112. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +50 -28
  113. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -1
  114. package/fesm2022/cdevhub-ngx-tw-transfer.mjs +1129 -0
  115. package/fesm2022/cdevhub-ngx-tw-transfer.mjs.map +1 -0
  116. package/fesm2022/cdevhub-ngx-tw-tree.mjs +554 -0
  117. package/fesm2022/cdevhub-ngx-tw-tree.mjs.map +1 -0
  118. package/fesm2022/cdevhub-ngx-tw.mjs +7 -0
  119. package/fesm2022/cdevhub-ngx-tw.mjs.map +1 -1
  120. package/package.json +45 -17
  121. package/theme/_dark.css +16 -12
  122. package/theme/_semantic.css +3 -2
  123. package/theme/index.css +10 -2
  124. package/types/cdevhub-ngx-tw-aspect-ratio.d.ts +40 -0
  125. package/types/cdevhub-ngx-tw-calendar.d.ts +39 -8
  126. package/types/cdevhub-ngx-tw-code-block.d.ts +8 -7
  127. package/types/cdevhub-ngx-tw-command-palette.d.ts +7 -0
  128. package/types/cdevhub-ngx-tw-date-picker.d.ts +36 -10
  129. package/types/cdevhub-ngx-tw-file-upload.d.ts +250 -0
  130. package/types/cdevhub-ngx-tw-form-field.d.ts +22 -3
  131. package/types/cdevhub-ngx-tw-icon-lucide.d.ts +21 -3
  132. package/types/cdevhub-ngx-tw-number-input.d.ts +130 -0
  133. package/types/cdevhub-ngx-tw-popover.d.ts +8 -0
  134. package/types/cdevhub-ngx-tw-select.d.ts +1 -1
  135. package/types/cdevhub-ngx-tw-stepper.d.ts +30 -2
  136. package/types/cdevhub-ngx-tw-tab-nav.d.ts +8 -0
  137. package/types/cdevhub-ngx-tw-table.d.ts +30 -1
  138. package/types/cdevhub-ngx-tw-tabs.d.ts +8 -0
  139. package/types/cdevhub-ngx-tw-tags-input.d.ts +217 -0
  140. package/types/cdevhub-ngx-tw-time-picker.d.ts +31 -6
  141. package/types/cdevhub-ngx-tw-toast.d.ts +1 -1
  142. package/types/cdevhub-ngx-tw-transfer.d.ts +330 -0
  143. package/types/cdevhub-ngx-tw-tree.d.ts +189 -0
  144. package/types/cdevhub-ngx-tw.d.ts +7 -0
@@ -0,0 +1,798 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, output, inject, ElementRef, DestroyRef, viewChild, viewChildren, signal, computed, effect, untracked, afterNextRender, isDevMode, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { NgControl, NgForm, FormGroupDirective, Validators } from '@angular/forms';
4
+ import { FocusMonitor, LiveAnnouncer } from '@angular/cdk/a11y';
5
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
+ import { isObservable, merge } from 'rxjs';
7
+ import { tv } from 'tailwind-variants';
8
+ import { TW_ERROR_STATE_MATCHER } from '@cdevhub/ngx-tw/core';
9
+ import { FormFieldControl, TW_FORM_FIELD, TW_FORM_FIELD_CONTROL } from '@cdevhub/ngx-tw/form-field';
10
+ import { BadgeComponent } from '@cdevhub/ngx-tw/badge';
11
+
12
+ // ── tv() config ──
13
+ // `focus-within:outline-{color}-500` and the chip color classes are emitted by
14
+ // combobox / badge already, so the Tailwind v4 JIT scanner picks them up.
15
+ const tagsInputVariants = tv({
16
+ slots: {
17
+ // Host is the `role="group"` chip strip. `flex-wrap` lets chips wrap; the
18
+ // container owns the focus-within ring (standalone) or strips its chrome
19
+ // inside a `<tw-form-field>` (the field draws the border / focus border).
20
+ root: 'flex flex-wrap items-center gap-1.5 w-full text-fg',
21
+ chip: 'max-w-full min-w-0',
22
+ chipLabel: 'truncate',
23
+ // Real DOM focus → canonical outline ring (NOT the activedescendant
24
+ // background-shift carve-out). `text-current` inherits the chip color.
25
+ // Per-size sets the square-interactive target size + negative margins.
26
+ removeButton: 'inline-flex items-center justify-center shrink-0 ml-0.5 rounded-md text-current opacity-70 hover:opacity-100 transition-opacity duration-normal motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 disabled:pointer-events-none',
27
+ removeIcon: 'shrink-0',
28
+ input: 'flex-1 min-w-24 bg-transparent text-fg placeholder:text-fg-subtle outline-none disabled:cursor-not-allowed',
29
+ },
30
+ variants: {
31
+ inFormField: {
32
+ // Inside a form-field: keep only the chip-strip layout; the field owns
33
+ // border, padding, background and the focused-border indicator.
34
+ true: { root: '' },
35
+ false: {
36
+ root: 'rounded-md border border-border bg-surface transition-colors duration-normal motion-reduce:transition-none hover:border-border-strong focus-within:outline-2 focus-within:outline-offset-2',
37
+ },
38
+ },
39
+ size: {
40
+ // removeButton uses the square-interactive sub-scale (the button IS the
41
+ // touch target): size-6 (24px) meets WCAG 2.5.8; negative margins absorb
42
+ // the overflow so the chip height is unchanged. Mirrors badge's dismiss.
43
+ // `size-3.5` half-step on the md remove glyph lines up the X with text-sm
44
+ // at md density (codified half-step per CLAUDE.md icon sizing).
45
+ xs: { root: 'text-xs', removeButton: 'size-6 -my-1 -mr-1', removeIcon: 'size-3' },
46
+ sm: { root: 'text-sm', removeButton: 'size-6 -my-1 -mr-1', removeIcon: 'size-3' },
47
+ md: { root: 'text-sm', removeButton: 'size-7 -my-1.5 -mr-1.5', removeIcon: 'size-3.5' },
48
+ lg: { root: 'text-base', removeButton: 'size-8 -my-1.5 -mr-1.5', removeIcon: 'size-4' },
49
+ xl: { root: 'text-base', removeButton: 'size-8 -my-1.5 -mr-1.5', removeIcon: 'size-4' },
50
+ },
51
+ color: {
52
+ primary: { root: 'focus-within:outline-primary-500' },
53
+ secondary: { root: 'focus-within:outline-secondary-500' },
54
+ accent: { root: 'focus-within:outline-accent-500' },
55
+ neutral: { root: 'focus-within:outline-border-strong' },
56
+ info: { root: 'focus-within:outline-info-500' },
57
+ success: { root: 'focus-within:outline-success-500' },
58
+ warning: { root: 'focus-within:outline-warning-500' },
59
+ error: { root: 'focus-within:outline-error-500' },
60
+ },
61
+ disabled: {
62
+ true: { root: 'opacity-50 pointer-events-none cursor-not-allowed' },
63
+ false: {},
64
+ },
65
+ errorState: {
66
+ true: {},
67
+ false: {},
68
+ },
69
+ },
70
+ compoundVariants: [
71
+ // Standalone density × size — inline padding scale (mirrors combobox trigger).
72
+ { inFormField: false, size: 'xs', class: { root: 'px-2 py-1' } },
73
+ { inFormField: false, size: 'sm', class: { root: 'px-3 py-1.5' } },
74
+ { inFormField: false, size: 'md', class: { root: 'px-4 py-2' } },
75
+ { inFormField: false, size: 'lg', class: { root: 'px-5 py-2.5' } },
76
+ { inFormField: false, size: 'xl', class: { root: 'px-6 py-3' } },
77
+ // Standalone error state — colored border + focus ring.
78
+ {
79
+ inFormField: false,
80
+ errorState: true,
81
+ class: { root: 'border-error-500 focus-within:outline-error-500' },
82
+ },
83
+ ],
84
+ defaultVariants: {
85
+ inFormField: false,
86
+ size: 'md',
87
+ color: 'primary',
88
+ disabled: false,
89
+ errorState: false,
90
+ },
91
+ }, { twMerge: true });
92
+ const DEFAULT_SEPARATORS = ['Enter', ','];
93
+ let nextId = 0;
94
+ /**
95
+ * Free-text multi-value input. The user types a token and commits it via Enter,
96
+ * a separator key, paste, or blur; committed tokens render as dismissible chips
97
+ * (composing `[twBadge]`) inline with the text input. The value is exposed as a
98
+ * real `T[]` (default `string[]`) through `ControlValueAccessor`, so it works
99
+ * with template-driven, reactive, and signal forms and integrates with
100
+ * `<tw-form-field>` for label / hint / error chrome.
101
+ *
102
+ * The control is a single tab stop with a roving-tabindex chip strip
103
+ * (Material `MatChipGrid` interaction model): Arrow keys traverse chips and the
104
+ * input, Delete / a two-step Backspace remove chips, and `LiveAnnouncer` voices
105
+ * add / remove. Autocomplete suggestions are out of scope (use `tw-combobox`).
106
+ */
107
+ class TagsInputComponent extends FormFieldControl {
108
+ /** Semantic color for the container focus-within ring and the chip accent. Defaults to `'primary'`. */
109
+ color = input('primary', /* @ts-ignore */
110
+ ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
111
+ /** Controls overall density: container padding, chip size, and text scale. Defaults to `'md'`. */
112
+ size = input('md', /* @ts-ignore */
113
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
114
+ /** When true, blocks typing, committing, and chip removal, and applies muted styling. Reactive forms also propagate disabled via `setDisabledState`. Defaults to `false`. */
115
+ disabledInput = input(false, { ...(ngDevMode ? { debugName: "disabledInput" } : /* istanbul ignore next */ {}), alias: 'disabled' });
116
+ /** Marks the control as required. Mirrored to the input's `aria-required`. Also inferred from `Validators.required` on a bound control. Defaults to `false`. */
117
+ requiredInput = input(false, { ...(ngDevMode ? { debugName: "requiredInput" } : /* istanbul ignore next */ {}), alias: 'required' });
118
+ /** Placeholder shown in the text input only while there are no chips and the input is empty. */
119
+ placeholder = input(undefined, /* @ts-ignore */
120
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
121
+ /** Keys that commit the in-progress text as a tag. Each entry is a `KeyboardEvent.key` value (`'Enter'`) or a single separator character (`','`). Single-character separators also split pasted text. Defaults to `['Enter', ',']`. */
122
+ separatorKeys = input(DEFAULT_SEPARATORS, /* @ts-ignore */
123
+ ...(ngDevMode ? [{ debugName: "separatorKeys" }] : /* istanbul ignore next */ []));
124
+ /** When true, blurring the control while the input holds non-empty text commits it as a tag. Defaults to `false`. */
125
+ addOnBlur = input(false, /* @ts-ignore */
126
+ ...(ngDevMode ? [{ debugName: "addOnBlur" }] : /* istanbul ignore next */ []));
127
+ /** Maximum number of tags. Once reached, further commits are blocked and announced. Does not truncate an oversized `writeValue`. Defaults to `undefined` (no limit). */
128
+ maxTags = input(undefined, /* @ts-ignore */
129
+ ...(ngDevMode ? [{ debugName: "maxTags" }] : /* istanbul ignore next */ []));
130
+ /** When false (default), a committed tag equal (per `compareWith`) to an existing tag is dropped silently. When true, duplicates are kept. Defaults to `false`. */
131
+ allowDuplicates = input(false, /* @ts-ignore */
132
+ ...(ngDevMode ? [{ debugName: "allowDuplicates" }] : /* istanbul ignore next */ []));
133
+ /** Maps committed text to a tag value. Default is identity — the trimmed string. Override to build object tags. */
134
+ createTag = input(((text) => text), /* @ts-ignore */
135
+ ...(ngDevMode ? [{ debugName: "createTag" }] : /* istanbul ignore next */ []));
136
+ /** Maps a tag value to its visible chip label and the remove-button accessible name. Defaults to `String(tag)`. */
137
+ tagLabel = input((tag) => String(tag), /* @ts-ignore */
138
+ ...(ngDevMode ? [{ debugName: "tagLabel" }] : /* istanbul ignore next */ []));
139
+ /** Equality comparator used for dedup when `allowDuplicates` is false. Defaults to `Object.is` (reference / value identity). String tags dedupe case-sensitively by default; pass `(a, b) => a.toLowerCase() === b.toLowerCase()` for case-insensitive dedup. */
140
+ compareWith = input((a, b) => Object.is(a, b), /* @ts-ignore */
141
+ ...(ngDevMode ? [{ debugName: "compareWith" }] : /* istanbul ignore next */ []));
142
+ /** Applied to the text input for labeling and identification only; does not submit the tag array via native (non-Angular) form posting. */
143
+ name = input(undefined, /* @ts-ignore */
144
+ ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
145
+ /** Id on the host element. Auto-generated as `tw-tags-input-N` when not provided. Used by the form-field's `<label for>` association. */
146
+ idInput = input(undefined, { ...(ngDevMode ? { debugName: "idInput" } : /* istanbul ignore next */ {}), alias: 'id' });
147
+ /** Accessible name applied to the control when no visible label is wired. Mirrored to `aria-label`. */
148
+ ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
149
+ /** ID of an external element that labels the control. Mirrored to `aria-labelledby`. */
150
+ ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
151
+ /** ID of an external element that describes the control. Form-field merges its hint / error ids alongside. */
152
+ ariaDescribedby = input(undefined, { ...(ngDevMode ? { debugName: "ariaDescribedby" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
153
+ /** Per-instance override of the {@link ErrorStateMatcher}. When omitted, uses the `TW_ERROR_STATE_MATCHER` token's value. */
154
+ errorStateMatcher = input(undefined, /* @ts-ignore */
155
+ ...(ngDevMode ? [{ debugName: "errorStateMatcher" }] : /* istanbul ignore next */ []));
156
+ /** Fires when the tag array changes through user interaction (add, remove, clear). Emits a fresh array reference. Does not fire on `writeValue`. */
157
+ valueChange = output();
158
+ /** Fires when a tag is committed via Enter, a separator key, paste, or `addTag()`. Does not fire for dropped duplicates, blocked-by-max commits, empty commits, or `writeValue`. */
159
+ tagAdded = output();
160
+ /** Fires when a tag is removed via the remove button, Backspace / Delete, or `removeTag()`. Does not fire on `writeValue` or `clear()`. */
161
+ tagRemoved = output();
162
+ focusMonitor = inject(FocusMonitor);
163
+ liveAnnouncer = inject(LiveAnnouncer);
164
+ elementRef = inject((ElementRef));
165
+ destroyRef = inject(DestroyRef);
166
+ ngControl = inject(NgControl, { optional: true, self: true });
167
+ parentForm = inject(NgForm, { optional: true });
168
+ parentFormGroup = inject(FormGroupDirective, { optional: true });
169
+ defaultMatcher = inject(TW_ERROR_STATE_MATCHER);
170
+ formField = inject(TW_FORM_FIELD, { optional: true });
171
+ textInputRef = viewChild('textInput', /* @ts-ignore */
172
+ ...(ngDevMode ? [{ debugName: "textInputRef" }] : /* istanbul ignore next */ []));
173
+ removeButtons = viewChildren('removeBtn', /* @ts-ignore */
174
+ ...(ngDevMode ? [{ debugName: "removeButtons" }] : /* istanbul ignore next */ []));
175
+ onChange = () => { };
176
+ onTouched = () => { };
177
+ tags = signal([], /* @ts-ignore */
178
+ ...(ngDevMode ? [{ debugName: "tags" }] : /* istanbul ignore next */ []));
179
+ /** The current in-progress (uncommitted) text in the input. Empty when nothing is typed. */
180
+ inputText = signal('', /* @ts-ignore */
181
+ ...(ngDevMode ? [{ debugName: "inputText" }] : /* istanbul ignore next */ []));
182
+ /** @internal Roving-tabindex position. `null` = the text input is the active element; a number = that chip's remove button. */
183
+ activeChipIndex = signal(null, /* @ts-ignore */
184
+ ...(ngDevMode ? [{ debugName: "activeChipIndex" }] : /* istanbul ignore next */ []));
185
+ cvaDisabled = signal(false, /* @ts-ignore */
186
+ ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
187
+ _focused = signal(false, /* @ts-ignore */
188
+ ...(ngDevMode ? [{ debugName: "_focused" }] : /* istanbul ignore next */ []));
189
+ _ngControlRev = signal(0, /* @ts-ignore */
190
+ ...(ngDevMode ? [{ debugName: "_ngControlRev" }] : /* istanbul ignore next */ []));
191
+ _formSubmitRev = signal(0, /* @ts-ignore */
192
+ ...(ngDevMode ? [{ debugName: "_formSubmitRev" }] : /* istanbul ignore next */ []));
193
+ describedByIdsSignal = signal([], /* @ts-ignore */
194
+ ...(ngDevMode ? [{ debugName: "describedByIdsSignal" }] : /* istanbul ignore next */ []));
195
+ labelledByIdsSignal = signal([], /* @ts-ignore */
196
+ ...(ngDevMode ? [{ debugName: "labelledByIdsSignal" }] : /* istanbul ignore next */ []));
197
+ /** Nonce bumped only on explicit focus intent — keeps the focus effect from stealing focus on unrelated renders. */
198
+ focusNonce = signal(0, /* @ts-ignore */
199
+ ...(ngDevMode ? [{ debugName: "focusNonce" }] : /* istanbul ignore next */ []));
200
+ lastMaxAnnounce = 0;
201
+ uid = nextId++;
202
+ /** @internal Fallback id used when the consumer does not set `[id]`. */
203
+ hostId = `tw-tags-input-${this.uid}`;
204
+ // ── FormFieldControl signals ──
205
+ /** @internal */
206
+ id = computed(() => this.idInput() ?? this.hostId, /* @ts-ignore */
207
+ ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
208
+ /** The current tag array (empty array when there are no tags). Read from a template ref (`#t="twTagsInput"`) for non-form usage. Doubles as the `FormFieldControl` contract member. */
209
+ value = this.tags.asReadonly();
210
+ /** @internal */
211
+ focused = this._focused.asReadonly();
212
+ /** @internal Empty when there are no chips AND the input text is blank — drives the form-field floating label. */
213
+ empty = computed(() => this.tags().length === 0 && this.inputText().trim().length === 0, /* @ts-ignore */
214
+ ...(ngDevMode ? [{ debugName: "empty" }] : /* istanbul ignore next */ []));
215
+ /** @internal */
216
+ disabled = computed(() => this.disabledInput() || this.cvaDisabled(), /* @ts-ignore */
217
+ ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
218
+ /** @internal */
219
+ required = computed(() => {
220
+ this._ngControlRev();
221
+ if (this.requiredInput())
222
+ return true;
223
+ return !!this.ngControl?.control?.hasValidator(Validators.required);
224
+ }, /* @ts-ignore */
225
+ ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
226
+ /** @internal */
227
+ errorState = computed(() => {
228
+ this._ngControlRev();
229
+ this._formSubmitRev();
230
+ this._focused();
231
+ const matcher = this.errorStateMatcher() ?? this.defaultMatcher;
232
+ const form = this.parentFormGroup ??
233
+ this.parentForm;
234
+ return matcher.isErrorState(this.ngControl?.control ?? null, form);
235
+ }, /* @ts-ignore */
236
+ ...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
237
+ /** @internal Active validation errors map from the bound control, for `[twError match="…"]` filtering. */
238
+ errors = computed(() => {
239
+ this._ngControlRev();
240
+ return this.ngControl?.control?.errors ?? null;
241
+ }, /* @ts-ignore */
242
+ ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
243
+ /** @internal */
244
+ controlType = 'tags-input';
245
+ /** @internal */
246
+ userAriaDescribedBy = computed(() => this.ariaDescribedby(), /* @ts-ignore */
247
+ ...(ngDevMode ? [{ debugName: "userAriaDescribedBy" }] : /* istanbul ignore next */ []));
248
+ /** @internal */
249
+ userAriaLabelledby = computed(() => this.ariaLabelledby(), /* @ts-ignore */
250
+ ...(ngDevMode ? [{ debugName: "userAriaLabelledby" }] : /* istanbul ignore next */ []));
251
+ // ── Effective ARIA ──
252
+ //
253
+ // When wrapped by `<tw-form-field>`, the form-field pushes the FULLY MERGED
254
+ // id list (hint/error/label + the consumer's `aria-*by` ids) via
255
+ // `setDescribedByIds` / `setLabelledByIds`. In that case the pushed ids alone
256
+ // are the source of truth — re-merging the consumer's `aria-*by` input on top
257
+ // would duplicate ids. When standalone, fall back to the consumer-supplied
258
+ // `aria-*by` input.
259
+ /** @internal Merged `aria-describedby` (form-field hint/error ids + consumer-supplied). */
260
+ resolvedDescribedBy = computed(() => {
261
+ const pushed = this.describedByIdsSignal();
262
+ if (pushed.length)
263
+ return pushed.join(' ');
264
+ const user = this.ariaDescribedby();
265
+ if (user)
266
+ return user.split(/\s+/).filter(Boolean).join(' ');
267
+ return '';
268
+ }, /* @ts-ignore */
269
+ ...(ngDevMode ? [{ debugName: "resolvedDescribedBy" }] : /* istanbul ignore next */ []));
270
+ /** @internal Merged `aria-labelledby` (form-field label id + consumer-supplied). */
271
+ resolvedLabelledBy = computed(() => {
272
+ const pushed = this.labelledByIdsSignal();
273
+ if (pushed.length)
274
+ return pushed.join(' ');
275
+ const user = this.ariaLabelledby();
276
+ if (user)
277
+ return user.split(/\s+/).filter(Boolean).join(' ');
278
+ return '';
279
+ }, /* @ts-ignore */
280
+ ...(ngDevMode ? [{ debugName: "resolvedLabelledBy" }] : /* istanbul ignore next */ []));
281
+ // ── tv() class bindings ──
282
+ variantResult = computed(() => tagsInputVariants({
283
+ inFormField: !!this.formField,
284
+ size: this.size(),
285
+ color: this.color(),
286
+ disabled: this.disabled(),
287
+ errorState: this.errorState(),
288
+ }), /* @ts-ignore */
289
+ ...(ngDevMode ? [{ debugName: "variantResult" }] : /* istanbul ignore next */ []));
290
+ /** @internal */
291
+ rootClasses = computed(() => this.variantResult().root(), /* @ts-ignore */
292
+ ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
293
+ /** @internal */
294
+ chipClasses = computed(() => this.variantResult().chip(), /* @ts-ignore */
295
+ ...(ngDevMode ? [{ debugName: "chipClasses" }] : /* istanbul ignore next */ []));
296
+ /** @internal */
297
+ chipLabelClasses = computed(() => this.variantResult().chipLabel(), /* @ts-ignore */
298
+ ...(ngDevMode ? [{ debugName: "chipLabelClasses" }] : /* istanbul ignore next */ []));
299
+ /** @internal */
300
+ removeButtonClasses = computed(() => this.variantResult().removeButton(), /* @ts-ignore */
301
+ ...(ngDevMode ? [{ debugName: "removeButtonClasses" }] : /* istanbul ignore next */ []));
302
+ /** @internal */
303
+ removeIconClasses = computed(() => this.variantResult().removeIcon(), /* @ts-ignore */
304
+ ...(ngDevMode ? [{ debugName: "removeIconClasses" }] : /* istanbul ignore next */ []));
305
+ /** @internal */
306
+ inputClasses = computed(() => this.variantResult().input(), /* @ts-ignore */
307
+ ...(ngDevMode ? [{ debugName: "inputClasses" }] : /* istanbul ignore next */ []));
308
+ /** @internal Pre-resolved chip view models so the template never calls accessor functions inline. */
309
+ chipViews = computed(() => {
310
+ const label = this.tagLabel();
311
+ return this.tags().map((tag, index) => {
312
+ const text = label(tag);
313
+ return { tag, label: text, removeLabel: `Remove ${text}`, index };
314
+ });
315
+ }, /* @ts-ignore */
316
+ ...(ngDevMode ? [{ debugName: "chipViews" }] : /* istanbul ignore next */ []));
317
+ constructor() {
318
+ super();
319
+ // Runtime CVA wiring — register on any host-level NgControl. The component
320
+ // injects `NgControl { self }` for TW_ERROR_STATE_MATCHER, so a static
321
+ // NG_VALUE_ACCESSOR provider would create circular DI on the same instance.
322
+ if (this.ngControl) {
323
+ this.ngControl.valueAccessor = this;
324
+ }
325
+ // Focus side-effect (leaves the signal graph; writes no signal → no cycle).
326
+ // Triggered only by `focusNonce` bumps so it never steals focus on an
327
+ // unrelated render (e.g. initial mount). Reads the active index and refs
328
+ // untracked so it does not re-fire when the chip list changes on its own.
329
+ effect(() => {
330
+ if (this.focusNonce() === 0)
331
+ return;
332
+ untracked(() => {
333
+ const idx = this.activeChipIndex();
334
+ if (idx === null) {
335
+ this.textInputRef()?.nativeElement.focus();
336
+ }
337
+ else {
338
+ this.removeButtons()[idx]?.nativeElement.focus();
339
+ }
340
+ });
341
+ });
342
+ afterNextRender(() => {
343
+ if (isDevMode() && !this.hasAccessibleNameHint()) {
344
+ console.warn('[tw-tags-input] The control has no accessible name. Provide `aria-label` / `aria-labelledby`, or wrap it in a <tw-form-field> with a <label twLabel>.');
345
+ }
346
+ });
347
+ }
348
+ // ── Public instance API ──
349
+ /** Commits `text` as a tag (trim → `createTag` → dedup unless `allowDuplicates` → `maxTags` check). Returns true if a tag was added, false if dropped (empty / duplicate / max). Emits `tagAdded` + `valueChange` on success. No-op when disabled. */
350
+ addTag(text) {
351
+ if (this.disabled())
352
+ return false;
353
+ const trimmed = text.trim();
354
+ if (!trimmed)
355
+ return false;
356
+ return this.tryAdd(this.createTag()(trimmed)) === 'added';
357
+ }
358
+ /** Removes a tag by value (first match via `compareWith`) or by index when a number is passed. Emits `tagRemoved` + `valueChange` and restores focus. No-op when disabled or when no match. Note: when the tag type is `number`, the argument is treated as an index. */
359
+ removeTag(tag) {
360
+ if (this.disabled())
361
+ return;
362
+ if (typeof tag === 'number') {
363
+ this.removeAt(tag);
364
+ return;
365
+ }
366
+ const cmp = this.compareWith();
367
+ const idx = this.tags().findIndex((t) => cmp(t, tag));
368
+ if (idx >= 0)
369
+ this.removeAt(idx);
370
+ }
371
+ /** Removes all tags and clears the in-progress input text. Emits `valueChange` with `[]`. Does NOT emit `tagRemoved` per tag (bulk reset). No-op when already empty or disabled. */
372
+ clear() {
373
+ if (this.disabled())
374
+ return;
375
+ if (this.tags().length === 0 && this.inputText().length === 0)
376
+ return;
377
+ this.tags.set([]);
378
+ this.setInputText('');
379
+ this.activeChipIndex.set(null);
380
+ this.onChange([]);
381
+ this.valueChange.emit([]);
382
+ this.liveAnnouncer.announce('All tags removed', 'polite');
383
+ }
384
+ /** Moves focus to the text input. */
385
+ focus() {
386
+ this.activeChipIndex.set(null);
387
+ this.requestFocus();
388
+ }
389
+ // ── Internal commit / remove pipeline ──
390
+ tryAdd(tag) {
391
+ const current = this.tags();
392
+ if (!this.allowDuplicates()) {
393
+ const cmp = this.compareWith();
394
+ if (current.some((e) => cmp(e, tag)))
395
+ return 'duplicate';
396
+ }
397
+ const max = this.maxTags();
398
+ if (max !== undefined && current.length >= max) {
399
+ this.announceMax();
400
+ return 'max';
401
+ }
402
+ const next = [...current, tag];
403
+ this.tags.set(next);
404
+ this.onChange(next);
405
+ this.tagAdded.emit({ tag, value: next });
406
+ this.valueChange.emit(next);
407
+ this.liveAnnouncer.announce(`${this.tagLabel()(tag)} added`, 'polite');
408
+ return 'added';
409
+ }
410
+ commitInput() {
411
+ if (this.disabled())
412
+ return;
413
+ const trimmed = this.inputText().trim();
414
+ if (!trimmed) {
415
+ this.setInputText('');
416
+ return;
417
+ }
418
+ const result = this.tryAdd(this.createTag()(trimmed));
419
+ // Clear on success or duplicate (gesture consumed); retain on max so the
420
+ // user can retry after removing a tag.
421
+ if (result !== 'max')
422
+ this.setInputText('');
423
+ }
424
+ removeAt(index) {
425
+ if (this.disabled())
426
+ return;
427
+ const current = this.tags();
428
+ if (index < 0 || index >= current.length)
429
+ return;
430
+ const tag = current[index];
431
+ const next = current.filter((_, i) => i !== index);
432
+ this.tags.set(next);
433
+ this.onChange(next);
434
+ this.tagRemoved.emit({ tag, value: next, index });
435
+ this.valueChange.emit(next);
436
+ this.liveAnnouncer.announce(`${this.tagLabel()(tag)} removed`, 'polite');
437
+ // Focus restoration: next chip at this index, else the previous chip, else the input.
438
+ if (next.length === 0) {
439
+ this.focusInput();
440
+ }
441
+ else if (index < next.length) {
442
+ this.focusChip(index);
443
+ }
444
+ else {
445
+ this.focusChip(next.length - 1);
446
+ }
447
+ }
448
+ // ── DOM event handlers ──
449
+ /** @internal */
450
+ onInput(event) {
451
+ this.inputText.set(event.target.value);
452
+ }
453
+ /** @internal */
454
+ onInputKeydown(event) {
455
+ if (this.disabled())
456
+ return;
457
+ const separators = this.separatorKeys();
458
+ const key = event.key;
459
+ // Single-character separator → commit (and swallow the character).
460
+ if (key.length === 1 && separators.includes(key)) {
461
+ event.preventDefault();
462
+ this.commitInput();
463
+ return;
464
+ }
465
+ // Enter separator → commit only when there is text (otherwise let the form submit).
466
+ if (key === 'Enter' && separators.includes('Enter')) {
467
+ if (this.inputText().trim().length > 0) {
468
+ event.preventDefault();
469
+ this.commitInput();
470
+ }
471
+ return;
472
+ }
473
+ if (key === 'ArrowLeft') {
474
+ const el = event.target;
475
+ const atStart = (el.selectionStart ?? 0) === 0 && (el.selectionEnd ?? 0) === 0;
476
+ if (atStart && this.tags().length > 0) {
477
+ event.preventDefault();
478
+ this.focusChip(this.tags().length - 1);
479
+ }
480
+ return;
481
+ }
482
+ if (key === 'Backspace' && this.inputText().length === 0) {
483
+ if (this.tags().length > 0) {
484
+ event.preventDefault();
485
+ this.focusChip(this.tags().length - 1);
486
+ }
487
+ return;
488
+ }
489
+ if (key === 'Escape' && this.inputText().length > 0) {
490
+ event.preventDefault();
491
+ this.setInputText('');
492
+ }
493
+ }
494
+ /** @internal */
495
+ onChipKeydown(event, index) {
496
+ if (this.disabled())
497
+ return;
498
+ const last = this.tags().length - 1;
499
+ switch (event.key) {
500
+ case 'ArrowLeft':
501
+ event.preventDefault();
502
+ if (index > 0)
503
+ this.focusChip(index - 1);
504
+ return;
505
+ case 'ArrowRight':
506
+ event.preventDefault();
507
+ if (index < last)
508
+ this.focusChip(index + 1);
509
+ else
510
+ this.focusInput();
511
+ return;
512
+ case 'Home':
513
+ event.preventDefault();
514
+ this.focusChip(0);
515
+ return;
516
+ case 'End':
517
+ event.preventDefault();
518
+ this.focusInput();
519
+ return;
520
+ case 'Delete':
521
+ case 'Backspace':
522
+ event.preventDefault();
523
+ this.removeAt(index);
524
+ return;
525
+ case 'Escape':
526
+ event.preventDefault();
527
+ this.focusInput();
528
+ return;
529
+ default:
530
+ return;
531
+ }
532
+ }
533
+ /** @internal */
534
+ onRemoveClick(index) {
535
+ this.removeAt(index);
536
+ }
537
+ /** @internal */
538
+ onPaste(event) {
539
+ if (this.disabled())
540
+ return;
541
+ const data = event.clipboardData?.getData('text') ?? '';
542
+ const charSeparators = this.separatorKeys().filter((k) => k.length === 1);
543
+ const hasSeparator = charSeparators.some((s) => data.includes(s));
544
+ if (!hasSeparator) {
545
+ // No separator: let the browser merge the text into the input natively.
546
+ return;
547
+ }
548
+ event.preventDefault();
549
+ const combined = this.inputText() + data;
550
+ this.setInputText('');
551
+ for (const piece of this.splitOnSeparators(combined, charSeparators)) {
552
+ const trimmed = piece.trim();
553
+ if (!trimmed)
554
+ continue;
555
+ if (this.tryAdd(this.createTag()(trimmed)) === 'max')
556
+ break;
557
+ }
558
+ }
559
+ // ── ControlValueAccessor ──
560
+ writeValue(value) {
561
+ this.tags.set(Array.isArray(value) ? [...value] : []);
562
+ this.setInputText('');
563
+ }
564
+ registerOnChange(fn) {
565
+ this.onChange = fn;
566
+ }
567
+ registerOnTouched(fn) {
568
+ this.onTouched = fn;
569
+ }
570
+ setDisabledState(isDisabled) {
571
+ this.cvaDisabled.set(isDisabled);
572
+ }
573
+ // ── FormFieldControl methods ──
574
+ /** @internal */
575
+ setDescribedByIds(ids) {
576
+ this.describedByIdsSignal.set([...ids]);
577
+ }
578
+ /** @internal */
579
+ setLabelledByIds(ids) {
580
+ this.labelledByIdsSignal.set([...ids]);
581
+ }
582
+ /** @internal Clicking the form-field surface (outside the control) focuses the text input. Clicks inside the control are left alone so chip removal keeps its own focus restoration. */
583
+ onContainerClick(event) {
584
+ if (this.disabled())
585
+ return;
586
+ if (event.defaultPrevented)
587
+ return;
588
+ const host = this.elementRef.nativeElement;
589
+ if (event.target === host || host.contains(event.target))
590
+ return;
591
+ this.focus();
592
+ }
593
+ // ── Lifecycle ──
594
+ ngOnInit() {
595
+ // checkChildren: true — internal focus moves (input ↔ chips) stay "focused";
596
+ // origin === null fires only when focus leaves the whole control, the single
597
+ // source for onTouched and the addOnBlur commit.
598
+ const monitor$ = this.focusMonitor.monitor(this.elementRef, true);
599
+ if (isObservable(monitor$)) {
600
+ monitor$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((origin) => {
601
+ const wasFocused = this._focused();
602
+ this._focused.set(!!origin);
603
+ if (wasFocused && !origin) {
604
+ if (this.addOnBlur() && this.inputText().trim().length > 0) {
605
+ this.commitInput();
606
+ }
607
+ this.onTouched();
608
+ this._ngControlRev.update((n) => n + 1);
609
+ // Reset roving so the next Tab into the control lands on the input.
610
+ this.activeChipIndex.set(null);
611
+ }
612
+ });
613
+ }
614
+ this.destroyRef.onDestroy(() => this.focusMonitor.stopMonitoring(this.elementRef));
615
+ const ctrl = this.ngControl?.control;
616
+ if (ctrl) {
617
+ const streams = [ctrl.statusChanges, ctrl.valueChanges].filter((s) => !!s);
618
+ if (streams.length) {
619
+ merge(...streams)
620
+ .pipe(takeUntilDestroyed(this.destroyRef))
621
+ .subscribe(() => this._ngControlRev.update((n) => n + 1));
622
+ }
623
+ }
624
+ const submit = this.parentFormGroup?.ngSubmit ?? this.parentForm?.ngSubmit;
625
+ if (submit) {
626
+ submit
627
+ .pipe(takeUntilDestroyed(this.destroyRef))
628
+ .subscribe(() => this._formSubmitRev.update((n) => n + 1));
629
+ }
630
+ }
631
+ // ── Helpers ──
632
+ focusChip(index) {
633
+ this.activeChipIndex.set(index);
634
+ this.requestFocus();
635
+ }
636
+ focusInput() {
637
+ this.activeChipIndex.set(null);
638
+ this.requestFocus();
639
+ }
640
+ requestFocus() {
641
+ this.focusNonce.update((n) => n + 1);
642
+ }
643
+ setInputText(value) {
644
+ this.inputText.set(value);
645
+ const el = this.textInputRef()?.nativeElement;
646
+ if (el && el.value !== value)
647
+ el.value = value;
648
+ }
649
+ announceMax() {
650
+ const now = Date.now();
651
+ if (now - this.lastMaxAnnounce < 500)
652
+ return;
653
+ this.lastMaxAnnounce = now;
654
+ this.liveAnnouncer.announce(`Maximum ${this.maxTags()} tags reached`, 'assertive');
655
+ }
656
+ splitOnSeparators(text, separators) {
657
+ if (separators.length === 0)
658
+ return [text];
659
+ let normalized = text;
660
+ for (const s of separators)
661
+ normalized = normalized.split(s).join('\u0000');
662
+ return normalized.split('\u0000');
663
+ }
664
+ hasAccessibleNameHint() {
665
+ return (!!this.ariaLabel() ||
666
+ !!this.ariaLabelledby() ||
667
+ this.labelledByIdsSignal().length > 0);
668
+ }
669
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: TagsInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
670
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: TagsInputComponent, isStandalone: true, selector: "tw-tags-input", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, requiredInput: { classPropertyName: "requiredInput", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, separatorKeys: { classPropertyName: "separatorKeys", publicName: "separatorKeys", isSignal: true, isRequired: false, transformFunction: null }, addOnBlur: { classPropertyName: "addOnBlur", publicName: "addOnBlur", isSignal: true, isRequired: false, transformFunction: null }, maxTags: { classPropertyName: "maxTags", publicName: "maxTags", isSignal: true, isRequired: false, transformFunction: null }, allowDuplicates: { classPropertyName: "allowDuplicates", publicName: "allowDuplicates", isSignal: true, isRequired: false, transformFunction: null }, createTag: { classPropertyName: "createTag", publicName: "createTag", isSignal: true, isRequired: false, transformFunction: null }, tagLabel: { classPropertyName: "tagLabel", publicName: "tagLabel", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, idInput: { classPropertyName: "idInput", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", tagAdded: "tagAdded", tagRemoved: "tagRemoved" }, host: { attributes: { "role": "group" }, properties: { "id": "id()", "class": "rootClasses()", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "resolvedLabelledBy() || null", "attr.aria-describedby": "resolvedDescribedBy() || null", "attr.aria-disabled": "disabled() || null", "attr.aria-invalid": "errorState() || null", "attr.data-focused": "focused() || null" } }, providers: [
671
+ {
672
+ provide: TW_FORM_FIELD_CONTROL,
673
+ useExisting: forwardRef(() => TagsInputComponent),
674
+ },
675
+ ], viewQueries: [{ propertyName: "textInputRef", first: true, predicate: ["textInput"], descendants: true, isSignal: true }, { propertyName: "removeButtons", predicate: ["removeBtn"], descendants: true, isSignal: true }], exportAs: ["twTagsInput"], usesInheritance: true, ngImport: i0, template: `
676
+ @for (chip of chipViews(); track chip.index) {
677
+ <span twBadge variant="soft" [color]="color()" [size]="size()" [class]="chipClasses()">
678
+ <span [class]="chipLabelClasses()">{{ chip.label }}</span>
679
+ <button
680
+ #removeBtn
681
+ type="button"
682
+ [class]="removeButtonClasses()"
683
+ [attr.tabindex]="activeChipIndex() === chip.index ? 0 : -1"
684
+ [attr.aria-label]="chip.removeLabel"
685
+ [disabled]="disabled()"
686
+ (click)="onRemoveClick(chip.index)"
687
+ (keydown)="onChipKeydown($event, chip.index)"
688
+ >
689
+ <svg
690
+ viewBox="0 0 16 16"
691
+ fill="currentColor"
692
+ aria-hidden="true"
693
+ [class]="removeIconClasses()"
694
+ >
695
+ <path
696
+ d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z"
697
+ />
698
+ </svg>
699
+ </button>
700
+ </span>
701
+ }
702
+ <input
703
+ #textInput
704
+ type="text"
705
+ [class]="inputClasses()"
706
+ [attr.tabindex]="activeChipIndex() === null ? 0 : -1"
707
+ [attr.placeholder]="placeholder() || null"
708
+ [attr.aria-required]="required() || null"
709
+ [attr.aria-label]="ariaLabel() || null"
710
+ [attr.aria-labelledby]="resolvedLabelledBy() || null"
711
+ [attr.aria-describedby]="resolvedDescribedBy() || null"
712
+ [attr.aria-invalid]="errorState() || null"
713
+ [attr.name]="name() || null"
714
+ [disabled]="disabled()"
715
+ (input)="onInput($event)"
716
+ (keydown)="onInputKeydown($event)"
717
+ (paste)="onPaste($event)"
718
+ />
719
+ `, isInline: true, dependencies: [{ kind: "component", type: BadgeComponent, selector: "[twBadge]", inputs: ["color", "variant", "size", "pill", "dismissible", "live", "dismissLabel"], outputs: ["dismissed"], exportAs: ["twBadge"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
720
+ }
721
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: TagsInputComponent, decorators: [{
722
+ type: Component,
723
+ args: [{
724
+ selector: 'tw-tags-input',
725
+ exportAs: 'twTagsInput',
726
+ changeDetection: ChangeDetectionStrategy.OnPush,
727
+ imports: [BadgeComponent],
728
+ providers: [
729
+ {
730
+ provide: TW_FORM_FIELD_CONTROL,
731
+ useExisting: forwardRef(() => TagsInputComponent),
732
+ },
733
+ ],
734
+ template: `
735
+ @for (chip of chipViews(); track chip.index) {
736
+ <span twBadge variant="soft" [color]="color()" [size]="size()" [class]="chipClasses()">
737
+ <span [class]="chipLabelClasses()">{{ chip.label }}</span>
738
+ <button
739
+ #removeBtn
740
+ type="button"
741
+ [class]="removeButtonClasses()"
742
+ [attr.tabindex]="activeChipIndex() === chip.index ? 0 : -1"
743
+ [attr.aria-label]="chip.removeLabel"
744
+ [disabled]="disabled()"
745
+ (click)="onRemoveClick(chip.index)"
746
+ (keydown)="onChipKeydown($event, chip.index)"
747
+ >
748
+ <svg
749
+ viewBox="0 0 16 16"
750
+ fill="currentColor"
751
+ aria-hidden="true"
752
+ [class]="removeIconClasses()"
753
+ >
754
+ <path
755
+ d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z"
756
+ />
757
+ </svg>
758
+ </button>
759
+ </span>
760
+ }
761
+ <input
762
+ #textInput
763
+ type="text"
764
+ [class]="inputClasses()"
765
+ [attr.tabindex]="activeChipIndex() === null ? 0 : -1"
766
+ [attr.placeholder]="placeholder() || null"
767
+ [attr.aria-required]="required() || null"
768
+ [attr.aria-label]="ariaLabel() || null"
769
+ [attr.aria-labelledby]="resolvedLabelledBy() || null"
770
+ [attr.aria-describedby]="resolvedDescribedBy() || null"
771
+ [attr.aria-invalid]="errorState() || null"
772
+ [attr.name]="name() || null"
773
+ [disabled]="disabled()"
774
+ (input)="onInput($event)"
775
+ (keydown)="onInputKeydown($event)"
776
+ (paste)="onPaste($event)"
777
+ />
778
+ `,
779
+ host: {
780
+ 'role': 'group',
781
+ '[id]': 'id()',
782
+ '[class]': 'rootClasses()',
783
+ '[attr.aria-label]': 'ariaLabel() || null',
784
+ '[attr.aria-labelledby]': 'resolvedLabelledBy() || null',
785
+ '[attr.aria-describedby]': 'resolvedDescribedBy() || null',
786
+ '[attr.aria-disabled]': 'disabled() || null',
787
+ '[attr.aria-invalid]': 'errorState() || null',
788
+ '[attr.data-focused]': 'focused() || null',
789
+ },
790
+ }]
791
+ }], ctorParameters: () => [], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], requiredInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], separatorKeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "separatorKeys", required: false }] }], addOnBlur: [{ type: i0.Input, args: [{ isSignal: true, alias: "addOnBlur", required: false }] }], maxTags: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxTags", required: false }] }], allowDuplicates: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDuplicates", required: false }] }], createTag: [{ type: i0.Input, args: [{ isSignal: true, alias: "createTag", required: false }] }], tagLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "tagLabel", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], idInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], ariaDescribedby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], tagAdded: [{ type: i0.Output, args: ["tagAdded"] }], tagRemoved: [{ type: i0.Output, args: ["tagRemoved"] }], textInputRef: [{ type: i0.ViewChild, args: ['textInput', { isSignal: true }] }], removeButtons: [{ type: i0.ViewChildren, args: ['removeBtn', { isSignal: true }] }] } });
792
+
793
+ /**
794
+ * Generated bundle index. Do not edit.
795
+ */
796
+
797
+ export { TagsInputComponent };
798
+ //# sourceMappingURL=cdevhub-ngx-tw-tags-input.mjs.map