Skip to main content
cngx-src documentation

CngxPaginator

ComponentPrimaryOnPushNo encapsulationv0.1.0WCAG AA

projects/ui/paginator/paginator.component.ts

Import#

import { CngxPaginator } from '@cngx/ui/paginator'

Description#

Declarative, skinnable pagination organism.
A thin shell over the CngxPaginate brain (applied as a hostDirective), assembled by the consumer from projected segment parts in DOM order - no show* config inputs. skin / density are single paint attributes reflected onto [data-skin] / [data-density]. \

Two-way [(pageIndex)] / [(pageSize)]:
the brain's controlled inputs are aliased through hostDirectives (cngxPageIndex -> pageIndex, cngxPageSize -> pageSize);
the shell owns the matching pageIndexChange / pageSizeChange outputs and is their single emitter, wired through the shared connectPaginateEmit bridge (identical to CngxIncrementalList).
The bridge feeds each output once from two paths: (1) a subscription forwards the brain's nav-only pageChange / pageSizeChange - the only signal that captures a controlled-mode setPage, because the brain's pageIndex() stays pinned to the input until the consumer feeds it back;
(2) an effect() reads the effective pageIndex() / pageSize() and covers a total-shrink clamp the nav-only output misses.
The brain output is forwarded, NOT aliased through hostDirectives:
a raw alias would bypass the shared guard and double-emit on uncontrolled navigation.

All ARIA attributes are signal-driven. aria-busy has a single owner here.

https://cngxjs.github.io/cngx/examples/#/ui/paginator/paginator-skins/numbered https://cngxjs.github.io/cngx/examples/#/ui/paginator/paginator-skins/minimal https://cngxjs.github.io/cngx/examples/#/ui/paginator/paginator-skins/pill https://cngxjs.github.io/cngx/examples/#/ui/paginator/paginator-skins/segmented https://cngxjs.github.io/cngx/examples/#/ui/paginator/paginator-skins/rail https://cngxjs.github.io/cngx/examples/#/ui/paginator/paginator-skins/dots https://cngxjs.github.io/cngx/examples/#/ui/paginator/paginator-skins/bar

Metadata#

Host#

Providers#

CNGX_PAGINATOR_HOST
useExisting CngxPaginate

Dependencies#

CNGX_PAGINATOR_ANNOUNCER_FACTORYinject()announcer

Live-region message source - mounted onto the cngxLiveRegion span in the template. Built through the swap token so a consumer can wrap the busy / settle / page-change derivation without forking the shell.

CngxPaginateinject()paginate

Relationships

Index#

Inputs#

ariaLabel#string | undefined
input()

Accessible name for the navigation landmark. Overrides the config default.

default undefined, { alias: 'aria-label' }
input()

Private size preset (hit-target + type); reflected onto [data-paginator-size], NOT the global [data-density]. Spacing is a global concern derived from --cngx-space-*, so a root [data-density] compacts this paginator's gaps like every other component; this input keeps only the touch-target sizing the global scale does not yet cover.

default 'default'
resetOn#unknown
input()

Reset key. When its value changes (after the initial render) the paginator jumps to the first page - bind the sort / filter / search value a result set depends on so a narrowed result never strands the user on a now-empty page. Bind a primitive or a computed; an inline array / object literal recomputes every change-detection pass and would reset on each. Shares the connectPaginateResetOn implementation with [cngxPaginateResetOn].

responsive#
input()

Opt into the responsive collapse; reflected onto [data-responsive]. When set, a @container rule swaps the projected cngx-pgn-pages number row for a cngx-pgn-status "Page n of m" readout once the paginator's own container narrows past the collapse breakpoint. Compose both segments for it to apply.

default false, { transform: booleanAttribute }
input()

Visual skin; reflected onto [data-skin].

default 'numbered'

Outputs#

pageIndexChange#number
output()

Emits the effective page index on every change - navigation or total-shrink clamp.

pageSizeChange#number
output()

Emits the effective page size on every change.

Instance Properties#

config#unknown
ProtectedReadonly
injectPaginatorConfig()
loadingSlot#unknown
contentChild()ProtectedReadonly
contentChild(CngxPaginatorLoading, { read: TemplateRef })

HostBindings#

BindingExpression
[attr.aria-label]resolvedAriaLabel()
[attr.aria-busy]paginate.isBusy()
[attr.data-skin]skin()
[attr.data-paginator-size]density()
[attr.data-responsive]responsive() ? '' : null

Pagination: UX and Accessibility

Pagination splits a long result set into numbered pages and gives the user a control to move between them. It earns its keep when the data is too long to show at once but the user still needs a sense of where they are and how much is left - a search result, an admin table, a catalogue. Unlike infinite scroll, a paginator gives a stable position ("page 4 of 12") that survives a refresh, a back button, and a shared link.

This document is about the pattern, not the CNGX API. It explains when to reach for a paginator, how it differs from load-more and infinite scroll, how to make it keyboard- and screen-reader-complete, and the mistakes that surface once the happy path is left behind. For the component inputs and outputs, see the Info and API tabs.

When pagination is the right tool

Reach for a paginator when all of these hold:

  • The result set is large enough that showing everything at once is slow or overwhelming.
  • The user benefits from a stable position they can return to, cite, or share.
  • Jumping to a specific page (first, last, page 7) is a real use, not just stepping forward.

Typical homes: data grids and admin tables, search results, catalogues, any list backed by a paged or offset-based API.

Prefer a different pattern when:

  • The user only ever scans forward and position never matters - that is infinite scroll or a load-more button, not a paginator.
  • The set is small enough to show in full - then pagination is friction with no payoff.
  • The content is a sequential task with a start and an end - that is a stepper, not pages of the same kind of thing.

Pagination versus load-more versus infinite scroll

All three break a long list into chunks; the difference is whether position is a first-class, addressable thing.

- Pagination Load-more Infinite scroll
Position Explicit and addressable ("page 4 of 12") Implicit (how many loaded) None
Jump to a page Yes (first / last / any number) No No
Shareable / refresh-stable Yes Partly No
Best for Tables, search, catalogues Feeds where order matters but page does not Media / social streams
Footer controls stay reachable Yes Yes Often pushed off-screen

Rule of thumb: if the user would ever say "go back to page 3" or wants to know how far through they are, you want pagination. If they only ever consume forward and never cite a position, load-more or infinite scroll fits better.

Anatomy

A paginator composes from independent parts; show only the ones the context needs:

  • Page row: the numbered buttons. One carries aria-current="page". A long run truncates into an ellipsis that reveals the hidden pages.
  • Previous / next: step one page at a time. Disabled at the respective bound.
  • First / last: jump to the boundaries. Disabled at the respective bound.
  • Range readout: "21-30 of 240" - the honest count of what this page shows.
  • Page-size select: how many items per page; changing it resets to the first page.
  • Go-to-page / page-of-pages: type or pick a page to jump directly.

