Skip to main content

ui_storybook/components/recorder/
on_screen_options.rs

1//! `OnScreenOptionsPopover` — tray-popover section for desktop
2//! cleanup / keypress overlay / sensitive-info blur (M-UI.10 /
3//! AUT-130). Composes UI-03 `PopoverSurface` + UI-04 `ToggleSwitch`.
4
5use leptos::prelude::*;
6
7use crate::components::menus::{MenuFooter, PopoverPlacement, PopoverSurface};
8use crate::components::primitives::ToggleSwitch;
9
10/// Which built-in on-screen option this row represents. Used as the
11/// id in `OnScreenOptionView` so callers can match on a stable enum
12/// instead of a string id.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub enum OnScreenOptionKind {
15    /// Hide desktop icons + dock during recording.
16    CleanDesktop,
17    /// Render keypress badges over the recording.
18    ShowKeys,
19    /// Auto-blur regions tagged as sensitive (e.g. password fields).
20    BlurSensitiveInfo,
21}
22
23impl OnScreenOptionKind {
24    /// Stable kebab-case slug.
25    #[must_use]
26    pub fn slug(self) -> &'static str {
27        match self {
28            OnScreenOptionKind::CleanDesktop => "clean-desktop",
29            OnScreenOptionKind::ShowKeys => "show-keys",
30            OnScreenOptionKind::BlurSensitiveInfo => "blur-sensitive-info",
31        }
32    }
33}
34
35/// View-model for one on-screen-option row.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct OnScreenOptionView {
38    /// Which option this row drives.
39    pub id: OnScreenOptionKind,
40    /// Display title.
41    pub title: String,
42    /// Multi-line description shown beneath the title.
43    pub description: String,
44    /// `true` when the toggle is on.
45    pub enabled: bool,
46    /// `true` to render the row dimmed and non-interactive (e.g.
47    /// auto-blur pending feature work).
48    pub disabled: bool,
49}
50
51#[component]
52pub fn OnScreenOptionsPopover(
53    /// Options in display order. Order is stable — the parent decides
54    /// which option appears first.
55    options: Vec<OnScreenOptionView>,
56    /// Footer label ("Applies to this recording" / "Applies to all
57    /// recordings"). Defaults to the most common case.
58    #[prop(optional, default = "Applies to this recording")]
59    applies_label: &'static str,
60) -> impl IntoView {
61    let rows: Vec<_> = options.into_iter().map(render_row).collect();
62    view! {
63        <PopoverSurface
64            placement=PopoverPlacement::BottomLeft
65            width_px=380_u16
66            title="On-screen".to_string()
67            description="Choose what shows during recording.".to_string()
68            footer=ToChildren::to_children(move || view! {
69                <MenuFooter>
70                    <span style="font-size:11px;color:var(--text-tertiary)">{applies_label}</span>
71                    <button class="btn btn-default btn-sm">"Done"</button>
72                </MenuFooter>
73            })
74        >
75            <ul class="on-screen-options" role="group" aria-label="On-screen options">
76                {rows}
77            </ul>
78        </PopoverSurface>
79    }
80}
81
82fn render_row(opt: OnScreenOptionView) -> impl IntoView {
83    let mut class = String::from("on-screen-option-row");
84    if opt.disabled {
85        class.push_str(" on-screen-option-row-disabled");
86    }
87    let toggle_label = format!("Enable {}", opt.title.to_ascii_lowercase());
88    view! {
89        <li class=class data-option=opt.id.slug()>
90            <span class="on-screen-option-toggle">
91                <ToggleSwitch checked=opt.enabled disabled=opt.disabled label=toggle_label />
92            </span>
93            <span class="on-screen-option-text">
94                <span class="on-screen-option-title">{opt.title}</span>
95                <span class="on-screen-option-description">{opt.description}</span>
96            </span>
97        </li>
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn each_kind_has_unique_slug() {
107        let slugs = [
108            OnScreenOptionKind::CleanDesktop.slug(),
109            OnScreenOptionKind::ShowKeys.slug(),
110            OnScreenOptionKind::BlurSensitiveInfo.slug(),
111        ];
112        let mut sorted = slugs.to_vec();
113        sorted.sort_unstable();
114        sorted.dedup();
115        assert_eq!(sorted.len(), slugs.len());
116    }
117
118    #[test]
119    fn slugs_are_kebab_case() {
120        for k in [
121            OnScreenOptionKind::CleanDesktop,
122            OnScreenOptionKind::ShowKeys,
123            OnScreenOptionKind::BlurSensitiveInfo,
124        ] {
125            let s = k.slug();
126            assert!(!s.is_empty());
127            assert!(s.chars().all(|c| c.is_ascii_lowercase() || c == '-'));
128        }
129    }
130}