From 316241e25e52e639e986a000c777be29b98485c2 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 24 Dec 2025 12:05:05 +0300 Subject: [PATCH 1/7] [*] 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/7] [*] 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/7] [*] 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%); } From d251395fd681eb2c1d7fa2d96dfe5c700f3a0f12 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 16 Jan 2026 18:34:05 +0300 Subject: [PATCH 4/7] [*] Badge: refact after review --- preview/src/components/badge/component.rs | 158 +++++++++++++++++- preview/src/components/badge/docs.md | 3 +- preview/src/components/badge/style.css | 29 +++- .../src/components/badge/variants/main/mod.rs | 86 ++-------- .../components/badge/variants/notify/mod.rs | 85 ++++++++++ preview/src/components/mod.rs | 2 +- primitives/src/badge.rs | 92 ---------- primitives/src/lib.rs | 1 - 8 files changed, 275 insertions(+), 181 deletions(-) create mode 100644 preview/src/components/badge/variants/notify/mod.rs delete mode 100644 primitives/src/badge.rs diff --git a/preview/src/components/badge/component.rs b/preview/src/components/badge/component.rs index 1d19e3ad..49caaf0e 100644 --- a/preview/src/components/badge/component.rs +++ b/preview/src/components/badge/component.rs @@ -1,19 +1,163 @@ use dioxus::prelude::*; -use dioxus_primitives::badge::{self, BadgeProps}; + +#[derive(Copy, Clone, PartialEq, Default)] +#[non_exhaustive] +pub enum BadgeVariant { + #[default] + Primary, + Secondary, + Destructive, + Outline, +} + +impl BadgeVariant { + pub fn class(&self) -> &'static str { + match self { + BadgeVariant::Primary => "primary", + BadgeVariant::Secondary => "secondary", + BadgeVariant::Destructive => "destructive", + BadgeVariant::Outline => "outline", + } + } +} + +/// The props for the [`Badge`] component. +#[derive(Props, Clone, PartialEq)] +pub struct BadgeProps { + #[props(default)] + pub variant: BadgeVariant, + + /// Additional attributes to extend the badge element + #[props(extends = GlobalAttributes)] + pub attributes: Vec, + + /// The children of the badge element + pub children: Element, +} #[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, - color: props.color, + BadgeElement { + "padding": true, + variant: props.variant, attributes: props.attributes, {props.children} } } } + +/// The props for the [`NotifyBadge`] component. +#[derive(Props, Clone, PartialEq)] +pub struct BadgeNotifyProps { + /// 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, + + #[props(default = BadgeVariant::Destructive)] + pub variant: BadgeVariant, + + /// Additional attributes to extend the badge element + #[props(extends = GlobalAttributes)] + pub attributes: Vec, + + /// The children of the badge element + pub children: Element, +} + +#[component] +pub fn NotifyBadge(props: BadgeNotifyProps) -> 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! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + + span { + {props.children} + + if props.count > 0 || props.show_zero { + BadgeElement { + class: "badge", + "padding": if add_padding { true }, + "dot": if props.dot { true }, + "notify": true, + variant: props.variant, + attributes: props.attributes, + {text} + } + } + } + } +} + +#[component] +fn BadgeElement(props: BadgeProps) -> Element { + rsx! { + span { + class: "badge", + "data-style": props.variant.class(), + ..props.attributes, + {props.children} + } + } +} + +#[component] +pub fn CardIcon() -> Element { + rsx! { + svg { + view_box: "0 0 24 24", + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + fill: "none", + stroke: "var(--secondary-color-4)", + stroke_linecap: "round", + stroke_linejoin: "round", + stroke_width: 2, + circle { cx: 8, cy: 21, r: 1 } + circle { cx: 19, cy: 21, r: 1 } + path { d: "M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12" } + } + } +} + +#[component] +pub fn VerifiedIcon() -> Element { + rsx! { + svg { + view_box: "0 0 24 24", + xmlns: "http://www.w3.org/2000/svg", + width: "12", + height: "12", + fill: "none", + stroke: "var(--secondary-color-4)", + stroke_linecap: "round", + stroke_linejoin: "round", + stroke_width: 2, + path { d: "M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z" } + path { d: "m9 12 2 2 4-4" } + } + } +} diff --git a/preview/src/components/badge/docs.md b/preview/src/components/badge/docs.md index 4c96fef8..ba1638ef 100644 --- a/preview/src/components/badge/docs.md +++ b/preview/src/components/badge/docs.md @@ -1,5 +1,4 @@ -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. +The Badge is a component designed to display small, distinct labels that help highlight important content or status indicators. Perfect for use cases like notifications, status labels, or categorization. ## Component Structure diff --git a/preview/src/components/badge/style.css b/preview/src/components/badge/style.css index c49b72a4..45615e28 100644 --- a/preview/src/components/badge/style.css +++ b/preview/src/components/badge/style.css @@ -1,6 +1,5 @@ .badge-example { display: flex; - flex-direction: row; align-items: center; gap: 1rem; } @@ -18,16 +17,19 @@ } .badge { - position: absolute; display: inline-flex; min-width: 20px; height: 20px; 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; + gap: 4px +} + +.badge[notify="true"] { + position: absolute; transform: translate(-50%, -50%); } @@ -38,4 +40,25 @@ .badge[dot="true"] { min-width: 8px; height: 8px; +} + +.badge[data-style="primary"] { + background-color: var(--secondary-color-2); + color: var(--primary-color); +} + +.badge[data-style="secondary"] { + background-color: var(--primary-color-5); + color: var(--secondary-color-1); +} + +.badge[data-style="outline"] { + border: 1px solid var(--primary-color-6); + background-color: var(--light, var(--primary-color)) var(--dark, var(--primary-color-3)); + color: var(--secondary-color-4); +} + +.badge[data-style="destructive"] { + background-color: var(--primary-error-color); + color: var(--contrast-error-color); } \ 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 index 6bfd5a85..cf5deb9e 100644 --- a/preview/src/components/badge/variants/main/mod.rs +++ b/preview/src/components/badge/variants/main/mod.rs @@ -1,85 +1,21 @@ 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" } - 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" } - - 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", "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" } - - Badge { - count: 5, - dot: true, - Avatar { - size: AvatarImageSize::Medium, - shape: AvatarShape::Rounded, - aria_label: "Space item", - } - } + div { class: "badge-example", + + Badge { "Primary" } + Badge { variant: BadgeVariant::Secondary, "Secondary" } + Badge { variant: BadgeVariant::Destructive, "Destructive" } + Badge { variant: BadgeVariant::Outline, "Outline" } + Badge { + variant: BadgeVariant::Secondary, + style: "background-color: var(--focused-border-color)", + VerifiedIcon {} + "Verified" } } } diff --git a/preview/src/components/badge/variants/notify/mod.rs b/preview/src/components/badge/variants/notify/mod.rs new file mode 100644 index 00000000..09f7bd8c --- /dev/null +++ b/preview/src/components/badge/variants/notify/mod.rs @@ -0,0 +1,85 @@ +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" } + NotifyBadge { + count: 5, + Avatar { + size: AvatarImageSize::Medium, + shape: AvatarShape::Rounded, + aria_label: "Space item", + CardIcon {} + } + } + } + + div { class: "badge-item", + p { class: "badge-label", "Show Zero" } + + NotifyBadge { + count: 0, + show_zero: true, + Avatar { + size: AvatarImageSize::Medium, + shape: AvatarShape::Rounded, + aria_label: "Space item", + CardIcon {} + } + } + } + + div { class: "badge-item", + p { class: "badge-label", "Overflow" } + + NotifyBadge { + count: 100, + overflow_count: 99, + Avatar { + size: AvatarImageSize::Medium, + shape: AvatarShape::Rounded, + aria_label: "Space item", + CardIcon {} + } + } + } + + div { class: "badge-item", + p { class: "badge-label", "Colorful" } + + NotifyBadge { + count: 7, + style: "background-color: var(--highlight-color-main)", + Avatar { + size: AvatarImageSize::Medium, + shape: AvatarShape::Rounded, + aria_label: "Space item", + CardIcon {} + } + } + } + + div { class: "badge-item", + p { class: "badge-label", "As Dot" } + + NotifyBadge { + count: 5, + dot: true, + Avatar { + size: AvatarImageSize::Medium, + shape: AvatarShape::Rounded, + aria_label: "Space item", + CardIcon {} + } + } + } + } + } +} diff --git a/preview/src/components/mod.rs b/preview/src/components/mod.rs index c19f0f54..bf6ed9f4 100644 --- a/preview/src/components/mod.rs +++ b/preview/src/components/mod.rs @@ -61,7 +61,7 @@ examples!( alert_dialog, aspect_ratio, avatar, - badge, + badge[notify], button, calendar[simple, internationalized, range, multi_month, unavailable_dates], checkbox, diff --git a/primitives/src/badge.rs b/primitives/src/badge.rs deleted file mode 100644 index f96501f8..00000000 --- a/primitives/src/badge.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! Defines the [`Badge`] component - -use dioxus::prelude::*; - -const DEF_COLOR: &str = "EB5160"; - -/// 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, - - /// 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, - - /// 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; - let color = if u32::from_str_radix(&props.color, 16).is_ok() { - props.color - } else { - DEF_COLOR.to_string() - }; - - rsx! { - span { - {props.children} - - 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, - {text} - } - } - } - } -} diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index c0c50bdf..b58fa05f 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -13,7 +13,6 @@ 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 a832e5aa5f8cc9fa81a19ec22edbf8dfdf575065 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 26 Jan 2026 15:58:41 -0600 Subject: [PATCH 5/7] remove the notification aspect --- .../components/avatar/variants/main/mod.rs | 19 ++++- preview/src/components/badge/component.json | 2 +- preview/src/components/badge/component.rs | 82 ------------------ preview/src/components/badge/style.css | 22 ----- .../components/badge/variants/notify/mod.rs | 85 ------------------- preview/src/components/mod.rs | 2 +- 6 files changed, 20 insertions(+), 192 deletions(-) delete mode 100644 preview/src/components/badge/variants/notify/mod.rs diff --git a/preview/src/components/avatar/variants/main/mod.rs b/preview/src/components/avatar/variants/main/mod.rs index 3674b993..7c69da77 100644 --- a/preview/src/components/avatar/variants/main/mod.rs +++ b/preview/src/components/avatar/variants/main/mod.rs @@ -27,12 +27,29 @@ pub fn Demo() -> Element { AvatarFallback { class: "avatar-fallback", "EA" } } } + div { class: "avatar-item", + p { class: "avatar-label", "Rounded" } + Avatar { + size: AvatarImageSize::Small, + shape: AvatarShape::Rounded, + on_state_change: move |state| { + avatar_state.set(format!("Avatar 2: {state:?}")); + }, + aria_label: "Basic avatar", + AvatarImage { + class: "avatar-image", + src: "https://avatars.githubusercontent.com/u/66571940?s=96&v=4", + alt: "User avatar", + } + AvatarFallback { class: "avatar-fallback", "EA" } + } + } div { class: "avatar-item", p { class: "avatar-label", "Error State" } Avatar { size: AvatarImageSize::Medium, on_state_change: move |state| { - avatar_state.set(format!("Avatar 2: {state:?}")); + avatar_state.set(format!("Avatar 3: {state:?}")); }, aria_label: "Error avatar", AvatarImage { diff --git a/preview/src/components/badge/component.json b/preview/src/components/badge/component.json index e061a270..39d79dfa 100644 --- a/preview/src/components/badge/component.json +++ b/preview/src/components/badge/component.json @@ -1,6 +1,6 @@ { "name": "Badge", - "description": "Show notifications, counts or status information on its children", + "description": "A small label to display status or categorization", "authors": ["Evan Almloff"], "exclude": ["variants", "docs.md", "component.json"], "cargoDependencies": [ diff --git a/preview/src/components/badge/component.rs b/preview/src/components/badge/component.rs index 49caaf0e..cd078aac 100644 --- a/preview/src/components/badge/component.rs +++ b/preview/src/components/badge/component.rs @@ -49,68 +49,6 @@ pub fn Badge(props: BadgeProps) -> Element { } } -/// The props for the [`NotifyBadge`] component. -#[derive(Props, Clone, PartialEq)] -pub struct BadgeNotifyProps { - /// 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, - - #[props(default = BadgeVariant::Destructive)] - pub variant: BadgeVariant, - - /// Additional attributes to extend the badge element - #[props(extends = GlobalAttributes)] - pub attributes: Vec, - - /// The children of the badge element - pub children: Element, -} - -#[component] -pub fn NotifyBadge(props: BadgeNotifyProps) -> 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! { - document::Link { rel: "stylesheet", href: asset!("./style.css") } - - span { - {props.children} - - if props.count > 0 || props.show_zero { - BadgeElement { - class: "badge", - "padding": if add_padding { true }, - "dot": if props.dot { true }, - "notify": true, - variant: props.variant, - attributes: props.attributes, - {text} - } - } - } - } -} - #[component] fn BadgeElement(props: BadgeProps) -> Element { rsx! { @@ -123,26 +61,6 @@ fn BadgeElement(props: BadgeProps) -> Element { } } -#[component] -pub fn CardIcon() -> Element { - rsx! { - svg { - view_box: "0 0 24 24", - xmlns: "http://www.w3.org/2000/svg", - width: "24", - height: "24", - fill: "none", - stroke: "var(--secondary-color-4)", - stroke_linecap: "round", - stroke_linejoin: "round", - stroke_width: 2, - circle { cx: 8, cy: 21, r: 1 } - circle { cx: 19, cy: 21, r: 1 } - path { d: "M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12" } - } - } -} - #[component] pub fn VerifiedIcon() -> Element { rsx! { diff --git a/preview/src/components/badge/style.css b/preview/src/components/badge/style.css index 45615e28..e36df538 100644 --- a/preview/src/components/badge/style.css +++ b/preview/src/components/badge/style.css @@ -4,18 +4,6 @@ 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 { display: inline-flex; min-width: 20px; @@ -28,20 +16,10 @@ gap: 4px } -.badge[notify="true"] { - position: absolute; - transform: translate(-50%, -50%); -} - .badge[padding="true"] { padding: 0 8px; } -.badge[dot="true"] { - min-width: 8px; - height: 8px; -} - .badge[data-style="primary"] { background-color: var(--secondary-color-2); color: var(--primary-color); diff --git a/preview/src/components/badge/variants/notify/mod.rs b/preview/src/components/badge/variants/notify/mod.rs deleted file mode 100644 index 09f7bd8c..00000000 --- a/preview/src/components/badge/variants/notify/mod.rs +++ /dev/null @@ -1,85 +0,0 @@ -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" } - NotifyBadge { - count: 5, - Avatar { - size: AvatarImageSize::Medium, - shape: AvatarShape::Rounded, - aria_label: "Space item", - CardIcon {} - } - } - } - - div { class: "badge-item", - p { class: "badge-label", "Show Zero" } - - NotifyBadge { - count: 0, - show_zero: true, - Avatar { - size: AvatarImageSize::Medium, - shape: AvatarShape::Rounded, - aria_label: "Space item", - CardIcon {} - } - } - } - - div { class: "badge-item", - p { class: "badge-label", "Overflow" } - - NotifyBadge { - count: 100, - overflow_count: 99, - Avatar { - size: AvatarImageSize::Medium, - shape: AvatarShape::Rounded, - aria_label: "Space item", - CardIcon {} - } - } - } - - div { class: "badge-item", - p { class: "badge-label", "Colorful" } - - NotifyBadge { - count: 7, - style: "background-color: var(--highlight-color-main)", - Avatar { - size: AvatarImageSize::Medium, - shape: AvatarShape::Rounded, - aria_label: "Space item", - CardIcon {} - } - } - } - - div { class: "badge-item", - p { class: "badge-label", "As Dot" } - - NotifyBadge { - count: 5, - dot: true, - Avatar { - size: AvatarImageSize::Medium, - shape: AvatarShape::Rounded, - aria_label: "Space item", - CardIcon {} - } - } - } - } - } -} diff --git a/preview/src/components/mod.rs b/preview/src/components/mod.rs index f479aaf1..747df071 100644 --- a/preview/src/components/mod.rs +++ b/preview/src/components/mod.rs @@ -61,7 +61,7 @@ examples!( alert_dialog, aspect_ratio, avatar, - badge[notify], + badge, button, calendar[simple, internationalized, range, multi_month, unavailable_dates], checkbox, From b6b9741276e3892cf43ae5296e30cd08f479b718 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 26 Jan 2026 16:01:00 -0600 Subject: [PATCH 6/7] lowercase badge --- preview/src/components/badge/component.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/preview/src/components/badge/component.json b/preview/src/components/badge/component.json index 39d79dfa..792f5c6a 100644 --- a/preview/src/components/badge/component.json +++ b/preview/src/components/badge/component.json @@ -1,5 +1,5 @@ { - "name": "Badge", + "name": "badge", "description": "A small label to display status or categorization", "authors": ["Evan Almloff"], "exclude": ["variants", "docs.md", "component.json"], From 31735d20e7bc7190598a0e1f66996a88f140eee8 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 26 Jan 2026 16:10:28 -0600 Subject: [PATCH 7/7] fix avatar tests --- playwright/avatar.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/playwright/avatar.spec.ts b/playwright/avatar.spec.ts index 4f6238f1..77ad6bcc 100644 --- a/playwright/avatar.spec.ts +++ b/playwright/avatar.spec.ts @@ -8,8 +8,8 @@ test("test", async ({ page }) => { let image = avatar.locator("img"); await expect(image).toHaveAttribute("src", "https://avatars.githubusercontent.com/u/66571940?s=96&v=4"); - // Get the second avatar element - const secondAvatar = page.locator(".avatar-item").nth(1); - // Verify the second avatar has fallback text - await expect(secondAvatar).toContainText("JK"); + // Get the third avatar element (Error State - has invalid image URL, shows fallback) + const errorAvatar = page.locator(".avatar-item").nth(2); + // Verify the error state avatar has fallback text + await expect(errorAvatar).toContainText("JK"); });