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}