@egose/shadcn-theme-ng-tw 0.6.0 → 0.6.2

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 (50) hide show
  1. package/README.md +119 -1
  2. package/autocomplete/fesm2022/autocomplete.mjs +14 -4
  3. package/autocomplete/types/autocomplete.d.ts +4 -1
  4. package/checkbox/README.md +6 -2
  5. package/checkbox/fesm2022/checkbox.mjs +15 -9
  6. package/checkbox/types/checkbox.d.ts +6 -4
  7. package/combobox/fesm2022/combobox.mjs +22 -6
  8. package/combobox/types/combobox.d.ts +8 -2
  9. package/data-table/README.md +11 -1
  10. package/data-table/fesm2022/data-table.mjs +76 -26
  11. package/data-table/types/data-table.d.ts +25 -6
  12. package/date-picker/README.md +41 -9
  13. package/date-picker/fesm2022/date-picker.mjs +265 -59
  14. package/date-picker/types/date-picker.d.ts +58 -27
  15. package/form-autocomplete/README.md +4 -2
  16. package/form-autocomplete/fesm2022/form-autocomplete.mjs +33 -8
  17. package/form-autocomplete/types/form-autocomplete.d.ts +9 -3
  18. package/form-combobox/README.md +4 -2
  19. package/form-combobox/fesm2022/form-combobox.mjs +65 -14
  20. package/form-combobox/types/form-combobox.d.ts +10 -3
  21. package/form-input-otp/README.md +7 -5
  22. package/form-input-otp/fesm2022/form-input-otp.mjs +28 -14
  23. package/form-input-otp/types/form-input-otp.d.ts +9 -3
  24. package/form-searchable-multiselect/README.md +15 -1
  25. package/form-searchable-multiselect/fesm2022/form-searchable-multiselect.mjs +23 -3
  26. package/form-searchable-multiselect/types/form-searchable-multiselect.d.ts +9 -1
  27. package/form-slider/README.md +3 -1
  28. package/form-slider/fesm2022/form-slider.mjs +21 -6
  29. package/form-slider/types/form-slider.d.ts +9 -3
  30. package/form-toggle/README.md +22 -18
  31. package/form-toggle/fesm2022/form-toggle.mjs +25 -8
  32. package/form-toggle/types/form-toggle.d.ts +6 -2
  33. package/input-otp/README.md +33 -9
  34. package/input-otp/fesm2022/input-otp.mjs +127 -11
  35. package/input-otp/types/input-otp.d.ts +24 -4
  36. package/package.json +1 -1
  37. package/pagination/README.md +21 -12
  38. package/pagination/fesm2022/pagination.mjs +83 -75
  39. package/pagination/types/pagination.d.ts +21 -8
  40. package/phone-input/README.md +16 -0
  41. package/phone-input/fesm2022/phone-input.mjs +41 -3
  42. package/phone-input/types/phone-input.d.ts +5 -2
  43. package/searchable-multiselect/README.md +62 -40
  44. package/searchable-multiselect/fesm2022/searchable-multiselect.mjs +97 -36
  45. package/searchable-multiselect/types/searchable-multiselect.d.ts +23 -6
  46. package/slider/fesm2022/slider.mjs +15 -5
  47. package/slider/types/slider.d.ts +6 -2
  48. package/stepper/README.md +39 -19
  49. package/stepper/fesm2022/stepper.mjs +71 -17
  50. package/stepper/types/stepper.d.ts +12 -1
@@ -61,7 +61,7 @@ import { HlmInputOtpModule } from '@egose/shadcn-theme-ng/input-otp';
61
61
  export class MyModule {}
