@egose/shadcn-theme-ng 0.5.3 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +131 -12
  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 +11 -7
  6. package/checkbox/types/checkbox.d.ts +6 -4
  7. package/combobox/fesm2022/combobox.mjs +13 -7
  8. package/combobox/types/combobox.d.ts +8 -2
  9. package/data-table/README.md +174 -0
  10. package/data-table/fesm2022/data-table.mjs +713 -0
  11. package/data-table/package.json +25 -0
  12. package/data-table/types/data-table.d.ts +495 -0
  13. package/date-picker/README.md +41 -9
  14. package/date-picker/fesm2022/date-picker.mjs +265 -59
  15. package/date-picker/types/date-picker.d.ts +58 -27
  16. package/dropdown-menu/README.md +2 -0
  17. package/form-autocomplete/README.md +4 -2
  18. package/form-autocomplete/fesm2022/form-autocomplete.mjs +19 -6
  19. package/form-autocomplete/types/form-autocomplete.d.ts +9 -3
  20. package/form-combobox/README.md +4 -2
  21. package/form-combobox/fesm2022/form-combobox.mjs +23 -6
  22. package/form-combobox/types/form-combobox.d.ts +10 -3
  23. package/form-input-otp/README.md +7 -5
  24. package/form-input-otp/fesm2022/form-input-otp.mjs +20 -8
  25. package/form-input-otp/types/form-input-otp.d.ts +9 -3
  26. package/form-searchable-multiselect/README.md +15 -1
  27. package/form-searchable-multiselect/fesm2022/form-searchable-multiselect.mjs +15 -3
  28. package/form-searchable-multiselect/types/form-searchable-multiselect.d.ts +9 -1
  29. package/form-slider/README.md +3 -1
  30. package/form-slider/fesm2022/form-slider.mjs +19 -6
  31. package/form-slider/types/form-slider.d.ts +9 -3
  32. package/form-text-input/fesm2022/form-text-input.mjs +1 -1
  33. package/form-textarea/fesm2022/form-textarea.mjs +1 -1
  34. package/form-toggle/README.md +22 -18
  35. package/form-toggle/fesm2022/form-toggle.mjs +22 -9
  36. package/form-toggle/types/form-toggle.d.ts +6 -2
  37. package/input/README.md +7 -12
  38. package/input/fesm2022/input.mjs +7 -4
  39. package/input/types/input.d.ts +3 -1
  40. package/input-otp/README.md +33 -9
  41. package/input-otp/fesm2022/input-otp.mjs +127 -11
  42. package/input-otp/types/input-otp.d.ts +24 -4
  43. package/layout-simple/fesm2022/layout-simple.mjs +1 -1
  44. package/package.json +6 -1
  45. package/pagination/README.md +21 -12
  46. package/pagination/fesm2022/pagination.mjs +73 -65
  47. package/pagination/types/pagination.d.ts +21 -8
  48. package/phone-input/README.md +16 -0
  49. package/phone-input/fesm2022/phone-input.mjs +42 -4
  50. package/phone-input/types/phone-input.d.ts +5 -2
  51. package/searchable-multiselect/README.md +62 -40
  52. package/searchable-multiselect/fesm2022/searchable-multiselect.mjs +59 -32
  53. package/searchable-multiselect/types/searchable-multiselect.d.ts +23 -6
  54. package/slider/fesm2022/slider.mjs +9 -5
  55. package/slider/types/slider.d.ts +6 -2
  56. package/stepper/README.md +39 -19
  57. package/stepper/fesm2022/stepper.mjs +64 -12
  58. package/stepper/types/stepper.d.ts +12 -1
  59. package/table/README.md +2 -0
  60. package/table/fesm2022/table.mjs +12 -8
  61. package/table/types/table.d.ts +7 -2