The brain that drives all of these is content-agnostic: it computes page math from the total item count and reports the active page back. The same brain paginates a table, a card grid, or a dropdown panel.

UX guidance

  • Show the range, not just the page number. "21-30 of 240" tells the user how much there is and where they are; a bare "page 3" does not.
  • Disable boundary controls visibly, but keep them perceivable - at the first page, previous and first are inert, and that inert state must be communicated, not silent.
  • Resetting the page size returns to the first page. Jumping the user to "page 7 of a re-sized list" is disorienting; start over from the top.
  • Keep the control in a predictable place (usually the footer of the list it pages) and keep it there as the data changes.
  • Match the visual skin to context, not novelty. The skin is pure CSS - it changes nothing about structure, keyboard, or ARIA - so pick the one that reads best and move on.
  • Plan the empty and loading states. A paginator over zero results should not show a broken "page 1 of 0"; a paginator over slow data should gate navigation, not let clicks queue.

Communicating state

Every change the paginator makes is communicated on three channels at once - visual, semantic, and to assistive tech - never by one alone.

State What changes How it is communicated
Current page The active page button aria-current="page" on the active button plus a live-region "Page N of M" announcement
At a boundary First/previous or next/last become inert aria-disabled on the button (it stays focusable so the bound state is heard), not a removed control
Busy / loading Navigation is gated aria-busy on the landmark, an indeterminate progress bar, and a "Loading" / "Updated" announcement
Range The first-last-of-total readout Live text derived from the same page math, so it never drifts from the buttons

A total-shrink that clamps the effective page (the data got smaller while the user was on a high page) is never silent: the clamped page is both reported back to the consumer and announced, so the user is not stranded on a page that no longer exists.

Accessibility

A11y is not a later audit pass; it is part of the design from the first implementation. Build on semantic HTML first (native <button>s, a named nav landmark) and add ARIA only where the semantics fall short.

Keyboard

The whole paginator must be operable from the keyboard alone.

Key Action
Tab Move into the page row (lands on the active page), then on to the next control
Arrow keys Move the active page button along the row
Home / End Jump to the first / last page button in the row
Enter / Space Activate the focused page or control

Notes that matter in practice:

  • The page row is one tab stop (roving tabindex): Tab lands on the active page and the next Tab moves on, rather than walking through every number.
  • Previous / next / first / last stay in normal tab order as ordinary buttons.
  • A button at its bound reports aria-disabled but keeps its place in the tab order, so a screen-reader user hears that the boundary is reached instead of finding the control gone.
  • While the bound async state is busy, navigation is a no-op - a page click cannot race an in-flight load.

Screen reader

  • Give the paginator an accessible name (it is a nav landmark) so it is not just an unnamed region in the page outline.
  • Each control carries an explicit accessible name ("Next page", "Go to page", "Page 4"), so the purpose is heard, not inferred from a glyph.
  • Announce page changes, the clamp, and async transitions through a live region rather than relying on a visual change a screen-reader user cannot see.
  • Keep the live region present in the DOM and toggle its content, rather than adding and removing it, so assistive tech has something stable to observe.
  • All accessible-name and announcement strings are English by default and fully overridable, so the control localises with the rest of the app.

Visual

  • Never encode the current page, a boundary, or a busy state in colour alone; pair it with a shape, weight, or text.
  • Keep the focus ring visible and high-contrast, and readable at 200% zoom.
  • Keep touch targets comfortable for coarse pointers; a cramped page row is hard to hit on mobile, where the control should bump its hit-targets automatically.

Async and loading

A paginator backed by a remote, paged API has to handle the gap between a click and the data arriving. Bind the async state and the paginator becomes a consumer of it: while busy it gates navigation, shows an indeterminate progress bar, flips aria-busy, and announces the transition. The point is that a slow page never leaves the control in an ambiguous state and never lets a second click queue behind the first.

Skins and density

The skin (numbered, minimal, pill, segmented, rail, dots, bar) changes only the CSS layer via a host attribute; structure, ARIA, and keyboard behaviour are identical across every skin, so the choice is purely cosmetic and never an accessibility decision. Density and the responsive collapse are likewise device- and context-driven, not separate behaviours.

Mobile and responsive

  • Keep the control stable across breakpoints; the active page must never get hidden by a layout rearrangement.
  • Let a long page row truncate into the ellipsis menu rather than wrapping into an unreadable grid of numbers.
  • On coarse pointers, prefer the compact skins and let the hit-targets grow; do not ship a desktop-dense row to a phone.

Quick checklist

  • Keyboard-only: can you reach the page row, move the active page with the arrow keys, and Tab on to the next control?
  • One screen-reader pass: is the landmark named, the current page announced with its position, and are the boundary, loading, and clamp states announced?
  • Is the current page (and any boundary or busy state) communicated without relying on colour?
  • Does a long row truncate into a reachable ellipsis menu instead of overflowing?
  • Does resetting the page size return to the first page?
  • Is pagination really the right pattern here, or does the user actually want load-more, infinite scroll (forward-only), or a stepper (an ordered task)?

Structural CSS shared by every paginator skin.
Layout-only - no colors, sizes, or borders beyond what is controlled via --cngx-paginator-* custom properties.
CngxPaginator (in @cngx/ui/paginator) references this file via styleUrls; ng-packagr inlines it at publish time, so consumers of @cngx/ui/paginator do not need @cngx/common/data at runtime. Selectors are class-prefixed (.cngx-paginator__*) and apply globally under the component's encapsulation: None.

State modifiers (driven by ARIA the organism sets)

  • [aria-current="page"] - the active page button
  • [aria-disabled="true"] - dimmed, cursor: not-allowed (bound nav buttons / busy)
  • :focus-visible - focus ring via --cngx-paginator-focus-ring

Size preset - [data-paginator-size="compact" | "comfortable"] (the private [density] input) shifts the button hit-target + font; the gap tokens derive from the global --cngx-space-* scale, so a root [data-density] compacts the paginator with every other component. The default size needs no rule.

Slots

  • .cngx-paginator - the flex row host
  • .cngx-paginator__segment - one projected segment wrapper
  • .cngx-paginator__pages - the roving page-number row
  • .cngx-paginator__button - any clickable cell (nav, page, ellipsis)
  • .cngx-paginator__page / __nav / __more - the three button kinds

