@cdevhub/ngx-tw 0.2.1 → 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 (139) 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 +24 -15
  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 +408 -215
  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 +144 -79
  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 +34 -20
  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 +195 -110
  31. package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -1
  32. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +140 -73
  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 +393 -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 +137 -71
  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 +217 -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 +44 -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 +101 -51
  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 +11 -7
  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 +89 -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 -20
  121. package/theme/index.css +10 -2
  122. package/types/cdevhub-ngx-tw-aspect-ratio.d.ts +40 -0
  123. package/types/cdevhub-ngx-tw-calendar.d.ts +39 -8
  124. package/types/cdevhub-ngx-tw-command-palette.d.ts +7 -0
  125. package/types/cdevhub-ngx-tw-date-picker.d.ts +36 -10
  126. package/types/cdevhub-ngx-tw-file-upload.d.ts +250 -0
  127. package/types/cdevhub-ngx-tw-form-field.d.ts +22 -3
  128. package/types/cdevhub-ngx-tw-icon-lucide.d.ts +21 -3
  129. package/types/cdevhub-ngx-tw-number-input.d.ts +130 -0
  130. package/types/cdevhub-ngx-tw-popover.d.ts +8 -0
  131. package/types/cdevhub-ngx-tw-stepper.d.ts +30 -2
  132. package/types/cdevhub-ngx-tw-tab-nav.d.ts +8 -0
  133. package/types/cdevhub-ngx-tw-table.d.ts +30 -1
  134. package/types/cdevhub-ngx-tw-tabs.d.ts +8 -0
  135. package/types/cdevhub-ngx-tw-tags-input.d.ts +217 -0
  136. package/types/cdevhub-ngx-tw-time-picker.d.ts +31 -6
  137. package/types/cdevhub-ngx-tw-transfer.d.ts +330 -0
  138. package/types/cdevhub-ngx-tw-tree.d.ts +189 -0
  139. package/types/cdevhub-ngx-tw.d.ts +7 -0