62
62
  ```
63
63
 
64
- Exported from `src/public-api.ts`: `HlmInputOtp`, `HlmInputOtpGroup`, `HlmInputOtpSeparator`, `HlmInputOtpSlot`, `HlmInputOtpFakeCaret`, plus `HlmInputOtpImports` and `HlmInputOtpModule`.
64
+ Exported from `src/public-api.ts`: `HlmInputOtp`, `HlmInputOtpControl`, `HlmInputOtpGroup`, `HlmInputOtpSeparator`, `HlmInputOtpSlot`, `HlmInputOtpFakeCaret`, plus `HlmInputOtpImports` and `HlmInputOtpModule`.
65
65
 
66
66
  ## Anatomy / Structure
67
67
 
@@ -88,6 +88,32 @@ Brain inputs on `brn-input-otp` (from `@spartan-ng/brain/input-otp`): `length` (
88
88
 
89
89
  ## API reference
90
90
 
91
+ ### `hlm-input-otp` — `HlmInputOtpControl`
92
+
93
+ Use this component for native-input accessibility forwarding. It extends `BrnInputOtp` with
94
+ a declarative input template and retains its inputs, outputs, CVA, paste and focus behavior.
95
+ `HlmInputOtpImports` / `HlmInputOtpModule` include it; a separate brain import is unnecessary.
96
+ Compose it with `HlmInputOtpSlot` (the raw brain slot's private injection token is not supported).
97
+
98
+ - `[aria-describedby]` (`string | null`, default `null`) reaches the native input and merges
99
+ with enclosing Spartan field description IDs, deduplicated. Bind only IDs of mounted elements.
100
+ - `required` (boolean-coerced, default `false`) exposes native `aria-required`. Add Angular
101
+ validators separately; native `aria-invalid` follows the bound form control.
102
+ - `inputId` connects a normal `<label for>` to the one focusable native input.
103
+
104
+ ```html
105
+ <label for="verification">Verification code</label>
106
+ <hlm-input-otp [length]="4" inputId="verification" aria-describedby="verification-help" required>
107
+ <div hlmInputOtpGroup>
108
+ <hlm-input-otp-slot index="0" />
109
+ <hlm-input-otp-slot index="1" />
110
+ <hlm-input-otp-slot index="2" />
111
+ <hlm-input-otp-slot index="3" />
112
+ </div>
113
+ </hlm-input-otp>
114
+ <p id="verification-help">Enter the four-character code.</p>
115
+ ```
116
+
91
117
  ### `brn-input-otp[hlmInputOtp]` — `HlmInputOtp`
92
118
 
93
119
  Pure styling directive (`data-slot="input-otp"`, flex row, dims on `has-disabled`). No inputs/outputs — bind `length`, `value`, `disabled`, `inputId`, … on the `brn-input-otp` element itself.
@@ -102,7 +128,7 @@ Slot cluster wrapper (`data-slot="input-otp-group"`). No inputs/outputs. Use one
102
128
  | ------- | -------------------------------------------------- | -------------------------------------------------------- |
103
129
  | `index` | `number` (**required**, `numberAttribute` coerced) | Zero-based position of the character this slot displays. |
104
130
 
105
- Renders `<brn-input-otp-slot [index]>` internally; active slot gets the `ring` highlight, invalid state the destructive border.
131
+ Under `brn-input-otp`, renders `<brn-input-otp-slot [index]>`; under `hlm-input-otp`, reads the adapter's public context and renders the same character/caret and state attributes declaratively. Active slots get the `ring` highlight.
106
132
 
107
133
  ### `hlm-input-otp-separator` — `HlmInputOtpSeparator`
108
134
 
@@ -267,17 +293,15 @@ export class LengthsExample {}
267
293
 
268
294
  ```ts
269
295
  import { Component, signal } from '@angular/core';
270
- import { BrnInputOtp } from '@spartan-ng/brain/input-otp';
271
296
  import { HlmInputOtpImports } from '@egose/shadcn-theme-ng/input-otp';
272
297
  import { HlmError } from '@egose/shadcn-theme-ng/form-field';
273
298
 