Thematic tokens + the numbered, pill, segmented, minimal, rail, and bar skins for CngxPaginator. Structural layout lives in paginator-base.css (@cngx/common/data); this file only paints. Every token is registered with inherits: true so a value set on the host (skin block, density, consumer override) cascades to the descendant buttons and the popover rows - inherits: false would strand the descendant on the initial value. The Popover group themes the panel shared by the items-per-page / page-of-pages listbox segments and the ellipsis overflow menu.

Cross-skin layers (orthogonal to [data-skin]): hover / press scale + focus halo + a busy-driven page-row fade (all reduced-motion-gated), a coarse- pointer hit-target bump, a @container collapse of the first/last conveniences on narrow widths, and an RTL chevron mirror. Density (compact / comfortable) is owned by paginator-base.css.

Index#

Layout

--cngx-paginator-gap#*
Default value 0.25rem

Gap between adjacent cells.

--cngx-paginator-row-gap#<length>
Default value 0.75rem

Gap between adjacent segment rows on the host. Split from --cngx-paginator-gap (the inter-page gap) so the projected segments breathe while the page numerals stay tight.

--cngx-paginator-font-size#<length>
Default value 0.875rem

Host font size. Cascades to every numeral and label; density shifts it. The font stack stays an unregistered custom property - no @property syntax token describes a font-family list.

--cngx-paginator-button-size#<length>
Default value 2.25rem

Hit-target size (min-width and height) of every paginator button.

--cngx-paginator-button-radius#<length>
Default value 0.5rem

Corner radius of a paginator button.

Surface

--cngx-paginator-button-color#*
Default value currentColor

Resting button text colour.

--cngx-paginator-button-bg#*
Default value transparent

Resting button background.

--cngx-paginator-button-hover-bg#*
Default value color-mix(in oklch, currentColor 10%, transparent)

Hover background of a button.

--cngx-paginator-button-border-color#<color>
Default value transparent

Border colour of a resting button.

State / Active

--cngx-paginator-current-bg#<color>
Default value oklch(0.66 0.19 50)

Background of the active page button. Delegates to --cngx-color-primary.

See: [[--cngx-color-primary]]

--cngx-paginator-current-color#<color>
Default value oklch(1 0 0)

Text colour of the active page button (reads against the primary fill).

--cngx-paginator-current-border-color#<color>
Default value transparent

Border colour of the active page button.

State / Focus

--cngx-paginator-focus-ring#*
Default value 2px solid currentColor

Focus-ring outline shorthand.

--cngx-paginator-focus-halo#*
Default value 0 0 0 4px color-mix(in oklch, currentColor 18%, transparent)

Soft halo painted behind the focus-visible outline.

State / Disabled

--cngx-paginator-disabled-opacity#<number>
Default value 0.45

Opacity multiplier applied to a disabled (bound or busy) button.

Skin

--cngx-paginator-track-bg#<color>
Default value oklch(0.96 0 0)

Muted track the page row sits in under the segmented skin. Derived from surface + border so it tracks the foundation light / dark scheme.

--cngx-paginator-active-shadow#*
Default value 0 1px 2px oklch(0 0 0 / 0.06)

Drop shadow that raises the active page off the track under the segmented skin.

--cngx-paginator-indicator-thickness#<length>
Default value 2px

Thickness of the numbered skin's active-page underline bar.

--cngx-paginator-rail-color#<color>
Deprecated
Default value oklch(0.88 0.005 250)

Colour of the hairline rail the page row rode under the old rail skin.

Superseded by the composed-progress rail. The rail skin now renders cngx-pgn-rail (a CngxProgress track + fill plus an overlaid knob), so this token has no reader; theme the track/fill through the CngxProgress bridge and the knob via --cngx-paginator-rail-knob-*. Kept registered for back-compat; slated for removal at the next breaking bump.

--cngx-paginator-rail-thickness#<length>
Deprecated
Default value 2px

Thickness of the old rail skin's underline + position marker.

Superseded by the composed-progress rail (see [[--cngx-paginator-rail-color]]); the rail height is the CngxProgress track height (--cngx-paginator-rail-height). No reader; kept for back-compat, slated for removal at the next breaking bump.

--cngx-paginator-rail-knob-size#<length>
Default value 0.875rem

Diameter of the rail skin's position knob (the dot riding the fill edge).

--cngx-paginator-rail-knob-bg#<color>
Default value oklch(1 0 0)

Fill of the rail skin's position knob. Defaults to the foundation surface.

See: [[--cngx-color-surface]]

--cngx-paginator-rail-knob-border-color#<color>
Default value oklch(0.66 0.19 50)

Ring colour of the rail skin's position knob. Defaults to the primary.

See: [[--cngx-color-primary]]

--cngx-paginator-bar-border-color#<color>
Default value oklch(0.88 0.005 250)

Border + divider colour of the enclosed bar skin.

--cngx-paginator-bar-radius#<length>
Default value 0.375rem

Corner radius of the enclosed bar skin's outer box.

--cngx-paginator-dot-size#<length>
Default value 0.5rem

Diameter of a resting (full-size) dot in the dots indicator.

--cngx-paginator-dot-gap#<length>
Default value 0.5rem

Gap between adjacent dots.

--cngx-paginator-dot-color#<color>
Default value oklch(0.7 0 0 / 0.35)

Fill of an inactive dot.

--cngx-paginator-dot-current-color#<color>
Default value oklch(0.66 0.19 50)

Fill of the active dot. Delegates to --cngx-color-primary.

See: [[--cngx-color-primary]]

Motion

--cngx-paginator-hover-scale#<number>
Default value 1

Scale a button grows to on hover. Defaults to 1 (no grow) - the resting hover is the background tint; opt into a grow by raising this. Flattened to 1 under reduced motion.

--cngx-paginator-press-scale#<number>
Default value 1

Scale a button grows to while pressed. Defaults to 1 (no pop); opt in by raising this. Flattened to 1 under reduced motion.

--cngx-paginator-motion-duration#<time>
Default value 200ms

Duration of the hover / press / focus transitions.

--cngx-paginator-fade-duration#<time>
Default value 250ms

Duration of the page-row crossfade while the host reports busy.

State / Busy

--cngx-paginator-busy-opacity#<number>
Default value 0.6

Opacity the page row fades to while the host reports busy.

Popover

--cngx-paginator-panel-bg#*
Default value Canvas

Surface of the popover panel shared by the items-per-page / page-of-pages listbox segments and the ellipsis overflow menu.

--cngx-paginator-panel-color#*
Default value currentColor

Text colour inside the popover panel.

--cngx-paginator-panel-border-color#*
Default value color-mix(in oklch, currentColor 20%, transparent)

Border colour of the popover panel.

--cngx-paginator-panel-radius#<length>
Default value 0.5rem

Corner radius of the popover panel.

--cngx-paginator-panel-padding#*
Default value 0.25rem

