Skip to main content

ui_storybook/components/primitives/
surface.rs

1//! `Surface` — semantic background container (M-UI.1 / AUT-121).
2//!
3//! Wraps content in a single `<div>` whose class encodes the surface
4//! kind (`Base` / `Elevated` / `Popover` / `Selected` / `Glass`). Every
5//! recorder / library / editor / cursor surface stacks on these
6//! kinds — popovers, panels, selected list rows, glassy overlays — so
7//! the visual layering stays consistent across product surfaces.
8
9use leptos::prelude::*;
10
11/// Semantic surface kind. Drives the background, border, and shadow
12/// tokens applied. Picked once per surface; doesn't change at runtime.
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
14pub enum SurfaceKind {
15    /// App background — the bottom of the stack. Use for the canvas
16    /// underneath everything else.
17    #[default]
18    Base,
19    /// One step elevated — panels, cards, tray surfaces. The default
20    /// background for grouped content.
21    Elevated,
22    /// Popover background — for tray menus, dropdowns, command menus.
23    /// Has a stronger drop shadow than `Elevated`.
24    Popover,
25    /// Selected list-row background — highlighted state inside a menu
26    /// or sidebar list.
27    Selected,
28    /// Translucent overlay — used for full-window overlays and the
29    /// hint surfaces that float over the recording.
30    Glass,
31}
32
33impl SurfaceKind {
34    /// CSS class that controls the surface look.
35    #[must_use]
36    pub fn css(self) -> &'static str {
37        match self {
38            SurfaceKind::Base => "surface-base",
39            SurfaceKind::Elevated => "surface-elevated",
40            SurfaceKind::Popover => "surface-popover",
41            SurfaceKind::Selected => "surface-selected",
42            SurfaceKind::Glass => "surface-glass",
43        }
44    }
45}
46
47#[component]
48pub fn Surface(
49    #[prop(optional)] kind: SurfaceKind,
50    #[prop(optional, into)] extra_class: String,
51    children: Children,
52) -> impl IntoView {
53    let class = format!(
54        "surface {}{}{}",
55        kind.css(),
56        if extra_class.is_empty() { "" } else { " " },
57        extra_class,
58    );
59    view! { <div class=class>{children()}</div> }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn each_kind_has_a_unique_class() {
68        let classes = [
69            SurfaceKind::Base.css(),
70            SurfaceKind::Elevated.css(),
71            SurfaceKind::Popover.css(),
72            SurfaceKind::Selected.css(),
73            SurfaceKind::Glass.css(),
74        ];
75        let mut sorted = classes.to_vec();
76        sorted.sort_unstable();
77        sorted.dedup();
78        assert_eq!(sorted.len(), classes.len(), "duplicate Surface CSS class");
79    }
80
81    #[test]
82    fn each_class_starts_with_surface_prefix() {
83        for k in [
84            SurfaceKind::Base,
85            SurfaceKind::Elevated,
86            SurfaceKind::Popover,
87            SurfaceKind::Selected,
88            SurfaceKind::Glass,
89        ] {
90            assert!(
91                k.css().starts_with("surface-"),
92                "missing prefix: {}",
93                k.css()
94            );
95        }
96    }
97}