1use blockworx_geom::{Rect, pos2};
15use dioxus::prelude::*;
16use dioxus::web::WebEventExt as _;
17
18use crate::control::{Face, Pressable};
19use crate::diagrams::Diagrams;
20use crate::icons;
21use crate::mode::Prefers;
22use crate::prefs::Preferences;
23use crate::settings::Settings;
24use crate::sheet::{History, MUTED, Parts};
25use crate::shell::{Band, Shell, measured};
26
27#[derive(Clone, Copy, PartialEq, Eq, Debug)]
29pub enum Section {
30 Diagrams,
31 Parts,
32 History,
33 Settings,
34}
35
36impl Section {
37 const TOP: [Section; 3] = [Section::Diagrams, Section::Parts, Section::History];
39
40 #[must_use]
41 pub fn label(self) -> &'static str {
42 match self {
43 Section::Diagrams => "Diagrams",
44 Section::Parts => "Parts",
45 Section::History => "History",
46 Section::Settings => "Settings",
47 }
48 }
49
50 #[must_use]
52 pub fn name(self) -> &'static str {
53 match self {
54 Section::Diagrams => "diagrams",
55 Section::Parts => "parts",
56 Section::History => "history",
57 Section::Settings => "settings",
58 }
59 }
60
61 fn icon(self) -> icons::Icon {
62 match self {
63 Section::Diagrams => icons::DIAGRAMS,
64 Section::Parts => icons::PARTS,
65 Section::History => icons::HISTORY,
66 Section::Settings => icons::GEAR,
67 }
68 }
69}
70
71const CELL: &str = "grid size-target place-items-center rounded-xl text-zinc-600 transition \
73 duration-150 ease-out hover:bg-zinc-950/6 active:scale-90 dark:text-zinc-300 \
74 dark:hover:bg-white/10 [&_svg]:size-5 [&_svg]:stroke-[1.8]";
75const OPEN: &str = "grid size-target place-items-center rounded-xl bg-sky-500/12 text-sky-600 \
76 transition duration-150 ease-out active:scale-90 dark:bg-sky-400/15 dark:text-sky-400 \
77 [&_svg]:size-5 [&_svg]:stroke-[1.8]";
78
79const PANEL: &str = "absolute bottom-3 left-2 top-2 z-30 flex w-[344px] max-w-[calc(100%-1rem)] \
82 flex-col rounded-band bg-white/85 text-zinc-900 shadow-lg ring-1 ring-zinc-950/5 \
83 backdrop-blur-xl transition duration-200 dark:bg-zinc-900/85 dark:text-zinc-100 \
84 dark:ring-white/10";
85
86#[component]
88pub fn ActivityBar(shell: Shell) -> Element {
89 let open = shell.section();
90 rsx! {
91 nav {
92 class: "flex w-15 flex-none flex-col items-center gap-1 border-r border-zinc-950/5 \
93 bg-white pb-3 pt-2.5 dark:border-white/10 dark:bg-zinc-900",
94 "aria-label": "Sidebar",
95 for section in Section::TOP {
96 Activity { key: "{section:?}", section, open }
97 }
98 div { class: "flex-1" }
99 Activity { section: Section::Settings, open }
100 }
101 }
102}
103
104#[component]
105fn Activity(section: Section, open: Signal<Option<Section>>) -> Element {
106 let showing = open() == Some(section);
107 let mut open = open;
108 rsx! {
109 Pressable {
110 class: if showing { OPEN } else { CELL },
111 title: "{section.label()}",
112 "aria-label": "{section.label()}",
113 "aria-pressed": if showing { "true" } else { "false" },
114 "data-section": "{section.name()}",
115 onpress: move |()| open.set(if showing { None } else { Some(section) }),
116 Face { icon: section.icon() }
117 }
118 }
119}
120
121#[component]
127pub fn SidebarPanel(shell: Shell, prefs: Signal<Preferences>, prefers: Signal<Prefers>) -> Element {
128 let section = shell.section();
129 let mut shown = use_signal(|| Section::Diagrams);
130 let mut was = use_signal(|| None::<Section>);
131 let mut opens = use_signal(|| 0_u64);
132 let mut element = use_signal(|| None::<web_sys::Element>);
133 let measuring = use_hook(|| CopyValue::new(shell.clone()));
134 use_effect(move || {
135 let now = section();
136 let before = *was.peek();
137 if now == before {
138 return;
139 }
140 was.set(now);
141 if let Some(now) = now {
142 shown.set(now);
143 if before.is_none() {
144 let fresh = *opens.peek() + 1;
145 opens.set(fresh);
146 }
147 }
148 if let Some(panel) = element.peek().as_ref() {
149 covers(&measuring.read(), panel, now);
150 }
151 });
152
153 let open = section();
154 let drawn = open.unwrap_or(*shown.read());
155 let state = if open.is_some() {
156 "ease-out"
157 } else {
158 "pointer-events-none -translate-x-2 opacity-0 ease-in"
159 };
160 let tally = match drawn {
161 Section::Diagrams => Some(format!("{} in this browser", shell.held().read().len())),
162 Section::Parts | Section::History | Section::Settings => None,
163 };
164 rsx! {
165 aside {
166 class: "{PANEL} {state}",
167 "aria-hidden": if open.is_some() { "false" } else { "true" },
168 "aria-label": "{drawn.label()}",
169 "data-open": if open.is_some() { "true" } else { "false" },
170 "data-section": "{drawn.name()}",
171 onmounted: move |event| {
172 if let Some(panel) = event.try_as_web_event() {
173 covers(&measuring.read(), &panel, *section.peek());
174 element.set(Some(panel));
175 }
176 },
177 onresize: move |event| {
178 if let Some(entry) = event.try_as_web_event() {
179 covers(&measuring.read(), &entry.target(), *section.peek());
180 }
181 },
182 div { class: "flex items-baseline justify-between px-4 pb-2 pt-3.5",
183 h2 { class: "text-[15px] font-semibold", "{drawn.label()}" }
184 if let Some(tally) = tally {
185 span { class: MUTED, "{tally}" }
186 }
187 }
188 div {
189 key: "{opens}-{drawn:?}",
190 class: "min-h-0 flex-1 overflow-y-auto px-2 pb-3",
191 match drawn {
192 Section::Diagrams => rsx! {
193 Diagrams { shell: shell.clone() }
194 },
195 Section::Parts => rsx! {
196 Parts { shell: shell.clone() }
197 },
198 Section::History => rsx! {
199 History { shell: shell.clone() }
200 },
201 Section::Settings => rsx! {
202 div { class: "px-2 pt-1",
203 Settings { shell: shell.clone(), prefs, prefers }
204 }
205 },
206 }
207 }
208 }
209 }
210}
211
212fn covers(shell: &Shell, panel: &web_sys::Element, open: Option<Section>) {
217 let at = match open {
218 Some(_) => measured(panel).map_or(Rect::ZERO, |at| {
219 Rect::from_min_max(pos2(-f32::MAX, at.min.y), at.max)
220 }),
221 None => Rect::ZERO,
222 };
223 shell.covers(Band::Sheet, at);
224}