Inner padding of the popover panel.

--cngx-paginator-panel-shadow#*
Default value 0 4px 12px rgb(0 0 0 / 0.15)

Drop shadow lifting the popover panel off the page.

--cngx-paginator-option-size#<length>
Default value 2rem

Minimum height of a popover row (page-size option / hidden-page item).

--cngx-paginator-option-padding#*
Default value 0 0.75rem

Inner padding of a popover row.

--cngx-paginator-option-radius#<length>
Default value 0.25rem

Corner radius of a popover row.

--cngx-paginator-option-hover-bg#*
Default value color-mix(in oklch, currentColor 10%, transparent)

Background of a hovered / keyboard-highlighted popover row. Shared by the listbox segments and the ellipsis overflow menu.

--cngx-paginator-option-selected-bg#*
Default value color-mix(in oklch, var(--cngx-color-primary, oklch(0.66 0.19 50)) 14%, transparent)

Background of the selected page-size row in the items-per-page panel.

--cngx-paginator-option-selected-color#*
Default value var(--cngx-color-primary, oklch(0.66 0.19 50))

Text colour of the selected page-size row (reads as accent against the tint).

--cngx-paginator-overflow-columns#<integer>
Default value 5

Column count of the ellipsis overflow grid panel.

--cngx-paginator-overflow-cell-size#<length>
Default value 2rem

Cell size (column width + row min-height) of the ellipsis overflow grid.

Material theme coverage across all skins

import { DOCUMENT } from '@angular/common';
import {
  ChangeDetectionStrategy,
  Component,
  ViewEncapsulation,
  computed,
  inject,
  signal,
} from '@angular/core';

import {
  CngxPaginator,
  CngxPaginatorDots,
  CngxPaginatorFirst,
  CngxPaginatorLast,
  CngxPaginatorNext,
  CngxPaginatorPageSize,
  CngxPaginatorPages,
  CngxPaginatorPrev,
  CngxPaginatorRail,
  CngxPaginatorRange,
  CngxPaginatorStatus,
} from '@cngx/ui/paginator';

/**
 * Every `CngxPaginator` skin rendered against a Material 3 palette, all seven
 * driven by one shared page state.
 *
 * `skin` is paint-only: the seven paginators differ by a single `[data-skin]`
 * attribute, yet they share the SAME `pageIndex` / `pageSize` signals - so
 * paging any one row (clicking a page number, the dots, prev / next, or the
 * bar's items-per-page) moves every other row in lockstep. The two halves are
 * wired explicitly (`[pageIndex]` + `(pageIndexChange)`) rather than via the
 * `[(pageIndex)]` sugar: the input is aliased onto the `CngxPaginate` host
 * directive while the matching output lives on the `CngxPaginator` shell, and
 * `strictTemplates` rejects a two-way box whose halves resolve to different
 * targets (NG8007). The colour comes
 * entirely from the published bridge: the example's stylesheet builds a real M3
 * theme with `mat.theme` (which emits the `--mat-sys-*` system tokens), then
 * `@cngx/themes/material/paginator-theme` maps every `--cngx-*` paginator knob
 * onto its Material counterpart:
 *
 * ```scss
 * @use '@angular/material' as mat;
 * @use '@cngx/themes/material/paginator-theme.scss' as paginator;
 *
 * $theme: mat.define-theme((color: (theme-type: light, primary: mat.$azure-palette)));
 * html {
 *   @include mat.theme($theme);
 *   @include paginator.theme($theme);
 * }
 * ```
 *
 * No per-skin colour overrides and no hand-copied tokens - each skin inherits
 * primary / surface / outline from the palette. `ViewEncapsulation.None` lets
 * the global `html` theme and the `:where(cngx-paginator)` bridge rules reach
 * every paginator (including the items-per-page / ellipsis-overflow popovers,
 * which the bridge paints as a mat-menu surface).
 *
 * Roboto is loaded at runtime: `mat.theme((typography: Roboto))` only NAMES the
 * family on `--mat-sys-*-font`, it never ships the webfont, and the playground
 * scaffold's `index.html` has no seam to add a `<link>` (Material auto-detect
 * only fires for `<mat-*>` templates, and this one is cngx-only). So the
 * constructor appends the Google Fonts `<link>` to `<head>` and tags `<body>`
 * with the `mat-typography` / `mat-app-background` classes - without the font
 * load every numeral falls back to the system sans and nothing reads as Material.
 */
@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  imports: [
    CngxPaginator,
    CngxPaginatorFirst,
    CngxPaginatorPrev,
    CngxPaginatorPages,
    CngxPaginatorNext,
    CngxPaginatorLast,
    CngxPaginatorRange,
    CngxPaginatorStatus,
    CngxPaginatorRail,
    CngxPaginatorDots,
    CngxPaginatorPageSize,
  ],
  styleUrl: './skins-coverage.component.scss',
  template: `
    <div class="coverage">
      <section>
        <h3>numbered</h3>
        <cngx-paginator
          skin="numbered"
          [total]="total"
          [pageIndex]="pageIndex()"
          (pageIndexChange)="pageIndex.set($event)"
          [pageSize]="pageSize()"
          (pageSizeChange)="pageSize.set($event)"
        >
          <cngx-pgn-first />
          <cngx-pgn-prev />
          <cngx-pgn-pages />
          <cngx-pgn-next />
          <cngx-pgn-last />
          <cngx-pgn-range />
        </cngx-paginator>
      </section>

      <section>
        <h3>minimal</h3>
        <cngx-paginator
          skin="minimal"
          data-nav-labels
          [total]="total"
          [pageIndex]="pageIndex()"
          (pageIndexChange)="pageIndex.set($event)"
          [pageSize]="pageSize()"
          (pageSizeChange)="pageSize.set($event)"
        >
          <cngx-pgn-prev />
          <cngx-pgn-status />
          <cngx-pgn-next />
        </cngx-paginator>
      </section>

      <section>
        <h3>pill</h3>
        <cngx-paginator
          skin="pill"
          [total]="total"
          [pageIndex]="pageIndex()"
          (pageIndexChange)="pageIndex.set($event)"
          [pageSize]="pageSize()"
          (pageSizeChange)="pageSize.set($event)"
        >
          <cngx-pgn-first />
          <cngx-pgn-prev />
          <cngx-pgn-pages />
          <cngx-pgn-next />
          <cngx-pgn-last />
        </cngx-paginator>
      </section>

      <section>
        <h3>segmented</h3>
        <cngx-paginator
          skin="segmented"
          [total]="total"
          [pageIndex]="pageIndex()"
          (pageIndexChange)="pageIndex.set($event)"
          [pageSize]="pageSize()"
          (pageSizeChange)="pageSize.set($event)"
        >
          <cngx-pgn-prev />
          <cngx-pgn-pages />
          <cngx-pgn-next />
        </cngx-paginator>
      </section>

      <section>
        <h3>rail</h3>
        <cngx-paginator
          skin="rail"
          [total]="total"
          [pageIndex]="pageIndex()"
          (pageIndexChange)="pageIndex.set($event)"
          [pageSize]="pageSize()"
          (pageSizeChange)="pageSize.set($event)"
        >
          <cngx-pgn-prev />
          <cngx-pgn-rail />
          <cngx-pgn-status />
          <cngx-pgn-next />
        </cngx-paginator>
      </section>

      <section>
        <h3>dots</h3>
        <cngx-paginator
          skin="dots"
          [total]="total"
          [pageIndex]="pageIndex()"
          (pageIndexChange)="pageIndex.set($event)"
          [pageSize]="pageSize()"
          (pageSizeChange)="pageSize.set($event)"
        >
          <cngx-pgn-prev />
          <cngx-pgn-dots />
          <cngx-pgn-next />
        </cngx-paginator>
      </section>

      <section>
        <h3>bar</h3>
        <cngx-paginator
          skin="bar"
          [total]="total"
          [pageIndex]="pageIndex()"
          (pageIndexChange)="pageIndex.set($event)"
          [pageSize]="pageSize()"
          (pageSizeChange)="pageSize.set($event)"
        >
          <cngx-pgn-page-size [options]="sizes" />
          <cngx-pgn-range />
          <cngx-pgn-first />
          <cngx-pgn-prev />
          <cngx-pgn-pages />
          <cngx-pgn-next />
          <cngx-pgn-last />
        </cngx-paginator>
      </section>

      <div class="coverage__toolbar">
        <span>Shared page: {{ pageIndex() + 1 }} of {{ pageCount() }}</span>
        <span>Page size: {{ pageSize() }}</span>
      </div>
    </div>
  `,
})
export class SkinsCoverageExample {
  protected readonly total = 240;
  protected readonly sizes = [10, 25, 50] as const;

