From 316241e25e52e639e986a000c777be29b98485c2 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 24 Dec 2025 12:05:05 +0300 Subject: [PATCH 1/3] [*] Badge: initial implementation --- README.md | 125 ++++++++++-------- component.json | 3 +- preview/src/components/avatar/component.rs | 21 ++- preview/src/components/avatar/style.css | 12 +- preview/src/components/badge/component.json | 13 ++ preview/src/components/badge/component.rs | 18 +++ preview/src/components/badge/docs.md | 9 ++ preview/src/components/badge/mod.rs | 2 + preview/src/components/badge/style.css | 42 ++++++ .../src/components/badge/variants/main/mod.rs | 71 ++++++++++ preview/src/components/mod.rs | 1 + primitives/src/badge.rs | 80 +++++++++++ primitives/src/date_picker.rs | 14 +- primitives/src/lib.rs | 1 + 14 files changed, 348 insertions(+), 64 deletions(-) create mode 100644 preview/src/components/badge/component.json create mode 100644 preview/src/components/badge/component.rs create mode 100644 preview/src/components/badge/docs.md create mode 100644 preview/src/components/badge/mod.rs create mode 100644 preview/src/components/badge/style.css create mode 100644 preview/src/components/badge/variants/main/mod.rs create mode 100644 primitives/src/badge.rs diff --git a/README.md b/README.md index beab192b..a60346cd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
-

🎲 Dioxus Primitives 🧱

-

Accessible, unstyled, foundational components for Dioxus.

+

Dioxus Components

+

Accessible, customizable components for Dioxus.

