ui_storybook/components/editor/
inspector_panel.rs1use leptos::prelude::*;
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub enum InspectorTab {
13 #[default]
15 Style,
16 Cursor,
18 Audio,
20 Captions,
22 Ai,
24}
25
26impl InspectorTab {
27 #[must_use]
29 pub fn label(self) -> &'static str {
30 match self {
31 InspectorTab::Style => "Style",
32 InspectorTab::Cursor => "Cursor",
33 InspectorTab::Audio => "Audio",
34 InspectorTab::Captions => "Captions",
35 InspectorTab::Ai => "AI",
36 }
37 }
38
39 #[must_use]
41 pub fn slug(self) -> &'static str {
42 match self {
43 InspectorTab::Style => "style",
44 InspectorTab::Cursor => "cursor",
45 InspectorTab::Audio => "audio",
46 InspectorTab::Captions => "captions",
47 InspectorTab::Ai => "ai",
48 }
49 }
50}
51
52#[derive(Clone, Debug, PartialEq, Eq)]
54pub struct PropertyRowView {
55 pub label: &'static str,
57 pub value: Option<&'static str>,
59 pub disabled: bool,
61 pub control: PropertyControlView,
64}
65
66#[derive(Clone, Debug, PartialEq, Eq)]
68pub enum PropertyControlView {
69 ValueOnly,
71 SliderPercent {
73 percent: u8,
75 },
76 Toggle {
78 on: bool,
80 },
81 ColorSwatches {
83 swatches: Vec<&'static str>,
85 selected: usize,
87 },
88 SelectPill {
90 current_label: &'static str,
92 },
93}
94
95#[derive(Clone, Debug, PartialEq, Eq)]
97pub struct PropertySectionView {
98 pub title: &'static str,
100 pub rows: Vec<PropertyRowView>,
102}
103
104#[derive(Clone, Debug, PartialEq, Eq)]
106pub struct InspectorPanelView {
107 pub tabs: Vec<InspectorTab>,
109 pub active: InspectorTab,
111 pub sections: Vec<PropertySectionView>,
113}
114
115#[component]
116pub fn InspectorTabs(
117 tabs: Vec<InspectorTab>,
119 active: InspectorTab,
121) -> impl IntoView {
122 let chips: Vec<_> = tabs
123 .into_iter()
124 .map(|t| {
125 let mut class = String::from("inspector-tab");
126 if t == active {
127 class.push_str(" inspector-tab-active");
128 }
129 let slug = t.slug();
130 let label = t.label();
131 let pressed = t == active;
132 view! {
133 <button class=class data-tab=slug aria-pressed=pressed>{label}</button>
134 }
135 })
136 .collect();
137 view! {
138 <div class="inspector-tabs" role="tablist" aria-label="Inspector tabs">{chips}</div>
139 }
140}
141
142#[component]
143pub fn PropertySection(section: PropertySectionView) -> impl IntoView {
144 let rows: Vec<_> = section.rows.into_iter().map(render_row).collect();
145 view! {
146 <section class="property-section">
147 <span class="property-section-heading">{section.title}</span>
148 <ul class="property-section-rows" role="list">{rows}</ul>
149 </section>
150 }
151}
152
153#[component]
154pub fn InspectorPanel(view: InspectorPanelView) -> impl IntoView {
155 let sections: Vec<_> = view
156 .sections
157 .into_iter()
158 .map(|s| view! { <PropertySection section=s /> })
159 .collect();
160 view! {
161 <aside class="inspector-panel" data-tab=view.active.slug() aria-label="Inspector">
162 <InspectorTabs tabs=view.tabs active=view.active />
163 <div class="inspector-body">{sections}</div>
164 </aside>
165 }
166}
167
168fn render_row(row: PropertyRowView) -> impl IntoView {
169 let mut class = String::from("property-row");
170 if row.disabled {
171 class.push_str(" property-row-disabled");
172 }
173 let control = render_control(row.control);
174 view! {
175 <li class=class>
176 <span class="property-row-label">{row.label}</span>
177 <span class="property-row-control">{control}</span>
178 {row.value.map(|v| view! { <span class="property-row-value">{v}</span> })}
179 </li>
180 }
181}
182
183fn render_control(control: PropertyControlView) -> AnyView {
184 match control {
185 PropertyControlView::ValueOnly => view! { <span class="property-control-value"></span> }.into_any(),
186 PropertyControlView::SliderPercent { percent } => {
187 let p = percent.min(100);
188 let style = format!("width: {p}%;");
189 view! {
190 <span class="property-slider" role="slider" aria-valuemin="0" aria-valuemax="100" aria-valuenow=p>
191 <span class="property-slider-fill" style=style></span>
192 </span>
193 }
194 .into_any()
195 }
196 PropertyControlView::Toggle { on } => view! {
197 <span class=if on { "property-toggle property-toggle-on" } else { "property-toggle" } aria-pressed=on>
198 <span class="property-toggle-knob"></span>
199 </span>
200 }
201 .into_any(),
202 PropertyControlView::ColorSwatches { swatches, selected } => {
203 let chips: Vec<_> = swatches
204 .iter()
205 .enumerate()
206 .map(|(i, color)| {
207 let style = format!("background: {color};");
208 let is_sel = i == selected;
209 let class = if is_sel {
210 "property-swatch property-swatch-selected"
211 } else {
212 "property-swatch"
213 };
214 view! {
215 <button class=class style=style aria-pressed=is_sel></button>
216 }
217 })
218 .collect();
219 view! { <span class="property-swatches">{chips}</span> }.into_any()
220 }
221 PropertyControlView::SelectPill { current_label } => view! {
222 <button class="property-pill">{current_label} " ▾"</button>
223 }
224 .into_any(),
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn tab_slugs_unique_kebab() {
234 let slugs = [
235 InspectorTab::Style.slug(),
236 InspectorTab::Cursor.slug(),
237 InspectorTab::Audio.slug(),
238 InspectorTab::Captions.slug(),
239 InspectorTab::Ai.slug(),
240 ];
241 let mut sorted = slugs.to_vec();
242 sorted.sort_unstable();
243 sorted.dedup();
244 assert_eq!(sorted.len(), slugs.len());
245 }
246}