├── .github
└── workflows
│ ├── check.yml
│ ├── gh-pages.yml
│ └── preview.yml
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── complaints.md
├── preview
├── Cargo.toml
├── Dioxus.toml
├── assets
│ ├── dioxus-logo.png
│ ├── dioxus_color.svg
│ ├── hero.css
│ ├── main.css
│ ├── prism.css
│ └── prism.js
├── build.rs
└── src
│ ├── components
│ ├── accordion
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── alert_dialog
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── aspect_ratio
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── avatar
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── calendar
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── checkbox
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── context_menu
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── dropdown_menu
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── form
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── hover_card
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── menubar
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── mod.rs
│ ├── progress
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── radio_group
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── scroll_area
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── select
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── separator
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── slider
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── switch
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── tabs
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── toast
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── toggle_group
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ ├── toolbar
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ └── tooltip
│ │ ├── docs.md
│ │ ├── mod.rs
│ │ └── style.css
│ └── main.rs
└── primitives
├── Cargo.toml
└── src
├── accordion.rs
├── alert_dialog.rs
├── aspect_ratio.rs
├── avatar.rs
├── calendar.rs
├── checkbox.rs
├── collapsible.rs
├── context_menu.rs
├── dialog.rs
├── dropdown_menu.rs
├── hover_card.rs
├── label.rs
├── lib.rs
├── menubar.rs
├── portal.rs
├── progress.rs
├── radio_group.rs
├── scroll_area.rs
├── select.rs
├── separator.rs
├── slider.rs
├── switch.rs
├── tabs.rs
├── toast.rs
├── toggle.rs
├── toggle_group.rs
├── toolbar.rs
└── tooltip.rs
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: check examples
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - /**
9 | - preview/**/*.rs
10 | - preview/**/Cargo.toml
11 | - primitives/**/*.rs
12 | - primitives/**/Cargo.toml
13 | - .github/**
14 | - Cargo.toml
15 |
16 | pull_request:
17 | types: [opened, synchronize, reopened, ready_for_review]
18 | branches:
19 | - main
20 | paths:
21 | - /**
22 | - preview/**/*.rs
23 | - preview/**/Cargo.toml
24 | - primitives/**/*.rs
25 | - primitives/**/Cargo.toml
26 | - .github/**
27 | - Cargo.toml
28 |
29 | concurrency:
30 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
31 | cancel-in-progress: true
32 |
33 | jobs:
34 | check:
35 | runs-on: ubuntu-latest
36 | steps:
37 | - uses: awalsh128/cache-apt-pkgs-action@latest
38 | with:
39 | packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev
40 | version: 1.0
41 | - uses: actions/checkout@v4
42 | - uses: dtolnay/rust-toolchain@stable
43 | - uses: Swatinem/rust-cache@v2
44 | with:
45 | cache-all-crates: "true"
46 | cache-on-failure: "false"
47 | cache-directories: "target/dx"
48 | - name: Check
49 | run: cargo check --workspace --all-features
50 |
--------------------------------------------------------------------------------
/.github/workflows/gh-pages.yml:
--------------------------------------------------------------------------------
1 | name: github pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | concurrency:
9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
10 | cancel-in-progress: true
11 |
12 | jobs:
13 | build-deploy:
14 | runs-on: ubuntu-latest
15 | env:
16 | CARGO_INCREMENTAL: 1
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: awalsh128/cache-apt-pkgs-action@latest
20 | with:
21 | packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev
22 | version: 1.0
23 | - name: Install Rust
24 | uses: dtolnay/rust-toolchain@master
25 | with:
26 | toolchain: stable
27 | targets: x86_64-unknown-linux-gnu,wasm32-unknown-unknown
28 | - uses: Swatinem/rust-cache@v2
29 | with:
30 | cache-all-crates: "true"
31 | cache-on-failure: "false"
32 | - uses: cargo-bins/cargo-binstall@main
33 | - name: Install CLI
34 | run: cargo install dioxus-cli --git https://github.com/ealmloff/dioxus --branch fix-bundle
35 | - name: Build
36 | run: cd preview && dx build --platform web --release
37 | - name: Copy output
38 | run: cp -r target/dx/preview/release/web/public docs
39 | - name: Add gh pages 404
40 | run: cp docs/index.html docs/404.html
41 | - name: Deploy 🚀
42 | uses: JamesIves/github-pages-deploy-action@v4.2.3
43 | with:
44 | branch: gh-pages # The branch the action should deploy to.
45 | folder: docs # The folder the action should deploy.
46 | target-folder: .
47 | clean: true
48 | single-commit: true
49 |
--------------------------------------------------------------------------------
/.github/workflows/preview.yml:
--------------------------------------------------------------------------------
1 | name: Deploy PR previews
2 |
3 | on:
4 | pull_request:
5 | types: [opened, synchronize, reopened, ready_for_review]
6 | branches:
7 | - main
8 |
9 | concurrency:
10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | deploy-preview:
15 | runs-on: ubuntu-latest
16 | env:
17 | CARGO_INCREMENTAL: 1
18 | steps:
19 | - uses: actions/checkout@v4
20 | - uses: awalsh128/cache-apt-pkgs-action@latest
21 | with:
22 | packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev
23 | version: 1.0
24 | - name: Install Rust
25 | uses: dtolnay/rust-toolchain@master
26 | with:
27 | toolchain: stable
28 | targets: x86_64-unknown-linux-gnu,wasm32-unknown-unknown
29 | - uses: Swatinem/rust-cache@v2
30 | with:
31 | cache-all-crates: "true"
32 | cache-on-failure: "false"
33 | - uses: cargo-bins/cargo-binstall@main
34 | - name: Install CLI
35 | run: cargo install dioxus-cli --git https://github.com/ealmloff/dioxus --branch fix-bundle
36 | - name: Set base path
37 | run: |
38 | echo "[application]
39 | name = \"Dioxus Preview\"
40 | version = \"0.1.0\"
41 |
42 | [web.app]
43 | base_path = \"components/pr-preview/pr-${{ github.event.pull_request.number }}/\"" > preview/Dioxus.toml
44 | - name: Build
45 | run: cd preview && dx build --platform web --release
46 | - name: Copy output
47 | run: cp -r target/dx/preview/release/web/public docs
48 | - name: Add gh pages 404
49 | run: cp docs/index.html docs/404.html
50 | - name: Deploy preview
51 | uses: rossjrw/pr-preview-action@v1
52 | with:
53 | preview-branch: gh-pages # The branch the action should deploy to.
54 | source-dir: docs # The folder the action should deploy.
55 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 |
3 | **/.claude/settings.local.json
4 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | resolver = "3"
3 | members = ["primitives", "preview"]
4 |
5 | [workspace.dependencies]
6 | dioxus-primitives = { path = "primitives" }
7 |
8 | dioxus = "0.7.0-alpha.0"
9 | dioxus-lib = "0.7.0-alpha.0"
10 | tracing = { version = "0.1", features = ["std"] }
11 |
12 | [patch.crates-io]
13 | dioxus-geolocation = { git = "https://github.com/ealmloff/dioxus-std", branch = "0.7" }
14 | dioxus-notification = { git = "https://github.com/ealmloff/dioxus-std", branch = "0.7" }
15 | dioxus-sdk = { git = "https://github.com/ealmloff/dioxus-std", branch = "0.7" }
16 | dioxus_storage = { git = "https://github.com/ealmloff/dioxus-std", branch = "0.7" }
17 | dioxus-sync = { git = "https://github.com/ealmloff/dioxus-std", branch = "0.7" }
18 | dioxus-time = { git = "https://github.com/ealmloff/dioxus-std", branch = "0.7" }
19 | dioxus-util = { git = "https://github.com/ealmloff/dioxus-std", branch = "0.7" }
20 | dioxus-window = { git = "https://github.com/ealmloff/dioxus-std", branch = "0.7" }
21 |
22 | [profile]
23 |
24 | [profile.wasm-dev]
25 | inherits = "dev"
26 | opt-level = 1
27 |
28 | [profile.server-dev]
29 | inherits = "dev"
30 |
31 | [profile.android-dev]
32 | inherits = "dev"
33 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | Permission is hereby granted, free of charge, to any
2 | person obtaining a copy of this software and associated
3 | documentation files (the "Software"), to deal in the
4 | Software without restriction, including without
5 | limitation the rights to use, copy, modify, merge,
6 | publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software
8 | is furnished to do so, subject to the following
9 | conditions:
10 |
11 | The above copyright notice and this permission notice
12 | shall be included in all copies or substantial portions
13 | of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23 | DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
🎲 Dioxus Primitives 🧱
3 |
Accessible, unstyled, foundational components for Dioxus.
4 |
5 |
6 |
23 |
24 | -----
25 |
26 |
27 | Dioxus primitives is an ARIA-accessible, unstyled, foundational component library for Dioxus based on Radix Primitives. We bring the logic, you bring the styling.
28 |
29 | Building styled and more featured component libraries on top of Dioxus Primitives is encouraged!
30 |
31 | ## Here's what we have.
32 | We're still in the early days - Many components are still being created and stabilized.
33 |
34 | 23/28
35 | - [x] Accordion - In Progress
36 | - [x] Alert Dialog
37 | - [x] Aspect Ratio
38 | - [x] Avatar
39 | - [x] Calendar - In Progress
40 | - [x] Checkbox
41 | - [x] Collapsible
42 | - [x] Context Menu - In Progress
43 | - [x] Dialog - In Progress
44 | - [x] Dropdown Menu
45 | - [x] Hover Card
46 | - [x] Label
47 | - [x] Menubar
48 | - [ ] Navigation Menu
49 | - [ ] Popover
50 | - [x] Progress
51 | - [x] Radio Group
52 | - [x] Scroll Area
53 | - [x] Select
54 | - [x] Separator
55 | - [x] Slider
56 | - [x] Switch
57 | - [x] Tabs
58 | - [x] Toast
59 | - [x] Toggle
60 | - [x] Toggle Group
61 | - [x] Toolbar
62 | - [x] Tooltip
63 |
64 | ## Running the preview.
65 | You can run the `preview` app with:
66 | ```
67 | dx serve -p preview
68 | ```
69 |
70 |
71 | ## License
72 | This project is dual licensed under the [MIT](./LICENSE-MIT) and [Apache 2.0](./LICENSE-APACHE) licenses.
73 |
74 | 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.
75 |
--------------------------------------------------------------------------------
/complaints.md:
--------------------------------------------------------------------------------
1 | This file is to track any dx issues with Dioxus found while developing this library.
2 |
3 | ### Unused props don't emit unused warnings.
4 | https://github.com/DioxusLabs/dioxus/issues/3919
5 |
6 | ### Setting default of signal prop is verbose.
7 | https://github.com/DioxusLabs/dioxus/issues/3920
8 |
9 | It's verbose to set a `Signal` or `ReadOnlySignal`'s default value through props.
10 | ```rust
11 | #[derive(Props, Clone, PartialEq)]
12 | pub struct SomeProps {
13 |
14 | // This sets bool to be false
15 | #[props(default)]
16 | value: ReadOnlySignal,
17 |
18 | // This is what I'd like, except it wants a ReadOnlySignal
19 | #[props(default = true)]
20 | value: ReadOnlySignal,
21 |
22 | // Instead you have to do this:
23 | #[props(default = ReadOnlySignal::new(Signal::new(true)))]
24 | value: ReadOnlySignal,
25 |
26 | // Same for a regular signal:
27 | #[props(default = Signal::new(true))]
28 | value: Signal,
29 | }
30 | ```
31 |
32 | ### No way to know a component or element's parent, siblings, or children.
33 |
34 | Some stuff relies on knowing their surrounding elements for proper behavior.
35 |
36 | Take [radix-primitives' switch](https://github.com/radix-ui/primitives/blob/6e75e117977c9e6ffa939e6951a707f16ba0f95e/packages/react/switch/src/switch.tsx#L51) as an example. It detects when the switch is in a form and creates an input so that the switch's value bubbles with the form submit event.
37 |
38 | This is also an issue with keybind navigation - we can give components ids to internally track them through a parent context, but how do we know which order they are in for navigation?
39 |
40 | At a minimum, I need the ordering index. This could be a special prop similar to `children`.
41 |
42 | ### Need Portals
43 | Components should behave as if they are still a child of the parent of the "portaled" item. Same scope basically - context is still consumable as if it was a child.
44 |
45 | Aka the component is still used in the same spot, and the portal only moves where that component is actually rendered. Portals can't have children or attributes:
46 |
47 | ```rust
48 |
49 | #[component]
50 | pub fn App() -> Element {
51 | let portal = use_portal();
52 |
53 | rsx! {
54 | div {
55 | // ... nested stuff
56 | PortalIn {
57 | portal,
58 |
59 | // Children of PortalIn becomes children of PortalOut.
60 | div {
61 | h1 { "Alert Dialog!" }
62 | p { "alert!!" }
63 | }
64 | }
65 | }
66 |
67 | div {
68 | // ... other nested stuff
69 | PortalOut { portal }
70 | }
71 | }
72 | }
73 |
74 | ```
75 |
76 | ### `From>` Is Not Implemented For `Option>`
77 |
78 | ### `From` Is Not Implemented For `Option>`
79 | `T` can already be converted to `Option>` when provided thru props.
80 | This however doesn't work for `Option>`.
81 |
82 | ### Number Props Don't Type Infer
83 | Normally Rust would automatically determine that a number should be of type thru inference but for props it doesn't work when the prop is a signal.
84 |
85 | `index: ReadOnlySignal,` fails
86 | `index: usize,` works
87 |
88 | ```rust
89 | SomeComponent {
90 | index: 1,
91 | }
92 | ```
93 |
94 | ### No `#[props(extends = MyPropsStruct)]`
95 | https://github.com/DioxusLabs/dioxus/issues/3938
96 |
97 | ```rust
98 | pub fn MyComp1(#[props(extends = MyComp2)] comp2_attr: Vec) -> Element {
99 | rsx! {
100 | MyComp2 {
101 | ..comp2_attr
102 | }
103 | }
104 | }
105 |
106 | ```
107 |
108 | ### Unable To Insert Attributes On `Element`.
109 |
110 | Ideally there would be a way to pass attributes through a top-level component. Radix has the `asChild` prop which replaces their element with the user provided one. E.g.
111 | ```rs
112 | rsx! {
113 |
114 | if as_child {
115 | p {
116 | aria_something: "abc",
117 | {children}
118 | }
119 | } else {
120 | {children} // Can't add `aria_something`
121 | }
122 | }
123 | ```
124 |
--------------------------------------------------------------------------------
/preview/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "preview"
3 | version = "0.1.0"
4 | edition = "2024"
5 |
6 | [dependencies]
7 | dioxus = { workspace = true, features = ["router"] }
8 | dioxus-primitives.workspace = true
9 | tracing.workspace = true
10 |
11 | [build-dependencies]
12 | syntect = "5.0"
13 | pulldown-cmark = "0.13.0"
14 |
15 | [features]
16 | web = ["dioxus/web"]
17 | desktop = ["dioxus/desktop"]
18 | fullstack = ["dioxus/fullstack"]
19 | server = ["dioxus/server"]
20 |
--------------------------------------------------------------------------------
/preview/Dioxus.toml:
--------------------------------------------------------------------------------
1 | [application]
2 | name = "Dioxus Preview"
3 | version = "0.1.0"
4 |
5 | [web.app]
6 | base_path = "components"
7 |
--------------------------------------------------------------------------------
/preview/assets/dioxus-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DioxusLabs/components/4ec9d3bdea7f55470f90bf96c5061e9f534ec9bd/preview/assets/dioxus-logo.png
--------------------------------------------------------------------------------
/preview/assets/dioxus_color.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
66 |
--------------------------------------------------------------------------------
/preview/assets/hero.css:
--------------------------------------------------------------------------------
1 | #hero {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | justify-content: center;
6 | padding: 50px;
7 | margin: 0;
8 | }
9 |
10 | #hero>h1, #hero>h2 {
11 | color: var(--text-color);
12 | font-family: Arial, Helvetica, sans-serif;
13 | font-size: 1.5rem;
14 | font-weight: 500;
15 | margin: 10px 0;
16 | }
17 |
18 | #hero>h1{
19 | font-size: 2rem;
20 | font-weight: 600;
21 | }
22 |
23 | #hero #title {
24 | font-weight: 500;
25 | font-size: 2rem;
26 | }
27 |
28 | #hero-separator {
29 | margin-bottom: 50px;
30 | height: 2px;
31 | }
32 |
33 | #hero-search-container {
34 | display: flex;
35 | flex-direction: column;
36 | align-items: center;
37 | justify-content: center;
38 | width: 100%;
39 | max-width: 800px;
40 | margin: auto;
41 | }
42 |
43 | #hero-search-input {
44 | width: 100%;
45 | margin: 20px 0;
46 | padding: 10px;
47 | color: var(--text-color);
48 | border: 1px solid var(--border-color);
49 | border-radius: 4px;
50 | background-color: var(--brighter-background-color);
51 | box-sizing: border-box;
52 | margin: 2em auto;
53 | padding: 10px 20px;
54 | }
55 |
--------------------------------------------------------------------------------
/preview/assets/main.css:
--------------------------------------------------------------------------------
1 | /* Theme style variables */
2 | :root {
3 | --highlight-color-main: #dd7230;
4 | --highlight-color-secondary: #eb5160;
5 | --highlight-color-tertiary: #2b7fff;
6 | }
7 |
8 | @media (prefers-color-scheme: light) {
9 | :root {
10 | --background-color: #fff;
11 | --brighter-background-color: #fbfbfb;
12 | --dim-background-color: oklch(0.967 0.003 264.542);
13 | --hover-background-color: #dadada;
14 | --focused-background-color: #d7d7d7;
15 | --contrast-background-color: #0d0d0d;
16 | --text-color: #000;
17 | --dim-text-color: rgb(43, 43, 43);
18 | --border-color: #000;
19 | --dim-border-color: rgb(43, 43, 43);
20 | --muted-text-color: #b0b0b0;
21 | --focused-border-color: #2b7fff;
22 | --success-background-color: #ecfdf5;
23 | --success-text-color: #10b981;
24 | --warning-background-color: #fffbeb;
25 | --warning-text-color: #f59e0b;
26 | --error-background-color: #fef2f2;
27 | --error-text-color: #ef4444;
28 | --info-background-color: #e0f2fe;
29 | --info-text-color: #0284c7;
30 | }
31 | }
32 |
33 | @media (prefers-color-scheme: dark) {
34 | :root {
35 | --background-color: #000;
36 | --brighter-background-color: #0e0e0e;
37 | --dim-background-color: #141313;
38 | --hover-background-color: #292929;
39 | --focused-background-color: #2c2b2b;
40 | --contrast-background-color: #e6e6e6;
41 | --text-color: #fff;
42 | --dim-text-color: rgb(220, 220, 220);
43 | --border-color: #fff;
44 | --dim-border-color: rgb(43, 43, 43);
45 | --muted-text-color: #b0b0b0;
46 | --focused-border-color: #2b7fff;
47 | --success-background-color: #02271c;
48 | --success-text-color: #b6fae3;
49 | --warning-background-color: #342203;
50 | --warning-text-color: #feeac7;
51 | --error-background-color: #360e0e;
52 | --error-text-color: #e0baba;
53 | --info-background-color: #0c1f2b;
54 | --info-text-color: #b3d7e6;
55 | }
56 | }
57 |
58 | @media (prefers-color-scheme: light) {
59 | .code-block-dark {
60 | display: none;
61 | }
62 | }
63 |
64 | @media (prefers-color-scheme: dark) {
65 | .code-block-light {
66 | display: none;
67 | }
68 | }
69 |
70 | body {
71 | margin: 0;
72 | padding: 0;
73 | color: var(--text-color);
74 | background-color: var(--background-color);
75 | }
76 |
77 | .navbar {
78 | display: flex;
79 | align-items: center;
80 | justify-content: space-between;
81 | position: sticky;
82 | padding: 1rem;
83 | top: 0;
84 | z-index: 1000;
85 | background-color: var(--brighter-background-color);
86 | border-bottom: 1px solid var(--border-color);
87 | }
88 |
89 | .navbar-link {
90 | color: var(--text-color);
91 | text-decoration: none;
92 | font-size: 1.2em;
93 | font-weight: bold;
94 | padding: 0.5em 1em;
95 | }
96 |
97 | .navbar-link:hover {
98 | color: var(--dim-text-color);
99 | }
100 |
101 | .navbar-links {
102 | display: flex;
103 | gap: 1rem;
104 | }
105 |
106 | .navbar-brand {
107 | display: flex;
108 | text-decoration: none;
109 | align-items: center;
110 | font-size: 1.5em;
111 | font-weight: bold;
112 | color: var(--text-color);
113 | }
114 |
115 | /* Code block styles */
116 | .code-block {
117 | font-family: "Courier New", Courier, monospace;
118 | border-radius: 4px;
119 | overflow: scroll;
120 | width: 100%;
121 | height: 100%;
122 | max-height: 80vh;
123 | box-sizing: border-box;
124 | }
125 |
126 | .code-block[data-collapsed="true"] {
127 | height: 10em;
128 | backdrop-filter: blur(1px);
129 | mask: linear-gradient(
130 | to bottom,
131 | rgba(0, 0, 0, 1) 12.5%,
132 | rgba(0, 0, 0, 1) 25%,
133 | rgba(0, 0, 0, 1) 37.5%,
134 | rgba(0, 0, 0, 0) 100%
135 | );
136 | overflow: hidden;
137 | user-select: none;
138 | -webkit-user-select: none;
139 | }
140 |
141 | .copy-button {
142 | display: flex;
143 | align-items: center;
144 | justify-content: center;
145 | position: absolute;
146 | top: 0.5em;
147 | right: 0.5em;
148 | background: none;
149 | color: var(--text-color);
150 | border: none;
151 | border-radius: 4px;
152 | cursor: pointer;
153 | }
154 |
155 | .copy-button:hover {
156 | color: var(--highlight-color-tertiary);
157 | }
158 |
159 | .copy-button[data-copied="true"] {
160 | color: var(--success-text-color);
161 | }
162 |
163 | /* Demo frame styles */
164 | .component-title {
165 | font-size: 1.5em;
166 | margin: 0.5em 0;
167 | text-transform: capitalize;
168 | }
169 |
170 | .component-preview {
171 | display: flex;
172 | flex-direction: column;
173 | align-items: center;
174 | justify-content: center;
175 | width: 100vw;
176 | }
177 |
178 | .component-preview-separator {
179 | width: 100%;
180 | height: 1px;
181 | background-color: var(--dim-border-color);
182 | }
183 |
184 | .component-preview-contents {
185 | display: flex;
186 | flex-direction: column;
187 | align-items: center;
188 | justify-content: center;
189 | background-color: var(--dim-background-color);
190 | border: 1px solid var(--dim-border-color);
191 | border-radius: 8px;
192 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
193 | box-sizing: border-box;
194 | margin: 20px 0;
195 | width: 75vw;
196 | }
197 |
198 | .component-preview-frame {
199 | display: flex;
200 | flex-direction: column;
201 | align-items: center;
202 | justify-content: center;
203 | width: 100%;
204 | min-height: 25vh;
205 | max-height: 100%;
206 | border-top-right-radius: 0.5em;
207 | border-top-left-radius: 0.5em;
208 | background-color: var(--dim-background-color);
209 | padding: 20px;
210 | box-sizing: border-box;
211 | }
212 |
213 | /* component info styles */
214 | .component-title {
215 | font-size: 2em;
216 | width: 100%;
217 | text-align: center;
218 | margin-bottom: 20px;
219 | margin-top: 20px;
220 | text-transform: capitalize;
221 | }
222 |
223 | .component-description {
224 | font-size: 1em;
225 | margin: 5vw;
226 | }
227 |
228 | .component-code {
229 | width: 100%;
230 | }
231 |
232 | /* Masonry styles */
233 | .masonry-with-columns {
234 | display: flex;
235 | flex-wrap: wrap;
236 | padding: 0 0 0 1rem;
237 | }
238 | .masonry-with-columns .masonry-preview-frame {
239 | display: flex;
240 | flex-direction: column;
241 | align-items: center;
242 | min-height: 10rem;
243 | max-width: calc(100vw - 2rem);
244 | margin: 0 1rem 1rem 0;
245 | padding: 3rem;
246 | flex: 1 0 auto;
247 | border-radius: 0.5rem;
248 | background-color: var(--dim-background-color);
249 | border: 1px solid var(--dim-border-color);
250 | box-sizing: border-box;
251 | }
252 |
253 | .masonry-with-columns .masonry-component-frame {
254 | display: flex;
255 | flex-direction: column;
256 | align-items: center;
257 | justify-content: center;
258 | height: 100%;
259 | width: 100%;
260 | }
261 |
--------------------------------------------------------------------------------
/preview/assets/prism.css:
--------------------------------------------------------------------------------
1 | /* PrismJS 1.30.0
2 | https://prismjs.com/download#themes=prism-twilight&languages=rust */
3 | code[class*=language-],pre[class*=language-]{color:#fff;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;text-shadow:0 -.1em .2em #000;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}:not(pre)>code[class*=language-],pre[class*=language-]{background:#141414}pre[class*=language-]{border-radius:.5em;border:.3em solid #545454;box-shadow:1px 1px .5em #000 inset;margin:.5em 0;overflow:auto;padding:1em}pre[class*=language-]::-moz-selection{background:#27292a}pre[class*=language-]::selection{background:#27292a}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:hsla(0,0%,93%,.15)}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:hsla(0,0%,93%,.15)}:not(pre)>code[class*=language-]{border-radius:.3em;border:.13em solid #545454;box-shadow:1px 1px .3em -.1em #000 inset;padding:.15em .2em .05em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#777}.token.punctuation{opacity:.7}.token.namespace{opacity:.7}.token.boolean,.token.deleted,.token.number,.token.tag{color:#ce6849}.token.builtin,.token.constant,.token.keyword,.token.property,.token.selector,.token.symbol{color:#f9ed99}.language-css .token.string,.style .token.string,.token.attr-name,.token.attr-value,.token.char,.token.entity,.token.inserted,.token.operator,.token.string,.token.url,.token.variable{color:#909e6a}.token.atrule{color:#7385a5}.token.important,.token.regex{color:#e8c062}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.language-markup .token.attr-name,.language-markup .token.punctuation,.language-markup .token.tag{color:#ac885c}.token{position:relative;z-index:1}.line-highlight.line-highlight{background:hsla(0,0%,33%,.25);background:linear-gradient(to right,hsla(0,0%,33%,.1) 70%,hsla(0,0%,33%,0));border-bottom:1px dashed #545454;border-top:1px dashed #545454;margin-top:.75em;z-index:0}.line-highlight.line-highlight:before,.line-highlight.line-highlight[data-end]:after{background-color:#8693a6;color:#f4f1ef}
4 |
--------------------------------------------------------------------------------
/preview/build.rs:
--------------------------------------------------------------------------------
1 | use std::sync::OnceLock;
2 | fn main() {
3 | let out_dir = std::env::var("OUT_DIR").unwrap();
4 | let out_dir = std::path::PathBuf::from(out_dir);
5 | println!("cargo:rerun-if-changed=src/components");
6 | for folder in std::fs::read_dir("src/components").unwrap().flatten() {
7 | if !folder.file_type().unwrap().is_dir() {
8 | continue;
9 | }
10 | let folder_name = folder.file_name();
11 | let folder_name = folder_name.to_string_lossy();
12 | let out_folder = out_dir.join(&*folder_name);
13 | std::fs::create_dir_all(&out_folder).unwrap();
14 | for file in std::fs::read_dir(folder.path()).unwrap().flatten() {
15 | if file.file_type().unwrap().is_dir() {
16 | continue;
17 | }
18 | if file.path().extension() == Some(std::ffi::OsStr::new("md")) {
19 | let markdown = process_markdown_to_html(&file.path());
20 | let out_file_path = out_folder.join(file.file_name()).with_extension("html");
21 | std::fs::write(out_file_path, markdown).unwrap();
22 | continue;
23 | }
24 | let file_name = file.file_name();
25 | let file_name = file_name.to_string_lossy();
26 | for theme in ["base16-ocean.dark", "base16-ocean.light"] {
27 | let html = highlight_file_to(&file.path(), theme);
28 | let out_file_path = out_folder.join(format!("{file_name}.{theme}.html"));
29 | std::fs::write(out_file_path, html).unwrap();
30 | }
31 | }
32 | }
33 | }
34 | fn highlight_file_to(file_path: &std::path::Path, theme: &str) -> String {
35 | use std::io::BufRead;
36 | use syntect::easy::HighlightFile;
37 | use syntect::highlighting::{Style, ThemeSet};
38 | use syntect::html::{IncludeBackground, styled_line_to_highlighted_html};
39 | use syntect::parsing::SyntaxSet;
40 | static SYNTAX_SET: OnceLock = OnceLock::new();
41 | static THEME_SET: OnceLock = OnceLock::new();
42 | let ss = SYNTAX_SET.get_or_init(SyntaxSet::load_defaults_newlines);
43 | let ts = THEME_SET.get_or_init(ThemeSet::load_defaults);
44 | let mut all_html = String::new();
45 | let mut highlighter = HighlightFile::new(&file_path, &ss, &ts.themes[theme]).unwrap();
46 | let mut line = String::new();
47 | while highlighter.reader.read_line(&mut line).unwrap_or_default() > 0 {
48 | {
49 | let regions: Vec<(Style, &str)> = highlighter
50 | .highlight_lines
51 | .highlight_line(&line, &ss)
52 | .unwrap();
53 | let html =
54 | styled_line_to_highlighted_html(®ions[..], IncludeBackground::No).unwrap();
55 | all_html += &html;
56 | }
57 | line.clear();
58 | }
59 | all_html
60 | }
61 | fn process_markdown_to_html(markdown_path: &std::path::Path) -> String {
62 | use pulldown_cmark::{Options, Parser};
63 | let markdown_input =
64 | std::fs::read_to_string(markdown_path).expect("Failed to read markdown file");
65 | let mut options = Options::empty();
66 | options.insert(Options::ENABLE_GFM);
67 | let parser = Parser::new_ext(&markdown_input, options);
68 | let mut html_output = String::new();
69 | pulldown_cmark::html::push_html(&mut html_output, parser);
70 | html_output
71 | }
72 |
--------------------------------------------------------------------------------
/preview/src/components/accordion/docs.md:
--------------------------------------------------------------------------------
1 | The accordion component is used to display collapsible content panels for presenting information in a limited amount of space. It allows users to expand and collapse sections to view more or less content.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // The accordion component must wrap all accordion items.
7 | Accordion {
8 | // Each accordion item contains both a trigger the expanded contents of the item.
9 | AccordionItem {
10 | // Each item must have an index starting from 0 to control where the item is placed.
11 | index: 0,
12 | // The trigger is used to expand or collapse the item.
13 | AccordionTrigger {}
14 | // The content that is shown when the item is expanded.
15 | AccordionContent {}
16 | }
17 | }
18 | ```
--------------------------------------------------------------------------------
/preview/src/components/accordion/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::accordion::{
3 | Accordion, AccordionContent, AccordionItem, AccordionTrigger,
4 | };
5 | #[component]
6 | pub(super) fn Demo() -> Element {
7 | rsx! {
8 | document::Link {
9 | rel: "stylesheet",
10 | href: asset!("/src/components/accordion/style.css"),
11 | }
12 | Accordion {
13 | class: "accordion",
14 | allow_multiple_open: false,
15 | horizontal: false,
16 | for i in 0..4 {
17 | AccordionItem {
18 | class: "accordion-item",
19 | index: i,
20 | on_change: move |open| {
21 | tracing::info!("{open};");
22 | },
23 | on_trigger_click: move || {
24 | tracing::info!("trigger");
25 | },
26 | AccordionTrigger { class: "accordion-trigger", "the quick brown fox" }
27 | AccordionContent { class: "accordion-content",
28 | div { class: "accordion-content-inner",
29 | p { "lorem ipsum lorem ipsum" }
30 | }
31 | }
32 | }
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/preview/src/components/accordion/style.css:
--------------------------------------------------------------------------------
1 | .accordion {
2 | background-color: var(--brighter-background-color);
3 | }
4 |
5 | .accordion-trigger {
6 | border: none;
7 | outline: none;
8 | border-bottom: 1px solid var(--dim-border-color);
9 | width: 100%;
10 | color: var(--text-color);
11 | background-color: var(--brighter-background-color);
12 | padding: 10px;
13 | text-align: left;
14 | font-weight: bold;
15 | }
16 |
17 | .accordion-trigger:focus {
18 | border: 1px solid maroon;
19 | }
20 |
21 | .accordion-trigger:hover {
22 | cursor: pointer;
23 | }
24 |
25 | .accordion-content {
26 | display: grid;
27 | grid-template-rows: 0fr;
28 | transition: grid-template-rows 0.3s ease-out;
29 | }
30 |
31 | .accordion-item[data-open="true"] .accordion-content {
32 | grid-template-rows: 1fr;
33 | }
34 |
35 | .accordion-content-inner {
36 | overflow: hidden;
37 | }
38 |
39 | .accordion-content-inner p {
40 | margin: 0;
41 | padding: 10px;
42 | }
--------------------------------------------------------------------------------
/preview/src/components/alert_dialog/docs.md:
--------------------------------------------------------------------------------
1 | The AlertDialog primitive provides an accessible, composable modal dialog for critical user confirmations (such as destructive actions). It is unstyled by default except for minimal centering/stacking, and can be fully themed by the consumer.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // Usage example:
7 | let open = use_signal(|| false);
8 | rsx! {
9 | button { onclick: move |_| open.set(true), "Show Alert Dialog" }
10 | AlertDialogRoot { open: Some(open), on_open_change: move |v| open.set(v),
11 | AlertDialogContent {
12 | // You may pass class/style for custom appearance
13 | AlertDialogTitle { "Title" }
14 | AlertDialogDescription { "Description" }
15 | AlertDialogActions {
16 | AlertDialogCancel { "Cancel" }
17 | AlertDialogAction { "Confirm" }
18 | }
19 | }
20 | }
21 | }
22 | ```
23 |
24 | ### Components
25 | - **AlertDialogRoot**: Provides context and manages open state.
26 | - **AlertDialogContent**: The dialog container. Handles accessibility and focus trap. Applies only minimal inline style for centering/stacking if no style is provided.
27 | - **AlertDialogTitle**: The dialog's heading.
28 | - **AlertDialogDescription**: Additional description for the dialog.
29 | - **AlertDialogActions**: Container for action buttons.
30 | - **AlertDialogAction**: Main action button (e.g., confirm/delete). Closes dialog and calls optional `on_click`.
31 | - **AlertDialogCancel**: Cancel/close button. Closes dialog and calls optional `on_click`.
32 |
33 | ### Notes
34 | - By default, only minimal centering/positioning styles are applied to `AlertDialogContent` (position, top, left, transform, z-index). All appearance is controlled by your CSS.
35 | - The dialog is accessible and closes on Escape, backdrop click, or Cancel/Action.
36 | - You can pass custom `on_click`, `class`, and `style` props to all subcomponents for full control.
37 | - Focus trap is not fully implemented; focus may escape the dialog in some cases.
38 |
--------------------------------------------------------------------------------
/preview/src/components/alert_dialog/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::alert_dialog::*;
3 |
4 | #[component]
5 | pub(super) fn Demo() -> Element {
6 | let mut open = use_signal(|| false);
7 | let mut confirmed = use_signal(|| false);
8 |
9 | rsx! {
10 | document::Link {
11 | rel: "stylesheet",
12 | href: asset!("/src/components/alert_dialog/style.css"),
13 | }
14 | div {
15 | class: "alert-dialog-example",
16 | style: "padding: 20px; max-width: 420px; margin: 0 auto; background: var(--dim-background-color); border-radius: 8px; border: 1px solid var(--dim-border-color); box-shadow: 0 2px 8px rgba(0,0,0,0.08);",
17 | button {
18 | class: "alert-dialog-trigger",
19 | style: "margin-bottom: 1.5rem;",
20 | onclick: move |_| open.set(true),
21 | "Show Alert Dialog (Primitive)"
22 | }
23 | AlertDialogRoot { open: Some(open), on_open_change: move |v| open.set(v),
24 | AlertDialogContent { class: "alert-dialog",
25 | AlertDialogTitle { "Delete item" }
26 | AlertDialogDescription { "Are you sure you want to delete this item? This action cannot be undone." }
27 | AlertDialogActions {
28 | AlertDialogCancel { class: "alert-dialog-cancel", "Cancel" }
29 | AlertDialogAction {
30 | class: "alert-dialog-action",
31 | on_click: move |_| confirmed.set(true),
32 | "Delete"
33 | }
34 | }
35 | }
36 | }
37 | if confirmed() {
38 | p { style: "color: var(--error-text-color); margin-top: 16px; font-weight: 600;",
39 | "Item deleted!"
40 | }
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/preview/src/components/alert_dialog/style.css:
--------------------------------------------------------------------------------
1 | /* Alert Dialog Backdrop */
2 | .alert-dialog-backdrop {
3 | position: fixed;
4 | inset: 0;
5 | background: rgba(0, 0, 0, 0.3);
6 | z-index: 1000;
7 | animation: fadeIn 0.2s ease;
8 | }
9 |
10 | /* Alert Dialog Container - improved for theme consistency */
11 | .alert-dialog {
12 | position: fixed;
13 | top: 50%;
14 | left: 50%;
15 | width: 400px;
16 | max-width: 90vw;
17 | min-width: 320px;
18 | margin: 0;
19 | background: var(--dim-background-color);
20 | color: var(--text-color);
21 | border: 1px solid var(--dim-border-color);
22 | border-radius: 8px;
23 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18);
24 | z-index: 1001;
25 | padding: 32px 24px 24px 24px;
26 | display: flex;
27 | flex-direction: column;
28 | gap: 16px;
29 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
30 | transform: translate(-50%, -50%);
31 | animation: none;
32 | }
33 |
34 | .alert-dialog-title {
35 | font-size: 1.25rem;
36 | font-weight: 700;
37 | color: var(--text-color);
38 | margin-bottom: 8px;
39 | }
40 |
41 | .alert-dialog-description {
42 | font-size: 1rem;
43 | color: var(--muted-text-color);
44 | margin-bottom: 16px;
45 | }
46 |
47 | .alert-dialog-actions {
48 | display: flex;
49 | justify-content: flex-end;
50 | gap: 12px;
51 | }
52 |
53 | .alert-dialog-cancel {
54 | background-color: var(--background-color);
55 | color: var(--text-color);
56 | border: 1px solid var(--dim-border-color);
57 | border-radius: 4px;
58 | padding: 8px 18px;
59 | font-size: 1rem;
60 | cursor: pointer;
61 | transition: all 0.2s ease;
62 | }
63 |
64 | .alert-dialog-cancel:hover,
65 | .alert-dialog-cancel:focus {
66 | background-color: var(--hover-background-color);
67 | color: var(--text-color);
68 | outline: none;
69 | box-shadow: 0 0 0 2px var(--focused-border-color);
70 | }
71 |
72 | .alert-dialog-action {
73 | background-color: var(--error-background-color);
74 | color: var(--error-text-color);
75 | border: 1px solid var(--error-text-color);
76 | border-radius: 4px;
77 | padding: 8px 18px;
78 | font-size: 1rem;
79 | cursor: pointer;
80 | font-weight: 600;
81 | transition: all 0.2s ease;
82 | }
83 |
84 | .alert-dialog-action:hover,
85 | .alert-dialog-action:focus {
86 | background-color: var(--error-text-color);
87 | color: var(--background-color);
88 | outline: none;
89 | box-shadow: 0 0 0 2px var(--focused-border-color);
90 | }
91 |
92 | .alert-dialog-trigger {
93 | background: var(--focused-border-color);
94 | color: var(--background-color);
95 | border: none;
96 | border-radius: 4px;
97 | padding: 10px 22px;
98 | font-size: 1rem;
99 | font-weight: 600;
100 | cursor: pointer;
101 | transition: background 0.15s, color 0.15s;
102 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
103 | }
104 |
105 | .alert-dialog-trigger:hover,
106 | .alert-dialog-trigger:focus {
107 | background: var(--highlight-color-main);
108 | color: var(--background-color);
109 | outline: 2px solid var(--focused-border-color);
110 | outline-offset: 2px;
111 | }
112 |
113 | @keyframes fadeIn {
114 | from {
115 | opacity: 0;
116 | }
117 |
118 | to {
119 | opacity: 1;
120 | }
121 | }
--------------------------------------------------------------------------------
/preview/src/components/aspect_ratio/docs.md:
--------------------------------------------------------------------------------
1 | The AspectRatio component is used to maintain a specific aspect ratio for its children. This is particularly useful for responsive designs where you want to ensure that an element retains its proportions regardless of the screen size.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | AspectRatio {
7 | // The aspect ratio to maintain (width / height)
8 | ratio: 4. / 3.,
9 | // The children of the AspectRatio component will be rendered within it.
10 | {children}
11 | }
12 | ```
13 |
--------------------------------------------------------------------------------
/preview/src/components/aspect_ratio/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::aspect_ratio::AspectRatio;
3 | #[component]
4 | pub(super) fn Demo() -> Element {
5 | rsx! {
6 | document::Link {
7 | rel: "stylesheet",
8 | href: asset!("/src/components/aspect_ratio/style.css"),
9 | }
10 | div {
11 | class: "aspect-ratio-container",
12 | width: "10em",
13 | min_width: "30vw",
14 | AspectRatio { ratio: 4.0 / 3.0,
15 | img {
16 | class: "aspect-ratio-image",
17 | src: asset!("/assets/dioxus-logo.png", ImageAssetOptions::new().with_avif()),
18 | }
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/preview/src/components/aspect_ratio/style.css:
--------------------------------------------------------------------------------
1 | .aspect-ratio-container {
2 | border-radius: 6px;
3 | overflow: hidden;
4 | box-shadow: 0 2px 10px #000000;
5 | background-color: var(--dim-background-color);
6 | padding: 1rem;
7 | box-sizing: border-box;
8 | }
9 |
10 | .aspect-ratio-image {
11 | object-fit: cover;
12 | width: 100%;
13 | height: 100%;
14 | }
--------------------------------------------------------------------------------
/preview/src/components/avatar/docs.md:
--------------------------------------------------------------------------------
1 | The Avatar component is used to display a user's profile picture or an icon representing the user. It handles the loading state of the image and can display a fallback icon if the image fails to load.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // All avatar contents must be wrapped in the Avatar component.
7 | Avatar {
8 | on_state_change: |state: AvatarState| {
9 | // This callback is triggered when the avatar's state changes. The state can be used to determine if the image is loading, loaded, or failed to load.
10 | },
11 | // The avatar image component is used to display the user's profile picture.
12 | AvatarImage {
13 | // The source URL of the image to be displayed.
14 | src: "",
15 | // The alt text for the image, used for accessibility.
16 | alt: "",
17 | }
18 | // The avatar fallback component is used to display an icon or text when the image fails to load.
19 | AvatarFallback {
20 | // The content to display when the image fails to load.
21 | // This can be an icon or text representing the user.
22 | {children}
23 | }
24 | }
25 | ```
26 |
--------------------------------------------------------------------------------
/preview/src/components/avatar/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::avatar::{Avatar, AvatarFallback, AvatarImage};
3 | #[component]
4 | pub(super) fn Demo() -> Element {
5 | let mut avatar_state = use_signal(|| "No state yet".to_string());
6 | rsx! {
7 | document::Link {
8 | rel: "stylesheet",
9 | href: asset!("/src/components/avatar/style.css"),
10 | }
11 | div { class: "avatar-example-section",
12 | div { class: "avatar-example",
13 | div { class: "avatar-item",
14 | p { class: "avatar-label", "Basic Usage" }
15 | Avatar {
16 | class: "avatar",
17 | on_state_change: move |state| {
18 | avatar_state.set(format!("Avatar 1: {:?}", state));
19 | },
20 | AvatarImage {
21 | src: asset!("/assets/dioxus-logo.png", ImageAssetOptions::new().with_avif()),
22 | alt: "User avatar",
23 | }
24 | AvatarFallback { class: "avatar-fallback", "UA" }
25 | }
26 | }
27 | div { class: "avatar-item",
28 | p { class: "avatar-label", "Error State" }
29 | Avatar {
30 | class: "avatar",
31 | on_state_change: move |state| {
32 | avatar_state.set(format!("Avatar 2: {:?}", state));
33 | },
34 | AvatarImage {
35 | src: "https://invalid-url.example/image.jpg",
36 | alt: "Invalid image",
37 | }
38 | AvatarFallback { class: "avatar-fallback", "JD" }
39 | }
40 | }
41 | div { class: "avatar-item",
42 | p { class: "avatar-label", "Emoji Fallback" }
43 | Avatar {
44 | class: "avatar",
45 | on_state_change: move |state| {
46 | avatar_state.set(format!("Avatar 3: {:?}", state));
47 | },
48 | AvatarImage {
49 | src: "https://invalid-url.example/image.jpg",
50 | alt: "Invalid image",
51 | }
52 | AvatarFallback { class: "avatar-fallback", "👤" }
53 | }
54 | }
55 | div { class: "avatar-item",
56 | p { class: "avatar-label", "Large Size" }
57 | Avatar {
58 | class: "avatar avatar-lg",
59 | on_state_change: move |state| {
60 | avatar_state.set(format!("Avatar 4: {:?}", state));
61 | },
62 | AvatarImage {
63 | src: asset!("/assets/dioxus-logo.png", ImageAssetOptions::new().with_avif()),
64 | alt: "Large avatar",
65 | }
66 | AvatarFallback { class: "avatar-fallback", "LG" }
67 | }
68 | }
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/preview/src/components/avatar/style.css:
--------------------------------------------------------------------------------
1 | /* Avatar Example Layout */
2 | .avatar-example {
3 | display: flex;
4 | flex-wrap: wrap;
5 | gap: 1.5rem;
6 | padding: 1rem;
7 | background: var(--brighter-background-color);
8 | border-radius: 8px;
9 | }
10 |
11 | .avatar-item {
12 | display: flex;
13 | flex-direction: column;
14 | align-items: center;
15 | gap: 0.5rem;
16 | }
17 |
18 | .avatar-label {
19 | font-size: 0.875rem;
20 | color: var(--text-color);
21 | margin: 0;
22 | }
23 |
24 | /* Avatar Component Styles */
25 | .avatar {
26 | width: 64px;
27 | height: 64px;
28 | border-radius: 50%;
29 | overflow: hidden;
30 | background: var(--brighter-background-color);
31 | color: var(--text-color);
32 | font-weight: 500;
33 | display: inline-flex;
34 | align-items: center;
35 | justify-content: center;
36 | position: relative;
37 | cursor: pointer;
38 | transition: transform 0.2s, box-shadow 0.2s;
39 | }
40 |
41 | .avatar:hover {
42 | transform: scale(1.05);
43 | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
44 | }
45 |
46 | /* Avatar sizes */
47 | .avatar-sm {
48 | width: 40px;
49 | height: 40px;
50 | font-size: 0.875rem;
51 | }
52 |
53 | .avatar-md {
54 | width: 80px;
55 | height: 80px;
56 | font-size: 1.25rem;
57 | }
58 |
59 | .avatar-lg {
60 | width: 120px;
61 | height: 120px;
62 | font-size: 1.75rem;
63 | }
64 |
65 | /* State-specific styles */
66 | .avatar[data-state="loading"] {
67 | animation: pulse 1.5s infinite ease-in-out;
68 | }
69 |
70 | .avatar[data-state="error"] {
71 | border: 2px solid #ef4444;
72 | }
73 |
74 | .avatar[data-state="empty"] {
75 | background: #cbd5e1;
76 | }
77 |
78 | @keyframes pulse {
79 | 0% {
80 | opacity: 1;
81 | }
82 |
83 | 50% {
84 | opacity: 0.7;
85 | }
86 |
87 | 100% {
88 | opacity: 1;
89 | }
90 | }
91 |
92 | .avatar-fallback {
93 | width: 100%;
94 | height: 100%;
95 | display: flex;
96 | align-items: center;
97 | justify-content: center;
98 | background: #e2e8f0;
99 | color: #64748b;
100 | font-size: 1.5rem;
101 | }
102 |
103 | .avatar[data-state="error"] .avatar-fallback {
104 | background: #fee2e2;
105 | color: #b91c1c;
106 | }
107 |
108 | /* State display */
109 | .avatar-state-display {
110 | width: 100%;
111 | margin-top: 1rem;
112 | padding: 0.75rem;
113 | background: #f0f0f0;
114 | border-radius: 4px;
115 | font-family: monospace;
116 | color: var(--dim-text-color);
117 | }
--------------------------------------------------------------------------------
/preview/src/components/calendar/docs.md:
--------------------------------------------------------------------------------
1 | The Calendar component is used to display a calendar interface, allowing users to select dates. It provides a grid layout of days for a specific month and year.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | Calendar {
7 | // The currently selected date in the calendar (if any).
8 | selected_date,
9 | on_date_change: |date: Option| {
10 | // This callback is triggered when a date is selected in the calendar.
11 | // The date parameter contains the selected date.
12 | },
13 | // The current view date of the calendar, which determines the month and year displayed.
14 | view_date,
15 | on_view_change: |date: CalendarDate| {
16 | // This callback is triggered when the view date changes.
17 | // The date parameter contains the new view date.
18 | },
19 | // The calendar header should contain the navigation controls and the title for the calendar.
20 | CalendarHeader {
21 | // The calendar navigation handles switching between months and years within the calendar view.
22 | CalendarNavigation {}
23 | }
24 | // The calendar grid displays the days of the month in a grid layout.
25 | CalendarGrid {}
26 | }
27 | ```
28 |
--------------------------------------------------------------------------------
/preview/src/components/calendar/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::calendar::{
3 | Calendar, CalendarDate, CalendarGrid, CalendarHeader, CalendarNavigation,
4 | };
5 | #[component]
6 | pub(super) fn Demo() -> Element {
7 | let mut selected_date = use_signal(|| None::);
8 | let mut view_date = use_signal(|| CalendarDate::new(2024, 5, 15));
9 | rsx! {
10 | document::Link {
11 | rel: "stylesheet",
12 | href: asset!("/src/components/calendar/style.css"),
13 | }
14 | div { class: "calendar-example", style: "padding: 20px;",
15 | div { class: "calendar",
16 | Calendar {
17 | selected_date: selected_date(),
18 | on_date_change: move |date| {
19 | tracing::info!("Selected date: {:?}", date);
20 | selected_date.set(date);
21 | },
22 | view_date: view_date(),
23 | on_view_change: move |new_view: CalendarDate| {
24 | tracing::info!("View changed to: {}-{}", new_view.year, new_view.month);
25 | view_date.set(new_view);
26 | },
27 | CalendarHeader { CalendarNavigation {} }
28 | CalendarGrid {}
29 | }
30 | }
31 | div { class: "selected-date", style: "margin-top: 20px;",
32 | if let Some(date) = selected_date() {
33 | p { style: "font-weight: bold;", "Selected date: {date}" }
34 | } else {
35 | p { "No date selected" }
36 | }
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/preview/src/components/calendar/style.css:
--------------------------------------------------------------------------------
1 | /* Calendar Container */
2 | .calendar {
3 | width: 100%;
4 | border: 1px solid var(--dim-border-color);
5 | border-radius: 8px;
6 | background-color: var(--background-color);
7 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
8 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
9 | }
10 |
11 | /* Calendar Navigation */
12 | .calendar-navigation {
13 | display: flex;
14 | align-items: center;
15 | justify-content: space-between;
16 | padding: 12px;
17 | border-bottom: 1px solid var(--dim-border-color);
18 | }
19 |
20 | .calendar-nav-title {
21 | font-weight: 600;
22 | font-size: 16px;
23 | color: var(--text-color);
24 | }
25 |
26 | .calendar-nav-prev,
27 | .calendar-nav-next {
28 | background: none;
29 | border: none;
30 | font-size: 18px;
31 | color: var(--dim-text-color);
32 | cursor: pointer;
33 | width: 30px;
34 | height: 30px;
35 | display: flex;
36 | align-items: center;
37 | justify-content: center;
38 | border-radius: 4px;
39 | }
40 |
41 | .calendar-nav-prev:hover,
42 | .calendar-nav-next:hover {
43 | background-color: var(--hover-background-color);
44 | color: var(--text-color);
45 | }
46 |
47 | .calendar-nav-prev:focus,
48 | .calendar-nav-next:focus {
49 | outline: 2px solid var(--focused-border-color);
50 | outline-offset: 2px;
51 | }
52 |
53 | .calendar-nav-prev:disabled,
54 | .calendar-nav-next:disabled {
55 | color: #ccc;
56 | cursor: not-allowed;
57 | }
58 |
59 | /* Calendar Grid */
60 | .calendar-grid {
61 | padding: 8px;
62 | }
63 |
64 | .calendar-grid-header {
65 | display: grid;
66 | grid-template-columns: repeat(7, 1fr);
67 | margin-bottom: 8px;
68 | }
69 |
70 | .calendar-grid-day-header {
71 | text-align: center;
72 | font-size: 12px;
73 | font-weight: 600;
74 | color: var(--muted-text-color);
75 | padding: 4px;
76 | }
77 |
78 | .calendar-grid-body {
79 | display: flex;
80 | flex-direction: column;
81 | gap: 2px;
82 | }
83 |
84 | .calendar-grid-week {
85 | display: grid;
86 | grid-template-columns: repeat(7, 1fr);
87 | gap: 2px;
88 | }
89 |
90 | .calendar-grid-days {
91 | display: grid;
92 | grid-template-columns: repeat(7, 1fr);
93 | gap: 4px;
94 | }
95 |
96 | .calendar-grid-cell {
97 | aspect-ratio: 1;
98 | display: flex;
99 | align-items: center;
100 | justify-content: center;
101 | font-size: 14px;
102 | border: none;
103 | background: none;
104 | border-radius: 4px;
105 | cursor: pointer;
106 | color: var(--dim-text-color);
107 | }
108 |
109 | .calendar-grid-cell:hover:not([data-disabled="true"]) {
110 | background-color: var(--hover-background-color);
111 | }
112 |
113 | .calendar-grid-cell:focus {
114 | outline: 2px solid var(--focused-border-color);
115 | outline-offset: 2px;
116 | }
117 |
118 | .calendar-grid-cell[data-disabled="true"] {
119 | color: var(--dim-text-color);
120 | cursor: not-allowed;
121 | }
122 |
123 | .calendar-grid-cell[data-selected="true"] {
124 | background-color: var(--focused-border-color);
125 | color: var(--text-color);
126 | }
127 |
128 | .calendar-grid-cell[data-today="true"]:not([data-selected="true"]) {
129 | border: 2px solid var(--focused-border-color);
130 | font-weight: bold;
131 | }
132 |
133 | .calendar-grid-cell-empty {
134 | aspect-ratio: 1;
135 | }
136 |
137 | .calendar-grid-weeknum {
138 | display: flex;
139 | align-items: center;
140 | justify-content: center;
141 | font-size: 12px;
142 | color: var(--dim-text-color);
143 | background-color: var(--background-color);
144 | border-radius: 4px;
145 | }
146 |
147 | /* Calendar with week numbers */
148 | .calendar-grid.show-week-numbers .calendar-grid-header,
149 | .calendar-grid.show-week-numbers .calendar-grid-week {
150 | grid-template-columns: auto repeat(7, 1fr);
151 | }
152 |
153 | /* Calendar states */
154 | .calendar[data-disabled="true"] {
155 | opacity: 0.6;
156 | pointer-events: none;
157 | }
158 |
159 | /* Animation for month transitions */
160 | @keyframes fadeIn {
161 | from {
162 | opacity: 0;
163 | transform: translateY(10px);
164 | }
165 |
166 | to {
167 | opacity: 1;
168 | transform: translateY(0);
169 | }
170 | }
171 |
172 | .calendar-grid-body {
173 | animation: fadeIn 0.2s ease-out;
174 | }
--------------------------------------------------------------------------------
/preview/src/components/checkbox/docs.md:
--------------------------------------------------------------------------------
1 | The Checkbox component is used to create an accessible checkbox input.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // The checkbox component creates a checkbox button
7 | Checkbox {
8 | // The checkbox indicator is a child component which will only be visible when the checkbox is checked.
9 | CheckboxIndicator {
10 | // The content of the checkbox indicator, typically an icon or image.
11 | {children}
12 | }
13 | }
14 | ```
15 |
--------------------------------------------------------------------------------
/preview/src/components/checkbox/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::checkbox::{Checkbox, CheckboxIndicator};
3 | #[component]
4 | pub(super) fn Demo() -> Element {
5 | rsx! {
6 | document::Link {
7 | rel: "stylesheet",
8 | href: asset!("/src/components/checkbox/style.css"),
9 | }
10 | Checkbox { id: "tos-check", name: "tos-check",
11 | CheckboxIndicator { "✓" }
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/preview/src/components/checkbox/style.css:
--------------------------------------------------------------------------------
1 | #tos-check {
2 | width: 25px;
3 | height: 25px;
4 | margin: 8px 16px;
5 | color: var(--text-color);
6 | background-color: var(--background-color);
7 | border: 1px solid var(--dim-border-color);
8 | border-radius: 4px;
9 | cursor: pointer;
10 | font-size: 14px;
11 | }
12 |
13 | #tos-check:hover {
14 | color: var(--text-color);
15 | background-color: var(--hover-background-color);
16 | transition: background-color 0.2s ease;
17 | }
18 |
19 | #tos-check:focus {
20 | color: var(--text-color);
21 | background-color: var(--focused-background-color);
22 | transition: background-color 0.2s ease;
23 | }
24 |
--------------------------------------------------------------------------------
/preview/src/components/context_menu/docs.md:
--------------------------------------------------------------------------------
1 | The context menu component can be used to define a context menu that is displayed when the user right-clicks on an element. It can contain various menu items that the user can interact with.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // The context menu component must wrap all context menu items.
7 | ContextMenu {
8 | // The context menu trigger is the element that will display the context menu when right-clicked.
9 | ContextMenuTrigger {
10 | // The content of the trigger
11 | {children}
12 | }
13 | // The context menu content contains all the items that will be displayed in the context menu.
14 | ContextMenuContent {
15 | // Each context menu item represents an individual action in the context menu. Items are displayed in order based on the order of the index property.
16 | ContextMenuItem {
17 | // The index of the item, used to determine the order in which items are displayed.
18 | index: 0,
19 | // The value of the item which will be passed to the on_select callback when the item is selected.
20 | value: "",
21 | on_select: |value: String| {
22 | // This callback is triggered when the item is selected.
23 | // The value parameter contains the value of the selected item.
24 | },
25 | }
26 | }
27 | }
28 | ```
--------------------------------------------------------------------------------
/preview/src/components/context_menu/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::context_menu::{
3 | ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger,
4 | };
5 | #[component]
6 | pub(super) fn Demo() -> Element {
7 | let mut selected_value = use_signal(String::new);
8 | rsx! {
9 | document::Link {
10 | rel: "stylesheet",
11 | href: asset!("/src/components/context_menu/style.css"),
12 | }
13 | div { class: "context-menu-example",
14 | ContextMenu {
15 | ContextMenuTrigger { class: "context-menu-trigger", "Right click here to open context menu" }
16 | ContextMenuContent { class: "context-menu-content",
17 | ContextMenuItem {
18 | class: "context-menu-item",
19 | value: "edit".to_string(),
20 | index: 0usize,
21 | on_select: move |value| {
22 | selected_value.set(value);
23 | },
24 | "Edit"
25 | }
26 | ContextMenuItem {
27 | class: "context-menu-item",
28 | value: "duplicate".to_string(),
29 | index: 1usize,
30 | on_select: move |value| {
31 | selected_value.set(value);
32 | },
33 | "Duplicate"
34 | }
35 | ContextMenuItem {
36 | class: "context-menu-item",
37 | value: "delete".to_string(),
38 | index: 2usize,
39 | on_select: move |value| {
40 | selected_value.set(value);
41 | },
42 | "Delete"
43 | }
44 | }
45 | }
46 | div { class: "selected-value",
47 | if selected_value().is_empty() {
48 | "No action selected"
49 | } else {
50 | "Selected action: {selected_value()}"
51 | }
52 | }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/preview/src/components/context_menu/style.css:
--------------------------------------------------------------------------------
1 | .context-menu-example {
2 | padding: 20px;
3 | }
4 |
5 | .context-menu-trigger {
6 | padding: 20px;
7 | background: var(--brighter-background-color);
8 | border: 1px solid var(--dim-border-color);
9 | border-radius: 4px;
10 | cursor: context-menu;
11 | user-select: none;
12 | }
13 |
14 | .context-menu-content {
15 | min-width: 220px;
16 | background: var(--brighter-background-color);
17 | border-radius: 6px;
18 | padding: 5px;
19 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
20 | border: 1px solid var(--dim-border-color);
21 | animation: slideIn 0.1s ease-out;
22 | }
23 |
24 | .context-menu-content[hidden] {
25 | display: none;
26 | }
27 |
28 | .context-menu-item {
29 | padding: 8px 12px;
30 | border-radius: 4px;
31 | cursor: pointer;
32 | user-select: none;
33 | outline: none;
34 | font-size: 14px;
35 | color: var(--text-color);
36 | display: flex;
37 | align-items: center;
38 | }
39 |
40 | .context-menu-item:hover {
41 | background: var(--hover-background-color);
42 | }
43 |
44 | .context-menu-item:focus {
45 | background: var(--focused-background-color);
46 | }
47 |
48 | @keyframes slideIn {
49 | from {
50 | opacity: 0;
51 | transform: scale(0.95);
52 | }
53 |
54 | to {
55 | opacity: 1;
56 | transform: scale(1);
57 | }
58 | }
--------------------------------------------------------------------------------
/preview/src/components/dropdown_menu/docs.md:
--------------------------------------------------------------------------------
1 | The DropdownMenu component is used to create a dropdown menu that can be triggered by a button click. It allows users to select an option from a list of items.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // The dropdown menu component must wrap all dropdown items.
7 | DropdownMenu {
8 | // The dropdown menu trigger is the button that will display the dropdown menu when clicked.
9 | DropdownMenuTrigger {
10 | // The content of the trigger to display inside the button.
11 | {children}
12 | }
13 | // The dropdown menu content contains all the items that will be displayed in the dropdown menu.
14 | DropdownMenuContent {
15 | // Each dropdown menu item represents an individual option in the dropdown menu. Items are displayed in order based on the order of the index property.
16 | DropdownMenuItem {
17 | // The index of the item, used to determine the order in which items are displayed.
18 | index: 0,
19 | // The value of the item which will be passed to the on_select callback when the item is selected.
20 | value: "",
21 | on_select: |value: String| {
22 | // This callback is triggered when the item is selected.
23 | // The value parameter contains the value of the selected item.
24 | },
25 | }
26 | }
27 | }
28 | ```
--------------------------------------------------------------------------------
/preview/src/components/dropdown_menu/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::dropdown_menu::{
3 | DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
4 | };
5 | #[component]
6 | pub(super) fn Demo() -> Element {
7 | rsx! {
8 | document::Link {
9 | rel: "stylesheet",
10 | href: asset!("/src/components/dropdown_menu/style.css"),
11 | }
12 | DropdownMenu { class: "dropdown-menu", default_open: false,
13 | DropdownMenuTrigger { class: "dropdown-menu-trigger", "Open Menu" }
14 | DropdownMenuContent { class: "dropdown-menu-content",
15 | DropdownMenuItem {
16 | class: "dropdown-menu-item",
17 | value: "item1".to_string(),
18 | index: 0usize,
19 | on_select: move |value| {
20 | tracing::info!("Selected: {}", value);
21 | },
22 | "Item 1"
23 | }
24 | DropdownMenuItem {
25 | class: "dropdown-menu-item",
26 | value: "item2".to_string(),
27 | index: 1usize,
28 | on_select: move |value| {
29 | tracing::info!("Selected: {}", value);
30 | },
31 | "Item 2"
32 | }
33 | DropdownMenuItem {
34 | class: "dropdown-menu-item",
35 | value: "item3".to_string(),
36 | index: 2usize,
37 | on_select: move |value| {
38 | tracing::info!("Selected: {}", value);
39 | },
40 | "Item 3"
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/preview/src/components/dropdown_menu/style.css:
--------------------------------------------------------------------------------
1 | /* Dropdown Menu Styles */
2 | .dropdown-menu {
3 | position: relative;
4 | display: inline-block;
5 | }
6 |
7 | .dropdown-menu-trigger {
8 | padding: 8px 16px;
9 | color: var(--text-color);
10 | background-color: var(--brighter-background-color);
11 | border: 1px solid var(--dim-border-color);
12 | border-radius: 4px;
13 | cursor: pointer;
14 | font-size: 14px;
15 | transition: all 0.2s ease;
16 | }
17 |
18 | .dropdown-menu-trigger:hover {
19 | background-color: var(--dim-background-color);
20 | }
21 |
22 | .dropdown-menu-trigger:focus {
23 | outline: none;
24 | box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
25 | }
26 |
27 | .dropdown-menu-content {
28 | position: absolute;
29 | top: 100%;
30 | left: 0;
31 | margin-top: 4px;
32 | min-width: 200px;
33 | background-color: var(--brighter-background-color);
34 | border: 1px solid var(--dim-border-color);
35 | border-radius: 4px;
36 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
37 | z-index: 1000;
38 | padding: 4px 0;
39 | }
40 |
41 | .dropdown-menu-content[hidden] {
42 | display: none;
43 | }
44 |
45 | .dropdown-menu-item {
46 | padding: 8px 16px;
47 | cursor: pointer;
48 | font-size: 14px;
49 | transition: all 0.2s ease;
50 | user-select: none;
51 | }
52 |
53 | .dropdown-menu-item:hover {
54 | background-color: var(--hover-background-color);
55 | }
56 |
57 | .dropdown-menu-item:focus {
58 | outline: none;
59 | background-color: var(--focused-background-color);
60 | }
61 |
62 | /* State styles */
63 | .dropdown-menu[data-disabled="true"] .dropdown-menu-trigger {
64 | opacity: 0.5;
65 | cursor: not-allowed;
66 | }
67 |
68 | .dropdown-menu-item[data-disabled="true"] {
69 | opacity: 0.5;
70 | cursor: not-allowed;
71 | }
--------------------------------------------------------------------------------
/preview/src/components/form/docs.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DioxusLabs/components/4ec9d3bdea7f55470f90bf96c5061e9f534ec9bd/preview/src/components/form/docs.md
--------------------------------------------------------------------------------
/preview/src/components/form/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::checkbox::{Checkbox, CheckboxIndicator};
3 | #[component]
4 | pub(super) fn Demo() -> Element {
5 | rsx! {
6 | document::Link { rel: "stylesheet", href: asset!("/src/components/form/style.css") }
7 | form {
8 | class: "form-example",
9 | onsubmit: move |e| {
10 | tracing::info!("{:?}", e.values());
11 | },
12 | Checkbox { id: "tos-check", name: "tos-check",
13 | CheckboxIndicator { "+" }
14 | }
15 | label { r#for: "tos-check", "I agree to the terms presented." }
16 | br {}
17 | button { r#type: "submit", "Submit" }
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/preview/src/components/form/style.css:
--------------------------------------------------------------------------------
1 | #tos-check {
2 | width: 50px;
3 | height: 50px;
4 | margin: 8px 16px;
5 | color: var(--text-color);
6 | background-color: var(--brighter-background-color);
7 | border: 1px solid var(--dim-border-color);
8 | border-radius: 4px;
9 | cursor: pointer;
10 | font-size: 14px;
11 | }
12 |
13 | .form-example button {
14 | padding: 8px 16px;
15 | color: var(--text-color);
16 | background-color: var(--brighter-background-color);
17 | border: 1px solid var(--dim-border-color);
18 | border-radius: 4px;
19 | cursor: pointer;
20 | font-size: 14px;
21 | }
22 |
--------------------------------------------------------------------------------
/preview/src/components/hover_card/docs.md:
--------------------------------------------------------------------------------
1 | The HoverCard component can be used to display additional information when a user hovers over an element. It is useful for showing tooltips, additional details, or any other content that should be revealed on hover.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // The HoverCard component wraps the trigger element and the content that will be displayed on hover.
7 | HoverCard {
8 | // The HoverCardTrigger contains the elements that will trigger the hover card to display when hovered.
9 | HoverCardTrigger {
10 | // The elements that will trigger the hover card when hovered over.
11 | {children}
12 | }
13 | // The HoverCardContent contains the content that will be displayed when the user hovers over the trigger.
14 | HoverCardContent {
15 | // The side of the HoverCardTrigger where the content will be displayed. Can be one Top, Right, Bottom, or Left.
16 | side: HoverCardSide::Bottom,
17 | // The alignment of the HoverCardContent relative to the HoverCardTrigger. Can be one of Start, Center, or End.
18 | align: HoverCardAlign::Start,
19 | // The content of the hover card, which can include text, images, or any other elements.
20 | {children}
21 | }
22 | }
23 | ```
--------------------------------------------------------------------------------
/preview/src/components/hover_card/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::hover_card::{
3 | HoverCard, HoverCardAlign, HoverCardContent, HoverCardSide, HoverCardTrigger,
4 | };
5 | #[component]
6 | pub(super) fn Demo() -> Element {
7 | rsx! {
8 | document::Link {
9 | rel: "stylesheet",
10 | href: asset!("/src/components/hover_card/style.css"),
11 | }
12 | div {
13 | style: "padding: 50px; display: flex; flex-direction: row; flex-wrap: wrap; gap: 40px; justify-content: center; align-items: center;",
14 | HoverCard { class: "hover-card",
15 | HoverCardTrigger { class: "hover-card-trigger",
16 | button { class: "user-trigger", "@johndoe" }
17 | }
18 | HoverCardContent { class: "hover-card-content", side: HoverCardSide::Bottom,
19 | div { class: "user-card",
20 | div { class: "user-card-header",
21 | img {
22 | class: "user-card-avatar",
23 | src: "https://github.com/DioxusLabs.png",
24 | alt: "User avatar",
25 | }
26 | div {
27 | h4 { class: "user-card-name", "John Doe" }
28 | p { class: "user-card-username", "@johndoe" }
29 | }
30 | }
31 | p { class: "user-card-bio",
32 | "Software developer passionate about Rust and web technologies. Building awesome UI components with Dioxus."
33 | }
34 | div { class: "user-card-stats",
35 | div { class: "user-card-stat",
36 | span { class: "user-card-stat-value", "142" }
37 | span { class: "user-card-stat-label", "Posts" }
38 | }
39 | div { class: "user-card-stat",
40 | span { class: "user-card-stat-value", "2.5k" }
41 | span { class: "user-card-stat-label", "Followers" }
42 | }
43 | div { class: "user-card-stat",
44 | span { class: "user-card-stat-value", "350" }
45 | span { class: "user-card-stat-label", "Following" }
46 | }
47 | }
48 | }
49 | }
50 | }
51 | HoverCard { class: "hover-card",
52 | HoverCardTrigger { class: "hover-card-trigger",
53 | button { class: "product-trigger", "View Product" }
54 | }
55 | HoverCardContent {
56 | class: "hover-card-content",
57 | side: HoverCardSide::Right,
58 | align: HoverCardAlign::Start,
59 | div { class: "product-card",
60 | img {
61 | class: "product-card-image",
62 | src: "https://images.unsplash.com/photo-1505740420928-5e560c06d30e",
63 | alt: "Product image",
64 | }
65 | h4 { class: "product-card-title", "Wireless Headphones" }
66 | p { class: "product-card-price", "$129.99" }
67 | p { class: "product-card-description",
68 | "High-quality wireless headphones with noise cancellation and 30-hour battery life."
69 | }
70 | div { class: "product-card-rating", "★★★★☆ (4.5)" }
71 | }
72 | }
73 | }
74 | HoverCard { class: "hover-card",
75 | HoverCardTrigger { class: "hover-card-trigger",
76 | a { href: "#", "Hover over this link" }
77 | }
78 | HoverCardContent {
79 | class: "hover-card-content",
80 | side: HoverCardSide::Top,
81 | align: HoverCardAlign::Center,
82 | div { style: "padding: 8px;",
83 | p { style: "margin: 0;", "This link will take you to an external website." }
84 | }
85 | }
86 | }
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/preview/src/components/hover_card/style.css:
--------------------------------------------------------------------------------
1 | /* Hover Card Styles */
2 | .hover-card {
3 | position: relative;
4 | display: inline-block;
5 | }
6 |
7 | .hover-card-trigger {
8 | display: inline-block;
9 | cursor: pointer;
10 | }
11 |
12 | .hover-card-content {
13 | position: absolute;
14 | z-index: 1000;
15 | min-width: 200px;
16 | padding: 12px 16px;
17 | border-radius: 6px;
18 | background-color: var(--brighter-background-color);
19 | color: var(--text-color);
20 | font-size: 14px;
21 | line-height: 1.5;
22 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
23 | border: 1px solid #eaeaea;
24 | animation: hoverCardFadeIn 0.2s ease-in-out;
25 | }
26 |
27 | /* Positioning based on side */
28 | .hover-card-content[data-side="top"] {
29 | position: absolute;
30 | bottom: 100%;
31 | margin-bottom: 10px;
32 | left: 50%;
33 | transform: translateX(-50%);
34 | }
35 |
36 | .hover-card-content[data-side="right"] {
37 | position: absolute;
38 | left: 100%;
39 | top: 50%;
40 | transform: translateY(-50%);
41 | margin-left: 10px;
42 | }
43 |
44 | .hover-card-content[data-side="bottom"] {
45 | position: absolute;
46 | top: 100%;
47 | margin-top: 10px;
48 | left: 50%;
49 | transform: translateX(-50%);
50 | }
51 |
52 | .hover-card-content[data-side="left"] {
53 | position: absolute;
54 | right: 100%;
55 | top: 50%;
56 | transform: translateY(-50%);
57 | margin-right: 10px;
58 | }
59 |
60 | /* Alignment styles for top and bottom */
61 | .hover-card-content[data-side="top"][data-align="start"],
62 | .hover-card-content[data-side="bottom"][data-align="start"] {
63 | left: 0;
64 | transform: none;
65 | }
66 |
67 | .hover-card-content[data-side="top"][data-align="center"],
68 | .hover-card-content[data-side="bottom"][data-align="center"] {
69 | left: 50%;
70 | transform: translateX(-50%);
71 | }
72 |
73 | .hover-card-content[data-side="top"][data-align="end"],
74 | .hover-card-content[data-side="bottom"][data-align="end"] {
75 | left: auto;
76 | right: 0;
77 | transform: none;
78 | }
79 |
80 | /* Alignment styles for left and right */
81 | .hover-card-content[data-side="left"][data-align="start"],
82 | .hover-card-content[data-side="right"][data-align="start"] {
83 | top: 0;
84 | transform: none;
85 | }
86 |
87 | .hover-card-content[data-side="left"][data-align="center"],
88 | .hover-card-content[data-side="right"][data-align="center"] {
89 | top: 50%;
90 | transform: translateY(-50%);
91 | }
92 |
93 | .hover-card-content[data-side="left"][data-align="end"],
94 | .hover-card-content[data-side="right"][data-align="end"] {
95 | top: auto;
96 | bottom: 0;
97 | transform: none;
98 | }
99 |
100 | /* Animation */
101 | @keyframes hoverCardFadeIn {
102 | from {
103 | opacity: 0;
104 | transform: scale(0.95);
105 | }
106 |
107 | to {
108 | opacity: 1;
109 | transform: scale(1);
110 | }
111 | }
112 |
113 | /* State styles */
114 | .hover-card[data-disabled="true"] .hover-card-trigger {
115 | cursor: default;
116 | }
117 |
118 | .hover-card-content[data-state="closed"] {
119 | display: none;
120 | }
121 |
122 | /* Example specific styles */
123 | .user-card {
124 | display: flex;
125 | flex-direction: column;
126 | gap: 8px;
127 | }
128 |
129 | .user-card-header {
130 | display: flex;
131 | align-items: center;
132 | gap: 12px;
133 | }
134 |
135 | .user-card-avatar {
136 | width: 48px;
137 | height: 48px;
138 | border-radius: 50%;
139 | object-fit: cover;
140 | }
141 |
142 | .user-card-name {
143 | margin: 0;
144 | font-size: 16px;
145 | font-weight: 600;
146 | }
147 |
148 | .user-card-username {
149 | margin: 0;
150 | font-size: 14px;
151 | color: var(--dim-text-color);
152 | }
153 |
154 | .user-card-bio {
155 | margin: 8px 0;
156 | font-size: 14px;
157 | color: var(--text-color);
158 | }
159 |
160 | .user-card-stats {
161 | display: flex;
162 | gap: 16px;
163 | margin-top: 8px;
164 | }
165 |
166 | .user-card-stat {
167 | display: flex;
168 | flex-direction: column;
169 | }
170 |
171 | .user-card-stat-value {
172 | font-weight: 600;
173 | font-size: 14px;
174 | }
175 |
176 | .user-card-stat-label {
177 | font-size: 12px;
178 | color: var(--dim-text-color);
179 | }
180 |
181 | .user-trigger {
182 | cursor: pointer;
183 | padding: 8px 16px;
184 | color: var(--text-color);
185 | background-color: var(--brighter-background-color);
186 | border: 1px solid var(--dim-border-color);
187 | border-radius: 4px;
188 | cursor: pointer;
189 | font-size: 14px;
190 | }
191 |
192 | .product-card {
193 | display: flex;
194 | flex-direction: column;
195 | gap: 8px;
196 | }
197 |
198 | .product-card-image {
199 | width: 100%;
200 | height: 120px;
201 | object-fit: cover;
202 | border-radius: 4px;
203 | }
204 |
205 | .product-card-title {
206 | margin: 0;
207 | font-size: 16px;
208 | font-weight: 600;
209 | }
210 |
211 | .product-card-price {
212 | font-size: 14px;
213 | font-weight: 600;
214 | color: #0070f3;
215 | }
216 |
217 | .product-card-description {
218 | margin: 8px 0;
219 | font-size: 14px;
220 | color: var(--text-color);
221 | }
222 |
223 | .product-card-rating {
224 | display: flex;
225 | align-items: center;
226 | gap: 4px;
227 | font-size: 14px;
228 | color: #f5a623;
229 | }
230 |
231 | .product-trigger {
232 | cursor: pointer;
233 | padding: 8px 16px;
234 | color: var(--text-color);
235 | background-color: var(--brighter-background-color);
236 | border: 1px solid var(--dim-border-color);
237 | border-radius: 4px;
238 | cursor: pointer;
239 | font-size: 14px;
240 | }
241 |
--------------------------------------------------------------------------------
/preview/src/components/menubar/docs.md:
--------------------------------------------------------------------------------
1 | The Menubar component can be used to display a menu bar with collapsible menus. It is useful for creating a navigation bar or a menu system in your application.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // The Menubar component wraps the entire menu bar and contains the individual menus in the order of their index.
7 | Menubar {
8 | // The MenubarMenu contains the individual menus that can be opened.
9 | MenubarMenu {
10 | // The index of the menu, used to determine the order in which menus are displayed.
11 | index: 0,
12 | // The menubar trigger is the element that will display the menu when activated.
13 | MenubarTrigger {
14 | // The content of the trigger button
15 | {children}
16 | }
17 | // The menubar content contains all the items that will be displayed in the menu when it is opened.
18 | MenubarContent {
19 | // Each menubar item represents an individual items in the menu.
20 | MenubarItem {
21 | // The value of the item which will be passed to the on_select callback when the item is selected.
22 | value: "",
23 | on_select: |value: String| {
24 | // This callback is triggered when the item is selected.
25 | // The value parameter contains the value of the selected item.
26 | },
27 | }
28 | }
29 | }
30 | }
31 | ```
32 |
--------------------------------------------------------------------------------
/preview/src/components/menubar/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::menubar::{
3 | Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarTrigger,
4 | };
5 | #[component]
6 | pub(super) fn Demo() -> Element {
7 | rsx! {
8 | document::Link {
9 | rel: "stylesheet",
10 | href: asset!("/src/components/menubar/style.css"),
11 | }
12 | div { class: "menubar-example",
13 | Menubar { class: "menubar",
14 | MenubarMenu { class: "menubar-menu", index: 0usize,
15 | MenubarTrigger { class: "menubar-trigger", "File" }
16 | MenubarContent { class: "menubar-content",
17 | MenubarItem {
18 | class: "menubar-item",
19 | value: "new".to_string(),
20 | on_select: move |value| {
21 | tracing::info!("Selected: {value}");
22 | },
23 | "New"
24 | }
25 | MenubarItem {
26 | class: "menubar-item",
27 | value: "open".to_string(),
28 | on_select: move |value| {
29 | tracing::info!("Selected: {value}");
30 | },
31 | "Open"
32 | }
33 | MenubarItem {
34 | class: "menubar-item",
35 | value: "save".to_string(),
36 | on_select: move |value| {
37 | tracing::info!("Selected: {value}");
38 | },
39 | "Save"
40 | }
41 | }
42 | }
43 | MenubarMenu { class: "menubar-menu", index: 1usize,
44 | MenubarTrigger { class: "menubar-trigger", "Edit" }
45 | MenubarContent { class: "menubar-content",
46 | MenubarItem {
47 | class: "menubar-item",
48 | value: "cut".to_string(),
49 | on_select: move |value| {
50 | tracing::info!("Selected: {value}");
51 | },
52 | "Cut"
53 | }
54 | MenubarItem {
55 | class: "menubar-item",
56 | value: "copy".to_string(),
57 | on_select: move |value| {
58 | tracing::info!("Selected: {value}");
59 | },
60 | "Copy"
61 | }
62 | MenubarItem {
63 | class: "menubar-item",
64 | value: "paste".to_string(),
65 | on_select: move |value| {
66 | tracing::info!("Selected: {value}");
67 | },
68 | "Paste"
69 | }
70 | }
71 | }
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/preview/src/components/menubar/style.css:
--------------------------------------------------------------------------------
1 | .menubar {
2 | display: flex;
3 | gap: 4px;
4 | padding: 4px;
5 | border: 1px solid var(--dim-border-color);
6 | border-radius: 4px;
7 | background: var(--background-color);
8 | }
9 |
10 | .menubar-menu {
11 | position: relative;
12 | }
13 |
14 | .menubar-trigger {
15 | padding: 8px 12px;
16 | border: none;
17 | background: none;
18 | cursor: pointer;
19 | color: var(--text-color);
20 | }
21 |
22 | .menubar-trigger:hover {
23 | background: var(--hover-background-color);
24 | border-radius: 4px;
25 | }
26 |
27 | .menubar-content {
28 | display: none;
29 | position: absolute;
30 | top: 100%;
31 | left: 0;
32 | min-width: 200px;
33 | background: var(--background-color);
34 | border: 1px solid var(--dim-border-color);
35 | border-radius: 4px;
36 | padding: 4px;
37 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
38 | }
39 |
40 | .menubar-menu[data-state="open"] .menubar-content {
41 | display: block;
42 | }
43 |
44 | .menubar-item {
45 | display: block;
46 | padding: 8px 12px;
47 | cursor: pointer;
48 | border-radius: 4px;
49 | }
50 |
51 | .menubar-item:hover {
52 | background: var(--hover-background-color);
53 | }
54 |
55 | [data-disabled="true"] {
56 | opacity: 0.5;
57 | cursor: not-allowed;
58 | }
--------------------------------------------------------------------------------
/preview/src/components/mod.rs:
--------------------------------------------------------------------------------
1 | use super::{ComponentDemoData, HighlightedCode};
2 | macro_rules! examples {
3 | ($($name:ident),*) => {
4 | $(mod $name;)* pub (crate) static DEMOS : & [ComponentDemoData] = &
5 | [$(ComponentDemoData { name : stringify!($name), docs :
6 | include_str!(concat!(env!("OUT_DIR"), "/", stringify!($name), "/docs.html")),
7 | rs_highlighted : HighlightedCode { light : include_str!(concat!(env!("OUT_DIR"),
8 | "/", stringify!($name), "/mod.rs.base16-ocean.light.html")), dark :
9 | include_str!(concat!(env!("OUT_DIR"), "/", stringify!($name),
10 | "/mod.rs.base16-ocean.dark.html")), }, css_highlighted : HighlightedCode { light
11 | : include_str!(concat!(env!("OUT_DIR"), "/", stringify!($name),
12 | "/style.css.base16-ocean.light.html")), dark :
13 | include_str!(concat!(env!("OUT_DIR"), "/", stringify!($name),
14 | "/style.css.base16-ocean.dark.html")), }, component : $name ::Demo, },)*];
15 | };
16 | }
17 | examples!(
18 | accordion,
19 | aspect_ratio,
20 | avatar,
21 | alert_dialog,
22 | calendar,
23 | context_menu,
24 | checkbox,
25 | dropdown_menu,
26 | hover_card,
27 | menubar,
28 | progress,
29 | radio_group,
30 | scroll_area,
31 | select,
32 | separator,
33 | slider,
34 | switch,
35 | tabs,
36 | toast,
37 | toggle_group,
38 | toolbar,
39 | tooltip
40 | );
41 |
--------------------------------------------------------------------------------
/preview/src/components/progress/docs.md:
--------------------------------------------------------------------------------
1 | The Progress component is used to display the progress of a task or operation. It can be used to indicate loading states, file uploads, or any other process that takes time to complete.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | Progress {
7 | // The current progress value (0 to max
8 | value: 0.5,
9 | // The maximum value of the progress (default is 100.0)
10 | max: 1.0,
11 | // Elements that will be displayed inside the progress bar
12 | {children}
13 | }
14 | ```
--------------------------------------------------------------------------------
/preview/src/components/progress/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::progress::{Progress, ProgressIndicator};
3 | #[component]
4 | pub(super) fn Demo() -> Element {
5 | let mut progress = use_signal(|| 80.0);
6 | rsx! {
7 | document::Link {
8 | rel: "stylesheet",
9 | href: asset!("/src/components/progress/style.css"),
10 | }
11 | div { style: "display: flex; flex-direction: column; align-items: center; gap: 4px;",
12 | Progress { class: "progress", value: progress(),
13 | ProgressIndicator { class: "progress-indicator" }
14 | }
15 | button {
16 | class: "progress-button",
17 | onclick: move |_| progress.set(progress() + 10.0),
18 | "Increment"
19 | }
20 | button {
21 | class: "progress-button",
22 | onclick: move |_| progress.set(progress() - 10.0),
23 | "Decrement"
24 | }
25 | button { class: "progress-button", onclick: move |_| progress.set(0.0), "Reset" }
26 | button {
27 | class: "progress-button",
28 | onclick: move |_| progress.set(100.0),
29 | "Complete"
30 | }
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/preview/src/components/progress/style.css:
--------------------------------------------------------------------------------
1 | .progress {
2 | position: relative;
3 | overflow: hidden;
4 | background: var(--brighter-background-color);
5 | border-radius: 9999px;
6 | width: 200px;
7 | height: 20px;
8 | border: 2px solid var(--dim-border-color);
9 | box-sizing: border-box;
10 | }
11 |
12 | .progress[data-state='indeterminate'] .progress-indicator {
13 | width: 50%;
14 | animation: indeterminate 1s infinite linear;
15 | }
16 |
17 | .progress-indicator {
18 | height: 100%;
19 | background-color: var(--contrast-background-color);
20 | transition: width 250ms ease;
21 | width: var(--progress-value, 0%);
22 | /* Use CSS variable for width */
23 | }
24 |
25 | @keyframes indeterminate {
26 | 0% {
27 | transform: translateX(-100%);
28 | }
29 |
30 | 100% {
31 | transform: translateX(200%);
32 | }
33 | }
34 |
35 | .progress-button {
36 | padding: 8px 12px;
37 | border-radius: 4px;
38 | cursor: pointer;
39 | user-select: none;
40 | outline: none;
41 | font-size: 14px;
42 | background-color: var(--brighter-background-color);
43 | border: 1px solid var(--dim-border-color);
44 | color: var(--text-color);
45 | }
46 |
47 | .progress-button:hover {
48 | background: var(--hover-background-color);
49 | }
50 |
51 | .progress-button:focus {
52 | background: var(--focused-background-color);
53 | }
54 |
--------------------------------------------------------------------------------
/preview/src/components/radio_group/docs.md:
--------------------------------------------------------------------------------
1 | The RadioGroup component is used to create a group of radio buttons that allows the user to select one option from a set.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // The RadioGroup component wraps all radio items in the group.
7 | RadioGroup {
8 | // The value property represents the currently selected radio button in the group.
9 | value: "option1",
10 | on_value_change: |value: String| {
11 | // This callback is triggered when the selected radio button changes.
12 | // The value parameter contains the value of the newly selected radio button.
13 | },
14 | // The RadioItem component represents each individual radio button in the group.
15 | RadioItem {
16 | // The index of the radio item, used to determine the order in which items are displayed.
17 | index: 0,
18 | // The value of the radio button, which is used to identify the selected option and will be passed to the on_value_change callback when selected.
19 | value: "option1",
20 | // The contents of the radio item button
21 | {children}
22 | }
23 | }
24 | ```
--------------------------------------------------------------------------------
/preview/src/components/radio_group/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::radio_group::{RadioGroup, RadioItem};
3 | #[component]
4 | pub(super) fn Demo() -> Element {
5 | let mut value = use_signal(|| String::from("option1"));
6 | rsx! {
7 | document::Link {
8 | rel: "stylesheet",
9 | href: asset!("/src/components/radio_group/style.css"),
10 | }
11 | RadioGroup {
12 | class: "radio-group",
13 | value,
14 | on_value_change: move |new_value| {
15 | value.set(new_value);
16 | },
17 | RadioItem {
18 | class: "radio-item",
19 | value: "option1".to_string(),
20 | index: 0usize,
21 | "Option 1"
22 | }
23 | RadioItem {
24 | class: "radio-item",
25 | value: "option2".to_string(),
26 | index: 1usize,
27 | "Option 2"
28 | }
29 | RadioItem {
30 | class: "radio-item",
31 | value: "option3".to_string(),
32 | index: 2usize,
33 | "Option 3"
34 | }
35 | }
36 | div { style: "margin-top: 1rem;", "Selected value: {value()}" }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/preview/src/components/radio_group/style.css:
--------------------------------------------------------------------------------
1 | .radio-group {
2 | display: flex;
3 | flex-direction: column;
4 | gap: 10px;
5 | padding: 15px;
6 | }
7 |
8 | .radio-item {
9 | display: flex;
10 | align-items: center;
11 | padding: 8px 16px;
12 | border-radius: 6px;
13 | border: 1px solid var(--dim-border-color);
14 | background: var(--brighter-background-color);
15 | cursor: pointer;
16 | font-size: 14px;
17 | transition: all 0.2s;
18 | color: var(--text-color);
19 | }
20 |
21 | .radio-item:hover {
22 | background: var(--hover-background-color);
23 | }
24 |
25 | .radio-item:focus {
26 | outline: none;
27 | box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
28 | }
29 |
30 | .radio-item[data-state="checked"] {
31 | background: var(--focused-background-color);
32 | border-color: var(--dim-border-color);
33 | }
34 |
35 | .radio-item[data-disabled="true"] {
36 | opacity: 0.5;
37 | cursor: not-allowed;
38 | }
--------------------------------------------------------------------------------
/preview/src/components/scroll_area/docs.md:
--------------------------------------------------------------------------------
1 | The ScrollArea component is used to create a scrollable container for its children. It can be used to enable scrolling for content that overflows the available space.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // The ScrollArea component wraps all scrollable content.
7 | ScrollArea {
8 | // The direction in which the scroll area can scroll. Can be one of Horizontal, Vertical, or Both.
9 | scroll_direction: ScrollDirection::Vertical,
10 | // The content of the scrollable area
11 | {children}
12 | }
13 | ```
--------------------------------------------------------------------------------
/preview/src/components/scroll_area/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::scroll_area::{ScrollArea, ScrollDirection};
3 | #[component]
4 | pub(super) fn Demo() -> Element {
5 | rsx! {
6 | document::Link {
7 | rel: "stylesheet",
8 | href: asset!("/src/components/scroll_area/style.css"),
9 | }
10 | div { class: "scroll-area-demo",
11 | div { class: "scroll-demo-section",
12 | h3 { "Vertical Scroll" }
13 | ScrollArea {
14 | class: "demo-scroll-area",
15 | direction: ScrollDirection::Vertical,
16 | div { class: "scroll-content",
17 | for i in 1..=20 {
18 | p { "Scrollable content item {i}" }
19 | }
20 | }
21 | }
22 | }
23 | div { class: "scroll-demo-section",
24 | h3 { "Horizontal Scroll" }
25 | ScrollArea {
26 | class: "demo-scroll-area",
27 | direction: ScrollDirection::Horizontal,
28 | div { class: "scroll-content-horizontal",
29 | for i in 1..=20 {
30 | span { "Column {i} " }
31 | }
32 | }
33 | }
34 | }
35 | div { class: "scroll-demo-section",
36 | h3 { "Both Directions" }
37 | ScrollArea {
38 | class: "demo-scroll-area",
39 | direction: ScrollDirection::Both,
40 | div { class: "scroll-content-both",
41 | for i in 1..=20 {
42 | div {
43 | for j in 1..=20 {
44 | span { "Cell {i},{j} " }
45 | }
46 | }
47 | }
48 | }
49 | }
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/preview/src/components/scroll_area/style.css:
--------------------------------------------------------------------------------
1 | .scroll-area-demo {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | justify-content: center;
6 | text-align: center;
7 | padding: 2em;
8 | width: 35rem;
9 | max-width: 100%;
10 | }
11 |
12 | .scroll-demo-section {
13 | max-width: 30rem;
14 | max-height: 30rem;
15 | margin-bottom: 30px;
16 | }
17 |
18 | .scroll-demo-section h3 {
19 | margin-bottom: 10px;
20 | color: var(--text-color);
21 | }
22 |
23 | .demo-scroll-area {
24 | border: 1px solid var(--dim-border-color);
25 | border-radius: 4px;
26 | background: var(--background-color);
27 | }
28 |
29 | /* Vertical scroll example */
30 | .demo-scroll-area[data-scroll-direction="vertical"] {
31 | height: 20rem;
32 | width: auto;
33 | }
34 |
35 | .scroll-content {
36 | padding: 10px;
37 | }
38 |
39 | .scroll-content p {
40 | margin: 8px 0;
41 | padding: 8px;
42 | background: var(--brighter-background-color);
43 | border-radius: 4px;
44 | }
45 |
46 | /* Horizontal scroll example */
47 | .demo-scroll-area[data-scroll-direction="horizontal"] {
48 | height: auto;
49 | width: 20rem;
50 | }
51 |
52 | .scroll-content-horizontal {
53 | padding: 10px;
54 | white-space: nowrap;
55 | }
56 |
57 | .scroll-content-horizontal span {
58 | display: inline-block;
59 | margin: 0 4px;
60 | padding: 8px 16px;
61 | background: var(--brighter-background-color);
62 | border-radius: 4px;
63 | }
64 |
65 | /* Both directions example */
66 | .demo-scroll-area[data-scroll-direction="both"] {
67 | height: 20rem;
68 | width: 20rem;
69 | }
70 |
71 | .scroll-content-both {
72 | padding: 10px;
73 | white-space: nowrap;
74 | }
75 |
76 | .scroll-content-both div {
77 | margin: 4px 0;
78 | }
79 |
80 | .scroll-content-both span {
81 | display: inline-block;
82 | margin: 0 4px;
83 | padding: 8px;
84 | background: var(--brighter-background-color);
85 | border-radius: 4px;
86 | min-width: 80px;
87 | text-align: center;
88 | }
89 |
90 | /* Scrollbar styles */
91 | .scroll-area-auto-hide {
92 | scrollbar-width: thin;
93 | scrollbar-color: rgba(0, 0, 0, 0.3) transparent;
94 | }
95 |
96 | .scroll-area-auto-hide::-webkit-scrollbar {
97 | width: 8px;
98 | height: 8px;
99 | }
100 |
101 | .scroll-area-auto-hide::-webkit-scrollbar-track {
102 | background: transparent;
103 | }
104 |
105 | .scroll-area-auto-hide::-webkit-scrollbar-thumb {
106 | background-color: rgba(0, 0, 0, 0.3);
107 | border-radius: 4px;
108 | }
109 |
110 | .scroll-area-auto-hide::-webkit-scrollbar-corner {
111 | background: transparent;
112 | }
113 |
114 | .scroll-area-always-show {
115 | scrollbar-width: thin;
116 | scrollbar-color: rgba(0, 0, 0, 0.3) rgba(0, 0, 0, 0.1);
117 | }
118 |
119 | .scroll-area-always-show::-webkit-scrollbar {
120 | width: 8px;
121 | height: 8px;
122 | }
123 |
124 | .scroll-area-always-show::-webkit-scrollbar-track {
125 | background: rgba(0, 0, 0, 0.1);
126 | }
127 |
128 | .scroll-area-always-show::-webkit-scrollbar-thumb {
129 | background-color: rgba(0, 0, 0, 0.3);
130 | border-radius: 4px;
131 | }
132 |
133 | .scroll-area-always-show::-webkit-scrollbar-corner {
134 | background: rgba(0, 0, 0, 0.1);
135 | }
--------------------------------------------------------------------------------
/preview/src/components/select/docs.md:
--------------------------------------------------------------------------------
1 | The Select component is used to create a dropdown menu that allows users to select one or more options from the select groups.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // The Select component wraps all select items in the dropdown.
7 | Select {
8 | // The currently selected value(s) in the dropdown.
9 | value: "option1",
10 | // Callback function triggered when the selected value changes.
11 | on_value_change: |value: String| {
12 | // Handle the change in selected value.
13 | },
14 | // An group within the select dropdown which may contain multiple items.
15 | SelectGroup {
16 | // The label for the group, which is displayed as a header in the dropdown.
17 | label: "Group 1",
18 | // Each select option represents an individual option in the dropdown.
19 | SelectOption {
20 | // The value of the item, which will be passed to the on_value_change callback when selected.
21 | value: "option1",
22 | // The content of the select option
23 | {children}
24 | }
25 | }
26 | }
27 | ```
--------------------------------------------------------------------------------
/preview/src/components/select/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::select::{Select, SelectGroup, SelectOption};
3 | #[component]
4 | pub(super) fn Demo() -> Element {
5 | let mut selected = use_signal(|| None::);
6 | use_effect(move || {
7 | if let Some(value) = selected() {
8 | tracing::info!("Selected value: {value}");
9 | }
10 | });
11 | rsx! {
12 | document::Link {
13 | rel: "stylesheet",
14 | href: asset!("/src/components/select/style.css"),
15 | }
16 | div { class: "select-example",
17 | div { class: "select-container",
18 | label { class: "select-label", "Choose a fruit:" }
19 | Select {
20 | class: "select",
21 | value: selected,
22 | on_value_change: move |value| selected.set(value),
23 | placeholder: "Select a fruit...",
24 | SelectGroup { label: "Fruits".to_string(),
25 | SelectOption { value: "apple".to_string(), "Apple" }
26 | SelectOption { value: "banana".to_string(), "Banana" }
27 | SelectOption { value: "orange".to_string(), "Orange" }
28 | SelectOption { value: "strawberry".to_string(), "Strawberry" }
29 | SelectOption { value: "watermelon".to_string(), "Watermelon" }
30 | }
31 | SelectGroup { label: "Other".to_string(),
32 | SelectOption { value: "other".to_string(), "Other" }
33 | }
34 | }
35 | }
36 | div { class: "selected-value",
37 | if let Some(value) = selected() {
38 | "Selected: {value}"
39 | } else {
40 | "No selection"
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/preview/src/components/select/style.css:
--------------------------------------------------------------------------------
1 | /* Select Styles */
2 | .select {
3 | position: relative;
4 | width: 100%;
5 | max-width: 300px;
6 | padding: 8px 16px;
7 | color: var(--text-color);
8 | background-color: var(--brighter-background-color);
9 | border: 1px solid var(--dim-border-color);
10 | border-radius: 4px;
11 | cursor: pointer;
12 | font-size: 14px;
13 | transition: all 0.2s ease;
14 | appearance: none;
15 | }
16 |
17 | .select-label {
18 | display: block;
19 | margin-bottom: 8px;
20 | font-size: 14px;
21 | font-weight: 500;
22 | color: var(--dim-text-color);
23 | }
24 |
25 | .select-trigger {
26 | display: flex;
27 | align-items: center;
28 | justify-content: space-between;
29 | width: 100%;
30 | padding: 10px 12px;
31 | background-color: var(--background-color);
32 | border: 1px solid var(--dim-border-color);
33 | border-radius: 4px;
34 | font-size: 14px;
35 | text-align: left;
36 | cursor: pointer;
37 | transition: border-color 0.2s, box-shadow 0.2s;
38 | }
39 |
40 | .select-trigger:hover {
41 | border-color: var(--hover-border-color);
42 | }
43 |
44 | .select-trigger:focus {
45 | outline: none;
46 | border-color: #0066cc;
47 | box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2);
48 | }
49 |
50 | .select-trigger[data-state="open"] {
51 | border-color: #0066cc;
52 | }
53 |
54 | .select-trigger[data-disabled="true"] {
55 | opacity: 0.5;
56 | cursor: not-allowed;
57 | }
58 |
59 | .select-placeholder {
60 | color: var(--text-color);
61 | }
62 |
63 | .select-value {
64 | color: var(--text-color);
65 | }
66 |
67 | .select-icon {
68 | margin-left: 8px;
69 | color: var(--text-color);
70 | transition: transform 0.2s;
71 | }
72 |
73 | .select-trigger[data-state="open"] .select-icon {
74 | transform: rotate(180deg);
75 | }
76 |
77 | .select-content {
78 | position: absolute;
79 | left: 0;
80 | width: 100%;
81 | max-height: 200px;
82 | overflow-y: auto;
83 | background-color: var(--brighter-background-color);
84 | border: 1px solid var(--dim-border-color);
85 | border-radius: 4px;
86 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
87 | z-index: 100;
88 | margin-top: 4px;
89 | }
90 |
91 | .select-content[data-position="top"] {
92 | bottom: 100%;
93 | margin-bottom: 4px;
94 | margin-top: 0;
95 | }
96 |
97 | .select-group {
98 | padding: 5px 0;
99 | }
100 |
101 | .select-item {
102 | display: flex;
103 | align-items: center;
104 | justify-content: space-between;
105 | padding: 8px 12px;
106 | font-size: 14px;
107 | cursor: pointer;
108 | transition: background-color 0.2s;
109 | }
110 |
111 | .select-item:hover {
112 | background-color: var(--hover-background-color);
113 | }
114 |
115 | .select-item:focus {
116 | outline: none;
117 | background-color: var(--focused-background-color);
118 | }
119 |
120 | .select-item[data-highlighted="true"] {
121 | background-color: var(--focused-background-color);
122 | }
123 |
124 | .select-item[data-state="selected"] {
125 | font-weight: 500;
126 | }
127 |
128 | .select-item[data-disabled="true"] {
129 | opacity: 0.5;
130 | cursor: not-allowed;
131 | }
132 |
133 | .select-item-indicator {
134 | color: #0066cc;
135 | margin-left: 8px;
136 | }
137 |
138 | .select-separator {
139 | height: 1px;
140 | background-color: var(--dim-border-color);
141 | margin: 5px 0;
142 | }
143 |
144 | /* Animation */
145 | @keyframes slideDown {
146 | from {
147 | opacity: 0;
148 | transform: translateY(-10px);
149 | }
150 | to {
151 | opacity: 1;
152 | transform: translateY(0);
153 | }
154 | }
155 |
156 | @keyframes slideUp {
157 | from {
158 | opacity: 0;
159 | transform: translateY(10px);
160 | }
161 | to {
162 | opacity: 1;
163 | transform: translateY(0);
164 | }
165 | }
166 |
167 | .select-content[data-state="open"][data-position="bottom"] {
168 | animation: slideDown 0.2s ease-out;
169 | }
170 |
171 | .select-content[data-state="open"][data-position="top"] {
172 | animation: slideUp 0.2s ease-out;
173 | }
174 |
175 | /* Example specific styles */
176 | .select-example {
177 | padding: 20px;
178 | max-width: 500px;
179 | margin: 0 auto;
180 | }
181 |
182 | .select-container {
183 | margin-bottom: 20px;
184 | }
185 |
186 | .selected-value {
187 | margin-top: 20px;
188 | padding: 10px;
189 | background-color: var(--brighter-background-color);
190 | border-radius: 4px;
191 | font-size: 14px;
192 | }
193 |
--------------------------------------------------------------------------------
/preview/src/components/separator/docs.md:
--------------------------------------------------------------------------------
1 | The Separator component is a simple horizontal or vertical line that can be used to visually separate content in your application.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // The Separator component can be oriented horizontally or vertically.
7 | Separator {
8 | // The orientation of the separator line. true for horizontal, false for vertical.
9 | horizontal: true,
10 | // The decorative property controls if the separator is decorative and should not be visible to screen readers.
11 | decorative: false,
12 | }
13 | ```
14 |
--------------------------------------------------------------------------------
/preview/src/components/separator/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::separator::Separator;
3 | #[component]
4 | pub(super) fn Demo() -> Element {
5 | rsx! {
6 | document::Link {
7 | rel: "stylesheet",
8 | href: asset!("/src/components/separator/style.css"),
9 | }
10 | "One thing"
11 | Separator {
12 | class: "separator",
13 | style: "margin: 15px 0; width: 50%;",
14 | horizontal: true,
15 | decorative: true,
16 | }
17 | "Another thing"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/preview/src/components/separator/style.css:
--------------------------------------------------------------------------------
1 | .separator {
2 | background-color: var(--dim-border-color);
3 | }
4 |
5 | .separator[data-orientation="horizontal"] {
6 | height: 1px;
7 | width: 100%;
8 | }
9 |
10 | .separator[data-orientation="vertical"] {
11 | height: 100%;
12 | width: 1px;
13 | }
--------------------------------------------------------------------------------
/preview/src/components/slider/docs.md:
--------------------------------------------------------------------------------
1 | The slider component allows users to select a value from a range by sliding a handle along a track.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // The slider component wraps all slider-related elements.
7 | Slider {
8 | // The current value of the slider, which should be updated as the user interacts with the slider.
9 | value: SliderValue::Single(0.0),
10 | // The orientation of the slider, true for horizontal and false for vertical.
11 | horizontal: true,
12 | // Callback function triggered when the slider value changes.
13 | on_value_change: |value: u32| {
14 | // Handle the change in slider value.
15 | },
16 | // The track represents the visual track along which the handle moves.
17 | SliderTrack {
18 | // The slider range represents the filled portion of the track
19 | SliderRange {
20 | // The content of the range
21 | {children}
22 | }
23 | // The slider thumb represents the draggable handle that the user moves along the track.
24 | SliderThumb {
25 | // An optional index which can be either 0 or 1 to indicate if this is the first or second thumb in a range slider.
26 | index: 0,
27 | // The content of the thumb button
28 | {children}
29 | }
30 | }
31 | }
32 | ```
--------------------------------------------------------------------------------
/preview/src/components/slider/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::slider::{
3 | Slider, SliderRange, SliderThumb, SliderTrack, SliderValue,
4 | };
5 | #[component]
6 | pub(super) fn Demo() -> Element {
7 | let mut value = use_signal(|| SliderValue::Single(50.0));
8 | rsx! {
9 | document::Link {
10 | rel: "stylesheet",
11 | href: asset!("/src/components/slider/style.css"),
12 | }
13 | div { class: "slider-example",
14 | div {
15 | label { "Single Value Slider" }
16 | div { style: "display: flex; flex-wrap: wrap; align-items: center; gap: 1rem;",
17 | Slider {
18 | class: "slider",
19 | value,
20 | horizontal: true,
21 | on_value_change: move |v| {
22 | value.set(v);
23 | },
24 | SliderTrack { class: "slider-track",
25 | SliderRange { class: "slider-range" }
26 | SliderThumb { class: "slider-thumb" }
27 | }
28 | }
29 | input {
30 | class: "slider-value",
31 | r#type: "text",
32 | readonly: true,
33 | value: match value() {
34 | SliderValue::Single(v) => format!("{v:.1}"),
35 | _ => String::new(),
36 | },
37 | }
38 | }
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/preview/src/components/slider/style.css:
--------------------------------------------------------------------------------
1 | .slider-example {
2 | display: flex;
3 | flex-direction: column;
4 | gap: 2rem;
5 | padding: 2rem;
6 | }
7 |
8 | .slider-example>div {
9 | display: flex;
10 | flex-direction: column;
11 | gap: 0.5rem;
12 | }
13 |
14 | .slider {
15 | position: relative;
16 | display: flex;
17 | align-items: center;
18 | width: 200px;
19 | touch-action: none;
20 | padding: 0.5rem 0;
21 | }
22 |
23 | .slider[data-orientation="vertical"] {
24 | height: 200px;
25 | width: auto;
26 | flex-direction: column;
27 | }
28 |
29 | .slider-track {
30 | position: relative;
31 | flex-grow: 1;
32 | background-color: rgba(100, 116, 139, 0.1);
33 | border-radius: 9999px;
34 | height: 4px;
35 | }
36 |
37 | .slider[data-orientation="vertical"] .slider-track {
38 | width: 4px;
39 | height: 100%;
40 | }
41 |
42 | .slider-range {
43 | position: absolute;
44 | background-color: var(--contrast-background-color);
45 | border-radius: 9999px;
46 | height: 100%;
47 | }
48 |
49 | .slider[data-orientation="vertical"] .slider-range {
50 | width: 100%;
51 | }
52 |
53 | .slider-thumb {
54 | all: unset;
55 | display: block;
56 | width: 16px;
57 | height: 16px;
58 | background-color: var(--brighter-background-color);
59 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
60 | border-radius: 50%;
61 | border: 2px solid var(--contrast-background-color);
62 | position: absolute;
63 | top: 50%;
64 | transform: translate(-50%, -50%);
65 | cursor: pointer;
66 | transition: border-color 150ms;
67 | }
68 |
69 | .slider[data-orientation="vertical"] .slider-thumb {
70 | left: 50%;
71 | transform: translate(-50%, 50%);
72 | }
73 |
74 | .slider-thumb:hover {
75 | border-color: var(--hover-border-color);
76 | }
77 |
78 | .slider-thumb:focus-visible {
79 | outline: 2px solid var(--focused-border-color);
80 | outline-offset: 2px;
81 | }
82 |
83 | .slider[data-disabled="true"] {
84 | opacity: 0.5;
85 | cursor: not-allowed;
86 | }
87 |
88 | .slider[data-disabled="true"] .slider-thumb {
89 | cursor: not-allowed;
90 | }
91 |
92 | .slider-value {
93 | font-size: 14px;
94 | color: var(--text-color);
95 | background-color: var(--brighter-background-color);
96 | border: 1px solid var(--dim-border-color);
97 | border-radius: 4px;
98 | padding: 0.25rem 0.5rem;
99 | }
100 |
--------------------------------------------------------------------------------
/preview/src/components/switch/docs.md:
--------------------------------------------------------------------------------
1 | The Switch component allows users to toggle between two states, such as on and off.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // The Switch component wraps the switch thumb
7 | Switch {
8 | // The current state of the switch, true for on and false for off.
9 | checked: true,
10 | // Callback function triggered when the switch state changes.
11 | on_checked_change: |checked: bool| {
12 | // Handle the change in switch state.
13 | },
14 | // The switch thumb represents the draggable handle that the user moves to toggle the switch.
15 | SwitchThumb {
16 | // The content of the thumb
17 | {children}
18 | }
19 | }
20 | ```
--------------------------------------------------------------------------------
/preview/src/components/switch/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::switch::{Switch, SwitchThumb};
3 | #[component]
4 | pub(super) fn Demo() -> Element {
5 | let mut checked = use_signal(|| false);
6 | rsx! {
7 | document::Link {
8 | rel: "stylesheet",
9 | href: asset!("/src/components/switch/style.css"),
10 | }
11 | div { class: "switch-example",
12 | Switch {
13 | class: "switch",
14 | checked,
15 | on_checked_change: move |new_checked| {
16 | checked.set(new_checked);
17 | tracing::info!("Switch toggled: {new_checked}");
18 | },
19 | SwitchThumb { class: "switch-thumb" }
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/preview/src/components/switch/style.css:
--------------------------------------------------------------------------------
1 | .switch-example {
2 | display: flex;
3 | align-items: center;
4 | gap: 15px;
5 | padding: 20px;
6 | }
7 |
8 | .switch {
9 | all: unset;
10 | width: 42px;
11 | height: 25px;
12 | background-color: var(--background-color);
13 | border-radius: 9999px;
14 | position: relative;
15 | transition: background-color 150ms;
16 | cursor: pointer;
17 | }
18 |
19 | .switch[data-state="checked"] {
20 | background-color: var(--contrast-background-color);
21 | }
22 |
23 | .switch-thumb {
24 | display: block;
25 | width: 21px;
26 | height: 21px;
27 | background-color: var(--contrast-background-color);
28 | border-radius: 9999px;
29 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
30 | transition: transform 150ms;
31 | transform: translateX(2px);
32 | will-change: transform;
33 | }
34 |
35 | .switch[data-state="checked"] .switch-thumb {
36 | transform: translateX(19px);
37 | background-color: var(--background-color);
38 | }
39 |
40 | /* Only apply disabled styles when data-disabled is "true" */
41 | .switch[data-disabled="true"] {
42 | opacity: 0.5;
43 | cursor: not-allowed;
44 | }
--------------------------------------------------------------------------------
/preview/src/components/tabs/docs.md:
--------------------------------------------------------------------------------
1 | The Tabs component is used to create a tabbed interface, allowing users to switch between different views or sections of content.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // The Tabs component wraps all tab triggers and contents and orders them based on their index.
7 | Tabs {
8 | // The TabTrigger component is used to create a clickable tab button that switches the active tab.
9 | TabTrigger {
10 | // The index of the tab trigger, used to determine the focus order of the tabs.
11 | index: 0,
12 | // The value of the tab trigger, which must be unique and is used to identify the active tab.
13 | value: "tab1",
14 | // The contents of the tab trigger button
15 | {children}
16 | }
17 | // The TabContent component contains the content that is displayed when the corresponding tab is active.
18 | TabContent {
19 | // The value of the tab content, which must match the value of the corresponding TabTrigger to be displayed.
20 | value: "tab1",
21 | // The content of the tab, which is displayed when the tab is active.
22 | {children}
23 | }
24 | }
25 | ```
--------------------------------------------------------------------------------
/preview/src/components/tabs/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::tabs::{TabContent, TabTrigger, Tabs};
3 | #[component]
4 | pub(super) fn Demo() -> Element {
5 | rsx! {
6 | document::Link { rel: "stylesheet", href: asset!("/src/components/tabs/style.css") }
7 | Tabs { class: "tabs", default_value: "tab1".to_string(),
8 | div { class: "tabs-list",
9 | TabTrigger {
10 | class: "tabs-trigger",
11 | value: "tab1".to_string(),
12 | index: 0usize,
13 | "Tab 1"
14 | }
15 | TabTrigger {
16 | class: "tabs-trigger",
17 | value: "tab2".to_string(),
18 | index: 1usize,
19 | "Tab 2"
20 | }
21 | TabTrigger {
22 | class: "tabs-trigger",
23 | value: "tab3".to_string(),
24 | index: 2usize,
25 | "Tab 3"
26 | }
27 | }
28 | TabContent { class: "tabs-content", value: "tab1".to_string(), "Tab 1 Content" }
29 | TabContent { class: "tabs-content", value: "tab2".to_string(), "Tab 2 Content" }
30 | TabContent { class: "tabs-content", value: "tab3".to_string(), "Tab 3 Content" }
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/preview/src/components/tabs/style.css:
--------------------------------------------------------------------------------
1 | .tabs {
2 | width: 100%;
3 | display: flex;
4 | flex-direction: column;
5 | background: var(--background-color);
6 | }
7 |
8 | .tabs-list {
9 | display: flex;
10 | /* border-style: solid;
11 | border-color: var(--dim-border-color);
12 | border-width: 1px 0px 1px 1px; */
13 | }
14 |
15 | .tabs-trigger {
16 | all: unset;
17 | width: 100%;
18 | display: flex;
19 | align-items: center;
20 | justify-content: center;
21 | padding: 8px 16px;
22 | font-size: 14px;
23 | color: var(--text-color);
24 | cursor: pointer;
25 | border-left: 1px solid var(--dim-border-color);
26 | border-bottom: 1px solid var(--dim-border-color);
27 | transition: background-color 0.2s;
28 | box-sizing: border-box;
29 | }
30 |
31 | .tabs-trigger:first-child {
32 | border-left: none;
33 | }
34 |
35 | .tabs-trigger:hover {
36 | color: var(--hover-text-color);
37 | background: var(--hover-background-color);
38 | }
39 |
40 | .tabs-trigger[data-state="active"] {
41 | color: var(--text-color);
42 | border-bottom: none;
43 | }
44 |
45 | .tabs-trigger:focus-visible {
46 | outline: 2px solid #3b82f6;
47 | outline-offset: -2px;
48 | }
49 |
50 | .tabs-trigger[data-disabled="true"] {
51 | opacity: 0.5;
52 | cursor: not-allowed;
53 | }
54 |
55 | .tabs-content {
56 | padding: 16px;
57 | font-size: 14px;
58 | box-sizing: border-box;
59 | }
60 |
61 | .tabs-content[data-state="inactive"] {
62 | display: none;
63 | }
64 |
--------------------------------------------------------------------------------
/preview/src/components/toast/docs.md:
--------------------------------------------------------------------------------
1 | The Toast component is used to display brief messages to the user, typically for notifications or alerts.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // The Toast provider provides the toast context to its children and handler rendering any toasts that are sent.
7 | ToastProvider {
8 | // Any child component can consume the toast context and send a toast to be rendered.
9 | button {
10 | onclick: |event: MouseEvent| {
11 | // Consume the toast context to send a toast.
12 | let toast_api = consume_toast();
13 | toast_api
14 | .error(
15 | "Critical Error".to_string(),
16 | Some(ToastOptions {
17 | permanent: true,
18 | ..Default::default()
19 | }),
20 | );
21 | },
22 | "Show Toast"
23 | }
24 | }
25 | ```
26 |
--------------------------------------------------------------------------------
/preview/src/components/toast/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::toast::{ToastOptions, ToastProvider, use_toast};
3 | use std::time::Duration;
4 |
5 | #[component]
6 | pub(super) fn Demo() -> Element {
7 | rsx! {
8 | ToastProvider { ToastButton {} }
9 | }
10 | }
11 |
12 | #[component]
13 | fn ToastButton() -> Element {
14 | let toast_api = use_toast();
15 |
16 | rsx! {
17 | document::Link {
18 | rel: "stylesheet",
19 | href: asset!("/src/components/toast/style.css"),
20 | }
21 | div { class: "toast-example",
22 | h4 { "Timed Toasts (auto-dismiss)" }
23 | div { class: "toast-buttons",
24 | button {
25 | onclick: move |_| {
26 | toast_api
27 | .success(
28 | "Success".to_string(),
29 | Some(ToastOptions {
30 | duration: Some(Duration::from_secs(3)),
31 | ..Default::default()
32 | }),
33 | );
34 | },
35 | "Success (3s)"
36 | }
37 | button {
38 | onclick: move |_| {
39 | toast_api
40 | .error(
41 | "Error".to_string(),
42 | Some(ToastOptions {
43 | duration: Some(Duration::from_secs(5)),
44 | ..Default::default()
45 | }),
46 | );
47 | },
48 | "Error (5s)"
49 | }
50 | button {
51 | onclick: move |_| {
52 | toast_api
53 | .warning(
54 | "Warning".to_string(),
55 | Some(ToastOptions {
56 | description: Some("This action might cause issues".to_string()),
57 | duration: Some(Duration::from_secs(3)),
58 | ..Default::default()
59 | }),
60 | );
61 | },
62 | "Warning (3s)"
63 | }
64 | button {
65 | onclick: move |_| {
66 | toast_api
67 | .info(
68 | "Custom Toast".to_string(),
69 | Some(ToastOptions {
70 | description: Some(
71 | "This is a custom toast with specific settings".to_string(),
72 | ),
73 | duration: Some(Duration::from_secs(10)),
74 | permanent: false,
75 | }),
76 | );
77 | },
78 | "Custom Info (10s)"
79 | }
80 | }
81 | h4 { "Permanent Toasts (manual close)" }
82 | div { class: "toast-buttons",
83 | button {
84 | onclick: move |_| {
85 | toast_api
86 | .success(
87 | "Important".to_string(),
88 | Some(ToastOptions {
89 | permanent: true,
90 | ..Default::default()
91 | }),
92 | );
93 | },
94 | "Permanent Success"
95 | }
96 | button {
97 | onclick: move |_| {
98 | toast_api
99 | .error(
100 | "Critical Error".to_string(),
101 | Some(ToastOptions {
102 | permanent: true,
103 | ..Default::default()
104 | }),
105 | );
106 | },
107 | "Permanent Error"
108 | }
109 | button {
110 | onclick: move |_| {
111 | toast_api
112 | .warning(
113 | "Attention Needed".to_string(),
114 | Some(ToastOptions {
115 | description: Some("This requires your attention".to_string()),
116 | permanent: true,
117 | ..Default::default()
118 | }),
119 | );
120 | },
121 | "Permanent Warning"
122 | }
123 | button {
124 | onclick: move |_| {
125 | toast_api
126 | .info(
127 | "Info Toast".to_string(),
128 | Some(ToastOptions {
129 | description: Some("This is an informational message".to_string()),
130 | permanent: true,
131 | ..Default::default()
132 | }),
133 | );
134 | },
135 | "Permanent Info"
136 | }
137 | }
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/preview/src/components/toast/style.css:
--------------------------------------------------------------------------------
1 | .toast-container {
2 | position: fixed;
3 | bottom: 20px;
4 | right: 20px;
5 | display: flex;
6 | flex-direction: column;
7 | gap: 10px;
8 | z-index: 9999;
9 | max-width: 350px;
10 | }
11 |
12 | .toast {
13 | background-color: var(--dim-background-color);
14 | border-radius: 6px;
15 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
16 | padding: 12px 16px;
17 | display: flex;
18 | align-items: flex-start;
19 | justify-content: space-between;
20 | animation: toast-slide-in 0.2s ease-out;
21 | overflow: hidden;
22 | border-left: 4px solid var(--dim-border-color);
23 | }
24 |
25 | .toast[data-type="success"] {
26 | border-left-color: var(--success-background-color);
27 | }
28 |
29 | .toast[data-type="error"] {
30 | border-left-color: var(--error-background-color);
31 | }
32 |
33 | .toast[data-type="warning"] {
34 | border-left-color: var(--warning-background-color);
35 | }
36 |
37 | .toast[data-type="info"] {
38 | border-left-color: var(--info-background-color);
39 | }
40 |
41 | /* Permanent toast styling */
42 | .toast[data-permanent="true"] {
43 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
44 | border-width: 4px;
45 | }
46 |
47 | .toast-content {
48 | flex: 1;
49 | margin-right: 8px;
50 | }
51 |
52 | .toast-title {
53 | font-weight: 600;
54 | margin-bottom: 4px;
55 | color: var(--text-color);
56 | }
57 |
58 | .toast-description {
59 | font-size: 0.875rem;
60 | color: var(--dim-text-color);
61 | }
62 |
63 | .toast-close {
64 | background: none;
65 | border: none;
66 | font-size: 18px;
67 | cursor: pointer;
68 | color: var(--dim-text-color);
69 | padding: 0;
70 | margin: 0;
71 | line-height: 1;
72 | align-self: flex-start;
73 | }
74 |
75 | .toast-close:hover {
76 | color: var(--text-color);
77 | }
78 |
79 | @keyframes toast-slide-in {
80 | from {
81 | transform: translateX(100%);
82 | opacity: 0;
83 | }
84 |
85 | to {
86 | transform: translateX(0);
87 | opacity: 1;
88 | }
89 | }
90 |
91 | .toast-example {
92 | padding: 20px;
93 | display: flex;
94 | flex-direction: column;
95 | align-items: center;
96 | }
97 |
98 | .toast-example h3 {
99 | margin-top: 0;
100 | margin-bottom: 10px;
101 | }
102 |
103 | .toast-example h4 {
104 | margin-top: 20px;
105 | margin-bottom: 10px;
106 | font-size: 1rem;
107 | }
108 |
109 | .toast-buttons {
110 | display: flex;
111 | flex-wrap: wrap;
112 | gap: 10px;
113 | margin-top: 15px;
114 | }
115 |
116 | .toast-buttons button {
117 | padding: 8px 16px;
118 | color: var(--text-color);
119 | background-color: var(--brighter-background-color);
120 | border: 1px solid var(--dim-border-color);
121 | border-radius: 4px;
122 | cursor: pointer;
123 | font-size: 14px;
124 | transition: all 0.2s ease;
125 | }
126 |
127 | .toast-buttons button:hover {
128 | background-color: var(--dim-background-color);
129 | }
130 |
131 | .toast-buttons button:focus {
132 | outline: none;
133 | box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
134 | }
135 |
136 | .toast-buttons button:nth-child(1) {
137 | background-color: var(--success-background-color);
138 | color: var(--success-text-color);
139 | }
140 |
141 | .toast-buttons button:nth-child(2) {
142 | background-color: var(--error-background-color);
143 | color: var(--error-text-color);
144 | }
145 |
146 | .toast-buttons button:nth-child(3) {
147 | background-color: var(--warning-background-color);
148 | color: var(--warning-text-color);
149 | }
150 |
151 | .toast-buttons button:nth-child(4) {
152 | background-color: var(--info-background-color);
153 | color: var(--info-text-color);
154 | }
155 |
156 | .toast-buttons button:hover {
157 | filter: brightness(0.95);
158 | }
159 |
--------------------------------------------------------------------------------
/preview/src/components/toggle_group/docs.md:
--------------------------------------------------------------------------------
1 | The Toggle Group component is used to create a group of toggle buttons that allows the user to select one or more options from a set. It is useful for creating a set of options.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // The ToggleGroup component wraps all toggle items in the group.
7 | ToggleGroup {
8 | // The orientation of the toggle group, true for horizontal and false for vertical.
9 | horizontal: true,
10 | // The toggle item represents each individual toggle button in the group.
11 | ToggleItem {
12 | // The index of the toggle item, used to determine the order in which items are focused.
13 | index: 0,
14 | // The contents of the toggle item button
15 | {children}
16 | }
17 | }
18 | ```
19 |
--------------------------------------------------------------------------------
/preview/src/components/toggle_group/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::toggle_group::{ToggleGroup, ToggleItem};
3 | #[component]
4 | pub(super) fn Demo() -> Element {
5 | rsx! {
6 | document::Link {
7 | rel: "stylesheet",
8 | href: asset!("/src/components/toggle_group/style.css"),
9 | }
10 | ToggleGroup { class: "toggle-group", horizontal: true,
11 | ToggleItem { class: "toggle-item", index: 0usize, "Align Left" }
12 | ToggleItem { class: "toggle-item", index: 1usize, "Align Middle" }
13 | ToggleItem { class: "toggle-item", index: 2usize, "Align Right" }
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/preview/src/components/toggle_group/style.css:
--------------------------------------------------------------------------------
1 | .toggle-group {
2 | margin: 50px 50px;
3 | width: fit-content;
4 | }
5 |
6 | .toggle-item {
7 | border-radius: 0px;
8 | outline: none;
9 | border: none;
10 | font-size: 14px;
11 | padding: 10px;
12 | min-width: 35px;
13 | color: var(--text-color);
14 | background-color: var(--brighter-background-color);
15 |
16 | transition: background-color 200ms ease, border 200ms ease;
17 |
18 | border-top: 1px solid var(--dim-border-color);
19 | border-bottom: 1px solid var(--dim-border-color);
20 | }
21 |
22 | .toggle-item:hover, .toggle-item:focus {
23 | cursor: pointer;
24 | background-color: var(--hover-background-color);
25 | }
26 |
27 | .toggle-item[data-state="on"] {
28 | background-color: var(--focused-background-color);
29 | }
30 |
31 | .toggle-item:first-child {
32 | border-top-left-radius: 10px;
33 | border-bottom-left-radius: 10px;
34 | }
35 |
36 | .toggle-item:last-child {
37 | border-top-right-radius: 10px;
38 | border-bottom-right-radius: 10px;
39 | }
40 |
41 | .toggle-item:first-child, .toggle-item:last-child {
42 | border: 1px solid var(--dim-border-color);
43 | }
--------------------------------------------------------------------------------
/preview/src/components/toolbar/docs.md:
--------------------------------------------------------------------------------
1 | The toolbar component is a flexible and customizable component that can be used to create a variety of toolbars. It can be used to create a simple toolbar with a title, or a more complex toolbar with multiple buttons and actions.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // The Toolbar component wraps all toolbar items.
7 | Toolbar {
8 | // The aria_label of the toolbar, used for accessibility purposes.
9 | aria_label: "Toolbar Title",
10 | // The ToolbarButton component represents each individual button in the toolbar.
11 | ToolbarButton {
12 | // The index of the toolbar button, used to determine the order in which buttons are focused.
13 | index: 0,
14 | on_click: |_: ()| {
15 | // This callback is triggered when the button is clicked.
16 | },
17 | // The contents of the toolbar button
18 | {children}
19 | }
20 | // The ToolbarSeparator component represents a separator line in the toolbar.
21 | ToolbarSeparator {
22 | // The orientation of the separator, true for horizontal and false for vertical.
23 | horizontal: true,
24 | // The decorative property controls if the separator is decorative and should not be visible to screen readers.
25 | decorative: false,
26 | }
27 | }
28 | ```
--------------------------------------------------------------------------------
/preview/src/components/toolbar/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::toolbar::{Toolbar, ToolbarButton, ToolbarSeparator};
3 | #[component]
4 | pub(super) fn Demo() -> Element {
5 | let mut text_style = use_signal(Vec::new);
6 | let mut text_align = use_signal(|| String::from("left"));
7 | let mut toggle_style = move |style: &str| {
8 | let mut current_styles = text_style();
9 | if current_styles.contains(&style.to_string()) {
10 | current_styles.retain(|s| s != style);
11 | } else {
12 | current_styles.push(style.to_string());
13 | }
14 | text_style.set(current_styles);
15 | };
16 | let mut set_align = move |align: &str| {
17 | text_align.set(align.to_string());
18 | };
19 | let text_classes = use_memo(move || {
20 | let mut classes = Vec::new();
21 | for style in text_style() {
22 | match style.as_str() {
23 | "bold" => classes.push("toolbar-bold"),
24 | "italic" => classes.push("toolbar-italic"),
25 | "underline" => classes.push("toolbar-underline"),
26 | _ => {}
27 | }
28 | }
29 | classes.join(" ")
30 | });
31 | let text_align_style = use_memo(move || format!("text-align: {};", text_align()));
32 | rsx! {
33 | document::Link {
34 | rel: "stylesheet",
35 | href: asset!("/src/components/toolbar/style.css"),
36 | }
37 | div { class: "toolbar-example",
38 | h3 {
39 | width: "100%",
40 | text_align: "center",
41 | "Text Formatting Toolbar"
42 | }
43 | Toolbar { class: "toolbar", aria_label: "Text formatting",
44 | div { class: "toolbar-group",
45 | ToolbarButton {
46 | class: "toolbar-button",
47 | index: 0usize,
48 | on_click: move |_| toggle_style("bold"),
49 | "Bold"
50 | }
51 | ToolbarButton {
52 | class: "toolbar-button",
53 | index: 1usize,
54 | on_click: move |_| toggle_style("italic"),
55 | "Italic"
56 | }
57 | ToolbarButton {
58 | class: "toolbar-button",
59 | index: 2usize,
60 | on_click: move |_| toggle_style("underline"),
61 | "Underline"
62 | }
63 | }
64 | ToolbarSeparator { class: "toolbar-separator" }
65 | div { class: "toolbar-group",
66 | ToolbarButton {
67 | class: "toolbar-button",
68 | index: 3usize,
69 | on_click: move |_| set_align("left"),
70 | "Align Left"
71 | }
72 | ToolbarButton {
73 | class: "toolbar-button",
74 | index: 4usize,
75 | on_click: move |_| set_align("center"),
76 | "Align Center"
77 | }
78 | ToolbarButton {
79 | class: "toolbar-button",
80 | index: 5usize,
81 | on_click: move |_| set_align("right"),
82 | "Align Right"
83 | }
84 | }
85 | }
86 | div { class: "toolbar-content",
87 | p { class: text_classes, style: text_align_style,
88 | "This is a sample text that will be formatted according to the toolbar buttons you click. Try clicking the buttons above to see how the text formatting changes."
89 | }
90 | }
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/preview/src/components/toolbar/style.css:
--------------------------------------------------------------------------------
1 | .toolbar-example {
2 | width: 35rem;
3 | max-width: 100%;
4 | margin: 20px;
5 | padding: 10px;
6 | border: 1px solid var(--dim-border-color);
7 | border-radius: .5em;
8 | background-color: var(--background-color);
9 | }
10 |
11 | .toolbar {
12 | display: flex;
13 | flex-wrap: wrap;
14 | flex-direction: row;
15 | align-items: center;
16 | justify-content: space-between;
17 | gap: 5px;
18 | padding: 5px;
19 | background-color: var(--brighter-background-color);
20 | border-radius: 4px;
21 | }
22 |
23 | .toolbar button {
24 | padding: 8px 12px;
25 | border: 1px solid var(--dim-border-color);
26 | border-radius: 4px;
27 | color: var(--text-color);
28 | background-color: var(--background-color);
29 | cursor: pointer;
30 | font-size: 14px;
31 | }
32 |
33 | .toolbar button:hover {
34 | background-color: var(--hover-background-color);
35 | }
36 |
37 | .toolbar button:focus {
38 | outline: 2px solid #0066cc;
39 | outline-offset: 2px;
40 | }
41 |
42 | .toolbar button:disabled {
43 | opacity: 0.5;
44 | cursor: not-allowed;
45 | }
46 |
47 | .toolbar-group {
48 | display: flex;
49 | flex-direction: row;
50 | gap: 5px;
51 | }
52 |
53 | .toolbar-separator {
54 | width: 1px;
55 | height: 24px;
56 | background-color: var(--dim-border-color);
57 | margin: 0 5px;
58 | }
59 |
60 | .toolbar-content {
61 | margin-top: 20px;
62 | padding: 20px;
63 | border: 1px solid var(--dim-border-color);
64 | border-radius: 4px;
65 | background-color: var(--background-color);
66 | }
67 |
68 | .toolbar-content p {
69 | margin: 0;
70 | padding: 0;
71 | }
72 |
73 | /* For text formatting buttons */
74 | .toolbar-bold {
75 | font-weight: bold;
76 | }
77 |
78 | .toolbar-italic {
79 | font-style: italic;
80 | }
81 |
82 | .toolbar-underline {
83 | text-decoration: underline;
84 | }
85 |
--------------------------------------------------------------------------------
/preview/src/components/tooltip/docs.md:
--------------------------------------------------------------------------------
1 | The Tooltip component is used to display additional information when a user hovers over an element.
2 |
3 | ## Component Structure
4 |
5 | ```rust
6 | // The Tooltip component wraps the trigger element and the content that will be displayed on hover.
7 | Tooltip {
8 | // The TooltipTrigger contains the elements that will trigger the tooltip to display when hovered over.
9 | TooltipTrigger {
10 | // The elements that will trigger the tooltip when hovered over.
11 | {children}
12 | }
13 | // The TooltipContent contains the content that will be displayed when the user hovers over the trigger.
14 | TooltipContent {
15 | // The side of the TooltipTrigger where the content will be displayed. Can be one of Top, Right, Bottom, or Left.
16 | side: TooltipSide::Top,
17 | // The alignment of the TooltipContent relative to the TooltipTrigger. Can be one of Start, Center, or End.
18 | align: TooltipAlign::Center,
19 | // The content of the tooltip, which can include text, images, or any other elements.
20 | {children}
21 | }
22 | }
23 | ```
--------------------------------------------------------------------------------
/preview/src/components/tooltip/mod.rs:
--------------------------------------------------------------------------------
1 | use dioxus::prelude::*;
2 | use dioxus_primitives::tooltip::{Tooltip, TooltipContent, TooltipSide, TooltipTrigger};
3 | #[component]
4 | pub(super) fn Demo() -> Element {
5 | rsx! {
6 | document::Link {
7 | rel: "stylesheet",
8 | href: asset!("/src/components/tooltip/style.css"),
9 | }
10 | div {
11 | class: "tooltip-example",
12 | style: "padding: 50px; display: flex; gap: 20px;",
13 | Tooltip { class: "tooltip",
14 | TooltipTrigger { class: "tooltip-trigger",
15 | button { "Hover me" }
16 | }
17 | TooltipContent { class: "tooltip-content", "This is a basic tooltip" }
18 | }
19 | Tooltip { class: "tooltip",
20 | TooltipTrigger { class: "tooltip-trigger",
21 | button { "Right tooltip" }
22 | }
23 | TooltipContent { class: "tooltip-content", side: TooltipSide::Right,
24 | "This tooltip appears on the right"
25 | }
26 | }
27 | Tooltip { class: "tooltip",
28 | TooltipTrigger { class: "tooltip-trigger",
29 | button { "Rich content" }
30 | }
31 | TooltipContent { class: "tooltip-content", style: "width: 200px;",
32 | h4 { style: "margin-top: 0; margin-bottom: 8px;", "Tooltip title" }
33 | p { style: "margin: 0;", "This tooltip contains rich HTML content with styling." }
34 | }
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/preview/src/components/tooltip/style.css:
--------------------------------------------------------------------------------
1 | /* Tooltip Styles */
2 | .tooltip {
3 | position: relative;
4 | display: inline-block;
5 | }
6 |
7 | .tooltip-trigger {
8 | display: inline-block;
9 | }
10 |
11 | .tooltip-content {
12 | position: absolute;
13 | z-index: 1000;
14 | max-width: 250px;
15 | padding: 8px 12px;
16 | border-radius: 4px;
17 | background-color: var(--brighter-background-color);
18 | color: var(--text-color);
19 | font-size: 14px;
20 | line-height: 1.4;
21 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
22 | animation: tooltipFadeIn 0.2s ease-in-out;
23 | }
24 |
25 | /* Positioning based on side */
26 | .tooltip-content[data-side="top"] {
27 | position: absolute;
28 | bottom: 100%;
29 | margin-bottom: 8px;
30 | left: 50%;
31 | transform: translateX(-50%);
32 | }
33 |
34 | .tooltip-content[data-side="right"] {
35 | position: absolute;
36 | left: 100%;
37 | top: 50%;
38 | transform: translateY(-50%);
39 | margin-left: 8px;
40 | }
41 |
42 | .tooltip-content[data-side="bottom"] {
43 | position: absolute;
44 | top: 100%;
45 | margin-top: 8px;
46 | left: 50%;
47 | transform: translateX(-50%);
48 | }
49 |
50 | .tooltip-content[data-side="left"] {
51 | position: absolute;
52 | right: 100%;
53 | top: 50%;
54 | transform: translateY(-50%);
55 | margin-right: 8px;
56 | }
57 |
58 | /* Alignment styles for top and bottom */
59 | .tooltip-content[data-side="top"][data-align="start"],
60 | .tooltip-content[data-side="bottom"][data-align="start"] {
61 | left: 0;
62 | transform: none;
63 | }
64 |
65 | .tooltip-content[data-side="top"][data-align="end"],
66 | .tooltip-content[data-side="bottom"][data-align="end"] {
67 | left: auto;
68 | right: 0;
69 | transform: none;
70 | }
71 |
72 | /* Alignment styles for left and right */
73 | .tooltip-content[data-side="left"][data-align="start"],
74 | .tooltip-content[data-side="right"][data-align="start"] {
75 | top: 0;
76 | transform: none;
77 | }
78 |
79 | .tooltip-content[data-side="left"][data-align="center"],
80 | .tooltip-content[data-side="right"][data-align="center"] {
81 | top: 50%;
82 | transform: translateY(-50%);
83 | }
84 |
85 | .tooltip-content[data-side="left"][data-align="end"],
86 | .tooltip-content[data-side="right"][data-align="end"] {
87 | top: auto;
88 | bottom: 0;
89 | transform: none;
90 | }
91 |
92 | /* Animation */
93 | @keyframes tooltipFadeIn {
94 | from {
95 | opacity: 0;
96 | }
97 |
98 | to {
99 | opacity: 1;
100 | }
101 | }
102 |
103 | /* State styles */
104 | .tooltip[data-disabled="true"] .tooltip-trigger {
105 | cursor: default;
106 | }
107 |
108 | .tooltip-content[data-state="closed"] {
109 | display: none;
110 | }
111 |
112 | .tooltip-content[data-state="open"] {
113 | display: block;
114 | }
115 |
116 | .tooltip button {
117 | padding: 8px 12px;
118 | border-radius: 4px;
119 | cursor: pointer;
120 | user-select: none;
121 | outline: none;
122 | font-size: 14px;
123 | background-color: var(--brighter-background-color);
124 | border: 1px solid var(--dim-border-color);
125 | color: var(--text-color);
126 | }
127 |
128 | .tooltip button:hover {
129 | background: var(--hover-background-color);
130 | }
131 |
132 | .tooltip button:focus {
133 | background: var(--focused-background-color);
134 | }
135 |
--------------------------------------------------------------------------------
/primitives/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "dioxus-primitives"
3 | version = "0.0.1"
4 | edition = "2024"
5 | readme = "../README.md"
6 | keywords = ["gui", "dioxus", "components", "primitives"]
7 | categories = ["gui", "wasm"]
8 | authors = ["Dioxus Labs", "DogeDark"]
9 | license = "MIT OR Apache-2.0"
10 | homepage = "https://dioxuslabs.com"
11 | repository = "https://github.com/DioxusLabs/components"
12 |
13 | [dependencies]
14 | dioxus-lib.workspace = true
15 | dioxus.workspace = true
16 | dioxus-time = "=0.1.0-alpha.1"
17 | tracing.workspace = true
18 |
--------------------------------------------------------------------------------
/primitives/src/aspect_ratio.rs:
--------------------------------------------------------------------------------
1 | use dioxus_lib::prelude::*;
2 |
3 | // TODO: Docs
4 |
5 | #[derive(Props, Clone, PartialEq)]
6 | pub struct AspectRatioProps {
7 | /// The desired ratio. E.g. 16.0 / 9.0
8 | #[props(default = 1.0)]
9 | ratio: f64,
10 |
11 | children: Element,
12 |
13 | #[props(extends = GlobalAttributes)]
14 | attributes: Vec,
15 | }
16 |
17 | #[component]
18 | pub fn AspectRatio(props: AspectRatioProps) -> Element {
19 | let ratio = 100.0 / (props.ratio);
20 |
21 | rsx! {
22 | div {
23 | style: "position: relative; width: 100%; padding-bottom: {ratio}%;",
24 | div {
25 | style: "position: absolute; inset: 0;",
26 | ..props.attributes,
27 |
28 | {props.children}
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/primitives/src/avatar.rs:
--------------------------------------------------------------------------------
1 | use dioxus_lib::prelude::*;
2 |
3 | /// Represents the different states an Avatar can be in
4 | #[derive(Debug, Clone, Copy, PartialEq, Eq)]
5 | pub enum AvatarState {
6 | /// Initial loading state
7 | Loading,
8 | /// Image loaded successfully
9 | Loaded,
10 | /// Error loading the image
11 | Error,
12 | /// No image source provided
13 | Empty,
14 | }
15 |
16 | #[derive(Clone)]
17 | struct AvatarCtx {
18 | // State
19 | state: Signal,
20 | has_fallback_child: Signal,
21 | has_image_child: Signal,
22 |
23 | // Callbacks
24 | on_load: Option>,
25 | on_error: Option>,
26 | on_state_change: Option>,
27 | }
28 |
29 | #[derive(Props, Clone, PartialEq)]
30 | pub struct AvatarProps {
31 | /// Callback when image loads successfully
32 | #[props(default)]
33 | pub on_load: Option>,
34 |
35 | /// Callback when image fails to load
36 | #[props(default)]
37 | pub on_error: Option>,
38 |
39 | /// Callback when the avatar state changes
40 | #[props(default)]
41 | pub on_state_change: Option>,
42 |
43 | #[props(extends = GlobalAttributes)]
44 | pub attributes: Vec,
45 |
46 | pub children: Element,
47 | }
48 |
49 | #[component]
50 | pub fn Avatar(props: AvatarProps) -> Element {
51 | // Internal state tracking
52 | let state = use_signal(|| AvatarState::Empty);
53 | let has_fallback_child = use_signal(|| false);
54 | let has_image_child = use_signal(|| false);
55 |
56 | // Notify about initial state
57 | use_effect(move || {
58 | if let Some(handler) = &props.on_state_change {
59 | handler.call(state());
60 | }
61 | });
62 |
63 | // Create context for child components
64 | let _ctx = use_context_provider(|| AvatarCtx {
65 | state,
66 | has_fallback_child,
67 | has_image_child,
68 | on_load: props.on_load,
69 | on_error: props.on_error,
70 | on_state_change: props.on_state_change,
71 | });
72 |
73 | // Determine if fallback should be shown
74 | let show_fallback =
75 | use_memo(move || matches!(state(), AvatarState::Error | AvatarState::Empty));
76 |
77 | rsx! {
78 | span {
79 | role: "img",
80 | "data-state": match state() {
81 | AvatarState::Loading => "loading",
82 | AvatarState::Loaded => "loaded",
83 | AvatarState::Error => "error",
84 | AvatarState::Empty => "empty",
85 | },
86 | ..props.attributes,
87 |
88 | // Children (which may include AvatarImage and AvatarFallback)
89 | {props.children}
90 |
91 | // Default fallback if no AvatarFallback is provided and fallback should be shown
92 | if show_fallback() && !has_fallback_child() && has_image_child() {
93 | span {
94 | style: "display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;",
95 | "??"
96 | }
97 | }
98 | }
99 | }
100 | }
101 |
102 | #[derive(Props, Clone, PartialEq)]
103 | pub struct AvatarFallbackProps {
104 | #[props(extends = GlobalAttributes)]
105 | pub attributes: Vec,
106 | pub children: Element,
107 | }
108 |
109 | #[component]
110 | pub fn AvatarFallback(props: AvatarFallbackProps) -> Element {
111 | let mut ctx: AvatarCtx = use_context();
112 |
113 | // Mark that a fallback child is provided
114 | use_effect(move || {
115 | ctx.has_fallback_child.set(true);
116 | });
117 |
118 | let show_fallback =
119 | use_memo(move || matches!((ctx.state)(), AvatarState::Error | AvatarState::Empty));
120 |
121 | if !show_fallback() {
122 | return rsx!({});
123 | }
124 |
125 | rsx! {
126 | span { ..props.attributes,{props.children} }
127 | }
128 | }
129 |
130 | #[derive(Props, Clone, PartialEq)]
131 | pub struct AvatarImageProps {
132 | /// The image source URL
133 | pub src: String,
134 |
135 | /// Alt text for the image
136 | #[props(default)]
137 | pub alt: Option,
138 |
139 | #[props(extends = GlobalAttributes)]
140 | pub attributes: Vec,
141 | }
142 |
143 | #[component]
144 | pub fn AvatarImage(props: AvatarImageProps) -> Element {
145 | let mut ctx: AvatarCtx = use_context();
146 |
147 | // Mark that an image child is provided and set initial loading state
148 | use_effect(move || {
149 | ctx.has_image_child.set(true);
150 | ctx.state.set(AvatarState::Loading);
151 | });
152 |
153 | let handle_load = move |_| {
154 | ctx.state.set(AvatarState::Loaded);
155 | if let Some(handler) = &ctx.on_load {
156 | handler.call(());
157 | }
158 | if let Some(handler) = &ctx.on_state_change {
159 | handler.call(AvatarState::Loaded);
160 | }
161 | };
162 |
163 | let handle_error = move |_| {
164 | ctx.state.set(AvatarState::Error);
165 | if let Some(handler) = &ctx.on_error {
166 | handler.call(());
167 | }
168 | if let Some(handler) = &ctx.on_state_change {
169 | handler.call(AvatarState::Error);
170 | }
171 | };
172 |
173 | rsx! {
174 | img {
175 | src: props.src.clone(),
176 | alt: props.alt.clone().unwrap_or_default(),
177 | onload: handle_load,
178 | onerror: handle_error,
179 | style: "width: 100%; height: 100%; object-fit: cover;",
180 | ..props.attributes,
181 | }
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/primitives/src/checkbox.rs:
--------------------------------------------------------------------------------
1 | use crate::{use_controlled, use_unique_id};
2 | use dioxus_lib::{document::eval, prelude::*};
3 | use std::ops::Not;
4 |
5 | // TODO: Docs
6 |
7 | #[derive(Debug, Clone, Copy, PartialEq)]
8 | pub enum CheckboxState {
9 | Checked,
10 | Indeterminate,
11 | Unchecked,
12 | }
13 |
14 | impl CheckboxState {
15 | pub fn to_aria_checked(&self) -> &str {
16 | match self {
17 | CheckboxState::Checked => "true",
18 | CheckboxState::Indeterminate => "mixed",
19 | CheckboxState::Unchecked => "false",
20 | }
21 | }
22 |
23 | pub fn to_data_state(&self) -> &str {
24 | match self {
25 | CheckboxState::Checked => "checked",
26 | CheckboxState::Indeterminate => "indeterminate",
27 | CheckboxState::Unchecked => "unchecked",
28 | }
29 | }
30 | }
31 |
32 | impl From for bool {
33 | fn from(value: CheckboxState) -> Self {
34 | !matches!(value, CheckboxState::Unchecked)
35 | }
36 | }
37 |
38 | impl Not for CheckboxState {
39 | type Output = Self;
40 |
41 | fn not(self) -> Self::Output {
42 | match self {
43 | Self::Unchecked => Self::Checked,
44 | _ => Self::Unchecked,
45 | }
46 | }
47 | }
48 |
49 | #[derive(Clone, Copy)]
50 | struct CheckboxCtx {
51 | checked: ReadOnlySignal,
52 | disabled: ReadOnlySignal,
53 | }
54 |
55 | #[derive(Props, Clone, PartialEq)]
56 | pub struct CheckboxProps {
57 | checked: Option>,
58 |
59 | #[props(default = CheckboxState::Unchecked)]
60 | default_checked: CheckboxState,
61 |
62 | #[props(default)]
63 | required: ReadOnlySignal,
64 |
65 | #[props(default)]
66 | disabled: ReadOnlySignal,
67 |
68 | #[props(default)]
69 | name: ReadOnlySignal,
70 |
71 | #[props(default = ReadOnlySignal::new(Signal::new(String::from("on"))))]
72 | value: ReadOnlySignal,
73 |
74 | #[props(default)]
75 | on_checked_change: Callback,
76 |
77 | #[props(extends = GlobalAttributes)]
78 | attributes: Vec,
79 |
80 | children: Element,
81 | }
82 |
83 | #[component]
84 | pub fn Checkbox(props: CheckboxProps) -> Element {
85 | let (checked, set_checked) = use_controlled(
86 | props.checked,
87 | props.default_checked,
88 | props.on_checked_change,
89 | );
90 |
91 | let _ctx = use_context_provider(|| CheckboxCtx {
92 | checked: checked.into(),
93 | disabled: props.disabled,
94 | });
95 |
96 | rsx! {
97 | button {
98 | type: "button",
99 | value: props.value,
100 | role: "checkbox",
101 | aria_checked: checked().to_aria_checked(),
102 | aria_required: props.required,
103 | disabled: props.disabled,
104 | "data-state": checked().to_data_state(),
105 | "data-disabled": props.disabled,
106 |
107 | onclick: move |_| {
108 | let new_checked = !checked();
109 | set_checked.call(new_checked);
110 | },
111 |
112 | // Aria says only spacebar can change state of checkboxes.
113 | onkeydown: move |e| {
114 | if e.key() == Key::Enter {
115 | e.prevent_default();
116 | }
117 | },
118 |
119 | ..props.attributes,
120 | {props.children}
121 | }
122 | BubbleInput {
123 | checked: checked,
124 | default_checked: props.default_checked,
125 |
126 | required: props.required,
127 | name: props.name,
128 | value: props.value,
129 | disabled: props.disabled,
130 | }
131 | }
132 | }
133 |
134 | #[component]
135 | pub fn CheckboxIndicator(
136 | #[props(extends = GlobalAttributes)] attributes: Vec,
137 | children: Element,
138 | ) -> Element {
139 | let ctx: CheckboxCtx = use_context();
140 | let checked = (ctx.checked)();
141 |
142 | rsx! {
143 | span {
144 | "data-state": checked.to_data_state(),
145 | "data-disabled": ctx.disabled,
146 | ..attributes,
147 |
148 | if checked.into() {
149 | {children}
150 | }
151 | }
152 | }
153 | }
154 |
155 | #[component]
156 | fn BubbleInput(
157 | checked: ReadOnlySignal,
158 | default_checked: CheckboxState,
159 | #[props(extends = input)] attributes: Vec,
160 | ) -> Element {
161 | let id = use_unique_id();
162 |
163 | // Update the actual input state to match our virtual state.
164 | use_effect(move || {
165 | let checked = checked();
166 | let js = eval(
167 | r#"
168 | let id = await dioxus.recv();
169 | let action = await dioxus.recv();
170 | let input = document.getElementById(id);
171 |
172 | switch(action) {
173 | case "checked":
174 | input.checked = true;
175 | input.indeterminate = false;
176 | break;
177 | case "indeterminate":
178 | input.indeterminate = true;
179 | input.checked = true;
180 | break;
181 | case "unchecked":
182 | input.checked = false;
183 | input.indeterminate = false;
184 | break;
185 | }
186 | "#,
187 | );
188 |
189 | let _ = js.send(id());
190 | let _ = js.send(checked.to_data_state());
191 | });
192 |
193 | rsx! {
194 | input {
195 | id,
196 | type: "checkbox",
197 | aria_hidden: "true",
198 | tabindex: "-1",
199 | position: "absolute",
200 | pointer_events: "none",
201 | opacity: "0",
202 | margin: "0",
203 | transform: "translateX(-100%)",
204 |
205 | // Default checked
206 | checked: default_checked != CheckboxState::Unchecked,
207 |
208 | ..attributes,
209 | }
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/primitives/src/collapsible.rs:
--------------------------------------------------------------------------------
1 | //! Content that can be collapsed.
2 |
3 | use crate::{use_controlled, use_id_or, use_unique_id};
4 | use dioxus_lib::prelude::*;
5 |
6 | // TODO: more docs
7 |
8 | #[derive(Clone, Copy)]
9 | struct CollapsibleCtx {
10 | open: Memo,
11 | set_open: Callback,
12 | disabled: ReadOnlySignal,
13 | keep_mounted: ReadOnlySignal,
14 | aria_controls_id: Signal,
15 | }
16 |
17 | #[derive(Props, Clone, PartialEq)]
18 | pub struct CollapsibleProps {
19 | /// Keep [`CollapsibleContent`] mounted in the DOM when the collapsible is closed.
20 | ///
21 | /// This does not apply any special ARIA or other attributes.
22 | #[props(default)]
23 | keep_mounted: ReadOnlySignal,
24 |
25 | /// The default `open` state.
26 | ///
27 | /// This will be overridden if the component is controlled.
28 | #[props(default)]
29 | default_open: bool,
30 |
31 | /// The disabled state of the collapsible.
32 | #[props(default)]
33 | disabled: ReadOnlySignal,
34 |
35 | /// The controlled `open` state of the collapsible.
36 | ///
37 | /// If this is provided, you must use `on_open_change`.
38 | open: Option>,
39 |
40 | /// A callback for when the open state changes.
41 | ///
42 | /// The provided argument is a bool of whether the collapsible is open or closed.
43 | #[props(default)]
44 | on_open_change: Callback,
45 |
46 | #[props(extends = GlobalAttributes)]
47 | attributes: Vec,
48 |
49 | children: Element,
50 | }
51 |
52 | /// The provider for a collapsible piece of content.
53 | #[component]
54 | pub fn Collapsible(props: CollapsibleProps) -> Element {
55 | let (open, set_open) = use_controlled(props.open, props.default_open, props.on_open_change);
56 |
57 | let aria_controls_id = use_unique_id();
58 | let _ctx = use_context_provider(|| CollapsibleCtx {
59 | open,
60 | set_open,
61 | disabled: props.disabled,
62 | keep_mounted: props.keep_mounted,
63 | aria_controls_id,
64 | });
65 |
66 | let state = open_state(open());
67 |
68 | rsx! {
69 | div {
70 | "data-state": state,
71 | "data-disabled": props.disabled,
72 | ..props.attributes,
73 |
74 | {props.children}
75 | }
76 | }
77 | }
78 |
79 | #[derive(Props, Clone, PartialEq)]
80 | pub struct CollapsibleContentProps {
81 | id: ReadOnlySignal