  protected readonly pageIndex = signal(2);
  protected readonly pageSize = signal(10);

  protected readonly pageCount = computed(() => Math.max(1, Math.ceil(this.total / this.pageSize())));

  private readonly document = inject(DOCUMENT);

  constructor() {
    const head = this.document.head;

    const preconnect = this.document.createElement('link');
    preconnect.rel = 'preconnect';
    preconnect.href = 'https://fonts.gstatic.com';
    preconnect.crossOrigin = '';
    head.appendChild(preconnect);

    const roboto = this.document.createElement('link');
    roboto.rel = 'stylesheet';
    roboto.href = 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap';
    head.appendChild(roboto);

    this.document.body.classList.add('mat-typography', 'mat-app-background');
  }
}
export { SkinsCoverageExample as AppComponent };

Data-table footer (Material)

import { DOCUMENT } from '@angular/common';
import {
  ChangeDetectionStrategy,
  Component,
  ViewEncapsulation,
  computed,
  inject,
  signal,
} from '@angular/core';

import {
  CngxPaginator,
  CngxPaginatorFirst,
  CngxPaginatorLast,
  CngxPaginatorNext,
  CngxPaginatorPageSize,
  CngxPaginatorPages,
  CngxPaginatorPrev,
  CngxPaginatorRange,
} from '@cngx/ui/paginator';

interface Row {
  readonly id: number;
  readonly name: string;
  readonly team: string;
  readonly commits: number;
}

const TEAMS = ['Platform', 'Payments', 'Growth', 'Data', 'Mobile'] as const;
const NAMES = [
  'Ada Lovelace', 'Alan Turing', 'Grace Hopper', 'Edsger Dijkstra', 'Donald Knuth',
  'Barbara Liskov', 'Ken Thompson', 'Margaret Hamilton', 'Linus Torvalds', 'Radia Perlman',
];

/**
 * A data table whose footer is a `cngx-paginator` in the `bar` skin, Material 3
 * themed - the canonical `mat-paginator` parity layout. The page slice is a pure
 * `computed()` of `pageIndex` / `pageSize`; the table re-renders as the shared
 * signals change. Changing the items-per-page resets to the first page (brain
 * semantics) and the range readout recomputes.
 */
@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  imports: [
    CngxPaginator,
    CngxPaginatorPageSize,
    CngxPaginatorRange,
    CngxPaginatorFirst,
    CngxPaginatorPrev,
    CngxPaginatorPages,
    CngxPaginatorNext,
    CngxPaginatorLast,
  ],
  styleUrl: './data-table.component.scss',
  template: `
    <div class="table-card">
      <table class="grid">
        <thead>
          <tr>
            <th class="num">#</th>
            <th>Name</th>
            <th>Team</th>
            <th class="num">Commits</th>
          </tr>
        </thead>
        <tbody>
          @for (row of pageRows(); track row.id) {
            <tr>
              <td class="num">{{ row.id }}</td>
              <td>{{ row.name }}</td>
              <td>{{ row.team }}</td>
              <td class="num">{{ row.commits }}</td>
            </tr>
          }
        </tbody>
      </table>

      <cngx-paginator
        class="grid-footer"
        skin="bar"
        [total]="rows.length"
        [pageIndex]="pageIndex()"
        (pageIndexChange)="pageIndex.set($event)"
        [pageSize]="pageSize()"
        (pageSizeChange)="pageSize.set($event)"
      >
        <span class="grid-footer__label" aria-hidden="true">Rows per page:</span>
        <cngx-pgn-page-size [options]="sizes" />
        <cngx-pgn-range />
        <cngx-pgn-first />
        <cngx-pgn-prev />
        <cngx-pgn-pages />
        <cngx-pgn-next />
        <cngx-pgn-last />
      </cngx-paginator>
    </div>
  `,
})
export class DataTableExample {
  protected readonly sizes = [5, 10, 25] as const;
  protected readonly pageIndex = signal(0);
  protected readonly pageSize = signal(5);

  protected readonly rows: readonly Row[] = Array.from({ length: 47 }, (_, i) => ({
    id: i + 1,
    name: NAMES[i % NAMES.length],
    team: TEAMS[i % TEAMS.length],
    commits: ((i * 37) % 240) + 3,
  }));

  protected readonly pageRows = computed<readonly Row[]>(() => {
    const start = this.pageIndex() * this.pageSize();
    return this.rows.slice(start, start + this.pageSize());
  });

  private readonly document = inject(DOCUMENT);

  constructor() {
    const link = this.document.createElement('link');
    link.rel = 'stylesheet';
    link.href = 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap';
    this.document.head.appendChild(link);
    this.document.body.classList.add('mat-typography', 'mat-app-background');
  }
}
export { DataTableExample as AppComponent };