274
299
  @Component({
275
300
  standalone: true,
276
- imports: [BrnInputOtp, HlmInputOtpImports, HlmError],
301
+ imports: [HlmInputOtpImports, HlmError],
277
302
  template: `
278
303
  <label for="otp-verify">Verification code</label>
279
- <brn-input-otp
280
- hlmInputOtp
304
+ <hlm-input-otp
281
305
  [length]="6"
282
306
  inputId="otp-verify"
283
307
  inputMode="numeric"
@@ -286,7 +310,7 @@ import { HlmError } from '@egose/shadcn-theme-ng/form-field';
286
310
  [value]="code()"
287
311
  (valueChange)="code.set($event); error.set(null)"
288
312
  (completed)="verify()"
289
- aria-describedby="otp-error"
313
+ [aria-describedby]="error() ? 'otp-error' : null"
290
314
  >
291
315
  <div hlmInputOtpGroup>
292
316
  <hlm-input-otp-slot index="0" />
@@ -299,7 +323,7 @@ import { HlmError } from '@egose/shadcn-theme-ng/form-field';
299
323
  <hlm-input-otp-slot index="4" />
300
324
  <hlm-input-otp-slot index="5" />
301
325
  </div>
302
- </brn-input-otp>
326
+ </hlm-input-otp>
303
327
  @if (error()) {
304
328
  <hlm-error id="otp-error">{{ error() }}</hlm-error>
305
329
  }
@@ -366,7 +390,7 @@ export class PasteExample {
366
390
 
367
391
  - Always render a visible `<label>` bound via `inputId` — the hidden native input is what AT focuses; without the label the purpose is unclear.
368
392
  - `inputMode="numeric"` (digits) vs `text` (alphanumeric) selects the right mobile keyboard; `inputAutocomplete="one-time-code"` lets browsers/OS autofill SMS codes.
369
- - Announce failures with a text error (`<hlm-error>` + `aria-describedby`), never by slot color alone.
393
+ - Associate failures with text (`<hlm-error>` + `hlm-input-otp`'s `[aria-describedby]`), never by slot color alone. The legacy brain component has no native description input. `HlmError` is not a live region; browser DOM/focus tests do not establish screen-reader announcements.
370
394
  - Keep `length` aligned with what the backend expects and validate `minLength(length)` on the form control so incomplete codes cannot submit.
371
395
 
372
396
  ## Theming / CSS variables
@@ -1,9 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Directive, ChangeDetectionStrategy, Component, input, numberAttribute, NgModule } from '@angular/core';
2
+ import { Directive, input, booleanAttribute, forwardRef, ChangeDetectionStrategy, Component, inject, computed, numberAttribute, NgModule } from '@angular/core';
3
3
  import { classes } from '@egose/shadcn-theme-ng-tw/utils';
4
+ import { NG_VALUE_ACCESSOR } from '@angular/forms';
5
+ import * as i1 from '@spartan-ng/brain/field';
6
+ import { BrnFieldControlDescribedBy, provideBrnLabelable, BrnFieldControl } from '@spartan-ng/brain/field';
7
+ import { BrnInputOtp, BrnInputOtpSlot } from '@spartan-ng/brain/input-otp';
4
8
  import { NgIcon, provideIcons } from '@ng-icons/core';
5
9
  import { lucideMinus } from '@ng-icons/lucide';
6
- import { BrnInputOtpSlot } from '@spartan-ng/brain/input-otp';
7
10
 
8
11
  class HlmInputOtp {
9
12
  constructor() {
@@ -20,6 +23,83 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
20
23
  }]
21
24
  }], ctorParameters: () => [] });
22
25
 
26
+ /**
27
+ * BrnInputOtp's CVA and editing behavior with declarative native-input ARIA forwarding.
28
+ * Use with HlmInputOtpSlot; the legacy brn-input-otp skin remains available.
29
+ */
30
+ class HlmInputOtpControl extends BrnInputOtp {
31
+ /** Native input description IDs, merged with enclosing Spartan field descriptions. */
32
+ ariaDescribedBy = input(null, { ...(ngDevMode ? { debugName: "ariaDescribedBy" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
33
+ required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
34
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmInputOtpControl, deps: null, target: i0.ɵɵFactoryTarget.Component });
35
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", type: HlmInputOtpControl, isStandalone: true, selector: "hlm-input-otp", inputs: { ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
36
+ { provide: BrnInputOtp, useExisting: forwardRef(() => HlmInputOtpControl) },
37
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => HlmInputOtpControl), multi: true },
38
+ provideBrnLabelable(HlmInputOtpControl),
39
+ ], usesInheritance: true, hostDirectives: [{ directive: i1.BrnFieldControl }, { directive: HlmInputOtp }], ngImport: i0, template: `
40
+ <ng-content />
41
+ <div [style]="containerStyles()">
42
+ <input
43
+ #otpInput
44
+ data-slot="input-otp"
45
+ [id]="inputId()"
46
+ [class]="inputClass()"
47
+ [autocomplete]="inputAutocomplete()"
48
+ [style]="inputStyles()"
49
+ [disabled]="_disabled()"
50
+ [inputMode]="inputMode()"
51
+ [value]="value() ?? ''"
52
+ brnFieldControlDescribedBy
53
+ [aria-describedby]="ariaDescribedBy()"
54
+ [attr.aria-required]="required() || null"
55
+ [attr.aria-invalid]="invalid?.() ? 'true' : null"
56
+ (input)="onInputChange($event)"
57
+ (paste)="onPaste($event)"
58
+ (focus)="_focused.set(true)"
59
+ (blur)="onBlur()"
60
+ />
61
+ </div>
62
+ `, isInline: true, dependencies: [{ kind: "directive", type: BrnFieldControlDescribedBy, selector: "[brnFieldControlDescribedBy]", inputs: ["aria-describedby"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
63
+ }
64
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmInputOtpControl, decorators: [{
65
+ type: Component,
66
+ args: [{
67
+ selector: 'hlm-input-otp',
68
+ changeDetection: ChangeDetectionStrategy.OnPush,
69
+ imports: [BrnFieldControlDescribedBy],
70
+ hostDirectives: [BrnFieldControl, HlmInputOtp],
71
+ providers: [
72
+ { provide: BrnInputOtp, useExisting: forwardRef(() => HlmInputOtpControl) },
73
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => HlmInputOtpControl), multi: true },
74
+ provideBrnLabelable(HlmInputOtpControl),
75
+ ],
76
+ template: `
77
+ <ng-content />
78
+ <div [style]="containerStyles()">
79
+ <input
80
+ #otpInput
81
+ data-slot="input-otp"
82
+ [id]="inputId()"
83
+ [class]="inputClass()"
84
+ [autocomplete]="inputAutocomplete()"
85
+ [style]="inputStyles()"
86
+ [disabled]="_disabled()"
87
+ [inputMode]="inputMode()"
88
+ [value]="value() ?? ''"
89
+ brnFieldControlDescribedBy
90
+ [aria-describedby]="ariaDescribedBy()"
91
+ [attr.aria-required]="required() || null"
92
+ [attr.aria-invalid]="invalid?.() ? 'true' : null"
93
+ (input)="onInputChange($event)"
94
+ (paste)="onPaste($event)"
95
+ (focus)="_focused.set(true)"
96
+ (blur)="onBlur()"
97
+ />
98
+ </div>
99
+ `,
100
+ }]
101
+ }], propDecorators: { ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }] } });
102
+
23
103
  class HlmInputOtpFakeCaret {
24
104
  constructor() {
25
105
  classes(() => 'tw:pointer-events-none tw:absolute tw:inset-0 tw:flex tw:items-center tw:justify-center');
@@ -74,16 +154,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
74
154
  }], ctorParameters: () => [] });
75
155
 
76
156
  class HlmInputOtpSlot {
157
+ _control = inject(HlmInputOtpControl, { optional: true });
158
+ _slot = computed(() => this._control?.context()[this.index()], /* @ts-ignore */
159
+ ...(ngDevMode ? [{ debugName: "_slot" }] : /* istanbul ignore next */ []));
77
160
  /** The index of the slot to render the char or a fake caret */
78
161
  index = input.required({ ...(ngDevMode ? { debugName: "index" } : /* istanbul ignore next */ {}), transform: numberAttribute });
79
162
  constructor() {
80
- classes(() => 'tw:dark:bg-input/30 tw:border-input tw:has-[brn-input-otp-slot[data-active="true"]]:border-ring tw:has-[brn-input-otp-slot[data-active="true"]]:ring-ring/50 tw:has-[brn-input-otp-slot[data-active="true"]]:data-[matches-spartan-invalid=true]:ring-destructive/20 tw:dark:has-[brn-input-otp-slot[data-active="true"]]:data-[matches-spartan-invalid=true]:ring-destructive/40 tw:data-[matches-spartan-invalid=true]:border-destructive tw:has-[brn-input-otp-slot[data-active="true"]]:data-[matches-spartan-invalid=true]:border-destructive tw:size-9 tw:border-y tw:border-e tw:text-sm tw:shadow-xs tw:transition-all tw:outline-none tw:first:rounded-s-md tw:first:border-s tw:last:rounded-e-md tw:has-[brn-input-otp-slot[data-active="true"]]:ring-3 tw:relative tw:flex tw:items-center tw:justify-center tw:has-[brn-input-otp-slot[data-active="true"]]:z-10');
163
+ classes(() => 'tw:dark:bg-input/30 tw:border-input tw:has-[[data-active="true"]]:border-ring tw:has-[[data-active="true"]]:ring-ring/50 tw:has-[[data-active="true"]]:data-[matches-spartan-invalid=true]:ring-destructive/20 tw:dark:has-[[data-active="true"]]:data-[matches-spartan-invalid=true]:ring-destructive/40 tw:data-[matches-spartan-invalid=true]:border-destructive tw:has-[[data-active="true"]]:data-[matches-spartan-invalid=true]:border-destructive tw:size-9 tw:border-y tw:border-e tw:text-sm tw:shadow-xs tw:transition-all tw:outline-none tw:first:rounded-s-md tw:first:border-s tw:last:rounded-e-md tw:has-[[data-active="true"]]:ring-3 tw:relative tw:flex tw:items-center tw:justify-center tw:has-[[data-active="true"]]:z-10');
81
164
  }
82
165
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmInputOtpSlot, deps: [], target: i0.ɵɵFactoryTarget.Component });
83
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", type: HlmInputOtpSlot, isStandalone: true, selector: "hlm-input-otp-slot", inputs: { index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "data-slot": "input-otp-slot" } }, ngImport: i0, template: `
84
- <brn-input-otp-slot [index]="index()">
85
- <hlm-input-otp-fake-caret />
86
- </brn-input-otp-slot>
166
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: HlmInputOtpSlot, isStandalone: true, selector: "hlm-input-otp-slot", inputs: { index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "data-slot": "input-otp-slot" } }, ngImport: i0, template: `
167
+ @if (_control) {
168
+ <span
169
+ [attr.data-active]="_slot()?.isActive"
170
+ [attr.data-invalid]="_control.invalid?.() ? 'true' : null"
171
+ [attr.data-touched]="_control.touched?.() ? 'true' : null"
172
+ [attr.data-dirty]="_control.dirty?.() ? 'true' : null"
173
+ [attr.data-matches-spartan-invalid]="_control.spartanInvalid?.() ? 'true' : null"
174
+ >
175
+ {{ _slot()?.char }}
176
+ @if (_slot()?.hasFakeCaret) {
177
+ <hlm-input-otp-fake-caret />
178
+ }
179
+ </span>
180
+ } @else {
181
+ <brn-input-otp-slot [index]="index()">
182
+ <hlm-input-otp-fake-caret />
183
+ </brn-input-otp-slot>
184
+ }
87
185
  `, isInline: true, dependencies: [{ kind: "component", type: BrnInputOtpSlot, selector: "brn-input-otp-slot", inputs: ["index", "forceInvalid"] }, { kind: "component", type: HlmInputOtpFakeCaret, selector: "hlm-input-otp-fake-caret" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
88
186
  }
89
187
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmInputOtpSlot, decorators: [{
@@ -94,9 +192,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
94
192
  changeDetection: ChangeDetectionStrategy.OnPush,
95
193
  host: { 'data-slot': 'input-otp-slot' },
96
194
  template: `
97
- <brn-input-otp-slot [index]="index()">
98
- <hlm-input-otp-fake-caret />
99
- </brn-input-otp-slot>
195
+ @if (_control) {
196
+ <span
197
+ [attr.data-active]="_slot()?.isActive"
198
+ [attr.data-invalid]="_control.invalid?.() ? 'true' : null"
199
+ [attr.data-touched]="_control.touched?.() ? 'true' : null"
200
+ [attr.data-dirty]="_control.dirty?.() ? 'true' : null"
201
+ [attr.data-matches-spartan-invalid]="_control.spartanInvalid?.() ? 'true' : null"
202
+ >
203
+ {{ _slot()?.char }}
204
+ @if (_slot()?.hasFakeCaret) {
205
+ <hlm-input-otp-fake-caret />
206
+ }
207
+ </span>
208
+ } @else {
209
+ <brn-input-otp-slot [index]="index()">
210
+ <hlm-input-otp-fake-caret />
211
+ </brn-input-otp-slot>
212
+ }
100
213
  `,
101
214
  }]
102
215
  }], ctorParameters: () => [], propDecorators: { index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: true }] }] } });