@@ -25,66 +25,85 @@
-Dioxus primitives is an ARIA-accessible, unstyled, foundational component library for Dioxus based on Radix Primitives. We bring the logic, you bring the styling. - -Building styled and more featured component libraries on top of Dioxus Primitives is encouraged! - -## Here's what we have. - -We're still in the early days - Many components are still being created and stabilized. - -31/31 - -- [x] Accordion -- [x] Alert Dialog -- [x] Aspect Ratio -- [x] Avatar -- [x] Calendar -- [x] Card -- [x] Checkbox -- [x] Collapsible -- [x] Context Menu -- [x] Date Picker -- [x] Dialog -- [x] Dropdown Menu -- [x] Hover Card -- [x] Label -- [x] Menubar -- [x] Navigation Menu -- [x] Popover -- [x] Progress -- [x] Radio Group -- [x] Scroll Area -- [x] Select -- [x] Separator -- [x] Sheet -- [x] Slider -- [x] Switch -- [x] Tabs -- [x] Textarea -- [x] Toast -- [x] Toggle -- [x] Toggle Group -- [x] Toolbar -- [x] Tooltip - -## Running the preview. - -You can run the `preview` app with: +Dioxus Components is a shadcn style component library for Dioxus built on top of the unstyled [Dioxus primitives](https://crates.io/crates/dioxus-primitives) library. The unstyled primitives serve as the foundation for building accessible and customizable UI components in Dioxus applications. The styled versions serve as a starting point to develop your own design system. + +## Getting started + +First, explore the [component gallery](https://dioxuslabs.github.io/components/) to find the components you want to use. + +Once you find a component, you can add it to your project with the Dioxus CLI. If you don't already have `dx` installed, you can do so with: ``` -cargo run -p preview --features desktop +cargo install dioxus-cli ``` -or for the web build +Then, you can add a component to your project with: ``` -cargo binstall dioxus-cli -y --force --version 0.7.0 -dx run -p preview --web +dx components add button +``` + +This will create a `components` folder in your project (if it doesn't already exist) and add the `Button` component files to it. If this is your first time adding a component, it will also prompt you to add a link to `/assets/dx-components.css` at the root of your app to provide the theme for your app. + +## Contributing + +### Project structure + +This repository contains two main crates: +- `dioxus-primitives`: The core unstyled component library. +- `preview`: A Dioxus application that showcases the components from `dioxus-primitives` with shadcn-styled versions. + +### Adding new components + +If you want to add a new component, you should: +1. If there is any new interaction logic or accessibility features required, implement an unstyled component in the `dioxus-primitives` crate. When adding components to the primitives library, ensure: + - It adheres to the [WAI-ARIA Authoring Practices for accessibility](https://www.w3.org/WAI/standards-guidelines/aria/). + - All styling can be modified via props. Every element should spread attributes and children from the props +2. In the `preview` crate, create a styled version of the component using shadcn styles. This will serve as an example of how to use the unstyled component and serve as the styled version `dx components` will add to projects. +3. Add tests in `playwright` to ensure the component behaves as expected. + +### Testing changes + +The components use a combination of unit tests with cargo, css linting, and end-to-end tests with Playwright. + +To run the unit tests for the `dioxus-primitives` crate, use: + +```sh +cargo test -p dioxus-primitives +``` + +To run the CSS linting, use: + +```sh +cd preview +npm install +npx stylelint "src/**/*.css" +``` + +To run the Playwright end-to-end tests, use: + +```sh +cd preview +npm install +npx playwright test +``` + +### Running the preview + +To test your changes, you can run the preview application. For a desktop build, use: + +```sh +dx serve -p preview --desktop +``` + +or for the web build: + +```sh +dx serve -p preview --web ``` ## License This project is dual licensed under the [MIT](./LICENSE-MIT) and [Apache 2.0](./LICENSE-APACHE) licenses. -Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this repository, by you, shall be licensed as MIT or Apache 2.0, without any additional terms or conditions. +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this repository, by you, shall be licensed as MIT or Apache 2.0, without any additional terms or conditions. \ No newline at end of file diff --git a/component.json b/component.json index c84d50e3..4e8cbadc 100644 --- a/component.json +++ b/component.json @@ -37,6 +37,7 @@ "preview/src/components/textarea", "preview/src/components/skeleton", "preview/src/components/card", - "preview/src/components/sheet" + "preview/src/components/sheet", + "preview/src/components/badge" ] } diff --git a/preview/src/components/avatar/component.rs b/preview/src/components/avatar/component.rs index ddb43d77..8cf019ba 100644 --- a/preview/src/components/avatar/component.rs +++ b/preview/src/components/avatar/component.rs @@ -19,6 +19,22 @@ impl AvatarImageSize { } } +#[derive(Clone, Copy, PartialEq, Default)] +pub enum AvatarShape { + #[default] + Circle, + Rounded, +} + +impl AvatarShape { + fn to_class(self) -> &'static str { + match self { + AvatarShape::Circle => "avatar-circle", + AvatarShape::Rounded => "avatar-rounded", + } + } +} + /// The props for the [`Avatar`] component. #[derive(Props, Clone, PartialEq)] pub struct AvatarProps { @@ -37,6 +53,9 @@ pub struct AvatarProps { #[props(default)] pub size: AvatarImageSize, + #[props(default)] + pub shape: AvatarShape, + /// Additional attributes for the avatar element #[props(extends = GlobalAttributes)] pub attributes: Vec, @@ -51,7 +70,7 @@ pub fn Avatar(props: AvatarProps) -> Element { document::Link { rel: "stylesheet", href: asset!("./style.css") } avatar::Avatar { - class: "avatar {props.size.to_class()}", + class: "avatar {props.size.to_class()} {props.shape.to_class()}", on_load: props.on_load, on_error: props.on_error, on_state_change: props.on_state_change, diff --git a/preview/src/components/avatar/style.css b/preview/src/components/avatar/style.css index 2d487dc1..d239e02e 100644 --- a/preview/src/components/avatar/style.css +++ b/preview/src/components/avatar/style.css @@ -21,7 +21,6 @@ flex-shrink: 0; align-items: center; justify-content: center; - border-radius: 3.40282e+38px; color: var(--secondary-color-4); cursor: pointer; font-weight: 500; @@ -52,13 +51,22 @@ font-size: 1.75rem; } +/* Avatar shape */ +.avatar-circle { + border-radius: 50%; +} + +.avatar-rounded { + border-radius: 8px; +} + /* State-specific styles */ .avatar[data-state="loading"] { animation: pulse 1.5s infinite ease-in-out; } .avatar[data-state="empty"] { - background: var(--primary-color-2); + background: var(--primary-color-7); } @keyframes pulse { diff --git a/preview/src/components/badge/component.json b/preview/src/components/badge/component.json new file mode 100644 index 00000000..e061a270 --- /dev/null +++ b/preview/src/components/badge/component.json @@ -0,0 +1,13 @@ +{ + "name": "Badge", + "description": "Show notifications, counts or status information on its children", + "authors": ["Evan Almloff"], + "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/badge/component.rs b/preview/src/components/badge/component.rs new file mode 100644 index 00000000..f774b632 --- /dev/null +++ b/preview/src/components/badge/component.rs @@ -0,0 +1,18 @@ +use dioxus::prelude::*; +use dioxus_primitives::badge::{self, BadgeProps}; + +#[component] +pub fn Badge(props: BadgeProps) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + + badge::Badge { + count: props.count, + overflow_count: props.overflow_count, + dot: props.dot, + show_zero: props.show_zero, + attributes: props.attributes, + {props.children} + } + } +} diff --git a/preview/src/components/badge/docs.md b/preview/src/components/badge/docs.md new file mode 100644 index 00000000..912f7709 --- /dev/null +++ b/preview/src/components/badge/docs.md @@ -0,0 +1,9 @@ +Badges are used as a small numerical value or status descriptor for its children elements. + +## Component Structure + +```rust +Badge { + {children} +} +``` \ No newline at end of file diff --git a/preview/src/components/badge/mod.rs b/preview/src/components/badge/mod.rs new file mode 100644 index 00000000..9a8ae556 --- /dev/null +++ b/preview/src/components/badge/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; \ No newline at end of file diff --git a/preview/src/components/badge/style.css b/preview/src/components/badge/style.css new file mode 100644 index 00000000..7ef4e808 --- /dev/null +++ b/preview/src/components/badge/style.css @@ -0,0 +1,42 @@ +.badge-example { + display: flex; + flex-direction: row; + align-items: center; + justify-content: between; + gap: 1rem; +} + +.badge-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +.badge-label { + color: var(--secondary-color-4); + font-size: 0.875rem; +} + +.badge { + position: absolute; + display: inline-flex; + justify-content: center; + align-items: center; + min-width: 20px; + height: 20px; + font-size: 12px; + background: var(--highlight-color-secondary); + border-radius: 10px; + box-shadow: 0 0 0 1px var(--primary-color-2); + transform: translate(-50%, -50%); +} + +.badge[padding="true"] { + padding: 0 8px; +} + +.badge[dot="true"] { + min-width: 8px; + height: 8px; +} \ No newline at end of file diff --git a/preview/src/components/badge/variants/main/mod.rs b/preview/src/components/badge/variants/main/mod.rs new file mode 100644 index 00000000..1ffcead4 --- /dev/null +++ b/preview/src/components/badge/variants/main/mod.rs @@ -0,0 +1,71 @@ +use dioxus::prelude::*; + +use super::super::component::*; +use crate::components::avatar::*; + +#[component] +pub fn Demo() -> Element { + rsx! { + div { + class: "badge-example", + + div { + class: "badge-item", + p { class: "badge-label", "Basic Usage" } + Badge { + count: 5, + Avatar { + size: AvatarImageSize::Medium, + shape: AvatarShape::Rounded, + aria_label: "Space item", + } + } + } + + div { + class: "badge-item", + p { class: "badge-label", "Show Zero" } + + Badge { + count: 0, + show_zero: true, + Avatar { + size: AvatarImageSize::Medium, + shape: AvatarShape::Rounded, + aria_label: "Space item", + } + } + } + + div { + class: "badge-item", + p { class: "badge-label", "Overflow Count" } + + Badge { + count: 100, + overflow_count: 99, + Avatar { + size: AvatarImageSize::Medium, + shape: AvatarShape::Rounded, + aria_label: "Space item", + } + } + } + + div { + class: "badge-item", + p { class: "badge-label", "As Dot" } + + Badge { + count: 5, + dot: true, + Avatar { + size: AvatarImageSize::Medium, + shape: AvatarShape::Rounded, + aria_label: "Space item", + } + } + } + } + } +} diff --git a/preview/src/components/mod.rs b/preview/src/components/mod.rs index a2195d26..c19f0f54 100644 --- a/preview/src/components/mod.rs +++ b/preview/src/components/mod.rs @@ -61,6 +61,7 @@ examples!( alert_dialog, aspect_ratio, avatar, + badge, button, calendar[simple, internationalized, range, multi_month, unavailable_dates], checkbox, diff --git a/primitives/src/badge.rs b/primitives/src/badge.rs new file mode 100644 index 00000000..8921445b --- /dev/null +++ b/primitives/src/badge.rs @@ -0,0 +1,80 @@ +//! Defines the [`Badge`] component + +use dioxus::prelude::*; + +/// The props for the [`Badge`] component. +#[derive(Props, Clone, PartialEq)] +pub struct BadgeProps { + /// Number to show in badge + pub count: u32, + + /// Max count to show + #[props(default = u32::MAX)] + pub overflow_count: u32, + + /// Whether to display a dot instead of count + #[props(default = false)] + pub dot: bool, + + /// Whether to show badge when count is zero + #[props(default = false)] + pub show_zero: bool, + + /// Additional attributes to extend the badge element + #[props(extends = GlobalAttributes)] + pub attributes: Vec, + + /// The children of the badge element + pub children: Element, +} + +/// # Badge +/// +/// The [`Badge`] component displays a small badge to the top-right of its child(ren). +/// +/// ## Example +/// ```rust +/// use dioxus::prelude::*; +/// use dioxus_primitives::badge::Badge; +/// use dioxus_primitives::avatar::*; +/// #[component] +/// fn Demo() -> Element { +/// rsx! { +/// Badge { +/// count: 100, +/// overflow_count: 99, +/// Avatar { +/// aria_label: "Space item", +/// } +/// } +/// } +/// } +/// ``` +#[component] +pub fn Badge(props: BadgeProps) -> Element { + let text = if props.dot { + String::default() + } else if props.overflow_count < props.count { + format!("{}+", props.overflow_count) + } else { + format!("{}", props.count) + }; + + let add_padding = text.chars().count() > 1; + + rsx! { + span { + {props.children} + + if props.count > 0 || props.show_zero { + span { + class: "badge", + "padding": if add_padding { true }, + "dot": if props.dot { true }, + ..props.attributes, + {text} + } + } + } + } +} diff --git a/primitives/src/date_picker.rs b/primitives/src/date_picker.rs index f9355c82..b1b0ec78 100644 --- a/primitives/src/date_picker.rs +++ b/primitives/src/date_picker.rs @@ -103,7 +103,7 @@ pub struct DatePickerProps { /// use dioxus_primitives::{calendar::Calendar, date_picker::*, popover::*, ContentAlign}; /// use time::Date; /// #[component] -/// pub fn Demo() -> Element { +/// fn Demo() -> Element { /// let mut selected_date = use_signal(|| None::); /// rsx! { /// div { @@ -242,7 +242,7 @@ pub struct DateRangePickerProps { /// use dioxus::prelude::*; /// use dioxus_primitives::{calendar::{DateRange, RangeCalendar}, date_picker::*, popover::*, ContentAlign}; /// #[component] -/// pub fn Demo() -> Element { +/// fn Demo() -> Element { /// let mut selected_range = use_signal(|| None::); /// rsx! { /// div { @@ -350,7 +350,7 @@ pub struct DatePickerPopoverProps { /// use dioxus_primitives::{calendar::Calendar, date_picker::*, popover::*, ContentAlign}; /// use time::Date; /// #[component] -/// pub fn Demo() -> Element { +/// fn Demo() -> Element { /// let mut selected_date = use_signal(|| None::); /// rsx! { /// div { @@ -473,7 +473,7 @@ pub struct DatePickerCalendarProps Element { +/// fn Demo() -> Element { /// let mut selected_date = use_signal(|| None::); /// rsx! { /// div { @@ -550,7 +550,7 @@ pub fn DatePickerCalendar(props: DatePickerCalendarProps) -> Elem /// use dioxus::prelude::*; /// use dioxus_primitives::{calendar::{DateRange, RangeCalendar}, date_picker::*, popover::*, ContentAlign}; /// #[component] -/// pub fn Demo() -> Element { +/// fn Demo() -> Element { /// let mut selected_range = use_signal(|| None::); /// rsx! { /// div { @@ -1010,7 +1010,7 @@ pub struct DatePickerInputProps { /// use dioxus_primitives::{calendar::Calendar, date_picker::*, popover::*, ContentAlign}; /// use time::Date; /// #[component] -/// pub fn Demo() -> Element { +/// fn Demo() -> Element { /// let mut selected_date = use_signal(|| None::); /// rsx! { /// div { @@ -1069,7 +1069,7 @@ pub fn DatePickerInput(props: DatePickerInputProps) -> Element { /// use dioxus::prelude::*; /// use dioxus_primitives::{calendar::{DateRange, RangeCalendar}, date_picker::*, popover::*, ContentAlign}; /// #[component] -/// pub fn Demo() -> Element { +/// fn Demo() -> Element { /// let mut selected_range = use_signal(|| None::); /// rsx! { /// div { diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index b58fa05f..c0c50bdf 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -13,6 +13,7 @@ pub mod accordion; pub mod alert_dialog; pub mod aspect_ratio; pub mod avatar; +pub mod badge; pub mod calendar; pub mod checkbox; pub mod collapsible; From 7c209e569b01e566e695d5a4ab4a596feeccc406 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 25 Dec 2025 16:15:19 +0300 Subject: [PATCH 2/3] [*] Badge: support custom color --- preview/src/components/badge/component.rs | 1 + preview/src/components/badge/docs.md | 1 + preview/src/components/badge/style.css | 2 +- .../src/components/badge/variants/main/mod.rs | 29 ++++++++++++++----- primitives/src/badge.rs | 16 ++++++++-- 5 files changed, 39 insertions(+), 10 deletions(-) diff --git a/preview/src/components/badge/component.rs b/preview/src/components/badge/component.rs index f774b632..1d19e3ad 100644 --- a/preview/src/components/badge/component.rs +++ b/preview/src/components/badge/component.rs @@ -11,6 +11,7 @@ pub fn Badge(props: BadgeProps) -> Element { overflow_count: props.overflow_count, dot: props.dot, show_zero: props.show_zero, + color: props.color, attributes: props.attributes, {props.children} } diff --git a/preview/src/components/badge/docs.md b/preview/src/components/badge/docs.md index 912f7709..4c96fef8 100644 --- a/preview/src/components/badge/docs.md +++ b/preview/src/components/badge/docs.md @@ -1,4 +1,5 @@ Badges are used as a small numerical value or status descriptor for its children elements. +Badge will be hidden when count is 0, but we can use show_zero to show it. ## Component Structure diff --git a/preview/src/components/badge/style.css b/preview/src/components/badge/style.css index 7ef4e808..a1a6f595 100644 --- a/preview/src/components/badge/style.css +++ b/preview/src/components/badge/style.css @@ -26,7 +26,7 @@ min-width: 20px; height: 20px; font-size: 12px; - background: var(--highlight-color-secondary); + background-color: var(--badge-color); border-radius: 10px; box-shadow: 0 0 0 1px var(--primary-color-2); transform: translate(-50%, -50%); diff --git a/preview/src/components/badge/variants/main/mod.rs b/preview/src/components/badge/variants/main/mod.rs index 1ffcead4..6bfd5a85 100644 --- a/preview/src/components/badge/variants/main/mod.rs +++ b/preview/src/components/badge/variants/main/mod.rs @@ -8,10 +8,10 @@ pub fn Demo() -> Element { rsx! { div { class: "badge-example", - - div { + + div { class: "badge-item", - p { class: "badge-label", "Basic Usage" } + p { class: "badge-label", "Basic" } Badge { count: 5, Avatar { @@ -22,7 +22,7 @@ pub fn Demo() -> Element { } } - div { + div { class: "badge-item", p { class: "badge-label", "Show Zero" } @@ -37,9 +37,9 @@ pub fn Demo() -> Element { } } - div { + div { class: "badge-item", - p { class: "badge-label", "Overflow Count" } + p { class: "badge-label", "Overflow" } Badge { count: 100, @@ -52,7 +52,22 @@ pub fn Demo() -> Element { } } - div { + div { + class: "badge-item", + p { class: "badge-label", "Colorful" } + + Badge { + count: 7, + color: String::from("52c41a"), + Avatar { + size: AvatarImageSize::Medium, + shape: AvatarShape::Rounded, + aria_label: "Space item", + } + } + } + + div { class: "badge-item", p { class: "badge-label", "As Dot" } diff --git a/primitives/src/badge.rs b/primitives/src/badge.rs index 8921445b..f96501f8 100644 --- a/primitives/src/badge.rs +++ b/primitives/src/badge.rs @@ -2,6 +2,8 @@ use dioxus::prelude::*; +const DEF_COLOR: &str = "EB5160"; + /// The props for the [`Badge`] component. #[derive(Props, Clone, PartialEq)] pub struct BadgeProps { @@ -20,6 +22,10 @@ pub struct BadgeProps { #[props(default = false)] pub show_zero: bool, + /// Customize Badge color (as HEX) + #[props(default = String::from(DEF_COLOR))] + pub color: String, + /// Additional attributes to extend the badge element #[props(extends = GlobalAttributes)] pub attributes: Vec, @@ -40,8 +46,8 @@ pub struct BadgeProps { /// #[component] /// fn Demo() -> Element { /// rsx! { -/// Badge { -/// count: 100, +/// Badge { +/// count: 100, /// overflow_count: 99, /// Avatar { /// aria_label: "Space item", @@ -61,6 +67,11 @@ pub fn Badge(props: BadgeProps) -> Element { }; let add_padding = text.chars().count() > 1; + let color = if u32::from_str_radix(&props.color, 16).is_ok() { + props.color + } else { + DEF_COLOR.to_string() + }; rsx! { span { @@ -69,6 +80,7 @@ pub fn Badge(props: BadgeProps) -> Element { if props.count > 0 || props.show_zero { span { class: "badge", + style: "--badge-color: #{color}", "padding": if add_padding { true }, "dot": if props.dot { true }, ..props.attributes, From 8bfd2e165644c453f6a332d43505b1bec01c286c Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 5 Jan 2026 23:45:31 +0300 Subject: [PATCH 3/3] [*] Badge: fix stylelint --- preview/src/components/badge/style.css | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/preview/src/components/badge/style.css b/preview/src/components/badge/style.css index a1a6f595..c49b72a4 100644 --- a/preview/src/components/badge/style.css +++ b/preview/src/components/badge/style.css @@ -2,7 +2,6 @@ display: flex; flex-direction: row; align-items: center; - justify-content: between; gap: 1rem; } @@ -21,14 +20,14 @@ .badge { position: absolute; display: inline-flex; - justify-content: center; - align-items: center; min-width: 20px; height: 20px; - font-size: 12px; - background-color: var(--badge-color); + align-items: center; + justify-content: center; border-radius: 10px; + background-color: var(--badge-color); box-shadow: 0 0 0 1px var(--primary-color-2); + font-size: 12px; transform: translate(-50%, -50%); }