diff --git a/component.json b/component.json index a67ae134..fd59c4f0 100644 --- a/component.json +++ b/component.json @@ -38,6 +38,7 @@ "preview/src/components/skeleton", "preview/src/components/card", "preview/src/components/sheet", + "preview/src/components/pagination", "preview/src/components/sidebar", "preview/src/components/badge" ] diff --git a/preview/src/components/mod.rs b/preview/src/components/mod.rs index 01e8f4ca..b50992e4 100644 --- a/preview/src/components/mod.rs +++ b/preview/src/components/mod.rs @@ -137,6 +137,7 @@ examples!( label, menubar, navbar, + pagination, popover, progress, radio_group, diff --git a/preview/src/components/pagination/component.json b/preview/src/components/pagination/component.json new file mode 100644 index 00000000..6aa22f97 --- /dev/null +++ b/preview/src/components/pagination/component.json @@ -0,0 +1,21 @@ +{ + "name": "pagination", + "description": "Navigation controls for paged content.", + "authors": [ + "zhiyanzhaijie" + ], + "exclude": [ + "variants", + "docs.md", + "component.json" + ], + "cargoDependencies": [ + { + "name": "dioxus-primitives", + "git": "https://github.com/DioxusLabs/components" + } + ], + "globalAssets": [ + "../../../assets/dx-components-theme.css" + ] +} diff --git a/preview/src/components/pagination/component.rs b/preview/src/components/pagination/component.rs new file mode 100644 index 00000000..f04eff42 --- /dev/null +++ b/preview/src/components/pagination/component.rs @@ -0,0 +1,216 @@ +use dioxus::prelude::*; + +#[derive(Copy, Clone, PartialEq, Default)] +#[non_exhaustive] +pub enum PaginationLinkSize { + #[default] + Icon, + Default, +} + +impl PaginationLinkSize { + pub fn class(&self) -> &'static str { + match self { + PaginationLinkSize::Icon => "icon", + PaginationLinkSize::Default => "default", + } + } +} + +#[derive(Copy, Clone, PartialEq)] +#[non_exhaustive] +pub enum PaginationLinkKind { + Previous, + Next, +} + +impl PaginationLinkKind { + pub fn attr(&self) -> &'static str { + match self { + PaginationLinkKind::Previous => "previous", + PaginationLinkKind::Next => "next", + } + } +} + +#[component] +pub fn Pagination( + #[props(extends = GlobalAttributes)] attributes: Vec, + children: Element, +) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + nav { + class: "pagination", + "data-slot": "pagination", + role: "navigation", + aria_label: "pagination", + ..attributes, + {children} + } + } +} + +#[component] +pub fn PaginationContent( + #[props(extends = GlobalAttributes)] attributes: Vec, + children: Element, +) -> Element { + rsx! { + ul { + class: "pagination-content", + "data-slot": "pagination-content", + ..attributes, + {children} + } + } +} + +#[component] +pub fn PaginationItem( + #[props(extends = GlobalAttributes)] attributes: Vec, + children: Element, +) -> Element { + rsx! { + li { + class: "pagination-item", + "data-slot": "pagination-item", + ..attributes, + {children} + } + } +} + +#[derive(Props, Clone, PartialEq)] +pub struct PaginationLinkProps { + #[props(default)] + pub is_active: bool, + #[props(default)] + pub size: PaginationLinkSize, + #[props(default)] + pub data_kind: Option, + onclick: Option>, + onmousedown: Option>, + onmouseup: Option>, + #[props(extends = GlobalAttributes)] + #[props(extends = a)] + pub attributes: Vec, + pub children: Element, +} + +#[component] +pub fn PaginationLink(props: PaginationLinkProps) -> Element { + let aria_current = if props.is_active { Some("page") } else { None }; + let data_kind = props.data_kind.map(|kind| kind.attr()); + rsx! { + a { + class: "pagination-link", + "data-slot": "pagination-link", + "data-active": props.is_active, + "data-size": props.size.class(), + "data-kind": data_kind, + aria_current: aria_current, + onclick: move |event| { + if let Some(f) = &props.onclick { + f.call(event); + } + }, + onmousedown: move |event| { + if let Some(f) = &props.onmousedown { + f.call(event); + } + }, + onmouseup: move |event| { + if let Some(f) = &props.onmouseup { + f.call(event); + } + }, + ..props.attributes, + {props.children} + } + } +} + +#[component] +pub fn PaginationPrevious( + onclick: Option>, + onmousedown: Option>, + onmouseup: Option>, + #[props(extends = GlobalAttributes)] + #[props(extends = a)] + attributes: Vec, +) -> Element { + rsx! { + PaginationLink { + size: PaginationLinkSize::Default, + aria_label: "Go to previous page", + data_kind: Some(PaginationLinkKind::Previous), + onclick, + onmousedown, + onmouseup, + attributes, + // ChevronLeft icon from lucide https://lucide.dev/icons/chevron-left + svg { + class: "pagination-icon", + view_box: "0 0 24 24", + xmlns: "http://www.w3.org/2000/svg", + polyline { points: "15 6 9 12 15 18" } + } + span { class: "pagination-label", "Previous" } + } + } +} + +#[component] +pub fn PaginationNext( + onclick: Option>, + onmousedown: Option>, + onmouseup: Option>, + #[props(extends = GlobalAttributes)] + #[props(extends = a)] + attributes: Vec, +) -> Element { + rsx! { + PaginationLink { + size: PaginationLinkSize::Default, + aria_label: "Go to next page", + data_kind: Some(PaginationLinkKind::Next), + onclick, + onmousedown, + onmouseup, + attributes, + span { class: "pagination-label", "Next" } + // ChevronRight icon from lucide https://lucide.dev/icons/chevron-right + svg { + class: "pagination-icon", + view_box: "0 0 24 24", + xmlns: "http://www.w3.org/2000/svg", + polyline { points: "9 6 15 12 9 18" } + } + } + } +} + +#[component] +pub fn PaginationEllipsis( + #[props(extends = GlobalAttributes)] attributes: Vec, +) -> Element { + rsx! { + span { + class: "pagination-ellipsis", + "data-slot": "pagination-ellipsis", + aria_hidden: "true", + ..attributes, + // MoreHorizontal icon from lucide https://lucide.dev/icons/more-horizontal + svg { + class: "pagination-icon", + view_box: "0 0 24 24", + xmlns: "http://www.w3.org/2000/svg", + circle { cx: "5", cy: "12", r: "1.5" } + circle { cx: "12", cy: "12", r: "1.5" } + circle { cx: "19", cy: "12", r: "1.5" } + } + span { class: "sr-only", "More pages" } + } + } +} diff --git a/preview/src/components/pagination/docs.md b/preview/src/components/pagination/docs.md new file mode 100644 index 00000000..8ab3ff85 --- /dev/null +++ b/preview/src/components/pagination/docs.md @@ -0,0 +1,36 @@ +The pagination component provides navigational controls for paged content. It exposes a consistent structure for previous/next actions, individual page links, and an optional ellipsis for truncated ranges. + +## Component Structure + +```rust +// The Pagination component wraps the entire control. +Pagination { + // PaginationContent groups all items in a horizontal list. + PaginationContent { + // PaginationItem is the container for a single pagination element. + // Use one item at a time and swap the inner component as needed. + PaginationItem { + // PaginationPrevious renders a previous-page link. + // - Set href to your previous page url. + PaginationPrevious { href: "#" } + + // PaginationLink renders a numbered page link. + // - is_active marks the current page. + // - href sets the target page. + PaginationLink { href: "#", is_active: true, "2" } + + // PaginationEllipsis indicates truncated pages. + PaginationEllipsis {} + + // PaginationNext renders a next-page link. + // - Set href to your next page url. + PaginationNext { href: "#" } + } + } +} +``` + +## Notes + +- `PaginationLink` uses `is_active` to indicate the current page. +- `PaginationPrevious` and `PaginationNext` show labels on larger (non-mobile) screens; labels are hidden on smaller screens to keep the control compact. diff --git a/preview/src/components/pagination/mod.rs b/preview/src/components/pagination/mod.rs new file mode 100644 index 00000000..2590c013 --- /dev/null +++ b/preview/src/components/pagination/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; diff --git a/preview/src/components/pagination/style.css b/preview/src/components/pagination/style.css new file mode 100644 index 00000000..ae0cf434 --- /dev/null +++ b/preview/src/components/pagination/style.css @@ -0,0 +1,119 @@ +.pagination { + display: flex; + width: 100%; + justify-content: center; + margin: 0 auto; +} + +.pagination-content { + display: flex; + align-items: center; + padding: 0; + margin: 0; + gap: 0.25rem; + list-style: none; +} + +.pagination-link { + display: inline-flex; + box-sizing: border-box; + align-items: center; + justify-content: center; + border-radius: 0.625rem; + color: var(--secondary-color-4); + font-size: 0.875rem; + font-weight: 500; + gap: 0.5rem; + line-height: 1; + text-decoration: none; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.pagination-link:focus-visible { + box-shadow: 0 0 0 2px var(--focused-border-color); +} + +.pagination-link[data-size="icon"] { + width: 2rem; + height: 2rem; + padding: 0; +} + +.pagination-ellipsis { + display: flex; + align-items: center; + justify-content: center; + color: var(--secondary-color-4); +} + +.pagination-ellipsis .pagination-icon { + fill: currentcolor; +} + +.pagination-link[data-size="icon"], +.pagination-ellipsis { + width: 2rem; + height: 2rem; +} + +.pagination-link[data-size="default"] { + height: 2rem; + padding: 0.5rem 1rem; +} + +.pagination-link[data-active="true"] { + border: 1px solid var(--primary-color-6); + background-color: var(--light, var(--primary-color)) var(--dark, var(--primary-color-3)); +} + +.pagination-link[data-active="true"]:hover { + background-color: var(--primary-color-4); +} + +.pagination-link[data-active="false"]:hover { + background-color: var(--primary-color-5); + color: var(--secondary-color-1); +} + +.pagination-link[data-kind="previous"], +.pagination-link[data-kind="next"] { + padding-right: 0.625rem; + padding-left: 0.625rem; + gap: 0.25rem; +} + +.pagination-label { + display: none; +} + +@media (width >= 640px) { + .pagination-label { + display: inline; + } +} + + +.pagination-icon { + width: 1rem; + height: 1rem; + fill: none; + stroke: currentcolor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; +} + +/* TODO: move to shared css */ + +/* Screen-reader only text */ +.sr-only { + position: absolute; + overflow: hidden; + width: 1px; + height: 1px; + padding: 0; + border: 0; + margin: -1px; + clip-path: inset(50%); + white-space: nowrap; +} diff --git a/preview/src/components/pagination/variants/main/mod.rs b/preview/src/components/pagination/variants/main/mod.rs new file mode 100644 index 00000000..131dce29 --- /dev/null +++ b/preview/src/components/pagination/variants/main/mod.rs @@ -0,0 +1,30 @@ +use super::super::component::*; +use dioxus::prelude::*; + +#[component] +pub fn Demo() -> Element { + rsx! { + Pagination { + PaginationContent { + PaginationItem { + PaginationPrevious { href: "#" } + } + PaginationItem { + PaginationLink { href: "#", "1" } + } + PaginationItem { + PaginationLink { href: "#", is_active: true, "2" } + } + PaginationItem { + PaginationLink { href: "#", "3" } + } + PaginationItem { + PaginationEllipsis {} + } + PaginationItem { + PaginationNext { href: "#" } + } + } + } + } +}