Live filter with reset-to-first-page

import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';

import {
  CngxPaginator,
  CngxPaginatorFirst,
  CngxPaginatorLast,
  CngxPaginatorNext,
  CngxPaginatorPages,
  CngxPaginatorPrev,
  CngxPaginatorRange,
} from '@cngx/ui/paginator';

const FRUITS = [
  'Apricot', 'Apple', 'Avocado', 'Banana', 'Blackberry', 'Blueberry', 'Cherry', 'Clementine',
  'Cranberry', 'Currant', 'Date', 'Dragonfruit', 'Elderberry', 'Fig', 'Gooseberry', 'Grape',
  'Grapefruit', 'Guava', 'Honeydew', 'Jackfruit', 'Kiwi', 'Kumquat', 'Lemon', 'Lime', 'Lychee',
  'Mango', 'Melon', 'Mulberry', 'Nectarine', 'Orange', 'Papaya', 'Passionfruit', 'Peach', 'Pear',
  'Persimmon', 'Pineapple', 'Plum', 'Pomegranate', 'Quince', 'Raspberry', 'Redcurrant',
  'Strawberry', 'Tangerine', 'Watermelon',
];

/**
 * A live-filtered list paged by `cngx-paginator`. The `[resetOn]` input is bound
 * to the filter term: whenever it changes the paginator jumps back to the first
 * page, so a narrowed result never strands the user on a now-empty page. Paging
 * is controlled - `pageIndex` is owned here and fed back through
 * `(pageIndexChange)`; `[total]` tracks the filtered count so the page math and
 * the range readout stay correct as the list shrinks.
 */
@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [
    CngxPaginator,
    CngxPaginatorFirst,
    CngxPaginatorPrev,
    CngxPaginatorPages,
    CngxPaginatorNext,
    CngxPaginatorLast,
    CngxPaginatorRange,
  ],
  styleUrl: './filter-reset.component.scss',
  template: `
    <div class="panel">
      <label class="field">
        <span class="field__label">Filter fruit</span>
        <input
          type="text"
          class="field__input"
          [value]="filter()"
          (input)="filter.set($any($event.target).value)"
          placeholder="Type to narrow the list"
        />
      </label>

      <p class="status">
        <strong>{{ filtered().length }}</strong> match(es) - page
        <strong>{{ pageIndex() + 1 }}</strong> of {{ pageCount() }}
      </p>

      @if (pageItems().length) {
        <ul class="list">
          @for (fruit of pageItems(); track fruit) {
            <li class="list__item">{{ fruit }}</li>
          }
        </ul>
      } @else {
        <p class="empty">No fruit matches "{{ filter() }}".</p>
      }

      <cngx-paginator
        skin="numbered"
        [total]="filtered().length"
        [resetOn]="filter()"
        [pageIndex]="pageIndex()"
        (pageIndexChange)="pageIndex.set($event)"
        [pageSize]="pageSize"
      >
        <cngx-pgn-first />
        <cngx-pgn-prev />
        <cngx-pgn-pages />
        <cngx-pgn-next />
        <cngx-pgn-last />
        <cngx-pgn-range />
      </cngx-paginator>
    </div>
  `,
})
export class FilterResetExample {
  protected readonly pageSize = 6;
  protected readonly pageIndex = signal(0);
  protected readonly filter = signal('');

  protected readonly filtered = computed<readonly string[]>(() => {
    const term = this.filter().trim().toLowerCase();
    return term ? FRUITS.filter((f) => f.toLowerCase().includes(term)) : FRUITS;
  });

  protected readonly pageItems = computed<readonly string[]>(() => {
    const start = this.pageIndex() * this.pageSize;
    return this.filtered().slice(start, start + this.pageSize);
  });

  protected readonly pageCount = computed(() =>
    Math.max(1, Math.ceil(this.filtered().length / this.pageSize)),
  );
}
export { FilterResetExample as AppComponent };

Card grid with go-to and responsive collapse

import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';

import {
  CngxPaginator,
  CngxPaginatorGoto,
  CngxPaginatorNext,
  CngxPaginatorPageSize,
  CngxPaginatorPages,
  CngxPaginatorPrev,
  CngxPaginatorRange,
  CngxPaginatorStatus,
} from '@cngx/ui/paginator';

interface Product {
  readonly id: number;
  readonly name: string;
  readonly price: number;
}

const ADJ = ['Matte', 'Glossy', 'Rugged', 'Slim', 'Pro', 'Mini', 'Max', 'Eco'];
const NOUN = ['Keyboard', 'Mouse', 'Monitor', 'Webcam', 'Dock', 'Stand', 'Hub', 'Lamp'];

/**
 * A paged card grid with a composed paginator: items-per-page, a "go to page"
 * number input, a range readout, and a responsive collapse. `[responsive]`
 * makes a `@container` query swap the projected page-number row for a compact
 * "Page n of m" status once the paginator's own container narrows - compose both
 * `cngx-pgn-pages` and `cngx-pgn-status` and the right one shows per width. The
 * visible slice is a pure `computed()` of the shared page signals.
 */
@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [
    CngxPaginator,
    CngxPaginatorPageSize,
    CngxPaginatorGoto,
    CngxPaginatorPrev,
    CngxPaginatorPages,
    CngxPaginatorStatus,
    CngxPaginatorNext,
    CngxPaginatorRange,
  ],
  styleUrl: './card-grid.component.scss',
  template: `
    <div class="wrap">
      <div class="cards">
        @for (p of pageItems(); track p.id) {
          <article class="card">
            <div class="card__thumb" aria-hidden="true">{{ p.name.charAt(0) }}</div>
            <h3 class="card__name">{{ p.name }}</h3>
            <p class="card__price">{{ '$' + p.price }}</p>
          </article>
        }
      </div>

      <cngx-paginator
        class="bar"
        skin="bar"
        [responsive]="true"
        [total]="products.length"
        [pageIndex]="pageIndex()"
        (pageIndexChange)="pageIndex.set($event)"
        [pageSize]="pageSize()"
        (pageSizeChange)="pageSize.set($event)"
      >
        <cngx-pgn-page-size [options]="sizes" />
        <cngx-pgn-range />
        <cngx-pgn-prev />
        <cngx-pgn-pages />
        <cngx-pgn-status />
        <cngx-pgn-next />
        <cngx-pgn-goto />
      </cngx-paginator>
    </div>
  `,
})
export class CardGridExample {
  protected readonly sizes = [6, 9, 12] as const;
  protected readonly pageIndex = signal(0);
  protected readonly pageSize = signal(6);

