1use blockworx_canvas2d::css_color;
10use blockworx_paint::palette::Base;
11use blockworx_paint::{FontChoice, Luminance, Mode, Scheme};
12use dioxus::prelude::*;
13
14use crate::control::{Face, Pressable};
15use crate::icons;
16use crate::mode::Prefers;
17use crate::prefs::Preferences;
18use crate::shell::Shell;
19
20const HEADING: &str = "text-xs font-medium uppercase tracking-wide text-zinc-500 \
21 dark:text-zinc-400";
22
23const SEGMENT: &str = "flex h-[34px] flex-1 items-center justify-center gap-1.5 rounded-[9px] \
25 text-[13px] text-zinc-500 transition-colors ease-out hover:text-zinc-800 \
26 dark:text-zinc-400 dark:hover:text-zinc-200 [&_svg]:size-4";
27const TAKEN_SEGMENT: &str = "flex h-[34px] flex-1 items-center justify-center gap-1.5 \
28 rounded-[9px] bg-white text-[13px] font-medium text-zinc-900 shadow-sm \
29 dark:bg-zinc-600 dark:text-zinc-100 dark:shadow-none dark:ring-1 dark:ring-white/10 \
30 [&_svg]:size-4";
31
32const CARD: &str = "rounded-xl border border-zinc-200 bg-white p-1.5 transition-colors ease-out \
34 hover:border-zinc-300 dark:border-zinc-700 dark:bg-zinc-800 dark:hover:border-zinc-600";
35const TAKEN_CARD: &str = "rounded-xl border-2 border-sky-500 bg-white p-[5px] dark:bg-zinc-800";
36
37const TICK: &str = "ml-auto grid place-items-center text-sky-600 dark:text-sky-400 \
39 [&_svg]:size-3.5 [&_svg]:stroke-[2.5]";
40
41#[derive(Clone, Copy, PartialEq, Eq, Debug)]
43enum Holding {
44 Taken,
45 Offered,
46}
47
48impl Holding {
49 fn card(self) -> &'static str {
50 match self {
51 Holding::Taken => TAKEN_CARD,
52 Holding::Offered => CARD,
53 }
54 }
55
56 fn of<T: PartialEq + Copy>(choice: T, taken: T) -> Self {
57 if choice == taken {
58 Holding::Taken
59 } else {
60 Holding::Offered
61 }
62 }
63}
64
65#[component]
66pub fn Settings(shell: Shell, prefs: Signal<Preferences>, prefers: Signal<Prefers>) -> Element {
67 let luminance = prefs.read().luminance(prefers());
74 let held = prefs.read().clone();
75 rsx! {
76 div { class: "flex flex-col gap-5",
77 Section { heading: "Appearance",
78 div { class: "flex gap-0.5 rounded-xl bg-zinc-950/5 p-[3px] dark:bg-white/5",
79 for mode in Mode::ALL {
80 ModeCell { key: "{mode:?}", mode, holding: Holding::of(mode, held.mode), prefs }
81 }
82 }
83 }
84 Section { heading: "Diagram colours",
85 div { class: "grid grid-cols-2 gap-2",
86 for scheme in Scheme::ALL {
87 SchemeCard {
88 key: "{scheme:?}",
89 scheme,
90 luminance,
91 holding: Holding::of(scheme, held.scheme),
92 prefs,
93 }
94 }
95 }
96 }
97 Section { heading: "Lettering",
98 div { class: "grid grid-cols-2 gap-2",
99 for font in FontChoice::ALL {
100 FontCard { key: "{font:?}", font, holding: Holding::of(font, held.font), prefs }
101 }
102 }
103 }
104 Profile { shell, prefs }
105 }
106 }
107}
108
109#[component]
111fn Section(heading: &'static str, children: Element) -> Element {
112 rsx! {
113 div { class: "flex flex-col gap-2",
114 span { class: HEADING, "{heading}" }
115 {children}
116 }
117 }
118}
119
120#[component]
121fn ModeCell(mode: Mode, holding: Holding, prefs: Signal<Preferences>) -> Element {
122 let face = match mode {
123 Mode::Light => icons::SUN,
124 Mode::Dark => icons::MOON,
125 Mode::System => icons::MONITOR,
126 };
127 rsx! {
128 Pressable {
129 class: if holding == Holding::Taken { TAKEN_SEGMENT } else { SEGMENT },
130 "aria-pressed": if holding == Holding::Taken { "true" } else { "false" },
131 "data-pref": "mode-{mode.display_name()}",
132 onpress: move |()| prefs.write().mode = mode,
133 Face { icon: face }
134 span { "{mode.display_name()}" }
135 }
136 }
137}
138
139#[component]
142fn SchemeCard(
143 scheme: Scheme,
144 luminance: Luminance,
145 holding: Holding,
146 prefs: Signal<Preferences>,
147) -> Element {
148 let palette = scheme.palette(luminance);
149 let [ground, ink, red, yellow, green, blue, purple] = [
150 Base::B00,
151 Base::B05,
152 Base::B08,
153 Base::B0A,
154 Base::B0B,
155 Base::B0D,
156 Base::B0E,
157 ]
158 .map(|base| css_color(palette.get(base)));
159 let card = holding.card();
160 rsx! {
161 Pressable {
162 class: "{card} flex flex-col gap-1.5",
163 "aria-pressed": if holding == Holding::Taken { "true" } else { "false" },
164 "data-pref": "scheme-{scheme.display_name()}",
165 onpress: move |()| prefs.write().scheme = scheme,
166 svg {
167 class: "block h-auto w-full",
168 view_box: "0 0 126 46",
169 "aria-hidden": "true",
170 rect { x: "0", y: "0", width: "126", height: "46", rx: "8", fill: "{ground}" }
171 rect { x: "12", y: "11", width: "30", height: "24", rx: "3", fill: "none", stroke: "{ink}", stroke_width: "1.5" }
172 rect { x: "84", y: "11", width: "30", height: "24", rx: "3", fill: "none", stroke: "{ink}", stroke_width: "1.5" }
173 path { d: "M42 18 H63 V28 H84", fill: "none", stroke: "{blue}", stroke_width: "1.5" }
174 circle { cx: "42", cy: "18", r: "2", fill: "{blue}" }
175 circle { cx: "84", cy: "28", r: "2", fill: "{blue}" }
176 rect { x: "16", y: "15", width: "10", height: "3", rx: "1.5", fill: "{red}" }
177 rect { x: "88", y: "15", width: "12", height: "3", rx: "1.5", fill: "{purple}" }
178 circle { cx: "58", cy: "39", r: "2.5", fill: "{yellow}" }
179 circle { cx: "66", cy: "39", r: "2.5", fill: "{green}" }
180 }
181 div { class: "flex h-[18px] items-center px-0.5",
182 Name { holding, "{scheme.display_name()}" }
183 }
184 }
185 }
186}
187
188#[component]
190fn FontCard(font: FontChoice, holding: Holding, prefs: Signal<Preferences>) -> Element {
191 let card = holding.card();
192 rsx! {
193 Pressable {
194 class: "{card} flex items-center gap-2.5 px-3 py-2",
195 "aria-pressed": if holding == Holding::Taken { "true" } else { "false" },
196 "data-pref": "font-{font.display_name()}",
197 onpress: move |()| prefs.write().font = font,
198 span {
199 class: "w-[34px] text-2xl leading-7",
200 style: "font-family:'{font.family_name()}'",
201 "Aa"
202 }
203 Name { holding, "{font.display_name()}" }
204 }
205 }
206}
207
208#[component]
210fn Name(holding: Holding, children: Element) -> Element {
211 rsx! {
212 span {
213 class: if holding == Holding::Taken { "text-[13px] font-semibold" } else { "text-[13px]" },
214 {children}
215 }
216 if holding == Holding::Taken {
217 span { class: TICK,
218 Face { icon: icons::CHECK }
219 }
220 }
221 }
222}
223
224#[component]
228fn Profile(shell: Shell, prefs: Signal<Preferences>) -> Element {
229 let typed = prefs.read().profile.clone().unwrap_or_default();
230 let attributed = shell.attributed();
231 rsx! {
232 Section { heading: "Your name",
233 input {
234 class: "h-[38px] rounded-[10px] bg-zinc-100 px-3 text-sm outline-none \
235 placeholder:text-zinc-500 focus:outline-2 focus:outline-sky-500 \
236 dark:bg-zinc-900 dark:placeholder:text-zinc-400",
237 value: "{typed}",
238 placeholder: "{attributed}",
239 "aria-label": "Your name",
240 oninput: move |event| {
241 let typed = event.value();
242 prefs.write().profile = (!typed.trim().is_empty()).then_some(typed);
243 },
244 }
245 span { class: "text-xs text-zinc-500 dark:text-zinc-400",
246 "Stamped on every change you make from now on."
247 }
248 }
249 }
250}