@@ -106,6 +219,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
106
219
  */
107
220
  const HlmInputOtpImports = [
108
221
  HlmInputOtp,
222
+ HlmInputOtpControl,
109
223
  HlmInputOtpGroup,
110
224
  HlmInputOtpSeparator,
111
225
  HlmInputOtpSlot,
@@ -114,10 +228,12 @@ const HlmInputOtpImports = [
114
228
  class HlmInputOtpModule {
115
229
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmInputOtpModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
116
230
  static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.1.3", ngImport: i0, type: HlmInputOtpModule, imports: [HlmInputOtp,
231
+ HlmInputOtpControl,
117
232
  HlmInputOtpGroup,
118
233
  HlmInputOtpSeparator,
119
234
  HlmInputOtpSlot,
120
235
  HlmInputOtpFakeCaret], exports: [HlmInputOtp,
236
+ HlmInputOtpControl,
121
237
  HlmInputOtpGroup,
122
238
  HlmInputOtpSeparator,
123
239
  HlmInputOtpSlot,
@@ -137,5 +253,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
137
253
  * Generated bundle index. Do not edit.
138
254
  */
139
255
 
140
- export { HlmInputOtp, HlmInputOtpFakeCaret, HlmInputOtpGroup, HlmInputOtpImports, HlmInputOtpModule, HlmInputOtpSeparator, HlmInputOtpSlot };
256
+ export { HlmInputOtp, HlmInputOtpControl, HlmInputOtpFakeCaret, HlmInputOtpGroup, HlmInputOtpImports, HlmInputOtpModule, HlmInputOtpSeparator, HlmInputOtpSlot };
141
257
 
@@ -1,5 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { NumberInput } from '@angular/cdk/coercion';
2
+ import { BooleanInput, NumberInput } from '@angular/cdk/coercion';
3
+ import { BrnInputOtp } from '@spartan-ng/brain/input-otp';
4
+ import * as i1 from '@spartan-ng/brain/field';
3
5
 
4
6
  declare class HlmInputOtp {
5
7
  constructor();
@@ -7,6 +9,18 @@ declare class HlmInputOtp {
7
9
  static ɵdir: i0.ɵɵDirectiveDeclaration<HlmInputOtp, "brn-input-otp[hlmInputOtp], brn-input-otp[hlm]", never, {}, {}, never, never, true, never>;
8
10
  }
9
11
 
12
+ /**
13
+ * BrnInputOtp's CVA and editing behavior with declarative native-input ARIA forwarding.
14
+ * Use with HlmInputOtpSlot; the legacy brn-input-otp skin remains available.
15
+ */
16
+ declare class HlmInputOtpControl extends BrnInputOtp {
17
+ /** Native input description IDs, merged with enclosing Spartan field descriptions. */
18
+ readonly ariaDescribedBy: i0.InputSignal<string | null>;
19
+ readonly required: i0.InputSignalWithTransform<boolean, BooleanInput>;
20
+ static ɵfac: i0.ɵɵFactoryDeclaration<HlmInputOtpControl, never>;
21
+ static ɵcmp: i0.ɵɵComponentDeclaration<HlmInputOtpControl, "hlm-input-otp", never, { "ariaDescribedBy": { "alias": "aria-describedby"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, [{ directive: typeof i1.BrnFieldControl; inputs: {}; outputs: {}; }, { directive: typeof HlmInputOtp; inputs: {}; outputs: {}; }]>;
22
+ }
23
+
10
24
  declare class HlmInputOtpFakeCaret {
11
25
  constructor();
12
26
  static ɵfac: i0.ɵɵFactoryDeclaration<HlmInputOtpFakeCaret, never>;
@@ -26,6 +40,12 @@ declare class HlmInputOtpSeparator {
26
40
  }
27
41
 
28
42
  declare class HlmInputOtpSlot {
43
+ protected readonly _control: HlmInputOtpControl | null;
44
+ protected readonly _slot: i0.Signal<{
45
+ char: string | null;
46
+ isActive: boolean;
47
+ hasFakeCaret: boolean;
48
+ } | undefined>;
29
49
  /** The index of the slot to render the char or a fake caret */
30
50
  readonly index: i0.InputSignalWithTransform<number, NumberInput>;
31
51
  constructor();
@@ -33,11 +53,11 @@ declare class HlmInputOtpSlot {
33
53
  static ɵcmp: i0.ɵɵComponentDeclaration<HlmInputOtpSlot, "hlm-input-otp-slot", never, { "index": { "alias": "index"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
34
54
  }
35
55
 
36
- declare const HlmInputOtpImports: readonly [typeof HlmInputOtp, typeof HlmInputOtpGroup, typeof HlmInputOtpSeparator, typeof HlmInputOtpSlot, typeof HlmInputOtpFakeCaret];
56
+ declare const HlmInputOtpImports: readonly [typeof HlmInputOtp, typeof HlmInputOtpControl, typeof HlmInputOtpGroup, typeof HlmInputOtpSeparator, typeof HlmInputOtpSlot, typeof HlmInputOtpFakeCaret];
37
57
  declare class HlmInputOtpModule {
38
58
  static ɵfac: i0.ɵɵFactoryDeclaration<HlmInputOtpModule, never>;
39
- static ɵmod: i0.ɵɵNgModuleDeclaration<HlmInputOtpModule, never, [typeof HlmInputOtp, typeof HlmInputOtpGroup, typeof HlmInputOtpSeparator, typeof HlmInputOtpSlot, typeof HlmInputOtpFakeCaret], [typeof HlmInputOtp, typeof HlmInputOtpGroup, typeof HlmInputOtpSeparator, typeof HlmInputOtpSlot, typeof HlmInputOtpFakeCaret]>;
59
+ static ɵmod: i0.ɵɵNgModuleDeclaration<HlmInputOtpModule, never, [typeof HlmInputOtp, typeof HlmInputOtpControl, typeof HlmInputOtpGroup, typeof HlmInputOtpSeparator, typeof HlmInputOtpSlot, typeof HlmInputOtpFakeCaret], [typeof HlmInputOtp, typeof HlmInputOtpControl, typeof HlmInputOtpGroup, typeof HlmInputOtpSeparator, typeof HlmInputOtpSlot, typeof HlmInputOtpFakeCaret]>;
40
60
  static ɵinj: i0.ɵɵInjectorDeclaration<HlmInputOtpModule>;
41
61
  }
42
62
 
43
- export { HlmInputOtp, HlmInputOtpFakeCaret, HlmInputOtpGroup, HlmInputOtpImports, HlmInputOtpModule, HlmInputOtpSeparator, HlmInputOtpSlot };
63
+ export { HlmInputOtp, HlmInputOtpControl, HlmInputOtpFakeCaret, HlmInputOtpGroup, HlmInputOtpImports, HlmInputOtpModule, HlmInputOtpSeparator, HlmInputOtpSlot };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@egose/shadcn-theme-ng-tw",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Angular shadcn UI theme",
5
5
  "author": "Junmin Ahn",
6
6
  "bundles": [
@@ -144,12 +144,21 @@ For the `tw:` build, swap the specifier to `@egose/shadcn-theme-ng-tw/pagination
144
144
  | `currentPage` | **model (required)** | `number` | Two-way: `[(currentPage)]`. Clicks set it; out-of-range values are auto-corrected via `outOfBoundCorrection` |
145
145
  | `itemsPerPage` | **model (required)** | `number` | Two-way: `[(itemsPerPage)]`, bound to the page-size `hlm-select` |
146
146
  | `totalItems` | input (required) | `number` (number coercion) | Server or client total; `0`/negative collapses to a single empty page |
147
- | `maxSize` | input | `number`, default `7` (number coercion) | Max page links in the window (ellipsis logic from `ngx-pagination`) |
147
+ | `maxSize` | input | `number`, default `7` (number coercion) | Window entries including ellipses; floored and bounded to 1–100; invalid/non-positive uses 7 |
148
148
  | `showEdges` | input | `boolean`, default `true` (boolean coercion) | Shows prev/next edge buttons (hidden on first/last page respectively) |
149
149
  | `pageSizes` | input | `number[]`, default `[10, 20, 50, 100]` | Page-size menu; the current size is merged in + sorted if missing |
150
150
 
151
151
  Layout: `flex justify-between` row — left count summary (`<b>total</b> total items | <b>pages</b> pages`), center pager, right `hlm-select` page-size picker. Previous/next are plain click handlers (`goToPrevious/goToNext`); page links set `currentPage` directly without routing.
152
152
 
153
+ #### Normalization and numeric limits (both numbered pagers)
154
+
155
+ - Page count is `ceil(totalItems / itemsPerPage)`, at least 1 and at most `Number.MAX_SAFE_INTEGER`. Finite positive fractional totals/sizes retain that division behavior, including sizes below 1. Overflowing positive division saturates at the safe-integer page limit.
156
+ - Empty, negative or non-finite totals use one page. Non-positive or non-finite sizes (`0`, negative, `NaN`, `Infinity`) also use one page, **without rewriting `itemsPerPage`**. Supply a finite positive size to resume paging. The models are numeric; template number attributes such as `totalItems` and `maxSize` are coerced by Angular.
157
+ - Finite `currentPage` values are floored and clamped to `[1, lastPage]`; non-finite values become 1. Changes to totals, size or the external page are reconciled by an effect, emitting `currentPageChange` once per correction. Valid models emit nothing. Bind `[(currentPage)]` to receive corrections.
158
+ - All derived signals, including the page list, are side-effect-free. Rendering uses the corrected page immediately. Previous/next disappear at their bounds, and direct client-state navigation handlers are also clamped. Empty results show active page 1 and neither edge.
159
+ - `maxSize` / helper `paginationRange` is a window-entry budget, including ellipses. Finite positive values are floored and clamped to **1–100**; non-finite/non-positive values fall back to **7**. Ranges below 5 use a contiguous window containing the active page; larger ranges retain the first/last/ellipsis layout. Work and allocation are proportional to `min(pageCount, normalizedRange)`, never to an unbounded total or range. The array-returning helper API is unchanged.
160
+ - The summary displays the supplied total; normalization controls page count/navigation and does not repair the application's data. For asynchronous server loading, keep the last known total until the new count arrives if a temporary zero should not reset the page.
161
+
153
162
  ### `HlmNumberedPaginationQueryParams` (`hlm-numbered-pagination-query-params`)
154
163
 
155
164
  Same models/inputs as `HlmNumberedPagination` **plus**:
@@ -160,11 +169,13 @@ Same models/inputs as `HlmNumberedPagination` **plus**:
160
169
 
161
170
  Every link (prev/numbered/next) navigates via `RouterLink` with `[queryParams]="{ page }"` and `queryParamsHandling="merge"`. The active page renders with `link === undefined` (no navigation) + `isActive`.
162
171
 
172
+ The component **does not subscribe to the route or navigate on correction/size changes**. The parent reads `?page=` and supplies `currentPage`; link clicks navigate, then the parent's route subscription updates that model. Corrections emit `currentPageChange` but leave the URL unchanged. To canonicalize a corrected page in the URL, handle that output in the parent with `router.navigate([], { relativeTo: route, queryParams: { page }, queryParamsHandling: 'merge', replaceUrl: true })`. Keep unrelated parameters when doing so. Page-size URL persistence is also parent-owned.
173
+
163
174
  ### Helpers
164
175
 
165
176
  | Symbol | Signature | Notes |
166
177
  | ------------------------------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
167
- | `outOfBoundCorrection(totalItems, itemsPerPage, currentPage)` | `(number, number, number) => number` | Clamps to `[1, totalPages]` (from `ngx-pagination`) |
178
+ | `outOfBoundCorrection(totalItems, itemsPerPage, currentPage)` | `(number, number, number) => number` | Pure floor/clamp using the normalization policy above; empty/invalid paging returns 1 |
168
179
  | `createPageArray(currentPage, itemsPerPage, totalItems, paginationRange)` | `(number, number, number, number) => (number \| '...')[]` | Window with `'...'` gaps; powers both numbered pagers |
169
180
  | `Page` | `type Page = number \| '...'` | Not exported from public-api (internal to the pager file) — redeclare locally if you type custom windows |
170
181
 
@@ -246,7 +257,8 @@ export class PagerClientComponent {
246
257
  ### 3. Router `?page=` pager (server-driven)
247
258
 
248
259
  ```ts
249
- import { Component, signal, effect } from '@angular/core';
260
+ import { Component, signal } from '@angular/core';
261
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
250
262
  import { ActivatedRoute } from '@angular/router';
251
263
  import { HlmPaginationImports } from '@egose/shadcn-theme-ng/pagination';
252
264
 
@@ -261,7 +273,7 @@ import { HlmPaginationImports } from '@egose/shadcn-theme-ng/pagination';
261
273
  [totalItems]="total()"
262
274
  link="."
263
275
  />
264
- <p class="tw:text-sm tw:text-muted-foreground">URL holds ?page={{ page }}.</p>
276
+ <p class="tw:text-sm tw:text-muted-foreground">Displayed page: {{ page() }}.</p>
265
277
  `,
266
278
  })
267
279
  export class PagerQueryComponent {
@@ -270,13 +282,10 @@ export class PagerQueryComponent {
270
282
  readonly total = signal(320);
271
283
 
272
284
  constructor(route: ActivatedRoute) {
273
- effect(() => {
274
- // keep ?page= as the source of truth on back/forward + deep links
275
- const sub = route.queryParamMap.subscribe((params) => {
276
- const p = Number(params.get('page') ?? 1);
277
- this.page.set(Number.isFinite(p) && p >= 1 ? Math.floor(p) : 1);
278
- });
279
- return () => sub.unsubscribe();
285
+ // Read deep links and back/forward changes; the pager corrects model bounds.
286
+ // Corrections here update the displayed page, not the URL.
287
+ route.queryParamMap.pipe(takeUntilDestroyed()).subscribe((params) => {
288
+ this.page.set(Number(params.get('page') ?? 1));
280
289
  });
281
290
  }
282
291
  }
@@ -330,7 +339,7 @@ import { HlmPaginationImports, createPageArray } from '@egose/shadcn-theme-ng/pa
330
339
  template: `
331
340
  <nav hlmPagination>
332
341
  <ul hlmPaginationContent>
333
- @for (p of window(); track p) {
342
+ @for (p of window(); track $index) {
334
343
  <li hlmPaginationItem>
335
344
  @if (p === '...') {
336
345
  <hlm-pagination-ellipsis />