  protected readonly products: readonly Product[] = Array.from({ length: 33 }, (_, i) => ({
    id: i + 1,
    name: `${ADJ[i % ADJ.length]} ${NOUN[(i * 3) % NOUN.length]}`,
    price: ((i * 17) % 180) + 19,
  }));

  protected readonly pageItems = computed<readonly Product[]>(() => {
    const start = this.pageIndex() * this.pageSize();
    return this.products.slice(start, start + this.pageSize());
  });
}
export { CardGridExample as AppComponent };

Alphabetical bucket paging

import {
  ChangeDetectionStrategy,
  Component,
  ViewEncapsulation,
  computed,
  signal,
} from '@angular/core';

import { CngxBucketPaginate, type CngxBucket } from '@cngx/common/data';
import { CngxPaginatorAlpha } from '@cngx/ui/paginator';

interface Person {
  readonly name: string;
  readonly role: string;
  readonly location: string;
}

const PEOPLE: readonly Person[] = [
  { name: 'Ada Lovelace', role: 'Analyst', location: 'London' },
  { name: 'Brendan Eich', role: 'Architect', location: 'Mountain View' },
  { name: 'Carmen Diaz', role: 'Designer', location: 'Madrid' },
  { name: 'Dennis Ritchie', role: 'Engineer', location: 'Murray Hill' },
  { name: 'Edsger Dijkstra', role: 'Researcher', location: 'Eindhoven' },
  { name: 'Frances Allen', role: 'Compiler Lead', location: 'Peekskill' },
  { name: 'Grace Hopper', role: 'Admiral', location: 'Arlington' },
  { name: 'Hedy Lamarr', role: 'Inventor', location: 'Vienna' },
  { name: 'Ivan Sutherland', role: 'Pioneer', location: 'Portland' },
  { name: 'Joan Clarke', role: 'Cryptanalyst', location: 'Bletchley' },
  { name: 'Ken Thompson', role: 'Engineer', location: 'Berkeley' },
  { name: 'Linus Torvalds', role: 'Maintainer', location: 'Portland' },
  { name: 'Margaret Hamilton', role: 'Director', location: 'Cambridge' },
  { name: 'Radia Perlman', role: 'Engineer', location: 'Seattle' },
  { name: 'Tim Berners-Lee', role: 'Director', location: 'Geneva' },
  { name: 'Vint Cerf', role: 'Evangelist', location: 'Reston' },
];

/**
 * Alphabetical bucket paging. `cngxBucketPaginate` partitions the list into
 * letter-range buckets (A-C, D-F ...) and `cngx-pgn-alpha` renders the chip
 * strip - a sibling model to the page-index brain, not a flag on it. Selecting
 * a chip filters the list; re-selecting it clears the filter. Empty buckets are
 * disabled with a stated reason, never silently dead.
 */
@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  imports: [CngxBucketPaginate, CngxPaginatorAlpha],
  styleUrl: './alpha-mode.component.scss',
  template: `
    <div
      class="demo-card demo-pgn-alpha-stack"
      cngxBucketPaginate
      [buckets]="buckets"
      [items]="people()"
      [active]="active()"
      (activeChange)="active.set($event)"
    >
      <cngx-pgn-alpha />
      <ul class="demo-list-flush">
        @for (p of visible(); track p.name) {
          <li class="demo-list-row"><strong>{{ p.name }}</strong> - {{ p.role }}, {{ p.location }}</li>
        }
      </ul>
    </div>
  `,
})
export class AlphaModeExample {
  protected readonly people = signal<readonly Person[]>(PEOPLE);

  protected readonly buckets: readonly CngxBucket<Person>[] = [
    { label: 'A-C', match: (p: Person) => this.inRange(p, 'A', 'C') },
    { label: 'D-F', match: (p: Person) => this.inRange(p, 'D', 'F') },
    { label: 'G-I', match: (p: Person) => this.inRange(p, 'G', 'I') },
    { label: 'J-L', match: (p: Person) => this.inRange(p, 'J', 'L') },
    { label: 'M-O', match: (p: Person) => this.inRange(p, 'M', 'O') },
    { label: 'P-R', match: (p: Person) => this.inRange(p, 'P', 'R') },
    { label: 'S-U', match: (p: Person) => this.inRange(p, 'S', 'U') },
    { label: 'V-Z', match: (p: Person) => this.inRange(p, 'V', 'Z') },
  ];

  protected readonly active = signal<string | null>(null);

  protected readonly visible = computed<readonly Person[]>(() => {
    const label = this.active();
    if (label === null) {
      return this.people();
    }
    const bucket = this.buckets.find((b) => b.label === label);
    return bucket ? this.people().filter((p) => bucket.match(p)) : this.people();
  });

  private inRange(p: Person, lo: string, hi: string): boolean {
    const c = p.name[0]?.toUpperCase() ?? '';
    return c >= lo && c <= hi;
  }
}
export { AlphaModeExample as AppComponent };

Load-more (append) paging

import {
  ChangeDetectionStrategy,
  Component,
  ViewEncapsulation,
  computed,
  signal,
} from '@angular/core';

import { CngxPaginator, CngxPaginatorLoadMore } from '@cngx/ui/paginator';

interface Person {
  readonly name: string;
  readonly role: string;
  readonly location: string;
}

const PEOPLE: readonly Person[] = [
  { name: 'Ada Lovelace', role: 'Analyst', location: 'London' },
  { name: 'Brendan Eich', role: 'Architect', location: 'Mountain View' },
  { name: 'Carmen Diaz', role: 'Designer', location: 'Madrid' },
  { name: 'Dennis Ritchie', role: 'Engineer', location: 'Murray Hill' },
  { name: 'Edsger Dijkstra', role: 'Researcher', location: 'Eindhoven' },
  { name: 'Frances Allen', role: 'Compiler Lead', location: 'Peekskill' },
  { name: 'Grace Hopper', role: 'Admiral', location: 'Arlington' },
  { name: 'Hedy Lamarr', role: 'Inventor', location: 'Vienna' },
  { name: 'Ivan Sutherland', role: 'Pioneer', location: 'Portland' },
  { name: 'Joan Clarke', role: 'Cryptanalyst', location: 'Bletchley' },
  { name: 'Ken Thompson', role: 'Engineer', location: 'Berkeley' },
  { name: 'Linus Torvalds', role: 'Maintainer', location: 'Portland' },
  { name: 'Margaret Hamilton', role: 'Director', location: 'Cambridge' },
  { name: 'Radia Perlman', role: 'Engineer', location: 'Seattle' },
  { name: 'Tim Berners-Lee', role: 'Director', location: 'Geneva' },
  { name: 'Vint Cerf', role: 'Evangelist', location: 'Reston' },
];