@@ -0,0 +1,622 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, ElementRef, input, output, signal, computed, afterNextRender, isDevMode, forwardRef, Directive, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { NG_VALUE_ACCESSOR } from '@angular/forms';
4
+ import { TW_INPUT_VALUE_ACCESSOR } from '@cdevhub/ngx-tw/input';
5
+ import { tv } from 'tailwind-variants';
6
+
7
+ /**
8
+ * Turns a plain `<input twInput twNumberInput>` into a robust numeric field
9
+ * without `type="number"` — fixing the broken-on-mobile, browser-inconsistent
10
+ * native control. It composes the sibling {@link InputDirective}: that directive
11
+ * keeps owning the form-field chrome, error-state, and `aria-invalid` /
12
+ * `aria-required`, while this directive owns the numeric value transport,
13
+ * locale-aware parse/format, spinbutton ARIA, `inputmode`, and keyboard stepping.
14
+ *
15
+ * Pair it with the companion `<tw-number-stepper [for]="ref">` for visible
16
+ * up/down spinner buttons.
17
+ *
18
+ * Forms: implements `ControlValueAccessor`, so the value round-trips as a real
19
+ * `number | null` (never a string) through template-driven, reactive, and
20
+ * signal-based forms.
21
+ */
22
+ class NumberInputDirective {
23
+ el = inject(ElementRef).nativeElement;
24
+ // ── Inputs ──
25
+ /** Smallest accepted value. Clamps the committed value (on blur, Enter, and stepping) and sets `aria-valuemin`. Does not clamp per keystroke. Defaults to `undefined` (no lower bound). */
26
+ min = input(undefined, /* @ts-ignore */
27
+ ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
28
+ /** Largest accepted value. Clamps the committed value (on blur, Enter, and stepping) and sets `aria-valuemax`. Does not clamp per keystroke. Defaults to `undefined` (no upper bound). */
29
+ max = input(undefined, /* @ts-ignore */
30
+ ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
31
+ /** Amount added or subtracted by ArrowUp/ArrowDown and the stepper buttons. Defaults to `1`. Values `<= 0` or non-finite fall back to `1`. */
32
+ step = input(1, /* @ts-ignore */
33
+ ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
34
+ /** `Intl.NumberFormat` options driving the blurred display (grouping, decimals, currency). The formatter's resolved `maximumFractionDigits` also sets commit-time rounding precision and switches `inputmode` to `'numeric'` when it is `0`. Percent style is not supported in v1. Defaults to `undefined` (locale default number formatting, grouping on). */
35
+ format = input(undefined, /* @ts-ignore */
36
+ ...(ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []));
37
+ /** BCP-47 locale for `Intl.NumberFormat` formatting and for locale-aware parsing (decimal and group separators). Defaults to `undefined` (the runtime default locale). */
38
+ locale = input(undefined, /* @ts-ignore */
39
+ ...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
40
+ // ── Output ──
41
+ /** Fires when the committed numeric value changes through user interaction (typing, stepping, clamping on blur/Enter). Does not fire on `writeValue` (programmatic form writes). Useful for non-form / template-ref usage alongside the `value` signal. */
42
+ valueChange = output();
43
+ // ── State ──
44
+ _value = signal(null, /* @ts-ignore */
45
+ ...(ngDevMode ? [{ debugName: "_value" }] : /* istanbul ignore next */ []));
46
+ cvaDisabled = signal(false, /* @ts-ignore */
47
+ ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
48
+ readonlySig = signal(false, /* @ts-ignore */
49
+ ...(ngDevMode ? [{ debugName: "readonlySig" }] : /* istanbul ignore next */ []));
50
+ /** The current committed numeric value, or `null` when empty / unparseable. Read this from a template ref (`#n="twNumberInput"`) for non-form usage. */
51
+ value = this._value.asReadonly();
52
+ /** @internal Formatted display text surfaced through `TW_INPUT_VALUE_ACCESSOR` so the sibling InputDirective's empty / float-label state tracks the text in the box (not the numeric model). Updated on every field mutation. */
53
+ displayText = signal('', /* @ts-ignore */
54
+ ...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
55
+ /** True when the control is disabled. Reactively tracks the reactive-forms path (`setDisabledState` via `control.disable()`) and a static `disabled` attribute read at mount. A runtime-toggled declarative `[disabled]` binding without reactive forms is NOT reactively tracked (v2 limitation — same class as `readonly`). Read by the companion stepper so its buttons disable in lock-step. */
56
+ disabled = computed(() => this.cvaDisabled() || this.el.disabled, /* @ts-ignore */
57
+ ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
58
+ /** True when the host input carries the `readonly` attribute (static / declarative, seeded after render). A runtime toggle of the bare `readonly` attribute is not reactively tracked (v2). Read by the companion stepper so its buttons disable in lock-step. */
59
+ readonly = this.readonlySig.asReadonly();
60
+ /** @internal Mobile-keyboard hint: `'numeric'` for integer-only formats, else `'decimal'`. */
61
+ inputMode = computed(() => this.format()?.maximumFractionDigits === 0 ? 'numeric' : 'decimal', /* @ts-ignore */
62
+ ...(ngDevMode ? [{ debugName: "inputMode" }] : /* istanbul ignore next */ []));
63
+ /** @internal Locale group / decimal separators, memoized per `locale()`. */
64
+ separators = computed(() => {
65
+ try {
66
+ const parts = new Intl.NumberFormat(this.locale()).formatToParts(11000.1);
67
+ return {
68
+ group: parts.find((p) => p.type === 'group')?.value ?? '',
69
+ decimal: parts.find((p) => p.type === 'decimal')?.value ?? '.',
70
+ };
71
+ }
72
+ catch {
73
+ return { group: '', decimal: '.' };
74
+ }
75
+ }, /* @ts-ignore */
76
+ ...(ngDevMode ? [{ debugName: "separators" }] : /* istanbul ignore next */ []));
77
+ // ── Plain (non-signal) bookkeeping ──
78
+ _focused = false;
79
+ _lastValue = null;
80
+ _lastDisplay = '';
81
+ onChange = () => { };
82
+ onTouched = () => { };
83
+ constructor() {
84
+ // Seed `readonly` after render. InputDirective mirrors its `readonly` input
85
+ // onto the native attribute via an effect that runs AFTER construction
86
+ // (input.ts:330-337), so a synchronous read here would be stale.
87
+ afterNextRender(() => this.readonlySig.set(this.el.readOnly));
88
+ // Dev-mode: warn once on a bogus step (mirrors time-picker).
89
+ afterNextRender(() => {
90
+ if (!isDevMode())
91
+ return;
92
+ const s = this.step();
93
+ if (s <= 0 || !Number.isFinite(s)) {
94
+ console.warn(`[twNumberInput] step=${s} is invalid. Using 1 instead.`);
95
+ }
96
+ });
97
+ }
98
+ // ── ControlValueAccessor ──
99
+ /** @internal */
100
+ writeValue(value) {
101
+ if (value == null || Number.isNaN(value)) {
102
+ this._value.set(null);
103
+ this.displayText.set('');
104
+ this._lastValue = null;
105
+ this._lastDisplay = '';
106
+ if (!this._focused)
107
+ this.el.value = '';
108
+ return;
109
+ }
110
+ // Normalize -0 → 0 (never store a negative zero); otherwise keep the value
111
+ // un-rounded — a programmatic write is authoritative and must not silently
112
+ // diverge from the parent FormControl.
113
+ const n = Object.is(value, -0) ? 0 : value;
114
+ const display = this.formatValue(n);
115
+ this._value.set(n);
116
+ this.displayText.set(display);
117
+ // Update the revert anchor even while focused, so an unparseable blur after
118
+ // a programmatic write snaps back to the form's value (not to '').
119
+ this._lastValue = n;
120
+ this._lastDisplay = display;
121
+ // Caret guard: never rewrite el.value while focused (clobbers the caret);
122
+ // defer the visible reformat to the next blur.
123
+ if (!this._focused)
124
+ this.el.value = display;
125
+ }
126
+ /** @internal */
127
+ registerOnChange(fn) {
128
+ this.onChange = fn;
129
+ }
130
+ /** @internal */
131
+ registerOnTouched(fn) {
132
+ this.onTouched = fn;
133
+ }
134
+ /** @internal */
135
+ setDisabledState(isDisabled) {
136
+ this.cvaDisabled.set(isDisabled);
137
+ }
138
+ // ── Public methods ──
139
+ /** Steps the value up: treats an empty field as `0`, adds `step`, clamps to `[min,max]`, rounds to the formatter's resolved precision, formats, and writes both the display and the model. Emits `valueChange`. No-op when the host input is disabled or readonly. Does not move focus. */
140
+ increment() {
141
+ this.stepBy(1);
142
+ }
143
+ /** Steps the value down: treats an empty field as `0`, subtracts `step`, clamps to `[min,max]`, rounds, formats, writes display and model. Emits `valueChange`. No-op when disabled or readonly. Does not move focus. */
144
+ decrement() {
145
+ this.stepBy(-1);
146
+ }
147
+ /** Moves focus to the underlying input element. */
148
+ focus(options) {
149
+ this.el.focus(options);
150
+ }
151
+ // ── Host handlers ──
152
+ /** @internal */
153
+ onInput() {
154
+ const raw = this.el.value;
155
+ this.displayText.set(raw);
156
+ const parsed = this.parseValue(raw);
157
+ this._value.set(parsed);
158
+ this.onChange(parsed);
159
+ this.valueChange.emit(parsed);
160
+ }
161
+ /** @internal */
162
+ onFocus() {
163
+ this._focused = true;
164
+ }
165
+ /** @internal */
166
+ onBlur() {
167
+ this._focused = false;
168
+ this.commitFromElement();
169
+ this.onTouched();
170
+ }
171
+ /** @internal */
172
+ onKeydown(event) {
173
+ if (this.disabled() || this.el.readOnly)
174
+ return;
175
+ switch (event.key) {
176
+ case 'ArrowUp':
177
+ event.preventDefault();
178
+ this.increment();
179
+ break;
180
+ case 'ArrowDown':
181
+ event.preventDefault();
182
+ this.decrement();
183
+ break;
184
+ case 'Home':
185
+ if (this.min() !== undefined) {
186
+ event.preventDefault();
187
+ this.commitNumber(this.min());
188
+ }
189
+ break;
190
+ case 'End':
191
+ if (this.max() !== undefined) {
192
+ event.preventDefault();
193
+ this.commitNumber(this.max());
194
+ }
195
+ break;
196
+ case 'Enter':
197
+ // Do NOT preventDefault — preserve native form submission. Commit in place.
198
+ this.commitFromElement();
199
+ break;
200
+ // All other keys (PageUp/Down, text entry, caret nav, Backspace) → native.
201
+ }
202
+ }
203
+ // ── Internal commit / step logic ──
204
+ stepBy(direction) {
205
+ if (this.disabled() || this.el.readOnly)
206
+ return;
207
+ // Treat an empty field as 0, apply ±step, then clamp — matches native
208
+ // <input type="number">.
209
+ const current = this.parseValue(this.el.value) ?? 0;
210
+ this.commitNumber(current + direction * this.resolvedStep());
211
+ }
212
+ commitFromElement() {
213
+ const raw = this.el.value;
214
+ if (raw.trim() === '') {
215
+ this.commitEmpty();
216
+ return;
217
+ }
218
+ const parsed = this.parseValue(raw);
219
+ if (parsed === null) {
220
+ // Non-empty unparseable → snap back to the last committed value.
221
+ this.revertToLastCommitted();
222
+ return;
223
+ }
224
+ this.commitNumber(parsed);
225
+ }
226
+ commitNumber(n) {
227
+ const rounded = this.roundToPrecision(this.clamp(n));
228
+ const normalized = Object.is(rounded, -0) ? 0 : rounded;
229
+ const display = this.formatValue(normalized);
230
+ const changed = !Object.is(this._value(), normalized);
231
+ this._value.set(normalized);
232
+ this.displayText.set(display);
233
+ this._lastValue = normalized;
234
+ this._lastDisplay = display;
235
+ this.el.value = display;
236
+ if (changed) {
237
+ this.onChange(normalized);
238
+ this.valueChange.emit(normalized);
239
+ }
240
+ }
241
+ commitEmpty() {
242
+ const changed = this._value() !== null;
243
+ this._value.set(null);
244
+ this.displayText.set('');
245
+ this._lastValue = null;
246
+ this._lastDisplay = '';
247
+ this.el.value = '';
248
+ if (changed) {
249
+ this.onChange(null);
250
+ this.valueChange.emit(null);
251
+ }
252
+ }
253
+ revertToLastCommitted() {
254
+ const changed = this._value() !== this._lastValue;
255
+ this._value.set(this._lastValue);
256
+ this.displayText.set(this._lastDisplay);
257
+ this.el.value = this._lastDisplay;
258
+ if (changed) {
259
+ this.onChange(this._lastValue);
260
+ this.valueChange.emit(this._lastValue);
261
+ }
262
+ }
263
+ // ── Inline numeric helpers ──
264
+ resolvedStep() {
265
+ const s = this.step();
266
+ return s > 0 && Number.isFinite(s) ? s : 1;
267
+ }
268
+ clamp(n) {
269
+ const min = this.min();
270
+ const max = this.max();
271
+ let r = n;
272
+ if (min !== undefined && r < min)
273
+ r = min;
274
+ if (max !== undefined && r > max)
275
+ r = max;
276
+ return r;
277
+ }
278
+ roundToPrecision(n) {
279
+ if (!Number.isFinite(n))
280
+ return n;
281
+ const digits = this.resolvedMaxFractionDigits();
282
+ if (digits === undefined)
283
+ return n;
284
+ const factor = 10 ** digits;
285
+ return Math.round(n * factor) / factor;
286
+ }
287
+ resolvedMaxFractionDigits() {
288
+ try {
289
+ return new Intl.NumberFormat(this.locale(), this.format()).resolvedOptions().maximumFractionDigits;
290
+ }
291
+ catch {
292
+ return undefined;
293
+ }
294
+ }
295
+ formatValue(n) {
296
+ const normalized = Object.is(n, -0) ? 0 : n;
297
+ try {
298
+ return new Intl.NumberFormat(this.locale(), this.format()).format(normalized);
299
+ }
300
+ catch {
301
+ return String(normalized);
302
+ }
303
+ }
304
+ /**
305
+ * Parse a raw string to `number | null` in a load-bearing order: the `e`/`E`
306
+ * rejection MUST precede stripping (otherwise `'1e3'` would survive as `'13'`).
307
+ */
308
+ parseValue(raw) {
309
+ const trimmed = raw.trim();
310
+ if (trimmed === '')
311
+ return null;
312
+ // 2. Reject exponent notation before any stripping.
313
+ if (/[eE]/.test(trimmed))
314
+ return null;
315
+ const { group, decimal } = this.separators();
316
+ // 3. Strip all Unicode whitespace (covers NBSP / narrow-NBSP group separators
317
+ // and a user-typed ASCII space alike).
318
+ let s = trimmed.replace(/\s/gu, '');
319
+ // 4. Strip group separators (redundant-but-harmless for space-grouped locales).
320
+ if (group)
321
+ s = s.split(group).join('');
322
+ // 5. Normalize the locale decimal separator to '.'.
323
+ if (decimal && decimal !== '.')
324
+ s = s.split(decimal).join('.');
325
+ // 6. Strip remaining non-numeric chars (currency symbols, '%', leading '+', …)
326
+ // and collapse to a single leading '-'.
327
+ s = s.replace(/[^\d.-]/g, '').replace(/(?!^)-/g, '');
328
+ // 7. Digit-presence + finiteness guard (prevents Number('') === 0 committing 0).
329
+ if (!/\d/.test(s))
330
+ return null;
331
+ const n = Number(s);
332
+ if (!Number.isFinite(n))
333
+ return null;
334
+ // Normalize -0 → 0 so it never enters the model from any path.
335
+ return Object.is(n, -0) ? 0 : n;
336
+ }
337
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NumberInputDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
338
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: NumberInputDirective, isStandalone: true, selector: "input[twNumberInput]", inputs: { min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { attributes: { "role": "spinbutton" }, listeners: { "input": "onInput()", "keydown": "onKeydown($event)", "focus": "onFocus()", "blur": "onBlur()" }, properties: { "attr.inputmode": "inputMode()", "attr.aria-valuemin": "min() ?? null", "attr.aria-valuemax": "max() ?? null", "attr.aria-valuenow": "value() ?? null", "attr.aria-valuetext": "displayText() || \"Empty\"" } }, providers: [
339
+ // Custom CVA — transports a real `number | null`, not a string. Registered
340
+ // statically because this directive injects NO `NgControl` (matcher /
341
+ // error-state stay on the sibling InputDirective), so the circular-DI trap
342
+ // that forces runtime registration does not apply. Follows the
343
+ // segmented-control / calendar precedent. A custom accessor takes precedence
344
+ // over the built-in DefaultValueAccessor, so there is no "more than one
345
+ // value accessor" error.
346
+ {
347
+ provide: NG_VALUE_ACCESSOR,
348
+ useExisting: forwardRef(() => NumberInputDirective),
349
+ multi: true,
350
+ },
351
+ // Lets the sibling InputDirective read the formatted *display text* (not the
352
+ // numeric model) for its empty / float-label state. The factory exposes the
353
+ // `displayText` signal so InputDirective's `isSignal(value)` effect stays
354
+ // self-healing. Must NOT be `useExisting` — that would expose the numeric
355
+ // `value` signal and report the field empty during intermediate typing.
356
+ {
357
+ provide: TW_INPUT_VALUE_ACCESSOR,
358
+ useFactory: (dir) => ({ value: dir.displayText }),
359
+ deps: [forwardRef(() => NumberInputDirective)],
360
+ },
361
+ ], exportAs: ["twNumberInput"], ngImport: i0 });
362
+ }
363
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NumberInputDirective, decorators: [{
364
+ type: Directive,
365
+ args: [{
366
+ selector: 'input[twNumberInput]',
367
+ exportAs: 'twNumberInput',
368
+ providers: [
369
+ // Custom CVA — transports a real `number | null`, not a string. Registered
370
+ // statically because this directive injects NO `NgControl` (matcher /
371
+ // error-state stay on the sibling InputDirective), so the circular-DI trap
372
+ // that forces runtime registration does not apply. Follows the
373
+ // segmented-control / calendar precedent. A custom accessor takes precedence
374
+ // over the built-in DefaultValueAccessor, so there is no "more than one
375
+ // value accessor" error.
376
+ {
377
+ provide: NG_VALUE_ACCESSOR,
378
+ useExisting: forwardRef(() => NumberInputDirective),
379
+ multi: true,
380
+ },
381
+ // Lets the sibling InputDirective read the formatted *display text* (not the
382
+ // numeric model) for its empty / float-label state. The factory exposes the
383
+ // `displayText` signal so InputDirective's `isSignal(value)` effect stays
384
+ // self-healing. Must NOT be `useExisting` — that would expose the numeric
385
+ // `value` signal and report the field empty during intermediate typing.
386
+ {
387
+ provide: TW_INPUT_VALUE_ACCESSOR,
388
+ useFactory: (dir) => ({ value: dir.displayText }),
389
+ deps: [forwardRef(() => NumberInputDirective)],
390
+ },
391
+ ],
392
+ host: {
393
+ 'role': 'spinbutton',
394
+ '[attr.inputmode]': 'inputMode()',
395
+ '[attr.aria-valuemin]': 'min() ?? null',
396
+ '[attr.aria-valuemax]': 'max() ?? null',
397
+ '[attr.aria-valuenow]': 'value() ?? null',
398
+ '[attr.aria-valuetext]': 'displayText() || "Empty"',
399
+ '(input)': 'onInput()',
400
+ '(keydown)': 'onKeydown($event)',
401
+ '(focus)': 'onFocus()',
402
+ '(blur)': 'onBlur()',
403
+ },
404
+ }]
405
+ }], ctorParameters: () => [], propDecorators: { min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
406
+
407
+ const numberStepper = tv({
408
+ slots: {
409
+ // The column stretches to the field's height (host is `flex self-stretch`)
410
+ // and the two buttons split it via `flex-1`.
411
+ group: 'flex flex-col self-stretch',
412
+ // `flex-1` gives each button HALF the field height. Codified departure
413
+ // from CLAUDE.md "Square interactive targets" (size-6/7/8): stacking two
414
+ // square buttons made the column 2× a button tall — far taller than the
415
+ // text line — which is exactly what bloated the field's height. So the
416
+ // buttons carry a fixed WIDTH only; the chevron glyph's min-content
417
+ // (size-3 = 12px) is the natural floor that keeps the column compact.
418
+ //
419
+ // WCAG 2.5.8 (target-size, 24px — which slider.ts / badge.ts codify):
420
+ // two stacked buttons cannot each be ≥24px tall inside a ~38–42px field
421
+ // without re-bloating it, so each spinner ends up < 24px tall. The
422
+ // equivalent-control exception covers most cases — the value is reachable
423
+ // by typing into the ≥24px spinbutton input and, with a keyboard, via ↑/↓.
424
+ // Known gap: a negative-allowed field on touch — iOS `inputmode`
425
+ // numeric/decimal exposes no minus key and there are no hardware arrows,
426
+ // so the sub-24px decrement button becomes the only path to a negative
427
+ // value; a consumer needing negatives on touch must provide an alternative.
428
+ // Matches the sub-24px stacked-spinner convention of native/Mantine/Chakra.
429
+ button: 'flex-1 inline-flex items-center justify-center text-fg-muted hover:text-fg hover:bg-surface-muted rounded-md transition-colors duration-normal motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 disabled:opacity-40 disabled:pointer-events-none',
430
+ icon: '',
431
+ },
432
+ variants: {
433
+ // Button WIDTH (the height comes from `flex-1`); glyph sub-scale for the
434
+ // chevrons (per CLAUDE.md "Icon Sizing").
435
+ size: {
436
+ xs: { button: 'w-6', icon: 'size-3' },
437
+ sm: { button: 'w-7', icon: 'size-3' },
438
+ md: { button: 'w-7', icon: 'size-3' },
439
+ lg: { button: 'w-8', icon: 'size-4' },
440
+ xl: { button: 'w-8', icon: 'size-4' },
441
+ },
442
+ },
443
+ defaultVariants: { size: 'md' },
444
+ }, { twMerge: true });
445
+ /**
446
+ * The visible up/down spinner column for a {@link NumberInputDirective}. A
447
+ * directive cannot emit sibling DOM, so the spinner buttons live in this tiny
448
+ * companion. Bind it to the directive via a template ref and drop it into a
449
+ * `<tw-form-field>`'s `[twSuffix]` slot (or a standalone flex row):
450
+ *
451
+ * ```html
452
+ * <input twInput twNumberInput #qty="twNumberInput" [formControl]="ctrl" />
453
+ * <tw-number-stepper twSuffix [for]="qty" />
454
+ * ```
455
+ *
456
+ * The buttons are kept out of the tab order (`tabindex="-1"`) — the spinbutton
457
+ * input owns value + keyboard semantics — and refocus the input after stepping.
458
+ */
459
+ class NumberStepperComponent {
460
+ /** The number-input directive instance this stepper controls. Bind to a template ref, e.g. `[for]="qty"` with `#qty="twNumberInput"`. When omitted, the buttons render but do nothing (no-op) and disable. */
461
+ for = input(undefined, /* @ts-ignore */
462
+ ...(ngDevMode ? [{ debugName: "for" }] : /* istanbul ignore next */ []));
463
+ /** Button + glyph density. Match the field's `size` for visual alignment. Defaults to `'md'`. */
464
+ size = input('md', /* @ts-ignore */
465
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
466
+ variant = computed(() => numberStepper({ size: this.size() }), /* @ts-ignore */
467
+ ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
468
+ /** @internal */
469
+ groupClasses = computed(() => this.variant().group(), /* @ts-ignore */
470
+ ...(ngDevMode ? [{ debugName: "groupClasses" }] : /* istanbul ignore next */ []));
471
+ /** @internal */
472
+ buttonClasses = computed(() => this.variant().button(), /* @ts-ignore */
473
+ ...(ngDevMode ? [{ debugName: "buttonClasses" }] : /* istanbul ignore next */ []));
474
+ /** @internal */
475
+ iconClasses = computed(() => this.variant().icon(), /* @ts-ignore */
476
+ ...(ngDevMode ? [{ debugName: "iconClasses" }] : /* istanbul ignore next */ []));
477
+ /** @internal Disabled when there is no target, or the target input is disabled / readonly. */
478
+ isDisabled = computed(() => {
479
+ const d = this.for();
480
+ return !d || d.disabled() || d.readonly();
481
+ }, /* @ts-ignore */
482
+ ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
483
+ /** @internal Step the bound directive, then refocus the input (the refocus lives here, not in `increment`/`decrement`). */
484
+ onStep(direction) {
485
+ const dir = this.for();
486
+ if (!dir)
487
+ return;
488
+ if (direction === 1) {
489
+ dir.increment();
490
+ }
491
+ else {
492
+ dir.decrement();
493
+ }
494
+ dir.focus();
495
+ }
496
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NumberStepperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
497
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.7", type: NumberStepperComponent, isStandalone: true, selector: "tw-number-stepper", inputs: { for: { classPropertyName: "for", publicName: "for", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "flex self-stretch" }, exportAs: ["twNumberStepper"], ngImport: i0, template: `
498
+ <div [class]="groupClasses()">
499
+ <button
500
+ type="button"
501
+ tabindex="-1"
502
+ aria-label="Increase"
503
+ [class]="buttonClasses()"
504
+ [disabled]="isDisabled()"
505
+ (mousedown)="$event.preventDefault()"
506
+ (click)="onStep(1)"
507
+ >
508
+ <ng-content select="[slot=up]">
509
+ <svg
510
+ viewBox="0 0 20 20"
511
+ fill="currentColor"
512
+ aria-hidden="true"
513
+ [class]="iconClasses()"
514
+ >
515
+ <path
516
+ fill-rule="evenodd"
517
+ d="M14.77 12.79a.75.75 0 0 1-1.06-.02L10 8.94l-3.71 3.83a.75.75 0 1 1-1.08-1.04l4.25-4.39a.75.75 0 0 1 1.08 0l4.25 4.39a.75.75 0 0 1-.02 1.06Z"
518
+ clip-rule="evenodd"
519
+ />
520
+ </svg>
521
+ </ng-content>
522
+ </button>
523
+ <button
524
+ type="button"
525
+ tabindex="-1"
526
+ aria-label="Decrease"
527
+ [class]="buttonClasses()"
528
+ [disabled]="isDisabled()"
529
+ (mousedown)="$event.preventDefault()"
530
+ (click)="onStep(-1)"
531
+ >
532
+ <ng-content select="[slot=down]">
533
+ <svg
534
+ viewBox="0 0 20 20"
535
+ fill="currentColor"
536
+ aria-hidden="true"
537
+ [class]="iconClasses()"
538
+ >
539
+ <path
540
+ fill-rule="evenodd"
541
+ d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.06l3.71-3.83a.75.75 0 1 1 1.08 1.04l-4.25 4.39a.75.75 0 0 1-1.08 0L5.21 8.27a.75.75 0 0 1 .02-1.06Z"
542
+ clip-rule="evenodd"
543
+ />
544
+ </svg>
545
+ </ng-content>
546
+ </button>
547
+ </div>
548
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
549
+ }
550
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NumberStepperComponent, decorators: [{
551
+ type: Component,
552
+ args: [{
553
+ selector: 'tw-number-stepper',
554
+ exportAs: 'twNumberStepper',
555
+ changeDetection: ChangeDetectionStrategy.OnPush,
556
+ // `flex` makes the inner group fill the host; `self-stretch` makes the host
557
+ // fill the field height — overriding `items-center` in a `<tw-form-field>`
558
+ // `[twSuffix]` slot, and matching `items-stretch` in a standalone flex row.
559
+ host: {
560
+ 'class': 'flex self-stretch',
561
+ },
562
+ template: `
563
+ <div [class]="groupClasses()">
564
+ <button
565
+ type="button"
566
+ tabindex="-1"
567
+ aria-label="Increase"
568
+ [class]="buttonClasses()"
569
+ [disabled]="isDisabled()"
570
+ (mousedown)="$event.preventDefault()"
571
+ (click)="onStep(1)"
572
+ >
573
+ <ng-content select="[slot=up]">
574
+ <svg
575
+ viewBox="0 0 20 20"
576
+ fill="currentColor"
577
+ aria-hidden="true"
578
+ [class]="iconClasses()"
579
+ >
580
+ <path
581
+ fill-rule="evenodd"
582
+ d="M14.77 12.79a.75.75 0 0 1-1.06-.02L10 8.94l-3.71 3.83a.75.75 0 1 1-1.08-1.04l4.25-4.39a.75.75 0 0 1 1.08 0l4.25 4.39a.75.75 0 0 1-.02 1.06Z"
583
+ clip-rule="evenodd"
584
+ />
585
+ </svg>
586
+ </ng-content>
587
+ </button>
588
+ <button
589
+ type="button"
590
+ tabindex="-1"
591
+ aria-label="Decrease"
592
+ [class]="buttonClasses()"
593
+ [disabled]="isDisabled()"
594
+ (mousedown)="$event.preventDefault()"
595
+ (click)="onStep(-1)"
596
+ >
597
+ <ng-content select="[slot=down]">
598
+ <svg
599
+ viewBox="0 0 20 20"
600
+ fill="currentColor"
601
+ aria-hidden="true"
602
+ [class]="iconClasses()"
603
+ >
604
+ <path
605
+ fill-rule="evenodd"
606
+ d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.06l3.71-3.83a.75.75 0 1 1 1.08 1.04l-4.25 4.39a.75.75 0 0 1-1.08 0L5.21 8.27a.75.75 0 0 1 .02-1.06Z"
607
+ clip-rule="evenodd"
608
+ />
609
+ </svg>
610
+ </ng-content>
611
+ </button>
612
+ </div>
613
+ `,
614
+ }]
615
+ }], propDecorators: { for: [{ type: i0.Input, args: [{ isSignal: true, alias: "for", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
616
+
617
+ /**
618
+ * Generated bundle index. Do not edit.
619
+ */
620
+
621
+ export { NumberInputDirective, NumberStepperComponent };
622
+ //# sourceMappingURL=cdevhub-ngx-tw-number-input.mjs.map