├── .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 |
7 | 8 | 9 | Crates.io version 11 | 12 | 13 | 14 | Download 16 | 17 | 18 | 19 | docs.rs docs 21 | 22 |
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 | 12 | 14 | 26 | 30 | 34 | 38 | 42 | 46 | 50 | 54 | 58 | 62 | 63 | 64 | 65 | 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>, 82 | 83 | #[props(extends = GlobalAttributes)] 84 | attributes: Vec, 85 | children: Element, 86 | } 87 | 88 | /// A section of content that can be collapsed. 89 | #[component] 90 | pub fn CollapsibleContent(props: CollapsibleContentProps) -> Element { 91 | let ctx: CollapsibleCtx = use_context(); 92 | let id = use_id_or(ctx.aria_controls_id, props.id); 93 | 94 | let open = ctx.open; 95 | let state = open_state(open()); 96 | 97 | rsx! { 98 | div { 99 | id: id, 100 | "data-state": state, 101 | "data-disabled": ctx.disabled, 102 | ..props.attributes, 103 | 104 | if open() || (ctx.keep_mounted)() { 105 | {props.children} 106 | } 107 | } 108 | } 109 | } 110 | 111 | #[derive(Props, Clone, PartialEq)] 112 | pub struct CollapsibleTriggerProps { 113 | #[props(extends = GlobalAttributes)] 114 | attributes: Vec, 115 | children: Element, 116 | } 117 | 118 | /// The trigger of a collapsible piece of content. 119 | #[component] 120 | pub fn CollapsibleTrigger(props: CollapsibleTriggerProps) -> Element { 121 | let ctx: CollapsibleCtx = use_context(); 122 | 123 | let open = ctx.open; 124 | let state = open_state(open()); 125 | 126 | rsx! { 127 | 128 | button { 129 | type: "button", 130 | "data-state": state, 131 | "data-disabled": ctx.disabled, 132 | disabled: ctx.disabled, 133 | 134 | aria_controls: ctx.aria_controls_id, 135 | aria_expanded: open, 136 | 137 | onclick: move |_| { 138 | let new_open = !open(); 139 | ctx.set_open.call(new_open); 140 | }, 141 | 142 | ..props.attributes, 143 | {props.children} 144 | } 145 | } 146 | } 147 | 148 | fn open_state(open: bool) -> &'static str { 149 | match open { 150 | true => "open", 151 | false => "closed", 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /primitives/src/dialog.rs: -------------------------------------------------------------------------------- 1 | use dioxus_lib::{document::eval, prelude::*}; 2 | 3 | use crate::{use_controlled, use_id_or, use_unique_id}; 4 | 5 | #[derive(Clone, Copy)] 6 | struct DialogCtx { 7 | open: Memo, 8 | set_open: Callback, 9 | 10 | // Whether the dialog is a modal and should capture focus. 11 | is_modal: ReadOnlySignal, 12 | dialog_labelledby: Signal, 13 | dialog_describedby: Signal, 14 | } 15 | 16 | #[derive(Props, Clone, PartialEq)] 17 | pub struct DialogProps { 18 | id: ReadOnlySignal>, 19 | 20 | #[props(default = ReadOnlySignal::new(Signal::new(true)))] 21 | is_modal: ReadOnlySignal, 22 | 23 | open: Option>, 24 | 25 | #[props(default)] 26 | default_open: bool, 27 | 28 | #[props(default)] 29 | on_open_change: Callback, 30 | 31 | children: Element, 32 | } 33 | 34 | #[component] 35 | pub fn Dialog(props: DialogProps) -> Element { 36 | let dialog_labelledby = use_unique_id(); 37 | let dialog_describedby = use_unique_id(); 38 | 39 | let (open, set_open) = use_controlled(props.open, props.default_open, props.on_open_change); 40 | 41 | let ctx = use_context_provider(|| DialogCtx { 42 | open, 43 | set_open, 44 | is_modal: props.is_modal, 45 | dialog_labelledby, 46 | dialog_describedby, 47 | }); 48 | 49 | let gen_id = use_unique_id(); 50 | let id = use_id_or(gen_id, props.id); 51 | use_effect(move || { 52 | let is_open = open(); 53 | let is_modal = (props.is_modal)(); 54 | 55 | let js = eval( 56 | r#" 57 | let id = await dioxus.recv(); 58 | let is_open = await dioxus.recv(); 59 | let is_modal = await dioxus.recv(); 60 | 61 | let dialog = document.getElementById(id); 62 | 63 | if (is_open) { 64 | switch (is_modal) { 65 | case true: 66 | dialog.showModal(); 67 | break; 68 | case false: 69 | dialog.show(); 70 | break; 71 | } 72 | } else { 73 | dialog.close(); 74 | } 75 | "#, 76 | ); 77 | 78 | let _ = js.send(id()); 79 | let _ = js.send(is_open); 80 | let _ = js.send(is_modal); 81 | }); 82 | 83 | rsx! { 84 | dialog { 85 | id: id, 86 | aria_modal: props.is_modal, 87 | aria_labelledby: ctx.dialog_labelledby, 88 | aria_describedby: ctx.dialog_describedby, 89 | 90 | {props.children} 91 | } 92 | } 93 | } 94 | 95 | #[derive(Props, Clone, PartialEq)] 96 | pub struct DialogTitleProps { 97 | id: ReadOnlySignal>, 98 | children: Element, 99 | } 100 | 101 | #[component] 102 | pub fn DialogTitle(props: DialogTitleProps) -> Element { 103 | let ctx: DialogCtx = use_context(); 104 | let id = use_id_or(ctx.dialog_labelledby, props.id); 105 | 106 | rsx! { 107 | h2 { 108 | id: id, 109 | {props.children} 110 | } 111 | } 112 | } 113 | 114 | #[derive(Props, Clone, PartialEq)] 115 | pub struct DialogDescriptionProps { 116 | id: ReadOnlySignal>, 117 | children: Element, 118 | } 119 | 120 | #[component] 121 | pub fn DialogDescription(props: DialogDescriptionProps) -> Element { 122 | let ctx: DialogCtx = use_context(); 123 | let id = use_id_or(ctx.dialog_describedby, props.id); 124 | 125 | rsx! { 126 | p { 127 | id: id, 128 | {props.children} 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /primitives/src/hover_card.rs: -------------------------------------------------------------------------------- 1 | use crate::{use_controlled, use_id_or, use_unique_id}; 2 | use dioxus_lib::prelude::*; 3 | 4 | #[derive(Clone)] 5 | struct HoverCardCtx { 6 | // State 7 | open: Memo, 8 | set_open: Callback, 9 | disabled: ReadOnlySignal, 10 | 11 | // ARIA attributes 12 | content_id: Signal, 13 | } 14 | 15 | #[derive(Props, Clone, PartialEq)] 16 | pub struct HoverCardProps { 17 | /// Whether the hover card is open 18 | open: Option>, 19 | 20 | /// Default open state 21 | #[props(default)] 22 | default_open: bool, 23 | 24 | /// Callback when open state changes 25 | #[props(default)] 26 | on_open_change: Callback, 27 | 28 | /// Whether the hover card is disabled 29 | #[props(default)] 30 | disabled: ReadOnlySignal, 31 | 32 | #[props(extends = GlobalAttributes)] 33 | attributes: Vec, 34 | 35 | children: Element, 36 | } 37 | 38 | #[component] 39 | pub fn HoverCard(props: HoverCardProps) -> Element { 40 | let (open, set_open) = use_controlled(props.open, props.default_open, props.on_open_change); 41 | // Generate a unique ID for the hover card content 42 | let content_id = use_unique_id(); 43 | 44 | let _ctx = use_context_provider(|| HoverCardCtx { 45 | open, 46 | set_open, 47 | disabled: props.disabled, 48 | content_id, 49 | }); 50 | 51 | rsx! { 52 | div { 53 | class: "hover-card", 54 | "data-state": if open() { "open" } else { "closed" }, 55 | "data-disabled": (props.disabled)(), 56 | ..props.attributes, 57 | 58 | {props.children} 59 | } 60 | } 61 | } 62 | 63 | #[derive(Props, Clone, PartialEq)] 64 | pub struct HoverCardTriggerProps { 65 | /// Optional ID for the trigger element 66 | #[props(default)] 67 | id: ReadOnlySignal>, 68 | 69 | #[props(extends = GlobalAttributes)] 70 | attributes: Vec, 71 | 72 | children: Element, 73 | } 74 | 75 | #[component] 76 | pub fn HoverCardTrigger(props: HoverCardTriggerProps) -> Element { 77 | let ctx: HoverCardCtx = use_context(); 78 | 79 | // Generate a unique ID for the trigger 80 | let trigger_id = use_unique_id(); 81 | 82 | // Use use_id_or to handle the ID 83 | let id = use_id_or(trigger_id, props.id); 84 | 85 | // Handle mouse events 86 | let handle_mouse_enter = move |_: Event| { 87 | if !(ctx.disabled)() { 88 | ctx.set_open.call(true); 89 | } 90 | }; 91 | 92 | let handle_mouse_leave = move |_: Event| { 93 | if !(ctx.disabled)() { 94 | ctx.set_open.call(false); 95 | } 96 | }; 97 | 98 | rsx! { 99 | div { 100 | id, 101 | class: "hover-card-trigger", 102 | 103 | // Mouse events 104 | onmouseenter: handle_mouse_enter, 105 | onmouseleave: handle_mouse_leave, 106 | 107 | // ARIA attributes 108 | aria_haspopup: "dialog", 109 | aria_expanded: (ctx.open)(), 110 | aria_controls: ctx.content_id.peek().clone(), 111 | 112 | ..props.attributes, 113 | {props.children} 114 | } 115 | } 116 | } 117 | 118 | #[derive(Debug, Clone, Copy, PartialEq)] 119 | pub enum HoverCardSide { 120 | Top, 121 | Right, 122 | Bottom, 123 | Left, 124 | } 125 | 126 | impl HoverCardSide { 127 | pub fn as_str(&self) -> &'static str { 128 | match self { 129 | HoverCardSide::Top => "top", 130 | HoverCardSide::Right => "right", 131 | HoverCardSide::Bottom => "bottom", 132 | HoverCardSide::Left => "left", 133 | } 134 | } 135 | } 136 | 137 | #[derive(Debug, Clone, Copy, PartialEq)] 138 | pub enum HoverCardAlign { 139 | Start, 140 | Center, 141 | End, 142 | } 143 | 144 | impl HoverCardAlign { 145 | pub fn as_str(&self) -> &'static str { 146 | match self { 147 | HoverCardAlign::Start => "start", 148 | HoverCardAlign::Center => "center", 149 | HoverCardAlign::End => "end", 150 | } 151 | } 152 | } 153 | 154 | #[derive(Props, Clone, PartialEq)] 155 | pub struct HoverCardContentProps { 156 | /// Optional ID for the hover card content 157 | #[props(default)] 158 | id: ReadOnlySignal>, 159 | 160 | /// Side of the trigger to place the hover card 161 | #[props(default = HoverCardSide::Top)] 162 | side: HoverCardSide, 163 | 164 | /// Alignment of the hover card relative to the trigger 165 | #[props(default = HoverCardAlign::Center)] 166 | align: HoverCardAlign, 167 | 168 | /// Whether to force the hover card to stay open when hovered 169 | #[props(default = true)] 170 | force_mount: bool, 171 | 172 | #[props(extends = GlobalAttributes)] 173 | attributes: Vec, 174 | 175 | children: Element, 176 | } 177 | 178 | #[component] 179 | pub fn HoverCardContent(props: HoverCardContentProps) -> Element { 180 | let ctx: HoverCardCtx = use_context(); 181 | 182 | // Only render if the hover card is open or force_mount is true 183 | let is_open = (ctx.open)(); 184 | if !is_open && !props.force_mount { 185 | return rsx!({}); 186 | } 187 | 188 | // Use use_id_or to handle the ID 189 | let id = use_id_or(ctx.content_id, props.id); 190 | 191 | // Handle mouse events to keep the hover card open when hovered 192 | let handle_mouse_enter = move |_: Event| { 193 | if !(ctx.disabled)() { 194 | ctx.set_open.call(true); 195 | } 196 | }; 197 | 198 | let handle_mouse_leave = move |_: Event| { 199 | if !(ctx.disabled)() { 200 | ctx.set_open.call(false); 201 | } 202 | }; 203 | 204 | rsx! { 205 | div { 206 | id: id, 207 | class: "hover-card-content", 208 | role: "dialog", 209 | "data-state": if is_open { "open" } else { "closed" }, 210 | "data-side": props.side.as_str(), 211 | "data-align": props.align.as_str(), 212 | 213 | // Mouse events to keep the hover card open when hovered 214 | onmouseenter: handle_mouse_enter, 215 | onmouseleave: handle_mouse_leave, 216 | 217 | ..props.attributes, 218 | {props.children} 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /primitives/src/label.rs: -------------------------------------------------------------------------------- 1 | use dioxus_lib::prelude::*; 2 | 3 | #[derive(Props, Clone, PartialEq)] 4 | pub struct LabelProps { 5 | html_for: ReadOnlySignal, 6 | 7 | #[props(extends = GlobalAttributes)] 8 | attributes: Vec, 9 | 10 | children: Element, 11 | } 12 | 13 | #[component] 14 | pub fn Label(props: LabelProps) -> Element { 15 | // TODO: (?) the Radix primitive prevents selection on double click (but not intentional highlighting) 16 | rsx! { 17 | label { 18 | for: props.html_for, 19 | ..props.attributes, 20 | 21 | {props.children} 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /primitives/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicUsize, Ordering}; 2 | 3 | use dioxus_lib::prelude::*; 4 | 5 | pub mod accordion; 6 | pub mod alert_dialog; 7 | pub mod aspect_ratio; 8 | pub mod avatar; 9 | pub mod calendar; 10 | pub mod checkbox; 11 | pub mod collapsible; 12 | pub mod context_menu; 13 | pub mod dialog; 14 | pub mod dropdown_menu; 15 | pub mod hover_card; 16 | pub mod label; 17 | pub mod menubar; 18 | mod portal; 19 | pub mod progress; 20 | pub mod radio_group; 21 | pub mod scroll_area; 22 | pub mod select; 23 | pub mod separator; 24 | pub mod slider; 25 | pub mod switch; 26 | pub mod tabs; 27 | pub mod toast; 28 | pub mod toggle; 29 | pub mod toggle_group; 30 | pub mod toolbar; 31 | pub mod tooltip; 32 | 33 | /// Generate a runtime-unique id. 34 | fn use_unique_id() -> Signal { 35 | static NEXT_ID: AtomicUsize = AtomicUsize::new(0); 36 | 37 | #[allow(unused_mut)] 38 | let mut initial_value = use_hook(|| { 39 | let id = NEXT_ID.fetch_add(1, Ordering::Relaxed); 40 | let id_str = format!("dxc-{id}"); 41 | id_str 42 | }); 43 | 44 | fullstack! { 45 | let server_id = dioxus::prelude::use_server_cached(move || { 46 | initial_value.clone() 47 | }); 48 | initial_value = server_id; 49 | } 50 | use_signal(|| initial_value) 51 | } 52 | 53 | // Elements can only have one id so if the user provides their own, we must use it as the aria id. 54 | fn use_id_or(mut gen_id: Signal, user_id: ReadOnlySignal>) -> Memo { 55 | // First, check if we have a user-provided ID 56 | let has_user_id = use_memo(move || user_id().is_some()); 57 | 58 | // If we have a user ID, update the gen_id in an effect 59 | use_effect(move || { 60 | if let Some(id) = user_id() { 61 | gen_id.set(id); 62 | } 63 | }); 64 | 65 | // Return the appropriate ID 66 | use_memo(move || { 67 | if has_user_id() { 68 | user_id().unwrap() 69 | } else { 70 | gen_id.peek().clone() 71 | } 72 | }) 73 | } 74 | 75 | /// Allows some state to be either controlled or uncontrolled. 76 | fn use_controlled( 77 | prop: Option>, 78 | default: T, 79 | on_change: Callback, 80 | ) -> (Memo, Callback) { 81 | let mut internal_value = use_signal(|| prop.map(|x| x()).unwrap_or(default)); 82 | let value = use_memo(move || prop.unwrap_or(internal_value)()); 83 | 84 | let set_value = use_callback(move |x: T| { 85 | internal_value.set(x.clone()); 86 | on_change.call(x); 87 | }); 88 | 89 | (value, set_value) 90 | } 91 | 92 | /// Run some cleanup code when the component is unmounted if the effect was run. 93 | fn use_effect_cleanup(#[allow(unused)] cleanup: F) { 94 | web!(use_drop(cleanup)) 95 | } 96 | -------------------------------------------------------------------------------- /primitives/src/menubar.rs: -------------------------------------------------------------------------------- 1 | use dioxus_lib::prelude::*; 2 | 3 | use crate::use_effect_cleanup; 4 | 5 | #[derive(Clone, Copy)] 6 | struct MenubarContext { 7 | // Currently open menu index 8 | open_menu: Signal>, 9 | set_open_menu: Callback>, 10 | disabled: ReadOnlySignal, 11 | 12 | // Keyboard nav data 13 | menu_count: Signal, 14 | recent_focus: Signal, 15 | current_focus: Signal>, 16 | } 17 | 18 | impl MenubarContext { 19 | fn set_focus(&mut self, index: Option) { 20 | if let Some(idx) = index { 21 | self.recent_focus.set(idx); 22 | } 23 | self.current_focus.set(index); 24 | } 25 | 26 | fn focus_next(&mut self) { 27 | let next = (*self.recent_focus.read() + 1) % *self.menu_count.read(); 28 | self.set_focus(Some(next)); 29 | } 30 | 31 | fn focus_prev(&mut self) { 32 | let prev = if *self.recent_focus.read() == 0 { 33 | *self.menu_count.read() - 1 34 | } else { 35 | *self.recent_focus.read() - 1 36 | }; 37 | self.set_focus(Some(prev)); 38 | } 39 | 40 | fn focus_first(&mut self) { 41 | self.set_focus(Some(0)); 42 | } 43 | 44 | fn focus_last(&mut self) { 45 | let last_index = *self.menu_count.read() - 1; 46 | self.set_focus(Some(last_index)); 47 | } 48 | } 49 | 50 | #[derive(Props, Clone, PartialEq)] 51 | pub struct MenubarProps { 52 | #[props(default)] 53 | disabled: ReadOnlySignal, 54 | 55 | #[props(extends = GlobalAttributes)] 56 | attributes: Vec, 57 | children: Element, 58 | } 59 | 60 | #[component] 61 | pub fn Menubar(props: MenubarProps) -> Element { 62 | let mut open_menu = use_signal(|| None); 63 | let set_open_menu = use_callback(move |idx| open_menu.set(idx)); 64 | 65 | let mut ctx = use_context_provider(|| MenubarContext { 66 | open_menu, 67 | set_open_menu, 68 | disabled: props.disabled, 69 | menu_count: Signal::new(0), 70 | recent_focus: Signal::new(0), 71 | current_focus: Signal::new(None), 72 | }); 73 | 74 | rsx! { 75 | div { 76 | role: "menubar", 77 | "data-disabled": (props.disabled)(), 78 | 79 | onfocusout: move |_| ctx.set_focus(None), 80 | ..props.attributes, 81 | 82 | {props.children} 83 | } 84 | } 85 | } 86 | 87 | #[derive(Props, Clone, PartialEq)] 88 | pub struct MenubarMenuProps { 89 | index: usize, 90 | 91 | #[props(default)] 92 | disabled: ReadOnlySignal, 93 | 94 | #[props(extends = GlobalAttributes)] 95 | attributes: Vec, 96 | children: Element, 97 | } 98 | 99 | #[component] 100 | pub fn MenubarMenu(props: MenubarMenuProps) -> Element { 101 | let mut ctx: MenubarContext = use_context(); 102 | 103 | use_effect(move || { 104 | ctx.menu_count += 1; 105 | }); 106 | 107 | use_effect_cleanup(move || { 108 | ctx.menu_count -= 1; 109 | if (ctx.current_focus)() == Some(props.index) { 110 | ctx.set_focus(None); 111 | } 112 | }); 113 | 114 | let is_open = use_memo(move || (ctx.open_menu)() == Some(props.index)); 115 | let tab_index = use_memo(move || { 116 | if (ctx.current_focus)() == Some(props.index) { 117 | "0" 118 | } else { 119 | "-1" 120 | } 121 | }); 122 | 123 | rsx! { 124 | div { 125 | role: "menu", 126 | tabindex: tab_index, 127 | "data-state": if is_open() { "open" } else { "closed" }, 128 | "data-disabled": (ctx.disabled)() || (props.disabled)(), 129 | 130 | onclick: move |_| { 131 | if !(ctx.disabled)() && !(props.disabled)() { 132 | let new_open = if is_open() { None } else { Some(props.index) }; 133 | ctx.set_open_menu.call(new_open); 134 | } 135 | }, 136 | 137 | onfocus: move |_| ctx.set_focus(Some(props.index)), 138 | 139 | onkeydown: move |event: Event| { 140 | let mut prevent_default = true; 141 | match event.key() { 142 | Key::Enter => { 143 | if !(ctx.disabled)() && !(props.disabled)() { 144 | let new_open = if is_open() { None } else { Some(props.index) }; 145 | ctx.set_open_menu.call(new_open); 146 | } 147 | } 148 | Key::Escape => ctx.set_open_menu.call(None), 149 | Key::ArrowLeft => ctx.focus_prev(), 150 | Key::ArrowRight => ctx.focus_next(), 151 | Key::Home => ctx.focus_first(), 152 | Key::End => ctx.focus_last(), 153 | _ => prevent_default = false, 154 | } 155 | if prevent_default { 156 | event.prevent_default(); 157 | } 158 | }, 159 | 160 | ..props.attributes, 161 | {props.children} 162 | } 163 | } 164 | } 165 | 166 | #[derive(Props, Clone, PartialEq)] 167 | pub struct MenubarTriggerProps { 168 | #[props(extends = GlobalAttributes)] 169 | attributes: Vec, 170 | children: Element, 171 | } 172 | 173 | #[component] 174 | pub fn MenubarTrigger(props: MenubarTriggerProps) -> Element { 175 | rsx! { 176 | button { ..props.attributes,{props.children} } 177 | } 178 | } 179 | 180 | #[derive(Props, Clone, PartialEq)] 181 | pub struct MenubarContentProps { 182 | #[props(extends = GlobalAttributes)] 183 | attributes: Vec, 184 | children: Element, 185 | } 186 | 187 | #[component] 188 | pub fn MenubarContent(props: MenubarContentProps) -> Element { 189 | rsx! { 190 | div { role: "menu", ..props.attributes, {props.children} } 191 | } 192 | } 193 | 194 | #[derive(Props, Clone, PartialEq)] 195 | pub struct MenubarItemProps { 196 | value: String, 197 | 198 | #[props(default)] 199 | disabled: ReadOnlySignal, 200 | 201 | #[props(default)] 202 | on_select: Callback, 203 | 204 | #[props(extends = GlobalAttributes)] 205 | attributes: Vec, 206 | children: Element, 207 | } 208 | 209 | #[component] 210 | pub fn MenubarItem(props: MenubarItemProps) -> Element { 211 | let ctx: MenubarContext = use_context(); 212 | 213 | rsx! { 214 | div { 215 | role: "menuitem", 216 | "data-disabled": (ctx.disabled)() || (props.disabled)(), 217 | 218 | onclick: { 219 | let value = props.value.clone(); 220 | move |_| { 221 | if !(ctx.disabled)() && !(props.disabled)() { 222 | props.on_select.call(value.clone()); 223 | ctx.set_open_menu.call(None); 224 | } 225 | } 226 | }, 227 | 228 | ..props.attributes, 229 | {props.children} 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /primitives/src/portal.rs: -------------------------------------------------------------------------------- 1 | use dioxus_lib::{prelude::*, warnings::Warning}; 2 | use std::collections::HashMap; 3 | 4 | use crate::use_effect_cleanup; 5 | 6 | #[derive(Debug, Clone, Copy, PartialEq)] 7 | pub struct PortalId(usize); 8 | 9 | #[derive(Clone, Copy, PartialEq)] 10 | struct PortalCtx { 11 | portals: Signal>>, 12 | } 13 | 14 | /// Create a portal. 15 | pub fn use_portal() -> PortalId { 16 | static NEXT_ID: GlobalSignal = Signal::global(|| 0); 17 | 18 | let (sig, id) = use_hook(|| { 19 | let mut next_id = NEXT_ID.write(); 20 | let id = *next_id; 21 | *next_id += 1; 22 | 23 | let mut ctx = match try_consume_context::() { 24 | Some(ctx) => ctx, 25 | None => { 26 | let portals = Signal::new_in_scope(HashMap::new(), ScopeId::ROOT); 27 | let ctx = PortalCtx { portals }; 28 | provide_root_context(ctx) 29 | } 30 | }; 31 | 32 | let sig = Signal::new_in_scope(Ok(VNode::placeholder()), ScopeId::ROOT); 33 | ctx.portals.write().insert(id, sig); 34 | 35 | (sig, PortalId(id)) 36 | }); 37 | 38 | // Cleanup the portal. 39 | use_effect_cleanup(move || { 40 | let mut ctx = consume_context::(); 41 | ctx.portals.write().remove(&id.0); 42 | sig.manually_drop(); 43 | }); 44 | 45 | id 46 | } 47 | 48 | #[component] 49 | pub fn PortalIn(portal: PortalId, children: Element) -> Element { 50 | if let Some(mut ctx) = try_use_context::() { 51 | dioxus_lib::signals::warnings::signal_write_in_component_body::allow(|| { 52 | let mut portals = ctx.portals.write(); 53 | if let Some(portal) = portals.get_mut(&portal.0) { 54 | portal.set(children); 55 | } 56 | }); 57 | } 58 | 59 | rsx! {} 60 | } 61 | 62 | #[component] 63 | pub fn PortalOut(portal: PortalId) -> Element { 64 | if let Some(ctx) = try_use_context::() { 65 | if let Some(children) = ctx.portals.peek().get(&portal.0) { 66 | return rsx! { 67 | {children} 68 | }; 69 | } 70 | } 71 | 72 | rsx! {} 73 | } 74 | -------------------------------------------------------------------------------- /primitives/src/progress.rs: -------------------------------------------------------------------------------- 1 | use dioxus_lib::prelude::*; 2 | 3 | #[derive(Props, Clone, PartialEq)] 4 | pub struct ProgressProps { 5 | /// The current progress value, between 0 and max 6 | value: ReadOnlySignal>, 7 | 8 | /// The maximum value. Defaults to 100 9 | #[props(default = ReadOnlySignal::new(Signal::new(100.0)))] 10 | max: ReadOnlySignal, 11 | 12 | #[props(extends = GlobalAttributes)] 13 | attributes: Vec, 14 | 15 | children: Element, 16 | } 17 | 18 | #[component] 19 | pub fn Progress(props: ProgressProps) -> Element { 20 | // Calculate percentage for styling and "data-state" 21 | let percentage = use_memo(move || { 22 | props.value.cloned().map(|v| { 23 | let max = (props.max)(); 24 | (v / max) * 100.0 25 | }) 26 | }); 27 | 28 | let state = use_memo(move || match percentage() { 29 | Some(_) => "loading", 30 | None => "indeterminate", 31 | }); 32 | 33 | rsx! { 34 | div { 35 | role: "progressbar", 36 | "aria-valuemin": 0, 37 | "aria-valuemax": props.max, 38 | "aria-valuenow": props.value.cloned(), 39 | "data-state": state, 40 | "data-value": props.value.cloned().map(|v| v.to_string()), 41 | "data-max": props.max, 42 | style: percentage().map(|p| format!("--progress-value: {}%", p)), 43 | ..props.attributes, 44 | 45 | {props.children} 46 | } 47 | } 48 | } 49 | 50 | /// The indicator that represents the progress visually 51 | #[derive(Props, Clone, PartialEq)] 52 | pub struct ProgressIndicatorProps { 53 | #[props(extends = GlobalAttributes)] 54 | attributes: Vec, 55 | } 56 | 57 | #[component] 58 | pub fn ProgressIndicator(props: ProgressIndicatorProps) -> Element { 59 | rsx! { 60 | div { ..props.attributes } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /primitives/src/scroll_area.rs: -------------------------------------------------------------------------------- 1 | use dioxus_lib::prelude::*; 2 | 3 | #[derive(Props, Clone, PartialEq)] 4 | pub struct ScrollAreaProps { 5 | /// The scroll direction. 6 | #[props(default)] 7 | direction: ReadOnlySignal, 8 | 9 | /// Whether the scrollbars should be always visible. 10 | #[props(default)] 11 | always_show_scrollbars: ReadOnlySignal, 12 | 13 | /// The scroll type. 14 | #[props(default)] 15 | scroll_type: ReadOnlySignal, 16 | 17 | #[props(extends = GlobalAttributes)] 18 | attributes: Vec, 19 | children: Element, 20 | } 21 | 22 | #[derive(Clone, Copy, PartialEq)] 23 | pub enum ScrollDirection { 24 | Vertical, 25 | Horizontal, 26 | Both, 27 | } 28 | 29 | impl Default for ScrollDirection { 30 | fn default() -> Self { 31 | Self::Both 32 | } 33 | } 34 | 35 | #[derive(Clone, Copy, PartialEq)] 36 | pub enum ScrollType { 37 | /// Browser default scrolling 38 | Auto, 39 | /// Always show scrollbars 40 | Always, 41 | /// Hide scrollbars but enable scrolling 42 | Hidden, 43 | } 44 | 45 | impl Default for ScrollType { 46 | fn default() -> Self { 47 | Self::Auto 48 | } 49 | } 50 | 51 | #[component] 52 | pub fn ScrollArea(props: ScrollAreaProps) -> Element { 53 | let direction = props.direction; 54 | let scroll_type = props.scroll_type; 55 | let always_show = props.always_show_scrollbars; 56 | 57 | let overflow_style = use_memo(move || match scroll_type() { 58 | ScrollType::Auto => match direction() { 59 | ScrollDirection::Vertical => "overflow-y: auto; overflow-x: hidden;", 60 | ScrollDirection::Horizontal => "overflow-x: auto; overflow-y: hidden;", 61 | ScrollDirection::Both => "overflow: auto;", 62 | }, 63 | ScrollType::Always => match direction() { 64 | ScrollDirection::Vertical => "overflow-y: scroll; overflow-x: hidden;", 65 | ScrollDirection::Horizontal => "overflow-x: scroll; overflow-y: hidden;", 66 | ScrollDirection::Both => "overflow: scroll;", 67 | }, 68 | ScrollType::Hidden => match direction() { 69 | ScrollDirection::Vertical => { 70 | "overflow-y: scroll; overflow-x: hidden; scrollbar-width: none;" 71 | } 72 | ScrollDirection::Horizontal => { 73 | "overflow-x: scroll; overflow-y: hidden; scrollbar-width: none;" 74 | } 75 | ScrollDirection::Both => "overflow: scroll; scrollbar-width: none;", 76 | }, 77 | }); 78 | 79 | let visibility_class = use_memo(move || { 80 | if always_show() { 81 | "scroll-area-always-show" 82 | } else { 83 | "scroll-area-auto-hide" 84 | } 85 | }); 86 | 87 | rsx! { 88 | div { 89 | class: "{visibility_class}", 90 | style: "{overflow_style}", 91 | "data-scroll-direction": match direction() { 92 | ScrollDirection::Vertical => "vertical", 93 | ScrollDirection::Horizontal => "horizontal", 94 | ScrollDirection::Both => "both", 95 | }, 96 | ..props.attributes, 97 | 98 | {props.children} 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /primitives/src/separator.rs: -------------------------------------------------------------------------------- 1 | use dioxus_lib::prelude::*; 2 | 3 | // TODO: Docs 4 | 5 | #[derive(Props, Clone, PartialEq)] 6 | pub struct SeparatorProps { 7 | /// Horizontal if true, vertical if false. 8 | #[props(default = true)] 9 | horizontal: bool, 10 | 11 | /// If the separator is decorative and should not be classified 12 | /// as a separator to the ARIA standard. 13 | #[props(default = false)] 14 | decorative: bool, 15 | 16 | #[props(extends = GlobalAttributes)] 17 | attributes: Vec, 18 | } 19 | 20 | #[component] 21 | pub fn Separator(props: SeparatorProps) -> Element { 22 | let orientation = match props.horizontal { 23 | true => "horizontal", 24 | false => "vertical", 25 | }; 26 | 27 | rsx! { 28 | div { 29 | role: if !props.decorative { "separator" } else { "none" }, 30 | aria_orientation: if !props.decorative { orientation }, 31 | "data-orientation": orientation, 32 | ..props.attributes, 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /primitives/src/switch.rs: -------------------------------------------------------------------------------- 1 | use crate::use_controlled; 2 | use dioxus_lib::prelude::*; 3 | 4 | #[derive(Props, Clone, PartialEq)] 5 | pub struct SwitchProps { 6 | checked: Option>, 7 | 8 | #[props(default = false)] 9 | default_checked: bool, 10 | 11 | #[props(default = ReadOnlySignal::new(Signal::new(false)))] 12 | disabled: ReadOnlySignal, 13 | 14 | #[props(default)] 15 | required: ReadOnlySignal, 16 | 17 | #[props(default)] 18 | name: ReadOnlySignal, 19 | 20 | #[props(default = ReadOnlySignal::new(Signal::new(String::from("on"))))] 21 | value: ReadOnlySignal, 22 | 23 | #[props(default)] 24 | on_checked_change: Callback, 25 | 26 | #[props(extends = GlobalAttributes)] 27 | attributes: Vec, 28 | 29 | children: Element, 30 | } 31 | 32 | #[component] 33 | pub fn Switch(props: SwitchProps) -> Element { 34 | let (checked, set_checked) = use_controlled( 35 | props.checked, 36 | props.default_checked, 37 | props.on_checked_change, 38 | ); 39 | 40 | rsx! { 41 | button { 42 | r#type: "button", 43 | role: "switch", 44 | value: props.value, 45 | aria_checked: checked, 46 | aria_required: props.required, 47 | disabled: props.disabled, 48 | "data-state": if checked() { "checked" } else { "unchecked" }, 49 | // Only add data-disabled when actually disabled 50 | "data-disabled": if (props.disabled)() { "true" } else { "false" }, 51 | 52 | onclick: move |_| { 53 | let new_checked = !checked(); 54 | set_checked.call(new_checked); 55 | }, 56 | 57 | // Switches should only toggle on Space, not Enter 58 | onkeydown: move |e| { 59 | if e.key() == Key::Enter { 60 | e.prevent_default(); 61 | } 62 | }, 63 | 64 | ..props.attributes, 65 | {props.children} 66 | } 67 | 68 | // Hidden input for form submission 69 | input { 70 | r#type: "checkbox", 71 | "aria-hidden": true, 72 | tabindex: -1, 73 | name: props.name, 74 | value: props.value, 75 | checked, 76 | disabled: props.disabled, 77 | style: "transform: translateX(-100%); position: absolute; pointer-events: none; opacity: 0; margin: 0; width: 0; height: 0;", 78 | } 79 | } 80 | } 81 | 82 | #[derive(Props, Clone, PartialEq)] 83 | pub struct SwitchThumbProps { 84 | #[props(extends = GlobalAttributes)] 85 | attributes: Vec, 86 | } 87 | 88 | #[component] 89 | pub fn SwitchThumb(props: SwitchThumbProps) -> Element { 90 | rsx! { 91 | span { ..props.attributes } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /primitives/src/toggle.rs: -------------------------------------------------------------------------------- 1 | use crate::use_controlled; 2 | use dioxus_lib::prelude::*; 3 | 4 | #[derive(Props, Clone, PartialEq)] 5 | pub struct ToggleProps { 6 | pressed: Option>, 7 | 8 | #[props(default)] 9 | default_pressed: bool, 10 | 11 | #[props(default)] 12 | disabled: ReadOnlySignal, 13 | 14 | #[props(default)] 15 | on_pressed_change: Callback, 16 | 17 | #[props(extends = GlobalAttributes)] 18 | attributes: Vec, 19 | 20 | // https://github.com/DioxusLabs/dioxus/issues/2467 21 | #[props(default)] 22 | onmounted: Callback>, 23 | #[props(default)] 24 | onfocus: Callback>, 25 | #[props(default)] 26 | onkeydown: Callback>, 27 | 28 | children: Element, 29 | } 30 | 31 | #[component] 32 | pub fn Toggle(props: ToggleProps) -> Element { 33 | let (pressed, set_pressed) = use_controlled( 34 | props.pressed, 35 | props.default_pressed, 36 | props.on_pressed_change, 37 | ); 38 | 39 | rsx! { 40 | button { 41 | onmounted: props.onmounted, 42 | onfocus: props.onfocus, 43 | onkeydown: props.onkeydown, 44 | 45 | type: "button", 46 | disabled: props.disabled, 47 | aria_pressed: pressed, 48 | "data-state": if pressed() { "on" } else { "off" }, 49 | "data-disabled": props.disabled, 50 | 51 | onclick: move |_| { 52 | let new_pressed = !pressed(); 53 | set_pressed.call(new_pressed); 54 | }, 55 | 56 | ..props.attributes, 57 | {props.children} 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /primitives/src/toolbar.rs: -------------------------------------------------------------------------------- 1 | use dioxus_lib::prelude::*; 2 | use std::rc::Rc; 3 | 4 | #[derive(Clone, Copy)] 5 | struct ToolbarCtx { 6 | // State 7 | disabled: ReadOnlySignal, 8 | 9 | // Focus management 10 | focused_index: Signal>, 11 | 12 | // Orientation 13 | horizontal: ReadOnlySignal, 14 | } 15 | 16 | impl ToolbarCtx { 17 | fn set_focus(&mut self, index: Option) { 18 | self.focused_index.set(index); 19 | } 20 | 21 | fn is_focused(&self, index: usize) -> bool { 22 | (self.focused_index)() == Some(index) 23 | } 24 | 25 | fn orientation(&self) -> &'static str { 26 | if (self.horizontal)() { 27 | "horizontal" 28 | } else { 29 | "vertical" 30 | } 31 | } 32 | } 33 | 34 | #[derive(Props, Clone, PartialEq)] 35 | pub struct ToolbarProps { 36 | /// Whether the toolbar is disabled 37 | #[props(default)] 38 | disabled: ReadOnlySignal, 39 | 40 | /// Whether the toolbar is horizontal (true) or vertical (false) 41 | #[props(default = ReadOnlySignal::new(Signal::new(true)))] 42 | horizontal: ReadOnlySignal, 43 | 44 | /// ARIA label for the toolbar 45 | #[props(default)] 46 | aria_label: Option, 47 | 48 | #[props(extends = GlobalAttributes)] 49 | attributes: Vec, 50 | 51 | children: Element, 52 | } 53 | 54 | #[component] 55 | pub fn Toolbar(props: ToolbarProps) -> Element { 56 | let mut ctx = use_context_provider(|| ToolbarCtx { 57 | disabled: props.disabled, 58 | focused_index: Signal::new(None), 59 | horizontal: props.horizontal, 60 | }); 61 | 62 | rsx! { 63 | div { 64 | role: "toolbar", 65 | "data-orientation": ctx.orientation(), 66 | "data-disabled": (props.disabled)(), 67 | aria_label: props.aria_label, 68 | 69 | onfocusout: move |_| ctx.set_focus(None), 70 | ..props.attributes, 71 | 72 | {props.children} 73 | } 74 | } 75 | } 76 | 77 | #[derive(Props, Clone, PartialEq)] 78 | pub struct ToolbarButtonProps { 79 | /// Index of the button in the toolbar 80 | index: ReadOnlySignal, 81 | 82 | /// Whether the button is disabled 83 | #[props(default)] 84 | disabled: ReadOnlySignal, 85 | 86 | /// Callback when the button is clicked 87 | #[props(default)] 88 | on_click: Callback<()>, 89 | 90 | #[props(extends = GlobalAttributes)] 91 | attributes: Vec, 92 | 93 | children: Element, 94 | } 95 | 96 | #[component] 97 | pub fn ToolbarButton(props: ToolbarButtonProps) -> Element { 98 | let mut ctx: ToolbarCtx = use_context(); 99 | 100 | // Handle button ref for focus management 101 | let mut button_ref: Signal>> = use_signal(|| None); 102 | 103 | // Check if this button is focused 104 | let is_focused = use_memo(move || ctx.is_focused((props.index)())); 105 | 106 | // Set focus when needed 107 | use_effect(move || { 108 | if is_focused() { 109 | if let Some(md) = button_ref() { 110 | spawn(async move { 111 | let _ = md.set_focus(true).await; 112 | }); 113 | } 114 | } 115 | }); 116 | 117 | rsx! { 118 | button { 119 | r#type: "button", 120 | tabindex: "0", 121 | disabled: (ctx.disabled)() || (props.disabled)(), 122 | "data-disabled": (ctx.disabled)() || (props.disabled)(), 123 | 124 | onmounted: move |data: Event| button_ref.set(Some(data.data())), 125 | onfocus: move |_| ctx.set_focus(Some((props.index)())), 126 | 127 | onclick: move |_| { 128 | if !(ctx.disabled)() && !(props.disabled)() { 129 | props.on_click.call(()); 130 | } 131 | }, 132 | 133 | onkeydown: move |event: Event| { 134 | let key = event.key(); 135 | let horizontal = (ctx.horizontal)(); 136 | let mut prevent_default = true; 137 | match key { 138 | Key::ArrowUp if !horizontal => { 139 | let index = (props.index)(); 140 | if index > 0 { 141 | ctx.set_focus(Some(index - 1)); 142 | } 143 | } 144 | Key::ArrowDown if !horizontal => { 145 | let index = (props.index)(); 146 | ctx.set_focus(Some(index + 1)); 147 | } 148 | Key::ArrowLeft if horizontal => { 149 | let index = (props.index)(); 150 | if index > 0 { 151 | ctx.set_focus(Some(index - 1)); 152 | } 153 | } 154 | Key::ArrowRight if horizontal => { 155 | let index = (props.index)(); 156 | ctx.set_focus(Some(index + 1)); 157 | } 158 | Key::Home => { 159 | ctx.set_focus(Some(0)); 160 | } 161 | Key::End => { 162 | ctx.set_focus(Some(100)); 163 | } 164 | _ => prevent_default = false, 165 | }; 166 | if prevent_default { 167 | event.prevent_default(); 168 | } 169 | }, 170 | 171 | ..props.attributes, 172 | {props.children} 173 | } 174 | } 175 | } 176 | 177 | #[derive(Props, Clone, PartialEq)] 178 | pub struct ToolbarSeparatorProps { 179 | /// Whether the separator is horizontal (true) or vertical (false) 180 | #[props(default)] 181 | horizontal: Option, 182 | 183 | /// If the separator is decorative and should not be classified 184 | /// as a separator to the ARIA standard. 185 | #[props(default = false)] 186 | decorative: bool, 187 | 188 | #[props(extends = GlobalAttributes)] 189 | attributes: Vec, 190 | } 191 | 192 | #[component] 193 | pub fn ToolbarSeparator(props: ToolbarSeparatorProps) -> Element { 194 | let ctx: ToolbarCtx = use_context(); 195 | 196 | // If horizontal is explicitly set, use that, otherwise invert the toolbar orientation 197 | let horizontal = props.horizontal.unwrap_or(!(ctx.horizontal)()); 198 | 199 | let orientation = match horizontal { 200 | true => "horizontal", 201 | false => "vertical", 202 | }; 203 | 204 | rsx! { 205 | div { 206 | role: if !props.decorative { "separator" } else { "none" }, 207 | aria_orientation: if !props.decorative { orientation }, 208 | "data-orientation": orientation, 209 | ..props.attributes, 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /primitives/src/tooltip.rs: -------------------------------------------------------------------------------- 1 | use crate::{use_controlled, use_unique_id}; 2 | use dioxus_lib::prelude::*; 3 | 4 | #[derive(Clone, Copy)] 5 | struct TooltipCtx { 6 | // State 7 | open: Memo, 8 | set_open: Callback, 9 | disabled: ReadOnlySignal, 10 | 11 | // ARIA attributes 12 | tooltip_id: Signal, 13 | } 14 | 15 | #[derive(Props, Clone, PartialEq)] 16 | pub struct TooltipProps { 17 | /// Whether the tooltip is open 18 | open: Option>, 19 | 20 | /// Default open state 21 | #[props(default)] 22 | default_open: bool, 23 | 24 | /// Callback when open state changes 25 | #[props(default)] 26 | on_open_change: Callback, 27 | 28 | /// Whether the tooltip is disabled 29 | #[props(default)] 30 | disabled: ReadOnlySignal, 31 | 32 | #[props(extends = GlobalAttributes)] 33 | attributes: Vec, 34 | 35 | children: Element, 36 | } 37 | 38 | #[component] 39 | pub fn Tooltip(props: TooltipProps) -> Element { 40 | let (open, set_open) = use_controlled(props.open, props.default_open, props.on_open_change); 41 | let tooltip_id = use_unique_id(); 42 | 43 | let _ctx = use_context_provider(|| TooltipCtx { 44 | open, 45 | set_open, 46 | disabled: props.disabled, 47 | tooltip_id, 48 | }); 49 | 50 | rsx! { 51 | div { 52 | "data-state": if open() { "open" } else { "closed" }, 53 | "data-disabled": (props.disabled)(), 54 | ..props.attributes, 55 | {props.children} 56 | } 57 | } 58 | } 59 | 60 | #[derive(Props, Clone, PartialEq)] 61 | pub struct TooltipTriggerProps { 62 | /// Optional ID for the trigger element 63 | #[props(default)] 64 | id: Option, 65 | 66 | /// Whether to use ARIA attributes 67 | #[props(default = true)] 68 | use_aria: bool, 69 | 70 | #[props(extends = GlobalAttributes)] 71 | attributes: Vec, 72 | 73 | children: Element, 74 | } 75 | 76 | #[component] 77 | pub fn TooltipTrigger(props: TooltipTriggerProps) -> Element { 78 | let ctx: TooltipCtx = use_context(); 79 | 80 | // Handle mouse events 81 | let handle_mouse_enter = move |_: Event| { 82 | if !(ctx.disabled)() { 83 | ctx.set_open.call(true); 84 | } 85 | }; 86 | 87 | let handle_mouse_leave = move |_: Event| { 88 | if !(ctx.disabled)() { 89 | ctx.set_open.call(false); 90 | } 91 | }; 92 | 93 | // Handle focus events 94 | let handle_focus = move |_: Event| { 95 | if !(ctx.disabled)() { 96 | ctx.set_open.call(true); 97 | } 98 | }; 99 | 100 | let handle_blur = move |_: Event| { 101 | if !(ctx.disabled)() { 102 | ctx.set_open.call(false); 103 | } 104 | }; 105 | 106 | // Handle keyboard events 107 | let handle_keydown = move |event: Event| { 108 | if event.key() == Key::Escape && (ctx.open)() { 109 | event.prevent_default(); 110 | ctx.set_open.call(false); 111 | } 112 | }; 113 | 114 | rsx! { 115 | div { 116 | id: props.id.clone(), 117 | // Mouse events 118 | onmouseenter: handle_mouse_enter, 119 | onmouseleave: handle_mouse_leave, 120 | // Focus events 121 | onfocus: handle_focus, 122 | onblur: handle_blur, 123 | // Keyboard events 124 | onkeydown: handle_keydown, 125 | // ARIA attributes 126 | aria_describedby: if props.use_aria { ctx.tooltip_id.peek().clone() } else { String::new() }, 127 | ..props.attributes, 128 | {props.children} 129 | } 130 | } 131 | } 132 | 133 | #[derive(Props, Clone, PartialEq)] 134 | pub struct TooltipContentProps { 135 | /// Optional ID for the tooltip content 136 | #[props(default)] 137 | id: Option, 138 | 139 | /// Side of the trigger to place the tooltip 140 | #[props(default = TooltipSide::Top)] 141 | side: TooltipSide, 142 | 143 | /// Alignment of the tooltip relative to the trigger 144 | #[props(default = TooltipAlign::Center)] 145 | align: TooltipAlign, 146 | 147 | #[props(extends = GlobalAttributes)] 148 | attributes: Vec, 149 | 150 | children: Element, 151 | } 152 | 153 | #[derive(Debug, Clone, Copy, PartialEq)] 154 | pub enum TooltipSide { 155 | Top, 156 | Right, 157 | Bottom, 158 | Left, 159 | } 160 | 161 | impl TooltipSide { 162 | fn as_str(self) -> &'static str { 163 | match self { 164 | TooltipSide::Top => "top", 165 | TooltipSide::Right => "right", 166 | TooltipSide::Bottom => "bottom", 167 | TooltipSide::Left => "left", 168 | } 169 | } 170 | } 171 | 172 | #[derive(Debug, Clone, Copy, PartialEq)] 173 | pub enum TooltipAlign { 174 | Start, 175 | Center, 176 | End, 177 | } 178 | 179 | impl TooltipAlign { 180 | fn as_str(self) -> &'static str { 181 | match self { 182 | TooltipAlign::Start => "start", 183 | TooltipAlign::Center => "center", 184 | TooltipAlign::End => "end", 185 | } 186 | } 187 | } 188 | 189 | #[component] 190 | pub fn TooltipContent(props: TooltipContentProps) -> Element { 191 | let ctx: TooltipCtx = use_context(); 192 | 193 | // Only render if the tooltip is open 194 | let is_open = (ctx.open)(); 195 | if !is_open { 196 | return rsx!({}); 197 | } 198 | 199 | // Create the tooltip content 200 | rsx! { 201 | div { 202 | id: props.id.clone().unwrap_or_else(|| ctx.tooltip_id.peek().clone()), 203 | role: "tooltip", 204 | "data-state": if is_open { "open" } else { "closed" }, 205 | "data-side": props.side.as_str(), 206 | "data-align": props.align.as_str(), 207 | ..props.attributes, 208 | {props.children} 209 | } 210 | } 211 | } 212 | --------------------------------------------------------------------------------