@@ -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/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(() => "pointer-events-none absolute inset-0 flex items-center 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(() => "dark:bg-input/30 border-input has-[brn-input-otp-slot[data-active=\"true\"]]:border-ring has-[brn-input-otp-slot[data-active=\"true\"]]:ring-ring/50 has-[brn-input-otp-slot[data-active=\"true\"]]:data-[matches-spartan-invalid=true]:ring-destructive/20 dark:has-[brn-input-otp-slot[data-active=\"true\"]]:data-[matches-spartan-invalid=true]:ring-destructive/40 data-[matches-spartan-invalid=true]:border-destructive has-[brn-input-otp-slot[data-active=\"true\"]]:data-[matches-spartan-invalid=true]:border-destructive size-9 border-y border-e text-sm shadow-xs transition-all outline-none first:rounded-s-md first:border-s last:rounded-e-md has-[brn-input-otp-slot[data-active=\"true\"]]:ring-3 relative flex items-center justify-center has-[brn-input-otp-slot[data-active=\"true\"]]:z-10");
163
+ classes(() => "dark:bg-input/30 border-input has-[[data-active=\"true\"]]:border-ring has-[[data-active=\"true\"]]:ring-ring/50 has-[[data-active=\"true\"]]:data-[matches-spartan-invalid=true]:ring-destructive/20 dark:has-[[data-active=\"true\"]]:data-[matches-spartan-invalid=true]:ring-destructive/40 data-[matches-spartan-invalid=true]:border-destructive has-[[data-active=\"true\"]]:data-[matches-spartan-invalid=true]:border-destructive size-9 border-y border-e text-sm shadow-xs transition-all outline-none first:rounded-s-md first:border-s last:rounded-e-md has-[[data-active=\"true\"]]:ring-3 relative flex items-center justify-center 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 };
@@ -169,7 +169,7 @@ class EgLayoutSearch {
169
169
  buttons[Math.min(index, buttons.length - 1)]?.nativeElement.focus();
170
170
  }
171
171
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EgLayoutSearch, deps: [], target: i0.ɵɵFactoryTarget.Component });
172
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: EgLayoutSearch, isStandalone: true, selector: "eg-layout-search", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, errorText: { classPropertyName: "errorText", publicName: "errorText", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", isSignal: true, isRequired: false, transformFunction: null }, retryText: { classPropertyName: "retryText", publicName: "retryText", isSignal: true, isRequired: false, transformFunction: null }, loader: { classPropertyName: "loader", publicName: "loader", isSignal: true, isRequired: false, transformFunction: null }, resultLabel: { classPropertyName: "resultLabel", publicName: "resultLabel", isSignal: true, isRequired: false, transformFunction: null }, resultTemplate: { classPropertyName: "resultTemplate", publicName: "resultTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { resultSelected: "resultSelected" }, host: { classAttribute: "block min-w-0" }, viewQueries: [{ propertyName: "searchInput", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }, { propertyName: "resultButtons", predicate: ["resultButton"], descendants: true, isSignal: true }], ngImport: i0, template: "\n <hlm-popover\n [state]=\"isOpen() ? 'open' : 'closed'\"\n (stateChanged)=\"setOpen($event === 'open')\"\n (closed)=\"setOpen(false)\"\n >\n <button\n hlmPopoverTrigger\n type=\"button\"\n class=\"flex min-h-11 w-full items-center gap-2 rounded-lg border border-border bg-foreground/3 px-3 text-left text-sm text-foreground/75 cursor-pointer hover:bg-foreground/5 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring md:w-56\"\n >\n <ng-icon [svg]=\"searchIcon\" size=\"16px\" aria-hidden=\"true\" class=\"shrink-0\" />\n <span class=\"truncate\">{{ placeholder() }}</span>\n </button>\n <hlm-popover-content *hlmPopoverPortal class=\"w-80 max-w-[calc(100vw-2rem)] p-2\">\n <input\n #searchInput\n hlmInput\n type=\"search\"\n autocomplete=\"off\"\n [value]=\"query()\"\n (input)=\"query.set($any($event.target).value)\"\n [placeholder]=\"placeholder()\"\n [attr.aria-label]=\"placeholder()\"\n (keydown.arrowdown)=\"focusResult(0, $event)\"\n class=\"mb-2 min-h-11\"\n />\n <div [attr.aria-busy]=\"isLoading()\" class=\"max-h-72 overflow-y-auto overscroll-contain\">\n @if (isLoading()) {\n <p role=\"status\" class=\"px-3 py-6 text-center text-sm text-foreground/75\">\n {{ loadingText() }}\n </p>\n } @else if (hasError()) {\n <div role=\"status\" class=\"space-y-3 px-3 py-4 text-sm\">\n <p>{{ errorText() }}</p>\n <button\n type=\"button\"\n (click)=\"retry()\"\n class=\"min-h-11 rounded-md border border-border px-3 font-medium cursor-pointer focus-visible:outline-2 focus-visible:outline-ring\"\n >\n {{ retryText() }}\n </button>\n </div>\n } @else if (!results().length) {\n <p role=\"status\" class=\"px-3 py-6 text-center text-sm text-foreground/75\">\n {{ emptyText() }}\n </p>\n } @else {\n @for (result of results(); track $index; let index = $index) {\n <button\n #resultButton\n type=\"button\"\n (click)=\"selectResult(result)\"\n (keydown.arrowdown)=\"focusResult(index + 1, $event)\"\n (keydown.arrowup)=\"focusResult(index - 1, $event)\"\n (keydown.home)=\"focusResult(0, $event)\"\n (keydown.end)=\"focusResult(results().length - 1, $event)\"\n class=\"flex min-h-11 w-full items-center rounded-md px-3 py-2 text-left text-sm cursor-pointer hover:bg-foreground/5 focus-visible:bg-foreground/10 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring\"\n >\n @if (resultTemplate(); as template) {\n <ng-container [ngTemplateOutlet]=\"template\" [ngTemplateOutletContext]=\"{ $implicit: result }\" />\n } @else {\n {{ labelFor(result) }}\n }\n </button>\n }\n }\n </div>\n </hlm-popover-content>\n </hlm-popover>\n ", isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: HlmPopover, selector: "[hlmPopover],hlm-popover" }, { kind: "directive", type: HlmPopoverTrigger, selector: "button[hlmPopoverTrigger],button[hlmPopoverTriggerFor]" }, { kind: "directive", type: HlmPopoverContent, selector: "[hlmPopoverContent],hlm-popover-content" }, { kind: "directive", type: HlmPopoverPortal, selector: "[hlmPopoverPortal]" }, { kind: "directive", type: HlmInput, selector: "[hlmInput]", inputs: ["ariaDescribedby"] }, { kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
172
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: EgLayoutSearch, isStandalone: true, selector: "eg-layout-search", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, errorText: { classPropertyName: "errorText", publicName: "errorText", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", isSignal: true, isRequired: false, transformFunction: null }, retryText: { classPropertyName: "retryText", publicName: "retryText", isSignal: true, isRequired: false, transformFunction: null }, loader: { classPropertyName: "loader", publicName: "loader", isSignal: true, isRequired: false, transformFunction: null }, resultLabel: { classPropertyName: "resultLabel", publicName: "resultLabel", isSignal: true, isRequired: false, transformFunction: null }, resultTemplate: { classPropertyName: "resultTemplate", publicName: "resultTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { resultSelected: "resultSelected" }, host: { classAttribute: "block min-w-0" }, viewQueries: [{ propertyName: "searchInput", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }, { propertyName: "resultButtons", predicate: ["resultButton"], descendants: true, isSignal: true }], ngImport: i0, template: "\n <hlm-popover\n [state]=\"isOpen() ? 'open' : 'closed'\"\n (stateChanged)=\"setOpen($event === 'open')\"\n (closed)=\"setOpen(false)\"\n >\n <button\n hlmPopoverTrigger\n type=\"button\"\n class=\"flex min-h-11 w-full items-center gap-2 rounded-lg border border-border bg-foreground/3 px-3 text-left text-sm text-foreground/75 cursor-pointer hover:bg-foreground/5 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring md:w-56\"\n >\n <ng-icon [svg]=\"searchIcon\" size=\"16px\" aria-hidden=\"true\" class=\"shrink-0\" />\n <span class=\"truncate\">{{ placeholder() }}</span>\n </button>\n <hlm-popover-content *hlmPopoverPortal class=\"w-80 max-w-[calc(100vw-2rem)] p-2\">\n <input\n #searchInput\n hlmInput\n type=\"search\"\n autocomplete=\"off\"\n [value]=\"query()\"\n (input)=\"query.set($any($event.target).value)\"\n [placeholder]=\"placeholder()\"\n [attr.aria-label]=\"placeholder()\"\n (keydown.arrowdown)=\"focusResult(0, $event)\"\n class=\"mb-2 min-h-11\"\n />\n <div [attr.aria-busy]=\"isLoading()\" class=\"max-h-72 overflow-y-auto overscroll-contain\">\n @if (isLoading()) {\n <p role=\"status\" class=\"px-3 py-6 text-center text-sm text-foreground/75\">\n {{ loadingText() }}\n </p>\n } @else if (hasError()) {\n <div role=\"status\" class=\"space-y-3 px-3 py-4 text-sm\">\n <p>{{ errorText() }}</p>\n <button\n type=\"button\"\n (click)=\"retry()\"\n class=\"min-h-11 rounded-md border border-border px-3 font-medium cursor-pointer focus-visible:outline-2 focus-visible:outline-ring\"\n >\n {{ retryText() }}\n </button>\n </div>\n } @else if (!results().length) {\n <p role=\"status\" class=\"px-3 py-6 text-center text-sm text-foreground/75\">\n {{ emptyText() }}\n </p>\n } @else {\n @for (result of results(); track $index; let index = $index) {\n <button\n #resultButton\n type=\"button\"\n (click)=\"selectResult(result)\"\n (keydown.arrowdown)=\"focusResult(index + 1, $event)\"\n (keydown.arrowup)=\"focusResult(index - 1, $event)\"\n (keydown.home)=\"focusResult(0, $event)\"\n (keydown.end)=\"focusResult(results().length - 1, $event)\"\n class=\"flex min-h-11 w-full items-center rounded-md px-3 py-2 text-left text-sm cursor-pointer hover:bg-foreground/5 focus-visible:bg-foreground/10 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring\"\n >\n @if (resultTemplate(); as template) {\n <ng-container [ngTemplateOutlet]=\"template\" [ngTemplateOutletContext]=\"{ $implicit: result }\" />\n } @else {\n {{ labelFor(result) }}\n }\n </button>\n }\n }\n </div>\n </hlm-popover-content>\n </hlm-popover>\n ", isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: HlmPopover, selector: "[hlmPopover],hlm-popover" }, { kind: "directive", type: HlmPopoverTrigger, selector: "button[hlmPopoverTrigger],button[hlmPopoverTriggerFor]" }, { kind: "directive", type: HlmPopoverContent, selector: "[hlmPopoverContent],hlm-popover-content" }, { kind: "directive", type: HlmPopoverPortal, selector: "[hlmPopoverPortal]" }, { kind: "directive", type: HlmInput, selector: "[hlmInput]", inputs: ["ariaDescribedby", "size"] }, { kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
173
173
  }
174
174
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EgLayoutSearch, decorators: [{
175
175
  type: Component,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@egose/shadcn-theme-ng",
3
- "version": "0.5.3",
3
+ "version": "0.6.1",
4
4
  "description": "Angular shadcn UI theme",
5
5
  "author": "Junmin Ahn",
6
6
  "bundles": [
@@ -31,6 +31,7 @@
31
31
  "@angular/router": ">=22.0.0 <23.0.0",
32
32
  "@ng-icons/core": ">=35.0.1 <36.0.0",
33
33
  "@spartan-ng/brain": ">=1.3.2 <2.0.0",
34
+ "@tanstack/angular-table": ">=9.1.2 <10.0.0",
34
35
  "rxjs": ">=7.8.0 <8.0.0"
35
36
  },
36
37
  "files": [
@@ -126,6 +127,10 @@
126
127
  "types": "./context-menu/types/context-menu.d.ts",
127
128
  "default": "./context-menu/fesm2022/context-menu.mjs"
128
129
  },
130
+ "./data-table": {
131
+ "types": "./data-table/types/data-table.d.ts",
132
+ "default": "./data-table/fesm2022/data-table.mjs"
133
+ },
129
134
  "./date-picker": {
130
135
  "types": "./date-picker/types/date-picker.d.ts",
131
136
  "default": "./date-picker/fesm2022/date-picker.mjs"
@@ -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 />