From 9a765667cd37d22f2123d0ee5ac8980b32bd87ed Mon Sep 17 00:00:00 2001 From: Fakhir-Israr-200219 Date: Sat, 9 May 2026 16:43:37 +0500 Subject: [PATCH] feat: add table component --- preview/src/components/mod.rs | 1 + preview/src/components/table/component.json | 7 + preview/src/components/table/component.rs | 9 + preview/src/components/table/docs.md | 28 ++ preview/src/components/table/mod.rs | 3 + preview/src/components/table/style.css | 308 ++++++++++++++++++ .../src/components/table/variants/main/mod.rs | 212 ++++++++++++ 7 files changed, 568 insertions(+) create mode 100644 preview/src/components/table/component.json create mode 100644 preview/src/components/table/component.rs create mode 100644 preview/src/components/table/docs.md create mode 100644 preview/src/components/table/mod.rs create mode 100644 preview/src/components/table/style.css create mode 100644 preview/src/components/table/variants/main/mod.rs diff --git a/preview/src/components/mod.rs b/preview/src/components/mod.rs index 91041902..1ab4ff22 100644 --- a/preview/src/components/mod.rs +++ b/preview/src/components/mod.rs @@ -160,4 +160,5 @@ examples!( toggle, toolbar, tooltip, + table, ); diff --git a/preview/src/components/table/component.json b/preview/src/components/table/component.json new file mode 100644 index 00000000..e99b15f4 --- /dev/null +++ b/preview/src/components/table/component.json @@ -0,0 +1,7 @@ +{ + "name": "table", + "description": "A data table component with filtering, sorting, column visibility, row selection, and pagination.", + "authors": ["Fakhir"], + "exclude": ["variants", "docs.md", "component.json"], + "styled": true +} \ No newline at end of file diff --git a/preview/src/components/table/component.rs b/preview/src/components/table/component.rs new file mode 100644 index 00000000..701e798b --- /dev/null +++ b/preview/src/components/table/component.rs @@ -0,0 +1,9 @@ +use dioxus::prelude::*; + +#[component] +pub fn DemoWithStyles() -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + super::variants::main::Demo {} + } +} \ No newline at end of file diff --git a/preview/src/components/table/docs.md b/preview/src/components/table/docs.md new file mode 100644 index 00000000..3597ea23 --- /dev/null +++ b/preview/src/components/table/docs.md @@ -0,0 +1,28 @@ +The Table component displays structured data with support for filtering, column visibility toggling, row selection, sortable columns, and pagination. + +## Component Structure + +```rust +// The Demo component renders a fully functional data table +Demo {} +``` + +## Features + +- **Filter**: Type in the search box to filter rows by email in real time. +- **Column Visibility**: Click the "Columns" button to show or hide the Status, Email, and Amount columns. +- **Row Selection**: Use the checkboxes to select individual rows or all rows at once. +- **Sorting**: Click the Email column header to toggle ascending/descending sort order. +- **Row Actions**: Click the `···` button on any row to open a context menu with actions like "Copy payment ID", "View customer", and "View payment details". +- **Pagination**: Previous and Next buttons are shown in the footer along with a selected row count. + +## Data Structure + +```rust +struct Payment { + id: usize, + status: &'static str, // "Success" | "Processing" | "Failed" + email: &'static str, + amount: &'static str, +} +``` \ No newline at end of file diff --git a/preview/src/components/table/mod.rs b/preview/src/components/table/mod.rs new file mode 100644 index 00000000..961b425e --- /dev/null +++ b/preview/src/components/table/mod.rs @@ -0,0 +1,3 @@ +mod variants; +mod component; +pub use component::Demo; \ No newline at end of file diff --git a/preview/src/components/table/style.css b/preview/src/components/table/style.css new file mode 100644 index 00000000..0786af9c --- /dev/null +++ b/preview/src/components/table/style.css @@ -0,0 +1,308 @@ +.dx-table-wrapper { + background-color: var(--background, #1a1a1a); + border: 1px solid var(--border-color, #2e2e2e); + border-radius: 0.75rem; + overflow: hidden; + font-family: inherit; + color: var(--foreground, #e5e5e5); +} + +.dx-table-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.875rem 1rem; + gap: 0.75rem; +} + +.dx-table-filter { + flex: 1; + max-width: 20rem; + padding: 0.4rem 0.75rem; + border: 1px solid var(--border-color, #3a3a3a); + border-radius: 0.375rem; + background-color: transparent; + color: var(--foreground, #e5e5e5); + font-size: 0.875rem; + outline: none; + transition: border-color 150ms; +} + +.dx-table-filter::placeholder { + color: var(--foreground-muted, #6b7280); +} + +.dx-table-filter:focus { + border-color: var(--border-focus, #6b7280); +} + +.dx-columns-btn { + display: flex; + align-items: center; + gap: 0.3rem; + padding: 0.4rem 0.875rem; + border: 1px solid var(--border-color, #3a3a3a); + border-radius: 0.375rem; + background-color: transparent; + color: var(--foreground, #e5e5e5); + font-size: 0.875rem; + cursor: pointer; + white-space: nowrap; + transition: background-color 150ms; +} + +.dx-columns-btn:hover { + background-color: var(--background-hover, #2a2a2a); +} + +.dx-chevron { + font-size: 0.75rem; + opacity: 0.7; +} + +.dx-table-container { + overflow-x: auto; + border-top: 1px solid var(--border-color, #2e2e2e); +} + +.dx-table { + width: 100%; + border-collapse: collapse; + font-size: 0.875rem; +} + +.dx-table thead tr { + border-bottom: 1px solid var(--border-color, #2e2e2e); +} + +.dx-table th { + padding: 0.6rem 0.75rem; + text-align: left; + font-weight: 600; + font-size: 0.8rem; + color: var(--foreground, #e5e5e5); + white-space: nowrap; +} + +.dx-table td { + padding: 0.65rem 0.75rem; + border-bottom: 1px solid var(--border-color, #222222); + color: var(--foreground-muted, #a1a1aa); +} + +.dx-table tbody tr:last-child td { + border-bottom: none; +} + +.dx-row:hover td { + background-color: var(--background-hover, #222222); +} + +.dx-row-selected td { + background-color: var(--background-selected, #1e2a1e); +} + +/* .dx-checkbox { + width: 1rem; + height: 1rem; + accent-color: var(--foreground, #e5e5e5); + cursor: pointer; +} */ + +.dx-checkbox { + width: 1rem; + height: 1rem; + accent-color: var(--foreground, #e5e5e5); + cursor: pointer; + appearance: none; + -webkit-appearance: none; + border: 1.5px solid #6b7280; + border-radius: 0.2rem; + background-color: transparent; + position: relative; + transition: all 150ms; +} + +.dx-checkbox:checked { + background-color: #ffffff; + border-color: #ffffff; +} + +.dx-checkbox:checked::after { + content: ""; + position: absolute; + left: 50%; + top: 45%; + width: 0.3rem; + height: 0.55rem; + border: 2.5px solid #000000; + border-top: none; + border-left: none; + transform: translate(-50%, -50%) rotate(45deg); +} + +.dx-sort-btn { + background: none; + border: none; + color: var(--foreground, #e5e5e5); + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + padding: 0; + display: inline-flex; + align-items: center; + gap: 0.25rem; +} + +.dx-col-amount { + text-align: right !important; + font-variant-numeric: tabular-nums; + color: var(--foreground, #e5e5e5) !important; +} + +.dx-badge { + display: inline-flex; + align-items: center; + padding: 0.2rem 0.6rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 500; +} + +.dx-badge-success { + background-color: transparent; + color: var(--foreground-muted, #a1a1aa); +} + +.dx-badge-processing { + background-color: transparent; + color: var(--foreground-muted, #a1a1aa); +} + +.dx-badge-failed { + background-color: transparent; + color: var(--foreground-muted, #a1a1aa); +} + +.dx-action-btn { + background: none; + border: none; + color: var(--foreground-muted, #6b7280); + cursor: pointer; + font-size: 1rem; + padding: 0 0.25rem; + letter-spacing: 0.05em; + transition: color 150ms; +} + +.dx-action-btn:hover { + color: var(--foreground, #e5e5e5); +} + +.dx-table-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + border-top: 1px solid var(--border-color, #2e2e2e); +} + +.dx-footer-info { + font-size: 0.8rem; + color: var(--foreground-muted, #6b7280); +} + +.dx-pagination-buttons { + display: flex; + gap: 0.5rem; +} + +.dx-pagination-button { + padding: 0.3rem 0.875rem; + border: 1px solid var(--border-color, #3a3a3a); + border-radius: 0.375rem; + font-size: 0.8rem; + background-color: transparent; + color: var(--foreground, #e5e5e5); + cursor: pointer; + transition: background-color 150ms; +} + +.dx-pagination-button:hover { + background-color: var(--background-hover, #2a2a2a); +} + +/* Action Menu (Dropdown) */ +.dx-action-menu { + position: absolute; + top: 100%; + right: 0; + margin-top: 0.25rem; + background-color: var(--background, #1a1a1a); + border: 1px solid var(--border-color, #2e2e2e); + border-radius: 0.5rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + min-width: 150px; + z-index: 20; + overflow: hidden; +} + +.dx-action-menu button { + display: block; + width: 100%; + text-align: left; + padding: 0.5rem 1rem; + background: none; + border: none; + color: var(--foreground, #e5e5e5); + font-size: 0.8rem; + cursor: pointer; + transition: background-color 0.15s; + white-space: nowrap; +} + +.dx-action-menu button:hover { + background-color: var(--background-hover, #2a2a2a); +} + +/* Overlay to close menu when clicking outside */ +.dx-menu-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10; +} +/* Columns dropdown */ +.dx-columns-menu { + position: absolute; + right: 0; + top: calc(100% + 0.25rem); + z-index: 50; + background-color: var(--background, #1a1a1a); + border: 1px solid var(--border-color, #2e2e2e); + border-radius: 0.5rem; + padding: 0.5rem; + min-width: 10rem; + display: flex; + flex-direction: column; + gap: 0.25rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); +} + +.dx-columns-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.35rem 0.5rem; + border-radius: 0.25rem; + cursor: pointer; + font-size: 0.875rem; + color: var(--foreground, #e5e5e5); + transition: background-color 150ms; +} + +.dx-columns-item:hover { + background-color: var(--background-hover, #2a2a2a); +} diff --git a/preview/src/components/table/variants/main/mod.rs b/preview/src/components/table/variants/main/mod.rs new file mode 100644 index 00000000..7546c359 --- /dev/null +++ b/preview/src/components/table/variants/main/mod.rs @@ -0,0 +1,212 @@ +use dioxus::prelude::*; + +#[derive(Clone, PartialEq)] +struct Payment { + id: usize, + status: &'static str, + email: &'static str, + amount: &'static str, +} + +// Ek jagah define karo — name + extractor function +struct ColumnDef { + name: &'static str, + get: fn(&Payment) -> &'static str, +} + +static COLUMNS: &[ColumnDef] = &[ + ColumnDef { name: "Status", get: |p| p.status }, + ColumnDef { name: "Email", get: |p| p.email }, + ColumnDef { name: "Amount", get: |p| p.amount }, +]; + +static DATA: &[Payment] = &[ + Payment { id: 1, status: "Success", email: "ken99@example.com", amount: "$316.00" }, + Payment { id: 2, status: "Success", email: "abe45@example.com", amount: "$242.00" }, + Payment { id: 3, status: "Processing", email: "monserrat44@example.com", amount: "$837.00" }, + Payment { id: 4, status: "Success", email: "silas22@example.com", amount: "$874.00" }, + Payment { id: 5, status: "Failed", email: "carmella@example.com", amount: "$721.00" }, + Payment { id: 6, status: "Failed", email: "carmella2@example.com", amount: "$521.00" }, +]; + +#[component] +pub fn Demo() -> Element { + let mut filter = use_signal(|| String::new()); + let mut selected = use_signal(|| vec![false; DATA.len()]); + let mut active_menu = use_signal(|| None::); + let mut show_columns_menu = use_signal(|| false); + + // COLUMNS.len() se automatically sahi size milegi + let mut col_visible = use_signal(|| vec![true; COLUMNS.len()]); + + let f = filter.read().to_lowercase(); + let filtered: Vec<(usize, &'static Payment)> = DATA + .iter() + .enumerate() + .filter(|(_, p)| f.is_empty() || p.email.to_lowercase().contains(f.as_str())) + .collect(); + + let selected_count = selected.read().iter().filter(|&&v| v).count(); + + rsx! { + document::Link { rel: "stylesheet", href: asset!("../../style.css") } + + div { class: "dx-table-wrapper", + + // Toolbar + div { class: "dx-table-toolbar", + input { + class: "dx-table-filter", + r#type: "text", + placeholder: "Filter emails...", + value: "{filter}", + oninput: move |e| filter.set(e.value()), + } + + div { style: "position: relative;", + button { + class: "dx-columns-btn", + onclick: move |_| { + let current = *show_columns_menu.read(); + show_columns_menu.set(!current); + }, + "Columns " + span { class: "dx-chevron", "⌄" } + } + + if *show_columns_menu.read() { + div { class: "dx-columns-menu", + {COLUMNS.iter().enumerate().map(|(i, col)| { + let visible = col_visible.read()[i]; + rsx! { + label { class: "dx-columns-item", + key: "{col.name}", + input { + r#type: "checkbox", + class: "dx-checkbox", + checked: visible, + onchange: move |e| { + col_visible.write()[i] = e.checked(); + } + } + span { "{col.name}" } + } + } + })} + } + } + } + } + + // Table + div { class: "dx-table-container", + table { class: "dx-table", + thead { + tr { + th { + input { + r#type: "checkbox", + class: "dx-checkbox", + onchange: move |e| { + let checked = e.checked(); + selected.write().iter_mut().for_each(|v| *v = checked); + } + } + } + {COLUMNS.iter().enumerate().map(|(i, col)| { + let visible = col_visible.read()[i]; + rsx! { + if visible { + th { key: "{col.name}", "{col.name}" } + } + } + })} + th {} + } + } + tbody { + {filtered.iter().map(|(i, p)| { + let i = *i; + let p: &'static Payment = p; + let is_selected = selected.read()[i]; + let show_menu = active_menu.read().is_some_and(|id| id == p.id); + rsx! { + tr { + key: "{p.id}", + class: if is_selected { "dx-row dx-row-selected" } else { "dx-row" }, + td { + input { + r#type: "checkbox", + class: "dx-checkbox", + checked: is_selected, + onchange: move |e| { + selected.write()[i] = e.checked(); + } + } + } + {COLUMNS.iter().enumerate().map(|(ci, col)| { + let visible = col_visible.read()[ci]; + let value = (col.get)(p); + rsx! { + if visible { + td { key: "{col.name}", "{value}" } + } + } + })} + td { + div { style: "position: relative;", + button { + class: "dx-action-btn", + onclick: move |_| { + if active_menu.read().is_some_and(|id| id == p.id) { + active_menu.set(None); + } else { + active_menu.set(Some(p.id)); + } + }, + "···" + } + if show_menu { + div { + class: "dx-action-menu", + onclick: move |_| { active_menu.set(None); }, + button { "Copy payment ID" } + button { "View customer" } + button { "View payment details" } + } + } + } + } + } + } + })} + } + } + } + + // Footer + div { class: "dx-table-footer", + span { class: "dx-footer-info", + "{selected_count} of {DATA.len()} row(s) selected." + } + div { class: "dx-pagination-buttons", + button { class: "dx-pagination-button", "Previous" } + button { class: "dx-pagination-button", "Next" } + } + } + } + + if active_menu.read().is_some() { + div { + class: "dx-menu-overlay", + onclick: move |_| active_menu.set(None), + } + } + if *show_columns_menu.read() { + div { + class: "dx-menu-overlay", + onclick: move |_| show_columns_menu.set(false), + } + } + } +} \ No newline at end of file