diff --git a/examples/component-showcase/scripts/showcase-profiles.mjs b/examples/component-showcase/scripts/showcase-profiles.mjs
index f36ea355..93ab74e7 100644
--- a/examples/component-showcase/scripts/showcase-profiles.mjs
+++ b/examples/component-showcase/scripts/showcase-profiles.mjs
@@ -253,6 +253,25 @@ const DATATABLE_ROWS =
'[{"id": 1, "name": "Northwind", "plan": "Scale", "owner": "A. Okafor", "seats": 6, "status": "Active", "statusHtml": "Active"}, {"id": 2, "name": "Acme Industrial", "plan": "Team", "owner": "R. Silva", "seats": 13, "status": "Trial", "statusHtml": "Trial"}, {"id": 3, "name": "Globex", "plan": "Enterprise", "owner": "M. Chen", "seats": 20, "status": "Past due", "statusHtml": "Past due"}, {"id": 4, "name": "Initech", "plan": "Starter", "owner": "J. Dubois", "seats": 27, "status": "Active", "statusHtml": "Active"}, {"id": 5, "name": "Umbrella", "plan": "Scale", "owner": "P. Novak", "seats": 34, "status": "Trial", "statusHtml": "Trial"}, {"id": 6, "name": "Stark Labs", "plan": "Team", "owner": "A. Okafor", "seats": 41, "status": "Past due", "statusHtml": "Past due"}, {"id": 7, "name": "Wayne Foods", "plan": "Enterprise", "owner": "R. Silva", "seats": 48, "status": "Active", "statusHtml": "Active"}, {"id": 8, "name": "Soylent", "plan": "Starter", "owner": "M. Chen", "seats": 55, "status": "Trial", "statusHtml": "Trial"}, {"id": 9, "name": "Hooli", "plan": "Scale", "owner": "J. Dubois", "seats": 62, "status": "Past due", "statusHtml": "Past due"}, {"id": 10, "name": "Vehement", "plan": "Team", "owner": "P. Novak", "seats": 69, "status": "Active", "statusHtml": "Active"}, {"id": 11, "name": "Massive Dynamic", "plan": "Enterprise", "owner": "A. Okafor", "seats": 76, "status": "Trial", "statusHtml": "Trial"}, {"id": 12, "name": "Cyberdyne", "plan": "Starter", "owner": "R. Silva", "seats": 83, "status": "Past due", "statusHtml": "Past due"}, {"id": 13, "name": "Tyrell", "plan": "Scale", "owner": "M. Chen", "seats": 90, "status": "Active", "statusHtml": "Active"}, {"id": 14, "name": "Aperture", "plan": "Team", "owner": "J. Dubois", "seats": 97, "status": "Trial", "statusHtml": "Trial"}, {"id": 15, "name": "Black Mesa", "plan": "Enterprise", "owner": "P. Novak", "seats": 104, "status": "Past due", "statusHtml": "Past due"}]';
export const componentProfiles = {
+ Pagination: {
+ demos: [
+ standard(
+ "Compact arrows",
+ "The default: previous and next with a page counter, and a summary of the range in view.",
+ { page: 2, pageSize: 10, total: 137, variant: "compact" },
+ ),
+ standard(
+ "Numbered pages",
+ "Page numbers windowed around the current page with ellipsis gaps, so a large set never renders hundreds of buttons.",
+ { page: 5, pageSize: 10, total: 200, variant: "numbered", siblingCount: 1 },
+ ),
+ advanced(
+ "Wider window",
+ "siblingCount widens how many pages sit either side of the current one.",
+ { page: 8, pageSize: 25, total: 900, variant: "numbered", siblingCount: 2 },
+ ),
+ ],
+ },
DataTable: {
demos: [
standard(
diff --git a/packages/ui/components/Pagination.wrn b/packages/ui/components/Pagination.wrn
index 2a55358e..85b14244 100644
--- a/packages/ui/components/Pagination.wrn
+++ b/packages/ui/components/Pagination.wrn
@@ -1,23 +1,262 @@
+// Pagination -- page controls over a known total.
+//
+//
+//
+// The component owns no data. It reports the requested page through its
+// change output and lets the caller fetch or slice.
+//
+// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
+// and silently swallows the rule that follows it.
component Pagination {
outputs {
- change(payload: { value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
- previous(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
- next(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
+ change(payload: { page: number; pageSize: number })
+ previous(payload: { page: number })
+ next(payload: { page: number })
}
props {
-size: string = "default"
- color: string = "primary"
+ page: number = 1
+ pageSize: number = 10
+ total: number = 0
+ variant: string = "compact"
+ siblingCount: number = 1
+ showSummary: boolean = true
label: string = "Pagination"
- items: unknown[] = []
- active: string = ""
- orientation: string = "horizontal"
+ previousLabel: string = "Previous"
+ nextLabel: string = "Next"
class: string = ""
}
+
+ functions {
+ shared function lastPage() {
+ var size = Number(pageSize) > 0 ? Number(pageSize) : 10
+ var count = Number(total) > 0 ? Number(total) : 0
+ return Math.max(1, Math.ceil(count / size))
+ }
+
+ // Out-of-range values arrive routinely: page travels as an HTML attribute
+ // and callers compute it from data that may have shrunk underneath them.
+ shared function currentPage() {
+ var value = Number(page)
+ if (!value || value < 1) {
+ return 1
+ }
+ return Math.min(value, lastPage())
+ }
+
+ shared function firstShown() {
+ if (Number(total) < 1) {
+ return 0
+ }
+ return (currentPage() - 1) * (Number(pageSize) || 10) + 1
+ }
+
+ shared function lastShown() {
+ return Math.min(currentPage() * (Number(pageSize) || 10), Number(total) || 0)
+ }
+
+ shared function pageNumbers() {
+ var last = lastPage()
+ var current = currentPage()
+ var siblings = Math.max(0, Number(siblingCount) || 0)
+ var start = Math.max(1, current - siblings)
+ var end = Math.min(last, current + siblings)
+ var pages = []
+ if (start > 1) {
+ pages.push({ value: 1, label: "1", gap: false })
+ if (start > 2) {
+ pages.push({ value: 0, label: "...", gap: true })
+ }
+ }
+ for (var index = start; index <= end; index += 1) {
+ pages.push({ value: index, label: String(index), gap: false })
+ }
+ if (end < last) {
+ if (end < last - 1) {
+ pages.push({ value: 0, label: "...", gap: true })
+ }
+ pages.push({ value: last, label: String(last), gap: false })
+ }
+ return pages
+ }
+
+ client function goToPage(target) {
+ var next = Math.min(Math.max(1, Number(target) || 1), lastPage())
+ output.change({ page: next, pageSize: Number(pageSize) || 10 })
+ }
+
+ client function goPrevious() {
+ var target = Math.max(1, currentPage() - 1)
+ output.previous({ page: target })
+ output.change({ page: target, pageSize: Number(pageSize) || 10 })
+ }
+
+ client function goNext() {
+ var target = Math.min(lastPage(), currentPage() + 1)
+ output.next({ page: target })
+ output.change({ page: target, pageSize: Number(pageSize) || 10 })
+ }
+ }
+
view {
-