Skip to main content

orbital_core_components/material/
material.rs

1use leptos::prelude::*;
2use orbital_base_components::BaseMaterial;
3use orbital_macros::component_doc;
4use orbital_style::inject_style;
5
6use super::styles::{material_modifier_classes, material_styles};
7pub use orbital_base_components::{MaterialCorners, MaterialElevation, MaterialVariant};
8
9/// Orbital surface container with material variant, elevation, and corner treatment.
10///
11/// Surface treatment only (background, shadow, radius, stroke) — no layout or padding. Put spacing and flex layout on inner [`Flex`] or [`Card`] slots.
12///
13/// Pick [`MaterialElevation`] for depth: `Flat` for co-planar shell and bordered panels, `Resting` for cards at rest, `Raised` for emphasized callouts, `Floating` for popovers, and `Modal` for dialogs. Use [`MaterialVariant::Scrim`] as a scrim/backdrop surface (often paired with [`Backdrop`]).
14///
15/// # When to use
16///
17/// - **Solid** for most layout regions, cards, and content canvases - **Outlined** for flat bordered panels — pair with [`MaterialElevation::Flat`] - **Frost** / **Shell** for shell chrome and transient panels - **Scrim** for modal overlays — often paired with [`Backdrop`]
18///
19/// # Usage
20///
21/// 1. Pick a [`MaterialVariant`] for opacity and backdrop treatment. 2. Set [`MaterialElevation`] — `Resting` for inline panels, `Raised` for emphasized cards. 3. Set [`MaterialCorners`] when square edges are required. 4. Put layout and padding on inner [`Flex`] or card compound slots, not on Material.
22///
23/// # Best Practices
24///
25/// ## Do's
26///
27/// * Prefer `Resting` elevation for cards at rest and `Raised` for hero callouts * Compose Material inside [`Card`] rather than duplicating surface CSS * Use Frost + Floating for popovers over busy backgrounds * Wrap preview and test hooks in a native `div` with `data-testid`
28///
29/// ## Don'ts
30///
31/// * Do not add flex layout or default padding on Material — use [`Flex`] or [`Card`] * Do not use inline `style` for one-off sizing — use Turf classes and CSS vars * Do not stack many elevated surfaces at the same tier without visual hierarchy
32///
33/// # Examples
34///
35/// ## Solid at rest
36/// Default opaque surface with resting elevation for inline panels and content regions.
37/// <!-- default -->
38/// <!-- preview -->
39/// ```rust
40/// use crate::{Material, MaterialElevation, MaterialVariant};
41/// view! {
42///     <div data-testid="material-preview">
43///         <Material variant=MaterialVariant::Solid elevation=MaterialElevation::Resting>
44///             <div style="padding: 16px;">"Surface content"</div>
45///         </Material>
46///     </div>
47/// }
48/// ```
49///
50/// ## Solid raised
51/// Raised elevation emphasizes a surface above its neighbors—common for hero cards and callouts.
52/// <!-- preview -->
53/// ```rust
54/// use crate::{Material, MaterialElevation, MaterialVariant};
55/// view! {
56///     <div data-testid="material-raised-preview">
57///         <Material variant=MaterialVariant::Solid elevation=MaterialElevation::Raised>
58///             <div style="padding: 16px;">"Raised surface"</div>
59///         </Material>
60///     </div>
61/// }
62/// ```
63///
64/// ## Variant matrix
65/// Compare Solid, Frost, Shell, and Scrim treatments side by side over a textured background.
66/// <!-- preview -->
67/// ```rust
68/// use crate::{Flex, FlexGap, FlexWrap, Material, MaterialElevation, MaterialVariant};
69/// view! {
70///     <div
71///         data-testid="material-variant-matrix"
72///         style="padding: 16px; background: linear-gradient(135deg, #1A6F94 0%, #6B3FA0 100%);"
73///     >
74///         <Flex gap=FlexGap::Medium wrap=FlexWrap::Wrap>
75///             <Material variant=MaterialVariant::Solid elevation=MaterialElevation::Resting>
76///                 <div style="padding: 12px;">"Solid"</div>
77///             </Material>
78///             <Material variant=MaterialVariant::Frost elevation=MaterialElevation::Resting>
79///                 <div style="padding: 12px;">"Frost"</div>
80///             </Material>
81///             <Material variant=MaterialVariant::Shell elevation=MaterialElevation::Resting>
82///                 <div style="padding: 12px;">"Shell"</div>
83///             </Material>
84///             <Material variant=MaterialVariant::Scrim elevation=MaterialElevation::Resting>
85///                 <div style="padding: 12px; color: white;">"Scrim"</div>
86///             </Material>
87///         </Flex>
88///     </div>
89/// }
90/// ```
91///
92/// ## Elevation matrix
93/// Shadow tiers from Flat through Modal on Solid — each tier maps to a design-token shadow scale.
94/// <!-- preview -->
95/// ```rust
96/// use crate::{Flex, FlexGap, FlexWrap, Material, MaterialElevation, MaterialVariant};
97/// view! {
98///     <div data-testid="material-elevation-matrix">
99///         <Flex gap=FlexGap::Medium wrap=FlexWrap::Wrap>
100///             <Material variant=MaterialVariant::Solid elevation=MaterialElevation::Flat>
101///                 <div style="padding: 12px;">"Flat"</div>
102///             </Material>
103///             <Material variant=MaterialVariant::Solid elevation=MaterialElevation::Resting>
104///                 <div style="padding: 12px;">"Resting"</div>
105///             </Material>
106///             <Material variant=MaterialVariant::Solid elevation=MaterialElevation::Raised>
107///                 <div style="padding: 12px;">"Raised"</div>
108///             </Material>
109///             <Material variant=MaterialVariant::Solid elevation=MaterialElevation::Floating>
110///                 <div style="padding: 12px;">"Floating"</div>
111///             </Material>
112///             <Material variant=MaterialVariant::Solid elevation=MaterialElevation::Modal>
113///                 <div style="padding: 12px;">"Modal"</div>
114///             </Material>
115///         </Flex>
116///     </div>
117/// }
118/// ```
119///
120/// ## Frost floating
121/// Frosted glass with floating elevation for transient panels and popovers over busy content.
122/// <!-- preview -->
123/// ```rust
124/// use crate::{Material, MaterialElevation, MaterialVariant};
125/// view! {
126///     <div
127///         data-testid="material-frost-preview"
128///         style="padding: 24px; background: linear-gradient(135deg, #1A6F94 0%, #6B3FA0 100%);"
129///     >
130///         <Material variant=MaterialVariant::Frost elevation=MaterialElevation::Floating>
131///             <div style="padding: 16px;">"Frosted panel"</div>
132///         </Material>
133///     </div>
134/// }
135/// ```
136///
137/// ## CSS var override
138/// Turf `class` sets `--orbital-material-width` for one-off sizing without a `style` prop.
139/// <!-- preview -->
140/// ```rust
141/// use crate::{Material, MaterialElevation, MaterialVariant};
142/// use turf::inline_style_sheet_values;
143/// view! {
144///     <div data-testid="material-var-override">
145///         {
146///             let (style_sheet, class_names) = inline_style_sheet_values! {
147///                 .NarrowMaterial {
148///                     --orbital-material-width: 240px;
149///                 }
150///             };
151///             view! {
152///                 <style>{style_sheet}</style>
153///                 <Material
154///                     variant=MaterialVariant::Solid
155///                     elevation=MaterialElevation::Resting
156///                     class=class_names.narrow_material
157///                 >
158///                     <div style="padding: 16px;">"Fixed width surface"</div>
159///                 </Material>
160///             }
161///         }
162///     </div>
163/// }
164/// ```
165///
166/// ## Theme: elevation scale
167/// Custom elevation scale in the theme scope increases resting shadow depth on the sample surface.
168/// <!-- preview -->
169/// ```rust
170/// use leptos::prelude::*;
171/// use crate::{Material, MaterialElevation, MaterialVariant};
172/// use orbital_theme::{ElevationScale, OrbitalThemeProvider, Theme, ThemeMode, ThemeOverrides};
173/// view! {
174///     <div data-testid="material-theme-elevation">
175///         <OrbitalThemeProvider theme=RwSignal::new(Theme::custom(
176///             ThemeMode::Light,
177///             ThemeOverrides {
178///                 elevation: Some(ElevationScale { multiplier: 1.75 }),
179///                 ..Default::default()
180///             },
181///         ))>
182///             <Material variant=MaterialVariant::Solid elevation=MaterialElevation::Resting>
183///                 <div style="padding: 16px;">"Scaled elevation"</div>
184///             </Material>
185///         </OrbitalThemeProvider>
186///     </div>
187/// }
188/// ```
189///
190/// ## Composed in Card
191/// Card delegates surface treatment to Material — layout gaps live on inner Flex, not the surface root.
192/// <!-- preview -->
193/// ```rust
194/// use crate::Card;
195/// view! {
196///     <div data-testid="material-in-card" style="max-width: 360px;">
197///         <Card>
198///             <div style="padding: 16px;">"Card body on Material surface"</div>
199///         </Card>
200///     </div>
201/// }
202/// ```
203///
204/// ## Outlined flat
205/// Stroke-bordered surface with no shadow — typical for outlined panels and list items.
206/// <!-- preview -->
207/// ```rust
208/// use crate::{Material, MaterialElevation, MaterialVariant};
209/// view! {
210///     <div data-testid="material-outlined-preview">
211///         <Material variant=MaterialVariant::Outlined elevation=MaterialElevation::Flat>
212///             <div style="padding: 16px;">"Outlined surface"</div>
213///         </Material>
214///     </div>
215/// }
216/// ```
217///
218/// ## Square corners
219/// Square corner treatment removes border radius on the surface root.
220/// <!-- preview -->
221/// ```rust
222/// use crate::{Material, MaterialCorners, MaterialElevation, MaterialVariant};
223/// view! {
224///     <div data-testid="material-square-preview">
225///         <Material
226///             variant=MaterialVariant::Solid
227///             elevation=MaterialElevation::Resting
228///             corners=MaterialCorners::Square
229///         >
230///             <div style="padding: 16px;">"Square surface"</div>
231///         </Material>
232///     </div>
233/// }
234/// ```
235///
236/// ## Outlined vs solid
237/// Side-by-side contrast between filled solid and stroke-outlined surfaces.
238/// <!-- preview -->
239/// ```rust
240/// use crate::{Flex, FlexGap, Material, MaterialElevation, MaterialVariant};
241/// view! {
242///     <div data-testid="material-appearance-matrix">
243///         <Flex gap=FlexGap::Medium>
244///             <Material variant=MaterialVariant::Solid elevation=MaterialElevation::Resting>
245///                 <div style="padding: 12px;">"Solid"</div>
246///             </Material>
247///             <Material variant=MaterialVariant::Outlined elevation=MaterialElevation::Flat>
248///                 <div style="padding: 12px;">"Outlined"</div>
249///             </Material>
250///         </Flex>
251///     </div>
252/// }
253/// ```
254#[component_doc(
255    category = "Surfaces",
256    preview_slug = "material",
257    preview_label = "Material",
258    preview_icon = icondata::AiFileOutlined,
259)]
260#[component]
261pub fn Material(
262    /// Optional CSS class on the root element.
263    #[prop(optional, into)]
264    class: MaybeProp<String>,
265    /// Orbital material treatment (texture / translucency).
266    #[prop(default = MaterialVariant::Solid)]
267    variant: MaterialVariant,
268    /// Orbital elevation tier (shadow depth).
269    #[prop(default = MaterialElevation::Resting)]
270    elevation: MaterialElevation,
271    /// Corner treatment on the surface root.
272    #[prop(default = MaterialCorners::Rounded)]
273    corners: MaterialCorners,
274    /// Surface content rendered inside the elevated or flat Material shell.
275    children: Children,
276) -> impl IntoView {
277    inject_style("orbital-material", material_styles());
278    let modifiers = material_modifier_classes(variant, elevation, corners);
279    let root_class = Signal::derive(move || {
280        let extra = class.get().unwrap_or_default();
281        if extra.is_empty() {
282            modifiers.clone()
283        } else {
284            format!("{modifiers} {extra}")
285        }
286    });
287
288    view! {
289        <BaseMaterial class=root_class variant=variant elevation=elevation corners=corners>
290            {children()}
291        </BaseMaterial>
292    }
293}