/**
 * Load-more (append) paging over the same brain. `cngx-pgn-load-more` steps
 * `pageIndex` forward; the host slices from the top through the current page,
 * so each click reveals the next batch instead of replacing the page. On the
 * last page the button disables and switches to the all-loaded label.
 */
@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  imports: [CngxPaginator, CngxPaginatorLoadMore],
  styleUrl: './load-more-mode.component.scss',
  template: `
    <div class="demo-card">
      <ul class="demo-list-flush">
        @for (p of shownItems(); track p.name) {
          <li class="demo-list-row"><strong>{{ p.name }}</strong> - {{ p.role }}, {{ p.location }}</li>
        }
      </ul>
      <cngx-paginator
        aria-label="People"
        [total]="people().length"
        [pageIndex]="pageIndex()"
        (pageIndexChange)="pageIndex.set($event)"
        [pageSize]="pageSize()"
        (pageSizeChange)="pageSize.set($event)"
      >
        <cngx-pgn-load-more />
      </cngx-paginator>
    </div>
  `,
})
export class LoadMoreModeExample {
  protected readonly people = signal<Person[]>([
    ...PEOPLE,
    ...PEOPLE.map((p: Person) => ({ ...p, name: p.name + ' Jr.' })),
    ...PEOPLE.map((p: Person) => ({ ...p, name: p.name + ' III' })),
  ]);
  protected readonly pageIndex = signal(0);
  protected readonly pageSize = signal(5);

  // Append-style: slice from the top through the current page.
  protected readonly shownItems = computed<Person[]>(() =>
    this.people().slice(0, (this.pageIndex() + 1) * this.pageSize()),
  );
}
export { LoadMoreModeExample as AppComponent };

Infinite scroll paging

import {
  ChangeDetectionStrategy,
  Component,
  ViewEncapsulation,
  computed,
  effect,
  signal,
  untracked,
} from '@angular/core';

import { createManualState } from '@cngx/common/data';
import { CngxPaginator, CngxPaginatorInfinite } from '@cngx/ui/paginator';

interface Person {
  readonly name: string;
  readonly role: string;
  readonly location: string;
}

const PEOPLE: readonly Person[] = [
  { name: 'Ada Lovelace', role: 'Analyst', location: 'London' },
  { name: 'Brendan Eich', role: 'Architect', location: 'Mountain View' },
  { name: 'Carmen Diaz', role: 'Designer', location: 'Madrid' },
  { name: 'Dennis Ritchie', role: 'Engineer', location: 'Murray Hill' },
  { name: 'Edsger Dijkstra', role: 'Researcher', location: 'Eindhoven' },
  { name: 'Frances Allen', role: 'Compiler Lead', location: 'Peekskill' },
  { name: 'Grace Hopper', role: 'Admiral', location: 'Arlington' },
  { name: 'Hedy Lamarr', role: 'Inventor', location: 'Vienna' },
  { name: 'Ivan Sutherland', role: 'Pioneer', location: 'Portland' },
  { name: 'Joan Clarke', role: 'Cryptanalyst', location: 'Bletchley' },
  { name: 'Ken Thompson', role: 'Engineer', location: 'Berkeley' },
  { name: 'Linus Torvalds', role: 'Maintainer', location: 'Portland' },
  { name: 'Margaret Hamilton', role: 'Director', location: 'Cambridge' },
  { name: 'Radia Perlman', role: 'Engineer', location: 'Seattle' },
  { name: 'Tim Berners-Lee', role: 'Director', location: 'Geneva' },
  { name: 'Vint Cerf', role: 'Evangelist', location: 'Reston' },
];

/**
 * Infinite scroll paging over the same brain. `cngx-pgn-infinite` drops a
 * sentinel that auto-advances `pageIndex` as it enters the scroll container, so
 * the list grows as you scroll - while each batch keeps its addressable page
 * boundary (the sticky "Page n" divider). Binding `[state]` makes the busy-gate
 * pace the auto-load so the sentinel waits instead of racing through pages.
 */
@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  imports: [CngxPaginator, CngxPaginatorInfinite],
  styleUrl: './infinite-mode.component.scss',
  template: `
    <div class="demo-card">
      <div class="demo-pgn-infinite-frame">
        @for (group of revealed(); track group.page) {
          <div class="demo-page-divider">Page {{ group.page }}</div>
          <ul class="demo-list-flush">
            @for (p of group.items; track p.name) {
              <li class="demo-list-row"><strong>{{ p.name }}</strong> - {{ p.role }}, {{ p.location }}</li>
            }
          </ul>
        }
        <cngx-paginator
          aria-label="People"
          [total]="people().length"
          [state]="loadState"
          [pageIndex]="pageIndex()"
          (pageIndexChange)="pageIndex.set($event)"
          [pageSize]="pageSize()"
          (pageSizeChange)="pageSize.set($event)"
        >
          <cngx-pgn-infinite root=".demo-pgn-infinite-frame" rootMargin="0px 0px 48px 0px" />
        </cngx-paginator>
      </div>
    </div>
  `,
})
export class InfiniteModeExample {
  protected readonly people = signal<Person[]>(
    Array.from({ length: 6 }, (_, copy: number) =>
      PEOPLE.map((p: Person) => (copy === 0 ? p : { ...p, name: p.name + ' #' + (copy + 1) })),
    ).flat(),
  );
  protected readonly pageSize = signal(6);
  protected readonly pageIndex = signal(0);

  // Simulated fetch state: each advance flips busy for a beat, so the sentinel's
  // busy-gate paces the auto-load instead of racing through every page at once.
  protected readonly loadState = createManualState<void>();

  // Reveal a page only once its simulated fetch has settled - the row appearing
  // on settle is what re-arms the observer for the next scroll.
  protected readonly loadedPages = signal(1);

  protected readonly revealed = computed<{ page: number; items: Person[] }[]>(() => {
    const size = this.pageSize();
    const groups: { page: number; items: Person[] }[] = [];
    for (let page = 0; page < this.loadedPages(); page++) {
      const items = this.people().slice(page * size, (page + 1) * size);
      if (items.length) {
        groups.push({ page: page + 1, items });
      }
    }
    return groups;
  });

  constructor() {
    // Pace the auto-load: each advance flips busy for a beat so the sentinel
    // waits on the busy-gate instead of racing through every page at once.
    effect(() => {
      const wanted = this.pageIndex() + 1;
      untracked(() => {
        if (wanted <= this.loadedPages()) {
          return;
        }
        this.loadState.set('loading');
        setTimeout(() => {
          this.loadedPages.set(wanted);
          this.loadState.set('success');
        }, 500);
      });
    });
  }
}
export { InfiniteModeExample as AppComponent };