@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
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, Directive, ChangeDetectionStrategy, Component, booleanAttribute, computed, model, numberAttribute, untracked, NgModule } from '@angular/core';
2
+ import { input, Directive, ChangeDetectionStrategy, Component, booleanAttribute, computed, model, numberAttribute, effect, untracked, NgModule } from '@angular/core';
3
3
  import * as i1$1 from '@egose/shadcn-theme-ng-tw/select';
4
4
  import { HlmSelectImports } from '@egose/shadcn-theme-ng-tw/select';
5
5
  import { classes, hlm } from '@egose/shadcn-theme-ng-tw/utils';
@@ -265,14 +265,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
265
265
  }]
266
266
  }], propDecorators: { userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], link: [{ type: i0.Input, args: [{ isSignal: true, alias: "link", required: false }] }], queryParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryParams", required: false }] }], queryParamsHandling: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryParamsHandling", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }], iconOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconOnly", required: false }] }] } });
267
267
 
268
+ /** Shared arithmetic policy; invalid totals/sizes represent a single page. */
269
+ function pageCount(totalItems, itemsPerPage) {
270
+ if (!Number.isFinite(totalItems) || totalItems <= 0 || !Number.isFinite(itemsPerPage) || itemsPerPage <= 0) {
271
+ return 1;
272
+ }
273
+ return Math.min(Number.MAX_SAFE_INTEGER, Math.max(1, Math.ceil(totalItems / itemsPerPage)));
274
+ }
275
+
268
276
  class HlmNumberedPagination {
269
277
  /**
270
- * The current (active) page.
278
+ * The current (active) page, floored and clamped to the available range.
279
+ * Corrections emit currentPageChange once; empty/invalid paging uses page 1.
271
280
  */
272
281
  currentPage = model.required(/* @ts-ignore */
273
282
  ...(ngDevMode ? [{ debugName: "currentPage" }] : /* istanbul ignore next */ []));
274
283
  /**
275
- * The number of items per paginated page.
284
+ * The number of items per paginated page. Non-finite/non-positive sizes
285
+ * disable paging (one page) without rewriting this model.
276
286
  */
277
287
  itemsPerPage = model.required(/* @ts-ignore */
278
288
  ...(ngDevMode ? [{ debugName: "itemsPerPage" }] : /* istanbul ignore next */ []));
@@ -283,7 +293,8 @@ class HlmNumberedPagination {
283
293
  */
284
294
  totalItems = input.required({ ...(ngDevMode ? { debugName: "totalItems" } : /* istanbul ignore next */ {}), transform: numberAttribute });
285
295
  /**
286
- * The number of page links to show.
296
+ * Maximum window entries, including ellipses: floored and bounded to 1–100.
297
+ * Non-finite/non-positive values use the default of 7.
287
298
  */
288
299
  maxSize = input(7, { ...(ngDevMode ? { debugName: "maxSize" } : /* istanbul ignore next */ {}), transform: numberAttribute });
289
300
  /**
@@ -303,33 +314,29 @@ class HlmNumberedPagination {
303
314
  : [...pageSizes, this.itemsPerPage()].sort((a, b) => a - b); // otherwise, add current page size and sort the array
304
315
  }, /* @ts-ignore */
305
316
  ...(ngDevMode ? [{ debugName: "_pageSizesWithCurrent" }] : /* istanbul ignore next */ []));
306
- _isFirstPageActive = computed(() => this.currentPage() === 1, /* @ts-ignore */
317
+ _currentPage = computed(() => outOfBoundCorrection(this.totalItems(), this.itemsPerPage(), this.currentPage()), /* @ts-ignore */
318
+ ...(ngDevMode ? [{ debugName: "_currentPage" }] : /* istanbul ignore next */ []));
319
+ _isFirstPageActive = computed(() => this._currentPage() === 1, /* @ts-ignore */
307
320
  ...(ngDevMode ? [{ debugName: "_isFirstPageActive" }] : /* istanbul ignore next */ []));
308
- _isLastPageActive = computed(() => this.currentPage() === this._lastPageNumber(), /* @ts-ignore */
321
+ _isLastPageActive = computed(() => this._currentPage() === this._lastPageNumber(), /* @ts-ignore */
309
322
  ...(ngDevMode ? [{ debugName: "_isLastPageActive" }] : /* istanbul ignore next */ []));
310
- _lastPageNumber = computed(() => {
311
- if (this.totalItems() < 1) {
312
- // when there are 0 or fewer (an error case) items, there are no "pages" as such,
313
- // but it makes sense to consider a single, empty page as the last page.
314
- return 1;
315
- }
316
- return Math.ceil(this.totalItems() / this.itemsPerPage());
317
- }, /* @ts-ignore */
323
+ _lastPageNumber = computed(() => pageCount(this.totalItems(), this.itemsPerPage()), /* @ts-ignore */
318
324
  ...(ngDevMode ? [{ debugName: "_lastPageNumber" }] : /* istanbul ignore next */ []));
319
- _pages = computed(() => {
320
- const correctedCurrentPage = outOfBoundCorrection(this.totalItems(), this.itemsPerPage(), this.currentPage());
321
- if (correctedCurrentPage !== this.currentPage()) {
322
- // update the current page
323
- untracked(() => this.currentPage.set(correctedCurrentPage));
324
- }
325
- return createPageArray(correctedCurrentPage, this.itemsPerPage(), this.totalItems(), this.maxSize());
326
- }, /* @ts-ignore */
325
+ _pages = computed(() => createPageArray(this._currentPage(), this.itemsPerPage(), this.totalItems(), this.maxSize()), /* @ts-ignore */
327
326
  ...(ngDevMode ? [{ debugName: "_pages" }] : /* istanbul ignore next */ []));
327
+ constructor() {
328
+ effect(() => {
329
+ const page = this._currentPage();
330
+ if (page !== this.currentPage()) {
331
+ untracked(() => this.currentPage.set(page));
332
+ }
333
+ });
334
+ }
328
335
  goToPrevious() {
329
- this.currentPage.set(this.currentPage() - 1);
336
+ this.currentPage.set(Math.max(1, this._currentPage() - 1));
330
337
  }
331
338
  goToNext() {
332
- this.currentPage.set(this.currentPage() + 1);
339
+ this.currentPage.set(Math.min(this._lastPageNumber(), this._currentPage() + 1));
333
340
  }
334
341
  goToFirst() {
335
342
  this.currentPage.set(1);
@@ -355,12 +362,12 @@ class HlmNumberedPagination {
355
362
  </li>
356
363
  }
357
364
 
358
- @for (page of _pages(); track page) {
365
+ @for (page of _pages(); track $index) {
359
366
  <li hlmPaginationItem>
360
367
  @if (page === '...') {
361
368
  <hlm-pagination-ellipsis />
362
369
  } @else {
363
- <a hlmPaginationLink [isActive]="currentPage() === page" (click)="currentPage.set(page)">
370
+ <a hlmPaginationLink [isActive]="_currentPage() === page" (click)="currentPage.set(page)">
364
371
  {{ page }}
365
372
  </a>
366
373
  }
@@ -423,12 +430,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
423
430
  </li>
424
431
  }
425
432
 
426
- @for (page of _pages(); track page) {
433
+ @for (page of _pages(); track $index) {
427
434
  <li hlmPaginationItem>
428
435
  @if (page === '...') {
429
436
  <hlm-pagination-ellipsis />
430
437
  } @else {
431
- <a hlmPaginationLink [isActive]="currentPage() === page" (click)="currentPage.set(page)">
438
+ <a hlmPaginationLink [isActive]="_currentPage() === page" (click)="currentPage.set(page)">
432
439
  {{ page }}
433
440
  </a>
434
441
  }
@@ -459,35 +466,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
459
466
  </div>
460
467
  `,
461
468
  }]
462
- }], propDecorators: { currentPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentPage", required: true }] }, { type: i0.Output, args: ["currentPageChange"] }], itemsPerPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemsPerPage", required: true }] }, { type: i0.Output, args: ["itemsPerPageChange"] }], totalItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalItems", required: true }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], showEdges: [{ type: i0.Input, args: [{ isSignal: true, alias: "showEdges", required: false }] }], pageSizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizes", required: false }] }] } });
469
+ }], ctorParameters: () => [], propDecorators: { currentPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentPage", required: true }] }, { type: i0.Output, args: ["currentPageChange"] }], itemsPerPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemsPerPage", required: true }] }, { type: i0.Output, args: ["itemsPerPageChange"] }], totalItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalItems", required: true }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], showEdges: [{ type: i0.Input, args: [{ isSignal: true, alias: "showEdges", required: false }] }], pageSizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizes", required: false }] }] } });
463
470
  /**
464
471
  * Checks that the instance.currentPage property is within bounds for the current page range.
465
472
  * If not, return a correct value for currentPage, or the current value if OK.
466
473
  *
467
- * Copied from 'ngx-pagination' package
474
+ * Empty/invalid totals or sizes use page 1. Finite pages are floored;
475
+ * non-finite pages use 1. Page counts saturate at Number.MAX_SAFE_INTEGER.
468
476
  */
469
477
  function outOfBoundCorrection(totalItems, itemsPerPage, currentPage) {
470
- const totalPages = Math.ceil(totalItems / itemsPerPage);
471
- if (totalPages < currentPage && 0 < totalPages) {
472
- return totalPages;
473
- }
474
- if (currentPage < 1) {
475
- return 1;
476
- }
477
- return currentPage;
478
+ const page = Number.isFinite(currentPage) ? Math.floor(currentPage) : 1;
479
+ return Math.max(1, Math.min(pageCount(totalItems, itemsPerPage), page));
478
480
  }
479
481
  /**
480
482
  * Returns an array of Page objects to use in the pagination controls.
481
483
  *
482
- * Copied from 'ngx-pagination' package
484
+ * Window math adapted from 'ngx-pagination'. Range is floored and bounded to
485
+ * 1–100 entries (including gaps); non-finite/non-positive ranges use 7.
486
+ * Ranges below 5 show a contiguous window containing the active page.
487
+ * Work and allocation are proportional to this bounded window, not total pages.
483
488
  */
484
489
  function createPageArray(currentPage, itemsPerPage, totalItems, paginationRange) {
485
- // paginationRange could be a string if passed from attribute, so cast to number.
486
- paginationRange = +paginationRange;
490
+ paginationRange =
491
+ Number.isFinite(paginationRange) && paginationRange > 0
492
+ ? Math.min(100, Math.max(1, Math.floor(paginationRange)))
493
+ : 7;
487
494
  const pages = [];
488
- // Return 1 as default page number
489
- // Make sense to show 1 instead of empty when there are no items
490
- const totalPages = Math.max(Math.ceil(totalItems / itemsPerPage), 1);
495
+ const totalPages = pageCount(totalItems, itemsPerPage);
496
+ currentPage = outOfBoundCorrection(totalItems, itemsPerPage, currentPage);
497
+ if (paginationRange < 5) {
498
+ const length = Math.min(totalPages, paginationRange);
499
+ const start = Math.max(1, Math.min(currentPage - Math.floor(length / 2), totalPages - length + 1));
500
+ return Array.from({ length }, (_, index) => start + index);
501
+ }
491
502
  const halfWay = Math.ceil(paginationRange / 2);
492
503
  const isStart = currentPage <= halfWay;
493
504
  const isEnd = totalPages - halfWay < currentPage;
@@ -538,12 +549,14 @@ function calculatePageNumber(i, currentPage, paginationRange, totalPages) {
538
549
 
539
550
  class HlmNumberedPaginationQueryParams {
540
551
  /**
541
- * The current (active) page.
552
+ * The current page, floored and clamped; corrections emit currentPageChange.
553
+ * The parent owns reading/synchronizing the URL; corrections do not navigate.
542
554
  */
543
555
  currentPage = model.required(/* @ts-ignore */
544
556
  ...(ngDevMode ? [{ debugName: "currentPage" }] : /* istanbul ignore next */ []));
545
557
  /**
546
- * The number of items per paginated page.
558
+ * The number of items per paginated page. Non-finite/non-positive sizes
559
+ * disable paging (one page) without rewriting this model.
547
560
  */
548
561
  itemsPerPage = model.required(/* @ts-ignore */
549
562
  ...(ngDevMode ? [{ debugName: "itemsPerPage" }] : /* istanbul ignore next */ []));
@@ -560,7 +573,8 @@ class HlmNumberedPaginationQueryParams {
560
573
  link = input('.', /* @ts-ignore */
561
574
  ...(ngDevMode ? [{ debugName: "link" }] : /* istanbul ignore next */ []));
562
575
  /**
563
- * The number of page links to show.
576
+ * Maximum window entries, including ellipses: floored and bounded to 1–100.
577
+ * Non-finite/non-positive values use the default of 7.
564
578
  */
565
579
  maxSize = input(7, { ...(ngDevMode ? { debugName: "maxSize" } : /* istanbul ignore next */ {}), transform: numberAttribute });
566
580
  /**
@@ -580,30 +594,24 @@ class HlmNumberedPaginationQueryParams {
580
594
  : [...pageSizes, this.itemsPerPage()].sort((a, b) => a - b); // otherwise, add current page size and sort the array
581
595
  }, /* @ts-ignore */
582
596
  ...(ngDevMode ? [{ debugName: "_pageSizesWithCurrent" }] : /* istanbul ignore next */ []));
583
- _isFirstPageActive = computed(() => this.currentPage() === 1, /* @ts-ignore */
597
+ _currentPage = computed(() => outOfBoundCorrection(this.totalItems(), this.itemsPerPage(), this.currentPage()), /* @ts-ignore */
598
+ ...(ngDevMode ? [{ debugName: "_currentPage" }] : /* istanbul ignore next */ []));
599
+ _isFirstPageActive = computed(() => this._currentPage() === 1, /* @ts-ignore */
584
600
  ...(ngDevMode ? [{ debugName: "_isFirstPageActive" }] : /* istanbul ignore next */ []));
585
- _isLastPageActive = computed(() => this.currentPage() === this._lastPageNumber(), /* @ts-ignore */
601
+ _isLastPageActive = computed(() => this._currentPage() === this._lastPageNumber(), /* @ts-ignore */
586
602
  ...(ngDevMode ? [{ debugName: "_isLastPageActive" }] : /* istanbul ignore next */ []));
587
- _lastPageNumber = computed(() => {
588
- if (this.totalItems() < 1) {
589
- // when there are 0 or fewer (an error case) items, there are no "pages" as such,
590
- // but it makes sense to consider a single, empty page as the last page.
591
- return 1;
592
- }
593
- return Math.ceil(this.totalItems() / this.itemsPerPage());
594
- }, /* @ts-ignore */
603
+ _lastPageNumber = computed(() => pageCount(this.totalItems(), this.itemsPerPage()), /* @ts-ignore */
595
604
  ...(ngDevMode ? [{ debugName: "_lastPageNumber" }] : /* istanbul ignore next */ []));
596
- _pages = computed(() => {
597
- const correctedCurrentPage = outOfBoundCorrection(this.totalItems(), this.itemsPerPage(), this.currentPage());
598
- if (correctedCurrentPage !== this.currentPage()) {
599
- // update the current page
600
- untracked(() => this.currentPage.set(correctedCurrentPage));
601
- }
602
- return createPageArray(correctedCurrentPage, this.itemsPerPage(), this.totalItems(), this.maxSize());
603
- }, /* @ts-ignore */
605
+ _pages = computed(() => createPageArray(this._currentPage(), this.itemsPerPage(), this.totalItems(), this.maxSize()), /* @ts-ignore */
604
606
  ...(ngDevMode ? [{ debugName: "_pages" }] : /* istanbul ignore next */ []));
605
607
  constructor() {
606
608
  classes(() => 'tw:flex tw:items-center tw:justify-between tw:gap-2 tw:px-4 tw:py-2');
609
+ effect(() => {
610
+ const page = this._currentPage();
611
+ if (page !== this.currentPage()) {
612
+ untracked(() => this.currentPage.set(page));
613
+ }
614
+ });
607
615
  }
608
616
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmNumberedPaginationQueryParams, deps: [], target: i0.ɵɵFactoryTarget.Component });
609
617
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: HlmNumberedPaginationQueryParams, isStandalone: true, selector: "hlm-numbered-pagination-query-params", inputs: { currentPage: { classPropertyName: "currentPage", publicName: "currentPage", isSignal: true, isRequired: true, transformFunction: null }, itemsPerPage: { classPropertyName: "itemsPerPage", publicName: "itemsPerPage", isSignal: true, isRequired: true, transformFunction: null }, totalItems: { classPropertyName: "totalItems", publicName: "totalItems", isSignal: true, isRequired: true, transformFunction: null }, link: { classPropertyName: "link", publicName: "link", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, showEdges: { classPropertyName: "showEdges", publicName: "showEdges", isSignal: true, isRequired: false, transformFunction: null }, pageSizes: { classPropertyName: "pageSizes", publicName: "pageSizes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentPage: "currentPageChange", itemsPerPage: "itemsPerPageChange" }, ngImport: i0, template: `
@@ -620,23 +628,23 @@ class HlmNumberedPaginationQueryParams {
620
628
  <li hlmPaginationItem>
621
629
  <hlm-pagination-previous
622
630
  [link]="link()"
623
- [queryParams]="{ page: currentPage() - 1 }"
631
+ [queryParams]="{ page: _currentPage() - 1 }"
624
632
  queryParamsHandling="merge"
625
633
  />
626
634
  </li>
627
635
  }
628
636
 
629
- @for (page of _pages(); track page) {
637
+ @for (page of _pages(); track $index) {
630
638
  <li hlmPaginationItem>
631
639
  @if (page === '...') {
632
640
  <hlm-pagination-ellipsis />
633
641
  } @else {
634
642
  <a
635
643
  hlmPaginationLink
636
- [link]="currentPage() !== page ? link() : undefined"
644
+ [link]="_currentPage() !== page ? link() : undefined"
637
645
  [queryParams]="{ page }"
638
646
  queryParamsHandling="merge"
639
- [isActive]="currentPage() === page"
647
+ [isActive]="_currentPage() === page"
640
648
  >
641
649
  {{ page }}
642
650
  </a>
@@ -648,7 +656,7 @@ class HlmNumberedPaginationQueryParams {
648
656
  <li hlmPaginationItem>
649
657
  <hlm-pagination-next
650
658
  [link]="link()"
651
- [queryParams]="{ page: currentPage() + 1 }"
659
+ [queryParams]="{ page: _currentPage() + 1 }"
652
660
  queryParamsHandling="merge"
653
661
  />
654
662
  </li>
@@ -700,23 +708,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
700
708
  <li hlmPaginationItem>
701
709
  <hlm-pagination-previous
702
710
  [link]="link()"
703
- [queryParams]="{ page: currentPage() - 1 }"
711
+ [queryParams]="{ page: _currentPage() - 1 }"
704
712
  queryParamsHandling="merge"
705
713
  />
706
714
  </li>
707
715
  }
708
716
 
709
- @for (page of _pages(); track page) {
717
+ @for (page of _pages(); track $index) {
710
718
  <li hlmPaginationItem>
711
719
  @if (page === '...') {
712
720
  <hlm-pagination-ellipsis />
713
721
  } @else {
714
722
  <a
715
723
  hlmPaginationLink
716
- [link]="currentPage() !== page ? link() : undefined"
724
+ [link]="_currentPage() !== page ? link() : undefined"
717
725
  [queryParams]="{ page }"
718
726
  queryParamsHandling="merge"
719
- [isActive]="currentPage() === page"
727
+ [isActive]="_currentPage() === page"
720
728
  >
721
729
  {{ page }}
722
730
  </a>
@@ -728,7 +736,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
728
736
  <li hlmPaginationItem>
729
737
  <hlm-pagination-next
730
738
  [link]="link()"
731
- [queryParams]="{ page: currentPage() + 1 }"
739
+ [queryParams]="{ page: _currentPage() + 1 }"
732
740
  queryParamsHandling="merge"
733
741
  />
734
742
  </li>
@@ -5,11 +5,13 @@ import { ClassValue } from 'clsx';
5
5
 
6
6
  declare class HlmNumberedPagination {
7
7
  /**
8
- * The current (active) page.
8
+ * The current (active) page, floored and clamped to the available range.
9
+ * Corrections emit currentPageChange once; empty/invalid paging uses page 1.
9
10
  */
10
11
  readonly currentPage: _angular_core.ModelSignal<number>;
11
12
  /**
12
- * The number of items per paginated page.
13
+ * The number of items per paginated page. Non-finite/non-positive sizes
14
+ * disable paging (one page) without rewriting this model.
13
15
  */
14
16
  readonly itemsPerPage: _angular_core.ModelSignal<number>;
15
17
  /**
@@ -19,7 +21,8 @@ declare class HlmNumberedPagination {
19
21
  */
20
22
  readonly totalItems: _angular_core.InputSignalWithTransform<number, NumberInput>;
21
23
  /**
22
- * The number of page links to show.
24
+ * Maximum window entries, including ellipses: floored and bounded to 1–100.
25
+ * Non-finite/non-positive values use the default of 7.
23
26
  */
24
27
  readonly maxSize: _angular_core.InputSignalWithTransform<number, NumberInput>;
25
28
  /**
@@ -32,10 +35,12 @@ declare class HlmNumberedPagination {
32
35
  */
33
36
  readonly pageSizes: _angular_core.InputSignal<number[]>;
34
37
  protected readonly _pageSizesWithCurrent: _angular_core.Signal<number[]>;
38
+ protected readonly _currentPage: _angular_core.Signal<number>;
35
39
  protected readonly _isFirstPageActive: _angular_core.Signal<boolean>;
36
40
  protected readonly _isLastPageActive: _angular_core.Signal<boolean>;
37
41
  protected readonly _lastPageNumber: _angular_core.Signal<number>;
38
42
  protected readonly _pages: _angular_core.Signal<Page[]>;
43
+ constructor();
39
44
  protected goToPrevious(): void;
40
45
  protected goToNext(): void;
41
46
  protected goToFirst(): void;
@@ -48,23 +53,29 @@ type Page = number | '...';
48
53
  * Checks that the instance.currentPage property is within bounds for the current page range.
49
54
  * If not, return a correct value for currentPage, or the current value if OK.
50
55
  *
51
- * Copied from 'ngx-pagination' package
56
+ * Empty/invalid totals or sizes use page 1. Finite pages are floored;
57
+ * non-finite pages use 1. Page counts saturate at Number.MAX_SAFE_INTEGER.
52
58
  */
53
59
  declare function outOfBoundCorrection(totalItems: number, itemsPerPage: number, currentPage: number): number;
54
60
  /**
55
61
  * Returns an array of Page objects to use in the pagination controls.
56
62
  *
57
- * Copied from 'ngx-pagination' package
63
+ * Window math adapted from 'ngx-pagination'. Range is floored and bounded to
64
+ * 1–100 entries (including gaps); non-finite/non-positive ranges use 7.
65
+ * Ranges below 5 show a contiguous window containing the active page.
66
+ * Work and allocation are proportional to this bounded window, not total pages.
58
67
  */
59
68
  declare function createPageArray(currentPage: number, itemsPerPage: number, totalItems: number, paginationRange: number): Page[];
60
69
 
61
70
  declare class HlmNumberedPaginationQueryParams {
62
71
  /**
63
- * The current (active) page.
72
+ * The current page, floored and clamped; corrections emit currentPageChange.
73
+ * The parent owns reading/synchronizing the URL; corrections do not navigate.
64
74
  */
65
75
  readonly currentPage: _angular_core.ModelSignal<number>;
66
76
  /**
67
- * The number of items per paginated page.
77
+ * The number of items per paginated page. Non-finite/non-positive sizes
78
+ * disable paging (one page) without rewriting this model.
68
79
  */
69
80
  readonly itemsPerPage: _angular_core.ModelSignal<number>;
70
81
  /**
@@ -79,7 +90,8 @@ declare class HlmNumberedPaginationQueryParams {
79
90
  */
80
91
  readonly link: _angular_core.InputSignal<string>;
81
92
  /**
82
- * The number of page links to show.
93
+ * Maximum window entries, including ellipses: floored and bounded to 1–100.
94
+ * Non-finite/non-positive values use the default of 7.
83
95
  */
84
96
  readonly maxSize: _angular_core.InputSignalWithTransform<number, NumberInput>;
85
97
  /**
@@ -92,6 +104,7 @@ declare class HlmNumberedPaginationQueryParams {
92
104
  */
93
105
  readonly pageSizes: _angular_core.InputSignal<number[]>;
94
106
  protected readonly _pageSizesWithCurrent: _angular_core.Signal<number[]>;
107
+ protected readonly _currentPage: _angular_core.Signal<number>;
95
108
  protected readonly _isFirstPageActive: _angular_core.Signal<boolean>;
96
109
  protected readonly _isLastPageActive: _angular_core.Signal<boolean>;
97
110
  protected readonly _lastPageNumber: _angular_core.Signal<number>;
@@ -49,6 +49,22 @@ import { HlmPhoneInput, formatNanpPhoneNumber, type PhoneNumberFormatter } from
49
49
 
50
50
  Model values may be `string | null | undefined`; non-digit characters are stripped defensively on write. An empty model renders as empty text (no dangling mask characters) and emits `null`. With `modelFormat: 'formatted'` the model holds display text instead of digits — useful when a backend expects the masked value; validators then apply to the formatted string.
51
51
 
52
+ ### Keyboard editing
53
+
54
+ - With a collapsed caret, Backspace removes the preceding digit and Delete removes the following digit, skipping `)`, spaces, hyphens and other non-digit separators. Repeated deletion continues through the mask.
55
+ - With a selection, only selected digits are removed. A punctuation-only selection collapses to its logical digit boundary without deleting an adjacent digit or emitting a model change.
56
+ - Typing and paste replace the native selection, strip non-digits and respect `maxDigits`. The caret is restored after the preceding logical digit; clearing emits `null`. Only effective digit changes emit CVA changes, and blur marks the control touched. Programmatic writes format without emitting user changes.
57
+ - Deletion uses the native cancelable `beforeinput` event's `inputType` and original selection. Other edits retain native editing followed by `input` normalization. Read-only and disabled controls reject edits. Custom formatters should preserve digit content/order and add only non-digit separators so logical positions map correctly.
58
+
59
+ The source regression suites and real Chrome keyboard/clipboard probe can be run from the repository root:
60
+
61
+ ```bash
62
+ pnpm --dir packages/angular test:library phone-input form-phone-input
63
+ node packages/angular/test/phone-keyboard-probe.mjs
64
+ ```
65
+
66
+ The probe reuses installed Puppeteer and the shared Chrome resolver (`CHROME_BIN` or the package browser cache), compiles only an opt-in Angular test fixture, and uses Home/Arrow/Shift, Backspace/Delete, typing and Ctrl+C/Ctrl+V. Its Karma port defaults to `9877` (override with `PHONE_KEYBOARD_PORT`). It checks both primitive and wrapper controls; no published artifacts are needed.
67
+
52
68
  ### `formatNanpPhoneNumber(digits)`
53
69
 
54
70
  North-American numbering plan formatter: `4` → `(4`, `4155` → `(415) 5`, `4155552671` → `(415) 555-2671`, truncated to 10 digits. Pass a custom `PhoneNumberFormatter` for other plans (e.g. chunk-and-join national formats) together with a matching `maxDigits`.
@@ -24,7 +24,8 @@ function formatNanpPhoneNumber(digits) {
24
24
  /**
25
25
  * A masked phone input. The form model holds raw digits (`"4155552671"`);
26
26
  * the visible text is formatted (`"(415) 555-2671"`). Caret position is
27
- * preserved across mask characters while typing or deleting.
27
+ * preserved across mask characters while typing or deleting. Backspace/Delete
28
+ * skip separators in their direction; selected ranges delete only selected digits.
28
29
  */
29
30
  class HlmPhoneInput {
30
31
  static _id = 0;
@@ -53,7 +54,7 @@ class HlmPhoneInput {
53
54
  */
54
55
  modelFormat = input('digits', /* @ts-ignore */
55
56
  ...(ngDevMode ? [{ debugName: "modelFormat" }] : /* istanbul ignore next */ []));
56
- /** Display formatter. Receives raw digits, returns display text. */
57
+ /** Display formatter. Preserve digit order/content and add only non-digit separators for caret mapping. */
57
58
  formatPhoneNumber = input(formatNanpPhoneNumber, /* @ts-ignore */
58
59
  ...(ngDevMode ? [{ debugName: "formatPhoneNumber" }] : /* istanbul ignore next */ []));
59
60
  /** Extra classes merged onto the inner `<input>` (over the `HlmInput` base). */
@@ -88,11 +89,46 @@ class HlmPhoneInput {
88
89
  this._onChange?.(digits);
89
90
  }
90
91
  }
92
+ _handleBeforeInput(event) {
93
+ if (event.defaultPrevented || !event.cancelable)
94
+ return;
95
+ if (this.disabledState() || this.readonly()) {
96
+ event.preventDefault();
97
+ return;
98
+ }
99
+ if (event.inputType !== 'deleteContentBackward' && event.inputType !== 'deleteContentForward')
100
+ return;
101
+ // Input events contain the already-edited value. Only beforeinput retains
102
+ // both the browser's deletion intent and the original selected range.
103
+ const element = event.target;
104
+ const start = element.selectionStart;
105
+ const end = element.selectionEnd;
106
+ if (start === null || end === null)
107
+ return;
108
+ const digits = element.value.replace(/\D/g, '');
109
+ let from = element.value.slice(0, start).replace(/\D/g, '').length;
110
+ let to = element.value.slice(0, end).replace(/\D/g, '').length;
111
+ if (start === end) {
112
+ if (event.inputType === 'deleteContentBackward')
113
+ from = Math.max(0, from - 1);
114
+ else
115
+ to = Math.min(digits.length, to + 1);
116
+ }
117
+ event.preventDefault();
118
+ this._renderEdit(element, digits.slice(0, from) + digits.slice(to), from);
119
+ }
91
120
  _handleInput(event) {
92
121
  const element = event.target;
122
+ if (this.disabledState() || this.readonly()) {
123
+ element.value = this._display();
124
+ return;
125
+ }
93
126
  const caret = element.selectionStart ?? element.value.length;
94
127
  const digitsBeforeCaret = element.value.slice(0, caret).replace(/\D/g, '').length;
95
- const digits = element.value.replace(/\D/g, '').slice(0, Math.max(0, this.maxDigits()));
128
+ this._renderEdit(element, element.value.replace(/\D/g, ''), digitsBeforeCaret);
129
+ }
130
+ _renderEdit(element, rawDigits, digitsBeforeCaret) {
131
+ const digits = rawDigits.slice(0, Math.max(0, this.maxDigits()));
96
132
  const formatted = this.formatPhoneNumber()(digits);
97
133
  const target = Math.min(digitsBeforeCaret, digits.length);
98
134
  let position = 0;
@@ -140,6 +176,7 @@ class HlmPhoneInput {
140
176
  [forceInvalid]="forceInvalid()"
141
177
  [class]="$inputClass()"
142
178
  [value]="_display()"
179
+ (beforeinput)="_handleBeforeInput($event)"
143
180
  (input)="_handleInput($event)"
144
181
  (blur)="_handleBlur()"
145
182
  />
@@ -172,6 +209,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
172
209
  [forceInvalid]="forceInvalid()"
173
210
  [class]="$inputClass()"
174
211
  [value]="_display()"
212
+ (beforeinput)="_handleBeforeInput($event)"
175
213
  (input)="_handleInput($event)"
176
214
  (blur)="_handleBlur()"
177
215
  />
@@ -16,7 +16,8 @@ declare function formatNanpPhoneNumber(digits: string): string;
16
16
  /**
17
17
  * A masked phone input. The form model holds raw digits (`"4155552671"`);
18
18
  * the visible text is formatted (`"(415) 555-2671"`). Caret position is
19
- * preserved across mask characters while typing or deleting.
19
+ * preserved across mask characters while typing or deleting. Backspace/Delete
20
+ * skip separators in their direction; selected ranges delete only selected digits.
20
21
  */
21
22
  declare class HlmPhoneInput implements ControlValueAccessor {
22
23
  private static _id;
@@ -37,7 +38,7 @@ declare class HlmPhoneInput implements ControlValueAccessor {
37
38
  * (`"(415) 555-2671"`).
38
39
  */
39
40
  readonly modelFormat: _angular_core.InputSignal<"digits" | "formatted">;
40
- /** Display formatter. Receives raw digits, returns display text. */
41
+ /** Display formatter. Preserve digit order/content and add only non-digit separators for caret mapping. */
41
42
  readonly formatPhoneNumber: _angular_core.InputSignal<PhoneNumberFormatter>;
42
43
  /** Extra classes merged onto the inner `<input>` (over the `HlmInput` base). */
43
44
  readonly userClass: _angular_core.InputSignal<ClassValue>;
@@ -52,7 +53,9 @@ declare class HlmPhoneInput implements ControlValueAccessor {
52
53
  protected _onChange?: ChangeFn<string | null>;
53
54
  protected _onTouched?: TouchFn;
54
55
  private _emitModel;
56
+ protected _handleBeforeInput(event: InputEvent): void;
55
57
  protected _handleInput(event: Event): void;
58
+ private _renderEdit;
56
59
  protected _handleBlur(): void;
57
60
  /** CONTROL VALUE ACCESSOR */
58
61
  writeValue(value: string | null | undefined): void;