@rozie-ui/pagination-react 0.0.0-stage → 0.1.9

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,62 @@
1
+ # @rozie-ui/pagination-react
2
+
3
+ ## 0.1.9
4
+
5
+ ### Patch Changes
6
+
7
+ - 3c4b293: README fixes: Angular examples use the component's `<rozie-pagination>` selector (the PascalCase tag they showed fails to compile with NG8001); the Svelte theming import is shown as a TypeScript snippet.
8
+
9
+ ## 0.1.8
10
+
11
+ ### Patch Changes
12
+
13
+ - 05cb9db: Regenerated with the compiler fixes found while building `@rozie-ui/select`. Depending on the target and the component:
14
+ - Angular: a two-way (`model: true`) prop is now controlled while the consumer binds a value — a write by the component only emits `<name>Change`, so a consumer that refuses the change keeps the control on its value. The prop keeps its `ModelSignal` type. `$onUpdate` hooks now run (after every render); they never ran before. An output named like a native bubbling event (`change`, `submit`, `reset`, `select`) no longer also delivers the native event to a consumer's binding.
15
+ - Vue: a two-way prop bound one-way (`:value` without `v-model`) is now controlled too: the component's write emits `update:value` and the control keeps the consumer's value.
16
+ - React: an `$expose` verb now runs in `flushSync`, so calls made in one tick each see what the call before left; primitive script constants are plain `const`s; a body statement no longer reads a binding declared after it on the first render.
17
+ - Svelte, Solid: a `$watch` now sees a `$data` write made in `$onMount`.
18
+ - Lit: a `$watch` whose getter reads both props and `$data` re-runs when only the prop changes; duplicated comments in lifecycle bodies are gone.
19
+
20
+ - 05cb9db: First release of `@rozie-ui/pagination` on React, Vue, Svelte, Angular and Lit. The Solid leaf has been on npm since the 2026-09-26 wave; all six targets of the family now ship at the same version, so one version number means one component revision everywhere.
21
+
22
+ The headless, WAI-ARIA pagination control has a windowed page-item model (page numbers and ellipses with `siblingCount` / `boundaryCount`), a page count from either `totalPages` or `total` + `pageSize`, prev/next bounds, roving keyboard navigation, four scoped slots (`item`, `ellipsis`, `prevControl`, `nextControl`), a two-way `modelValue` (a `ControlValueAccessor` on Angular), a typed `change` event (`{ page }`), `goto` / `next` / `prev` / `first` / `last` handle verbs, and design-system token bridges for shadcn/ui, Material 3 and Bootstrap 5.
23
+
24
+ The Vue leaf ships a `web-types.json` and the Lit leaf a `custom-elements.json`, so JetBrains IDEs and `lit-plugin` complete the props, events and slots.
25
+
26
+ - 05cb9db: Pagination fixes found by the pre-release audit (all six targets):
27
+ - Activating a page from the keyboard (`Enter` on a page button) dropped focus to `<body>` on React, Vue, Svelte, Angular and Lit, because the list key included the item's position and every window shift re-created the button. Page numbers are now keyed by value, so focus stays on the page just chosen.
28
+ - Lit: the arrow keys always jumped to "Previous page", because the focused control was read from the document, which stops at the shadow host. It is read from the pager's own root now.
29
+ - `change` now has a declared payload, `PaginationChangePayload` (`{ page: number }`, exported from the package entry). Before, the Angular output was `unknown` (so `p.page` failed to compile) and Lit had no typed `addEventListener('change')`. A handler written against a different payload shape may now be rejected by the type checker.
30
+ - The four slot contexts and the handle are typed: `item` / `ellipsis` / `prevControl` / `nextControl` receive `page: number`, `selected: boolean`, `disabled: boolean`, `index: number` and `goto: () => void`; `goto(page: number)`, `next()`, `prev()`, `first()` and `last()` are typed on the handle (they were `any`). A call with a wrong argument is now a type error.
31
+
32
+ - 05cb9db: React and Solid: slot props passed to the component are no longer part of the attribute pass-through spread onto its root element. On Solid a slot render function passed as `<name>Slot` (or a `slots` record) was written to the DOM as an attribute holding the function source; on React the slot members (`render<Name>`, `children`, `slots`) were left in the rest bucket. Regenerated with the compiler fix; no API change.
33
+ - 05cb9db: Props types now accept the root element's HTML attributes when a component passes attributes through to a single root element (typed public surface, phase 3).
34
+ - React: `interface XProps extends Omit<React.ComponentPropsWithoutRef<'<tag>'>, …>` — `className`, `style`, `id`, `aria-*`, `data-*`, element-specific attributes (`disabled` on a `<button>` root, …) and DOM listeners typecheck without a cast.
35
+ - Solid: the same with `ComponentProps<'<tag>'>`.
36
+ - Svelte: `interface Props extends Omit<SvelteHTMLElements['<tag>'], …>`; an attribute the root element does not support is a type error.
37
+ - The component's own props win on a name collision. `children` stays rejected when the component has no default slot, as do the content-replacing `dangerouslySetInnerHTML` (React) and `innerHTML` / `innerText` / `textContent` (Solid).
38
+ - An `<svg>` root gets the SVG element's attributes (`fill`, `stroke`, `viewBox`, …). Any other non-HTML root (a custom element) gets the generic `HTMLAttributes<HTMLElement>` on React and Solid, and Svelte's permissive custom-element entry.
39
+ - Components with `inherit-attrs="false"`, an `r-if` root or a component root are unchanged. Vue, Angular and Lit are unchanged; they already accepted pass-through attributes.
40
+
41
+ The `.d.rozie.ts` sidecars (React, Solid, Svelte) carry the same types. Types only — no runtime change.
42
+
43
+ - 05cb9db: React: a consumer's DOM listener now fires alongside the component's own handler when the component's root element binds its own `@event` and passes attributes through (the default). Before, the root emitted `<el {...attrs} onClick={own}>`, and JSX's last-wins rule silently dropped the consumer's `onClick` — while Vue, Svelte, Solid and Lit already fired both. The root now emits `{...mergeListeners({ onClick: own }, pickListeners(attrs))}` after its merged `className`, so both handlers fire (own first) and the merged class is never re-applied.
44
+
45
+ `@rozie/runtime-react` gains `pickListeners`, the React port of the Solid helper: it filters the pass-through bucket down to function-valued `on*` keys before the merge.
46
+
47
+ Every React `mergeListeners(...)` events partial is now typed with `satisfies <element props> & Record<string, unknown>`, so its handlers get the same `$event` type they would have as JSX props — previously the listener-merge partial (e.g. `r-on="$listeners"` plus a local `@click` with `inherit-attrs="false"`) emitted an untyped `$event`, an implicit-`any` error under a strict consumer tsconfig.
48
+
49
+ - Updated dependencies [b52bd14]
50
+ - @rozie/runtime-react@0.10.1
51
+
52
+ ## 0.1.2
53
+
54
+ ### Patch Changes
55
+
56
+ - @rozie/runtime-react@0.2.1
57
+
58
+ ## 0.1.1
59
+
60
+ ### Patch Changes
61
+
62
+ - @rozie/runtime-react@0.2.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 One Learning Community LTD
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,112 @@
1
- # Temporary Holding Version
1
+ # @rozie-ui/pagination-react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Idiomatic **react** `Pagination` — a headless, fully-accessible (WAI-ARIA) pagination control (a windowed page-item model with ellipses, sibling/boundary windowing, prev/next bounds, roving keyboard navigation, and a `<nav>` landmark) compiled from one [Rozie](https://github.com/One-Learning-Community/rozie.js) source. It is HEADLESS: expose the page-item model and render each control yourself via the scoped slots, or accept the default token-themed buttons. Every visual value is a CSS custom property, so it re-skins to any design system. This package is generated; do not edit `src/` by hand.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i @rozie-ui/pagination-react
9
+ ```
10
+
11
+ Peer dependencies: `react + react-dom`. Install them alongside this package.
12
+
13
+ Also installed: `@rozie/runtime-react` — Rozie's small, tree-shaken runtime helper package (controllable state, keyboard navigation, event modifiers, and safe interpolation). It arrives as a regular dependency, so npm pulls it for you. Your bundler keeps only the helpers this component actually uses — typically a few hundred bytes to a few KB, minified and gzipped. [What's in it and what it costs](https://github.com/One-Learning-Community/rozie.js/blob/main/docs/guide/output-and-runtime.md).
14
+
15
+ ## Usage
16
+
17
+ ```tsx
18
+ import { useState } from 'react';
19
+ import { Pagination } from '@rozie-ui/pagination-react';
20
+
21
+ export function Demo() {
22
+ const [page, setPage] = useState(1);
23
+ return (
24
+ <Pagination
25
+ modelValue={page}
26
+ onModelValueChange={setPage}
27
+ total={195}
28
+ pageSize={10}
29
+ onChange={(e) => console.log('page:', e.page)}
30
+ />
31
+ );
32
+ }
33
+
34
+ // Headless: render your own page buttons via the renderItem render-prop (a
35
+ // self-closing element — React's scoped slots are props, not children; the
36
+ // equivalent form is slots={{ item: ... }}).
37
+ export function CustomDemo() {
38
+ const [page, setPage] = useState(1);
39
+ return (
40
+ <Pagination
41
+ modelValue={page}
42
+ onModelValueChange={setPage}
43
+ totalPages={20}
44
+ siblingCount={2}
45
+ renderItem={({ page, selected, goto }) => (
46
+ <button aria-current={selected ? 'page' : undefined} onClick={goto}>
47
+ {page}
48
+ </button>
49
+ )}
50
+ />
51
+ );
52
+ }
53
+ ```
54
+
55
+ ## Theming
56
+
57
+ Every visual value is a `--rozie-pagination-*` CSS custom property — override any of them at any ancestor scope. Ready-made design-system bridges ship in the package:
58
+
59
+ ```tsx
60
+ import '@rozie-ui/pagination-react/themes/shadcn.css'; // or material.css, bootstrap.css, base.css
61
+ ```
62
+
63
+ ## Props
64
+
65
+ | Name | Type | Default | Two-way (model) | Required | Description |
66
+ | --- | --- | --- | :---: | :---: | --- |
67
+ | `modelValue` | `Number` | `1` | ✓ | | The 1-based current page (two-way model). Clamped into `[1, totalPages]`. Bind it with `r-model:modelValue` / `v-model:modelValue` / `modelValue` + `onModelValueChange`; it is also the Angular ControlValueAccessor control value. |
68
+ | `totalPages` | `Number` | `null` | | | Explicit total page count. When provided (> 0) it takes precedence over `total` + `pageSize`. Use it when the backend already reports the page count. |
69
+ | `total` | `Number` | `null` | | | Total item count. Combined with `pageSize` to derive the page count (`ceil(total / pageSize)`) when `totalPages` is not given. |
70
+ | `pageSize` | `Number` | `null` | | | Items per page. Combined with `total` to derive the page count when `totalPages` is not given. |
71
+ | `siblingCount` | `Number` | `1` | | | Number of page buttons shown on each side of the current page (the sibling window). Larger values show more context around the current page. |
72
+ | `boundaryCount` | `Number` | `1` | | | Number of page buttons always shown at each boundary (the first and last `boundaryCount` pages), regardless of the current page. |
73
+ | `disabled` | `Boolean` | `false` | | | Disable the entire control — every page button and the prev/next controls become non-interactive and are marked `aria-disabled`. |
74
+ | `ariaLabel` | `String` | `"Pagination"` | | | Accessible name for the surrounding `<nav>` landmark (its `aria-label`). Defaults to `"Pagination"`. |
75
+
76
+ ## Events
77
+
78
+ | Event | Description |
79
+ | --- | --- |
80
+ | `change` | Fired whenever the current page changes (a page button, prev/next, or a programmatic `goto`/`next`/`prev`/`first`/`last`). Payload `{ page }` — the new clamped 1-based page. Not fired when the target page equals the current page. |
81
+
82
+ ## Imperative handle
83
+
84
+ Beyond props, the component exposes imperative methods (declared once in the Rozie source via `$expose`). Grab a handle with the native ref mechanism and call them directly:
85
+
86
+ | Method | Description |
87
+ | --- | --- |
88
+ | `goto` | Go to a specific 1-based page; the argument is clamped into `[1, totalPages]`. Emits `change` unless the target equals the current page. |
89
+ | `next` | Advance to the next page (no-op at the last page). Emits `change` on success. |
90
+ | `prev` | Go back to the previous page (no-op at the first page). Emits `change` on success. |
91
+ | `first` | Jump to the first page (page 1). Emits `change` unless already there. |
92
+ | `last` | Jump to the last page (the effective `totalPages`). Emits `change` unless already there. |
93
+
94
+ ```tsx
95
+ import { useRef } from 'react';
96
+ import { Pagination, type PaginationHandle } from '@rozie-ui/pagination-react';
97
+
98
+ const pager = useRef<PaginationHandle>(null);
99
+ // <Pagination ref={pager} ... />
100
+ pager.current?.goto(5);
101
+ pager.current?.next();
102
+ pager.current?.first();
103
+ ```
104
+
105
+ ## Slots
106
+
107
+ | Slot | Params |
108
+ | --- | --- |
109
+ | prevControl | disabled, goto, page |
110
+ | ellipsis | index |
111
+ | item | page, selected, goto |
112
+ | nextControl | disabled, goto, page |
@@ -0,0 +1,59 @@
1
+ .rozie-pagination[data-rozie-s-de247ae2] {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: var(--rozie-pagination-gap, var(--rpg-gap, 0.25rem));
5
+ font: var(--rozie-pagination-font, inherit);
6
+ }
7
+ .rozie-pagination-page[data-rozie-s-de247ae2],
8
+ .rozie-pagination-control[data-rozie-s-de247ae2] {
9
+ box-sizing: border-box;
10
+ min-width: var(--rozie-pagination-size, var(--rpg-size, 2.25rem));
11
+ height: var(--rozie-pagination-size, var(--rpg-size, 2.25rem));
12
+ padding: 0 var(--rozie-pagination-padding-x, var(--rpg-padding-x, 0.5rem));
13
+ display: inline-flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ font: inherit;
17
+ font-weight: var(--rozie-pagination-font-weight, var(--rpg-font-weight, 500));
18
+ color: var(--rozie-pagination-fg, var(--rpg-fg, #1a1a1a));
19
+ background: var(--rozie-pagination-bg, var(--rpg-bg, transparent));
20
+ border: var(--rozie-pagination-border-width, var(--rpg-border-width, 1px)) solid var(--rozie-pagination-border, var(--rpg-border, rgba(0, 0, 0, 0.18)));
21
+ border-radius: var(--rozie-pagination-radius, var(--rpg-radius, 6px));
22
+ cursor: pointer;
23
+ user-select: none;
24
+ transition: background 0.12s, border-color 0.12s, color 0.12s;
25
+ }
26
+ .rozie-pagination-page[data-rozie-s-de247ae2]:hover,
27
+ .rozie-pagination-control[data-rozie-s-de247ae2]:hover {
28
+ background: var(--rozie-pagination-hover-bg, var(--rpg-hover-bg, rgba(0, 0, 0, 0.05)));
29
+ border-color: var(--rozie-pagination-hover-border, var(--rpg-hover-border, rgba(0, 0, 0, 0.28)));
30
+ }
31
+ .rozie-pagination-page[data-rozie-s-de247ae2]:focus-visible,
32
+ .rozie-pagination-control[data-rozie-s-de247ae2]:focus-visible {
33
+ outline: var(--rozie-pagination-ring-width, var(--rpg-ring-width, 2px)) solid var(--rozie-pagination-ring, var(--rozie-pagination-accent, var(--rpg-ring, var(--rpg-accent, #0066cc))));
34
+ outline-offset: var(--rozie-pagination-ring-offset, var(--rpg-ring-offset, 1px));
35
+ }
36
+ .rozie-pagination-page.is-active[data-rozie-s-de247ae2] {
37
+ color: var(--rozie-pagination-active-fg, var(--rpg-active-fg, #fff));
38
+ background: var(--rozie-pagination-active-bg, var(--rozie-pagination-accent, var(--rpg-active-bg, var(--rpg-accent, #0066cc))));
39
+ border-color: var(--rozie-pagination-active-border, var(--rozie-pagination-accent, var(--rpg-active-border, var(--rpg-accent, #0066cc))));
40
+ }
41
+ .rozie-pagination-page[data-rozie-s-de247ae2]:disabled,
42
+ .rozie-pagination-control[data-rozie-s-de247ae2]:disabled {
43
+ cursor: not-allowed;
44
+ opacity: var(--rozie-pagination-disabled-opacity, var(--rpg-disabled-opacity, 0.5));
45
+ pointer-events: none;
46
+ }
47
+ .rozie-pagination--disabled[data-rozie-s-de247ae2] {
48
+ opacity: var(--rozie-pagination-disabled-opacity, var(--rpg-disabled-opacity, 0.5));
49
+ pointer-events: none;
50
+ }
51
+ .rozie-pagination-ellipsis[data-rozie-s-de247ae2] {
52
+ min-width: var(--rozie-pagination-size, var(--rpg-size, 2.25rem));
53
+ height: var(--rozie-pagination-size, var(--rpg-size, 2.25rem));
54
+ display: inline-flex;
55
+ align-items: center;
56
+ justify-content: center;
57
+ color: var(--rozie-pagination-ellipsis-fg, var(--rpg-ellipsis-fg, rgba(0, 0, 0, 0.5)));
58
+ user-select: none;
59
+ }
package/dist/index.cjs ADDED
@@ -0,0 +1,311 @@
1
+ Object.defineProperties(exports, {
2
+ __esModule: { value: true },
3
+ [Symbol.toStringTag]: { value: "Module" }
4
+ });
5
+ let react = require("react");
6
+ let react_dom = require("react-dom");
7
+ let _rozie_runtime_react = require("@rozie/runtime-react");
8
+ require("./Pagination.css");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ //#region src/internal/paginationItems.ts
11
+ /** Inclusive integer range [start, end]; empty when start > end. */
12
+ function range(start, end) {
13
+ const out = [];
14
+ for (let i = start; i <= end; i++) out.push(i);
15
+ return out;
16
+ }
17
+ /**
18
+ * Resolve the effective total page count from either an explicit `totalPages`
19
+ * or `ceil(total / pageSize)`. Always at least 1 (an empty data set still has a
20
+ * single page).
21
+ */
22
+ function resolveTotalPages(input) {
23
+ const tp = input.totalPages;
24
+ if (typeof tp === "number" && tp > 0) return Math.max(1, Math.floor(tp));
25
+ const total = typeof input.total === "number" ? input.total : 0;
26
+ const size = typeof input.pageSize === "number" && input.pageSize > 0 ? input.pageSize : 0;
27
+ if (total > 0 && size > 0) return Math.max(1, Math.ceil(total / size));
28
+ return 1;
29
+ }
30
+ /**
31
+ * Build the windowed page-item model. Pure: no clamping side effects, no DOM.
32
+ *
33
+ * Algorithm (MUI usePagination semantics):
34
+ * - `startPages` = first `boundaryCount` pages; `endPages` = last
35
+ * `boundaryCount` pages.
36
+ * - a sibling window of `siblingCount` pages on each side of `page`, kept
37
+ * clear of the boundary blocks.
38
+ * - an `'ellipsis'` collapses a gap of 2+ pages; a gap of exactly one page is
39
+ * rendered as that single bridging page (never a 1-page ellipsis).
40
+ */
41
+ function paginationItems(input) {
42
+ const totalPages = resolveTotalPages(input);
43
+ const rawPage = typeof input.page === "number" && Number.isFinite(input.page) ? Math.floor(input.page) : 1;
44
+ const page = Math.min(Math.max(rawPage, 1), totalPages);
45
+ const siblingCount = Math.max(0, Math.floor(input.siblingCount ?? 1));
46
+ const boundaryCount = Math.max(0, Math.floor(input.boundaryCount ?? 1));
47
+ const startPages = range(1, Math.min(boundaryCount, totalPages));
48
+ const endPages = range(Math.max(totalPages - boundaryCount + 1, boundaryCount + 1), totalPages);
49
+ const siblingsStart = Math.max(Math.min(page - siblingCount, totalPages - boundaryCount - siblingCount * 2 - 1), boundaryCount + 2);
50
+ const endPagesFirst = endPages[0];
51
+ const siblingsEnd = Math.min(Math.max(page + siblingCount, boundaryCount + siblingCount * 2 + 2), endPagesFirst !== void 0 ? endPagesFirst - 2 : totalPages - 1);
52
+ const pages = [];
53
+ pages.push(...startPages);
54
+ if (siblingsStart > boundaryCount + 2) pages.push("ellipsis");
55
+ else if (boundaryCount + 1 < totalPages - boundaryCount) pages.push(boundaryCount + 1);
56
+ pages.push(...range(siblingsStart, siblingsEnd));
57
+ if (siblingsEnd < totalPages - boundaryCount - 1) pages.push("ellipsis");
58
+ else if (totalPages - boundaryCount > boundaryCount) pages.push(totalPages - boundaryCount);
59
+ pages.push(...endPages);
60
+ return {
61
+ totalPages,
62
+ page,
63
+ pages,
64
+ hasPrev: page > 1,
65
+ hasNext: page < totalPages
66
+ };
67
+ }
68
+ //#endregion
69
+ //#region src/Pagination.tsx
70
+ const Pagination = (0, react.forwardRef)(function Pagination(_props, ref) {
71
+ const props = {
72
+ ..._props,
73
+ totalPages: _props.totalPages ?? null,
74
+ total: _props.total ?? null,
75
+ pageSize: _props.pageSize ?? null,
76
+ siblingCount: _props.siblingCount ?? 1,
77
+ boundaryCount: _props.boundaryCount ?? 1,
78
+ disabled: _props.disabled ?? false,
79
+ ariaLabel: _props.ariaLabel ?? "Pagination"
80
+ };
81
+ const attrs = (() => {
82
+ const { modelValue, totalPages, total, pageSize, siblingCount, boundaryCount, disabled, ariaLabel, defaultValue, onModelValueChange, defaultModelValue, onChange, renderPrevControl, renderEllipsis, renderItem, renderNextControl, slots, ...rest } = _props;
83
+ return rest;
84
+ })();
85
+ const [modelValue, setModelValue] = (0, _rozie_runtime_react.useControllableState)({
86
+ value: props.modelValue,
87
+ defaultValue: props.defaultModelValue ?? 1,
88
+ onValueChange: props.onModelValueChange
89
+ });
90
+ const nav = (0, react.useRef)(null);
91
+ function model() {
92
+ return paginationItems({
93
+ page: modelValue,
94
+ totalPages: props.totalPages,
95
+ total: props.total,
96
+ pageSize: props.pageSize,
97
+ siblingCount: props.siblingCount,
98
+ boundaryCount: props.boundaryCount
99
+ });
100
+ }
101
+ function effectivePages() {
102
+ return model().totalPages;
103
+ }
104
+ function currentPage() {
105
+ return model().page;
106
+ }
107
+ function canPrev() {
108
+ return model().hasPrev;
109
+ }
110
+ function canNext() {
111
+ return model().hasNext;
112
+ }
113
+ function isActive(page) {
114
+ return page === currentPage();
115
+ }
116
+ function tabIndexFor(active) {
117
+ return active ? 0 : -1;
118
+ }
119
+ const { onChange: _rozieProp_onChange } = props;
120
+ const goToPage = (0, react.useCallback)((page) => {
121
+ if (props.disabled) return;
122
+ const tp = effectivePages();
123
+ let target = typeof page === "number" ? Math.floor(page) : 1;
124
+ if (target < 1) target = 1;
125
+ if (target > tp) target = tp;
126
+ if (target === currentPage()) return;
127
+ setModelValue(target);
128
+ _rozieProp_onChange && _rozieProp_onChange({ page: target });
129
+ }, [
130
+ _rozieProp_onChange,
131
+ currentPage,
132
+ effectivePages,
133
+ props.disabled,
134
+ setModelValue
135
+ ]);
136
+ const goNext = (0, react.useCallback)(() => {
137
+ if (canNext()) goToPage(currentPage() + 1);
138
+ }, [
139
+ canNext,
140
+ currentPage,
141
+ goToPage
142
+ ]);
143
+ const goPrev = (0, react.useCallback)(() => {
144
+ if (canPrev()) goToPage(currentPage() - 1);
145
+ }, [
146
+ canPrev,
147
+ currentPage,
148
+ goToPage
149
+ ]);
150
+ function goFirst() {
151
+ return goToPage(1);
152
+ }
153
+ function goLast() {
154
+ return goToPage(effectivePages());
155
+ }
156
+ function controls() {
157
+ const nav$local = nav.current;
158
+ if (!nav$local) return [];
159
+ return Array.from(nav$local.querySelectorAll("[data-page-control]"));
160
+ }
161
+ function focusControlAt(idx) {
162
+ const els = controls();
163
+ if (els.length === 0) return;
164
+ let i = idx;
165
+ if (i < 0) i = 0;
166
+ if (i >= els.length) i = els.length - 1;
167
+ const el = els[i];
168
+ if (el && el.focus) el.focus();
169
+ }
170
+ function focusedIndex() {
171
+ const els = controls();
172
+ const nav$local = nav.current;
173
+ const root = nav$local ? nav$local.getRootNode() : null;
174
+ const active = root ? root.activeElement : null;
175
+ return els.indexOf(active);
176
+ }
177
+ const onControlKeydown = (0, react.useCallback)(($event) => {
178
+ if (props.disabled) return;
179
+ const key = $event.key;
180
+ const cur = focusedIndex();
181
+ if (key === "ArrowRight" || key === "ArrowDown") {
182
+ $event.preventDefault();
183
+ focusControlAt(cur + 1);
184
+ } else if (key === "ArrowLeft" || key === "ArrowUp") {
185
+ $event.preventDefault();
186
+ focusControlAt(cur - 1);
187
+ } else if (key === "Home") {
188
+ $event.preventDefault();
189
+ focusControlAt(0);
190
+ } else if (key === "End") {
191
+ $event.preventDefault();
192
+ focusControlAt(controls().length - 1);
193
+ }
194
+ }, [
195
+ controls,
196
+ focusControlAt,
197
+ focusedIndex,
198
+ props.disabled
199
+ ]);
200
+ function goto(page) {
201
+ return goToPage(page);
202
+ }
203
+ function next() {
204
+ return goNext();
205
+ }
206
+ function prev() {
207
+ return goPrev();
208
+ }
209
+ function first() {
210
+ return goFirst();
211
+ }
212
+ function last() {
213
+ return goLast();
214
+ }
215
+ const _rozieExposeRef = (0, react.useRef)({
216
+ goto,
217
+ next,
218
+ prev,
219
+ first,
220
+ last
221
+ });
222
+ _rozieExposeRef.current = {
223
+ goto,
224
+ next,
225
+ prev,
226
+ first,
227
+ last
228
+ };
229
+ (0, react.useImperativeHandle)(ref, () => ({
230
+ goto: (...args) => (0, react_dom.flushSync)(() => _rozieExposeRef.current.goto(...args)),
231
+ next: (...args) => (0, react_dom.flushSync)(() => _rozieExposeRef.current.next(...args)),
232
+ prev: (...args) => (0, react_dom.flushSync)(() => _rozieExposeRef.current.prev(...args)),
233
+ first: (...args) => (0, react_dom.flushSync)(() => _rozieExposeRef.current.first(...args)),
234
+ last: (...args) => (0, react_dom.flushSync)(() => _rozieExposeRef.current.last(...args))
235
+ }), []);
236
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
237
+ ref: nav,
238
+ "aria-label": props.ariaLabel,
239
+ ...attrs,
240
+ className: (0, _rozie_runtime_react.clsx)((0, _rozie_runtime_react.clsx)("rozie-pagination", { "rozie-pagination--disabled": props.disabled }), attrs.className),
241
+ ...(0, _rozie_runtime_react.mergeListeners)({ onKeyDown: ($event) => {
242
+ onControlKeydown($event);
243
+ } }, (0, _rozie_runtime_react.pickListeners)(attrs)),
244
+ "data-rozie-s-de247ae2": "",
245
+ children: [
246
+ props.renderPrevControl ?? props.slots?.["prevControl"] ? (props.renderPrevControl ?? props.slots?.["prevControl"])({
247
+ disabled: !canPrev() || props.disabled,
248
+ goto: goPrev,
249
+ page: currentPage() - 1
250
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
251
+ type: "button",
252
+ className: "rozie-pagination-control rozie-pagination-prev",
253
+ "data-page-control": "",
254
+ tabIndex: tabIndexFor(true),
255
+ disabled: !canPrev() || props.disabled,
256
+ "aria-disabled": !!(!canPrev() || props.disabled),
257
+ "aria-label": "Previous page",
258
+ onClick: goPrev,
259
+ "data-rozie-s-de247ae2": "",
260
+ children: "‹"
261
+ }),
262
+ model().pages.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [!!(item === "ellipsis") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
263
+ className: "rozie-pagination-ellipsis",
264
+ "aria-hidden": "true",
265
+ "data-rozie-s-de247ae2": "",
266
+ children: props.renderEllipsis ?? props.slots?.["ellipsis"] ? (props.renderEllipsis ?? props.slots?.["ellipsis"])({ index }) : "…"
267
+ }), !!(item !== "ellipsis") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
268
+ className: "rozie-pagination-item",
269
+ "data-rozie-s-de247ae2": "",
270
+ children: props.renderItem ?? props.slots?.["item"] ? (props.renderItem ?? props.slots?.["item"])({
271
+ page: item,
272
+ selected: isActive(item),
273
+ goto: () => goToPage(item)
274
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
275
+ type: "button",
276
+ className: (0, _rozie_runtime_react.clsx)("rozie-pagination-page", { "is-active": isActive(item) }),
277
+ "data-page-control": "",
278
+ tabIndex: tabIndexFor(isActive(item)),
279
+ disabled: !!props.disabled,
280
+ "aria-disabled": !!props.disabled,
281
+ "aria-current": (0, _rozie_runtime_react.rozieAttr)(isActive(item) ? "page" : void 0),
282
+ "aria-label": (0, _rozie_runtime_react.rozieAttr)("Go to page " + item),
283
+ onClick: ($event) => {
284
+ goToPage(item);
285
+ },
286
+ "data-rozie-s-de247ae2": "",
287
+ children: (0, _rozie_runtime_react.rozieDisplay)(item)
288
+ })
289
+ })] }, item === "ellipsis" ? "ellipsis-" + index : item)),
290
+ props.renderNextControl ?? props.slots?.["nextControl"] ? (props.renderNextControl ?? props.slots?.["nextControl"])({
291
+ disabled: !canNext() || props.disabled,
292
+ goto: goNext,
293
+ page: currentPage() + 1
294
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
295
+ type: "button",
296
+ className: "rozie-pagination-control rozie-pagination-next",
297
+ "data-page-control": "",
298
+ tabIndex: tabIndexFor(true),
299
+ disabled: !canNext() || props.disabled,
300
+ "aria-disabled": !!(!canNext() || props.disabled),
301
+ "aria-label": "Next page",
302
+ onClick: goNext,
303
+ "data-rozie-s-de247ae2": "",
304
+ children: "›"
305
+ })
306
+ ]
307
+ }) });
308
+ });
309
+ //#endregion
310
+ exports.Pagination = Pagination;
311
+ exports.default = Pagination;
@@ -0,0 +1,75 @@
1
+ import * as _$react from "react";
2
+ import { ReactNode } from "react";
3
+
4
+ //#region src/Pagination.d.ts
5
+ /** The `change` event payload: the page that was just navigated to. */
6
+ interface PaginationChangePayload {
7
+ /** The new, clamped, 1-based current page. */
8
+ page: number;
9
+ }
10
+ interface PaginationProps extends Omit<_$react.ComponentPropsWithoutRef<'nav'>, 'modelValue' | 'defaultModelValue' | 'onModelValueChange' | 'totalPages' | 'total' | 'pageSize' | 'siblingCount' | 'boundaryCount' | 'disabled' | 'ariaLabel' | 'onChange' | 'renderPrevControl' | 'renderEllipsis' | 'renderItem' | 'renderNextControl' | 'slots' | 'children' | 'dangerouslySetInnerHTML'> {
11
+ /**
12
+ * The 1-based current page (two-way model). Clamped into `[1, totalPages]`. Bind it with `r-model:modelValue` / `v-model:modelValue` / `modelValue` + `onModelValueChange`; it is also the Angular ControlValueAccessor control value.
13
+ */
14
+ modelValue?: number;
15
+ defaultModelValue?: number;
16
+ onModelValueChange?: (next: number) => void;
17
+ /**
18
+ * Explicit total page count. When provided (> 0) it takes precedence over `total` + `pageSize`. Use it when the backend already reports the page count.
19
+ */
20
+ totalPages?: (number) | null;
21
+ /**
22
+ * Total item count. Combined with `pageSize` to derive the page count (`ceil(total / pageSize)`) when `totalPages` is not given.
23
+ */
24
+ total?: (number) | null;
25
+ /**
26
+ * Items per page. Combined with `total` to derive the page count when `totalPages` is not given.
27
+ */
28
+ pageSize?: (number) | null;
29
+ /**
30
+ * Number of page buttons shown on each side of the current page (the sibling window). Larger values show more context around the current page.
31
+ */
32
+ siblingCount?: number;
33
+ /**
34
+ * Number of page buttons always shown at each boundary (the first and last `boundaryCount` pages), regardless of the current page.
35
+ */
36
+ boundaryCount?: number;
37
+ /**
38
+ * Disable the entire control — every page button and the prev/next controls become non-interactive and are marked `aria-disabled`.
39
+ */
40
+ disabled?: boolean;
41
+ /**
42
+ * Accessible name for the surrounding `<nav>` landmark (its `aria-label`). Defaults to `"Pagination"`.
43
+ */
44
+ ariaLabel?: string;
45
+ onChange?: (payload: PaginationChangePayload) => void;
46
+ renderPrevControl?: (params: {
47
+ disabled: boolean;
48
+ goto: () => void;
49
+ page: number;
50
+ }) => ReactNode;
51
+ renderEllipsis?: (params: {
52
+ index: number;
53
+ }) => ReactNode;
54
+ renderItem?: (params: {
55
+ page: number;
56
+ selected: boolean;
57
+ goto: () => void;
58
+ }) => ReactNode;
59
+ renderNextControl?: (params: {
60
+ disabled: boolean;
61
+ goto: () => void;
62
+ page: number;
63
+ }) => ReactNode;
64
+ slots?: Record<string, () => ReactNode>;
65
+ }
66
+ interface PaginationHandle {
67
+ goto: (page: number) => void;
68
+ next: () => void;
69
+ prev: () => void;
70
+ first: () => void;
71
+ last: () => void;
72
+ }
73
+ declare const Pagination: _$react.ForwardRefExoticComponent<PaginationProps & _$react.RefAttributes<PaginationHandle>>;
74
+ //#endregion
75
+ export { Pagination, Pagination as default, PaginationChangePayload, type PaginationHandle, PaginationProps };