2 Commits

Author SHA1 Message Date
cf9737fc76 Add Modal component 2024-03-30 13:46:53 +01:00
5c91df206c 💄 Store colors in a nested map to make them reachable using criteria 2024-03-30 08:23:52 +01:00
12 changed files with 495 additions and 33 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,3 +1,5 @@
@use "sass:map";
$font-size: 100vh * 0.01; $font-size: 100vh * 0.01;
$icon-size: $font-size * 2; $icon-size: $font-size * 2;
@@ -51,11 +53,124 @@ $color-ternary-70: #8B0046;
$color-ternary-60: #720033; $color-ternary-60: #720033;
$color-ternary-50: #5A0022; $color-ternary-50: #5A0022;
$color-critical: #C91B13; $color-critical-130: #F29088;
$color-warning: #FFA316; $color-critical-120: #E86F62;
$color-success: #4AAB79; $color-critical-110: #DA4B3C;
$color-critical-100: #C91B13;
$color-critical-90: #A9191A;
$color-critical-80: #88181C;
$color-critical-70: #68171B;
$border-default-color: $greyscale-90; $color-success-130: #A6D3B1;
$color-success-120: #8AC59D;
$color-success-110: #6CB88A;
$color-success-100: #4AAB79;
$color-success-90: #418E63;
$color-success-80: #38724E;
$color-success-70: #2E573A;
$color-warning-130: #FFCCC0;
$color-warning-120: #FFBC9E;
$color-warning-110: #FFAC6A;
$color-warning-100: #FFA114;
$color-warning-90: #D18915;
$color-warning-80: #A67016;
$color-warning-70: #7D5816;
$colors: (
"greyscale": (
90: $greyscale-90,
80: $greyscale-80,
70: $greyscale-70,
60: $greyscale-60,
50: $greyscale-50,
40: $greyscale-40,
30: $greyscale-30,
20: $greyscale-20,
10: $greyscale-10,
0: $greyscale-0,
),
"primary": (
150: $color-primary-150,
140: $color-primary-140,
130: $color-primary-130,
120: $color-primary-120,
110: $color-primary-110,
100: $color-primary-100,
90: $color-primary-90,
80: $color-primary-80,
70: $color-primary-70,
60: $color-primary-60,
50: $color-primary-50,
),
"secondary": (
150: $color-secondary-150,
140: $color-secondary-140,
130: $color-secondary-130,
120: $color-secondary-120,
110: $color-secondary-110,
100: $color-secondary-100,
90: $color-secondary-90,
80: $color-secondary-80,
70: $color-secondary-70,
60: $color-secondary-60,
50: $color-secondary-50,
),
"ternary": (
150: $color-ternary-150,
140: $color-ternary-140,
130: $color-ternary-130,
120: $color-ternary-120,
110: $color-ternary-110,
100: $color-ternary-100,
90: $color-ternary-90,
80: $color-ternary-80,
70: $color-ternary-70,
60: $color-ternary-60,
50: $color-ternary-50,
),
"critical": (
130: $color-critical-130,
120: $color-critical-120,
110: $color-critical-110,
100: $color-critical-100,
90: $color-critical-90,
80: $color-critical-80,
70: $color-critical-70,
),
"success": (
130: $color-success-130,
120: $color-success-120,
110: $color-success-110,
100: $color-success-100,
90: $color-success-90,
80: $color-success-80,
70: $color-success-70,
),
"warning": (
130: $color-warning-130,
120: $color-warning-120,
110: $color-warning-110,
100: $color-warning-100,
90: $color-warning-90,
80: $color-warning-80,
70: $color-warning-70,
),
);
@function get-color($color-name, $color-level) {
$color: map.get($colors, $color-name);
@return map.get($color, $color-level);
}
$border-default-color: get-color(greyscale, 90);
$border-big-width: 4px; $border-big-width: 4px;
$border-big: solid $border-big-width $border-default-color; $border-big: solid $border-big-width $border-default-color;
$border-normal-width: 2px; $border-normal-width: 2px;

View File

@@ -7,7 +7,7 @@
border: $border-big; border: $border-big;
border-radius: $border-radius; border-radius: $border-radius;
color: $greyscale-0; color: get-color(greyscale, 0);
font-family: "Geist"; font-family: "Geist";
font-weight: bold; font-weight: bold;
@@ -18,31 +18,36 @@
} }
} }
.register-button { @mixin button($color-name, $color-level) {
@extend %button; @extend %button;
background-color: $color-ternary-90; background-color: get_color($color-name, $color-level);
&:hover { &:hover {
background-color: $color-ternary-80; background-color: get_color($color-name, calc($color-level - 10));
} }
&:active { &:active {
background-color: $color-ternary-70; background-color: get_color($color-name, calc($color-level - 20));
} }
} }
.register-button {
@include button(ternary, 90);
}
.login-button { .login-button {
@extend %button; @include button(secondary, 90);
background-color: $color-secondary-90;
&:hover {
background-color: $color-secondary-80;
} }
&:active { .success-button {
background-color: $color-secondary-70; @include button(success, 100);
} }
.warning-button {
@include button(warning, 100);
}
.error-button {
@include button(critical, 100);
} }

View File

@@ -75,6 +75,6 @@
top: calc($logo-center-pos + ($background-height * 18)); top: calc($logo-center-pos + ($background-height * 18));
} }
background-color: $greyscale-0; background-color: get-color(greyscale, 0);
} }
} }

View File

@@ -22,10 +22,10 @@
aspect-ratio: $form-aspect-ratio; aspect-ratio: $form-aspect-ratio;
border: $border-big; border: $border-big;
border-color: $color-primary-90; border-color: get-color(primary, 90);
border-radius: $border-radius; border-radius: $border-radius;
background-color: $greyscale-0; background-color: get-color(greyscale, 0);
display: grid; display: grid;

View File

@@ -7,6 +7,7 @@ pub mod icons;
pub mod loading; pub mod loading;
pub mod login; pub mod login;
pub mod main_window; pub mod main_window;
pub mod modal;
pub mod spinner; pub mod spinner;
pub mod text_input; pub mod text_input;
pub mod wallpaper; pub mod wallpaper;

231
src/components/modal.rs Normal file
View File

@@ -0,0 +1,231 @@
use std::collections::HashMap;
use std::fmt;
use std::sync::OnceLock;
use dioxus::prelude::*;
use rand::distributions::{Alphanumeric, DistString};
use tracing::{error, warn};
use super::button::{ErrorButton, SuccessButton, WarningButton};
include!(concat!(env!("OUT_DIR"), "/style_vars.rs"));
use style::{COLOR_CRITICAL_100, COLOR_SUCCESS_100, COLOR_WARNING_100};
turf::style_sheet!("src/components/modal.scss");
#[derive(Clone, Eq, PartialEq, Hash)]
pub enum Severity {
Ok,
Warning,
Critical,
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let repr = match self {
Self::Ok => "Severity::Ok",
Self::Warning => "Severity::Warning",
Self::Critical => "Severity::Critical",
};
write!(f, "{repr}")
}
}
struct DicebearConfig<'a> {
gesture: &'a str,
color: &'a str,
browns: Vec<u32>,
eyes: Vec<u32>,
lips: Vec<u32>,
}
#[derive(Props)]
pub struct ModalProps<'a> {
pub severity: Severity,
#[props(optional)]
pub title: Option<&'a str>,
pub children: Element<'a>,
#[props(optional)]
pub on_confirm: Option<EventHandler<'a, MouseEvent>>,
}
fn dicebear_variants() -> &'static HashMap<Severity, DicebearConfig<'static>> {
static HASHMAP: OnceLock<HashMap<Severity, DicebearConfig>> = OnceLock::new();
HASHMAP.get_or_init(|| {
let mut variants = HashMap::new();
variants.insert(
Severity::Critical,
DicebearConfig {
gesture: "wavePointLongArms",
color: COLOR_CRITICAL_100,
browns: vec![2, 6, 11, 13],
eyes: vec![2, 4],
lips: vec![1, 2, 7, 11, 19, 20, 24, 27],
},
);
variants.insert(
Severity::Warning,
DicebearConfig {
gesture: "pointLongArm",
color: COLOR_WARNING_100,
browns: vec![2, 5, 10, 13],
eyes: vec![1, 3],
lips: vec![1, 2, 4, 8, 10, 13, 18, 21, 29],
},
);
variants.insert(
Severity::Ok,
DicebearConfig {
gesture: "okLongArm",
color: COLOR_SUCCESS_100,
browns: vec![1, 3, 4, 7, 8, 9, 12],
eyes: vec![5],
lips: vec![3, 5, 9, 14, 17, 22, 23, 25, 30],
},
);
variants
})
}
fn render_dicebear_variants(values: &[u32]) -> String {
values
.iter()
.map(|l| format!("variant{:02}", l))
.collect::<Vec<String>>()
.join(",")
}
async fn generate_random_figure(url: &String, severity: Severity) -> Option<String> {
let mut res: Option<String> = None;
let config = match dicebear_variants().get(&severity) {
Some(config) => config,
None => {
error!("No dicebear configuration found for \"{severity}\"");
return res;
}
};
let seed = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
let color = config.color;
let gesture = config.gesture;
let rendered_browns = render_dicebear_variants(&config.browns);
let rendered_eyes = render_dicebear_variants(&config.eyes);
let rendered_lips = render_dicebear_variants(&config.lips);
let req = format!(
"https://{url}/7.x/notionists/svg?\
seed={seed}&\
backgroundColor={color}&\
gestureProbability=100&gesture={gesture}&\
browsProbability=100&brows={rendered_browns}&\
eyesProbability=100&eyes={rendered_eyes}&\
lipsProbability=100&lips={rendered_lips}"
);
match reqwest::get(req).await {
Ok(result) => {
match result.text().await {
Ok(svg) => {
res = Some(svg);
}
Err(err) => {
error!("Error during placeholder loading: {}", err);
}
};
}
Err(err) => {
error!("Error during placeholder loading: {}", err);
}
};
res
}
#[component]
pub fn Modal<'a>(cx: Scope<'a, ModalProps<'a>>) -> Element<'a> {
// TODO: Use configuration file
let url = "dicebear.tools.adrien.run".to_string();
let severity = cx.props.severity.clone();
let random_figure_future = use_future(cx, &url, |url| async move {
generate_random_figure(&url, severity).await
});
let figure = match random_figure_future.value() {
Some(Some(svg)) => Some(rsx! {
div {
class: ClassName::MODAL_CONTENT_ICON_PLACEHOLDER,
dangerous_inner_html: svg.as_str(),
}
}),
Some(None) => {
warn!("No profile image set or generated, display the placeholder");
let path = match cx.props.severity {
Severity::Ok => "./images/modal-default-ok-icon.svg",
Severity::Warning => "./images/modal-default-warning-icon.svg",
Severity::Critical => "./images/modal-default-critical-icon.svg",
};
Some(rsx! {
div {
class: ClassName::MODAL_CONTENT_ICON_PLACEHOLDER,
img {
src: path
}
}
})
}
None => None,
};
let button_class = match cx.props.severity {
Severity::Ok => SuccessButton,
Severity::Warning => WarningButton,
Severity::Critical => ErrorButton,
};
if figure.is_none() {
return None;
}
cx.render(rsx! {
style { STYLE_SHEET },
div {
class: ClassName::MODAL,
div {
class: ClassName::MODAL_CONTENT,
div {
class: ClassName::MODAL_CONTENT_ICON,
{figure}
},
div {
class: ClassName::MODAL_CONTENT_TITLE,
cx.props.title,
},
div {
class: ClassName::MODAL_CONTENT_MSG,
&cx.props.children,
},
div {
class: ClassName::MODAL_CONTENT_BUTTONS,
button_class {
onclick: move |evt| {
if let Some(cb) = &cx.props.on_confirm {
cb.call(evt);
}
},
},
},
},
},
})
}

107
src/components/modal.scss Normal file
View File

@@ -0,0 +1,107 @@
@import "../_base.scss"
$modal-min-height: 15vh;
$modal-max-height: 55vh;
.modal {
width: 100%;
height: 100%;
position: relative;
top: -100%;
margin-bottom: -100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
&__content {
$content-width: 70vw;
$title-height: 3vh;
$space-height: 1vh;
$buttons-height: 5vh;
$icon-width: 10vw;
$msg-max-width: 50vw;
$msg-min-height: calc($modal-min-height - $title-height - $space-height - $buttons-height);
$msg-max-height: calc($modal-max-height - $title-height - $space-height - $buttons-height);
min-height: $modal-min-height;
max-height: $modal-max-height;
width: min-content;
display: grid;
grid-template-columns: $icon-width 1vw minmax(max-content, $msg-max-width);
grid-template-rows: $title-height $space-height max-content $space-height $buttons-height;
grid-template-areas:
"icon . title"
"icon . ."
"icon . msg"
". . ."
"buttons buttons buttons"
;
padding: 2.5%;
background: get-color(greyscale, 0);
border: $border-normal;
border-radius: $border-radius;
border-color: get-color(greyscale, 90);
&__icon {
grid-area: icon;
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border: $border-normal;
border-radius: $border-radius;
&__placeholder {
width: calc(100% + (2 * $border-normal-width));
height: calc(100% - (2 * $border-normal-width));
}
}
&__title {
grid-area: title;
width: max-content;
font-size: calc($title-height - 0.5vh);
font-weight: bold;
}
&__msg {
grid-area: msg;
height: fit-content;
min-height: $msg-min-height;
max-height: $msg-max-height;
max-width: $msg-max-width;
overflow: scroll;
font-size: 1.5vh;
color: get-color(greyscale, 80);
}
&__buttons {
grid-area: buttons;
display: flex;
justify-content: center;
align-items: center;
}
}
}

View File

@@ -18,21 +18,21 @@ $logo-aspect-ratio: calc($logo-width / $logo-height);
height: 100%; height: 100%;
width: 100%; width: 100%;
fill: $color-primary-100; fill: get-color(primary, 100);
stroke: $greyscale-90; stroke: get-color(greyscale, 90);
animation: 3s multicolor linear infinite; animation: 3s multicolor linear infinite;
animation-timing-function: steps($steps, end); animation-timing-function: steps($steps, end);
@keyframes multicolor { @keyframes multicolor {
0% { 0% {
fill: $color-primary-100; fill: get-color(primary, 100);
} }
33% { 33% {
fill: $color-secondary-100; fill: get-color(secondary, 100);
} }
66% { 66% {
fill: $color-ternary-100; fill: get-color(ternary, 100);
} }
} }

View File

@@ -7,7 +7,7 @@
width: calc(100% - (2 * $horizontal-padding)); width: calc(100% - (2 * $horizontal-padding));
border: $border-normal; border: $border-normal;
border-color: $color-primary-90; border-color: get-color(primary, 90);
border-radius: $border-radius; border-radius: $border-radius;
padding-left: $horizontal-padding; padding-left: $horizontal-padding;
@@ -33,7 +33,7 @@
} }
&.invalid { &.invalid {
border-color: $color-critical; border-color: get-color(critical, 100);
} }
} }
@@ -43,13 +43,13 @@
font-size: 1.2vh; font-size: 1.2vh;
color: $color-primary-90; color: get-color(primary, 90);
p { p {
margin: 0; margin: 0;
&.invalid { &.invalid {
color: $color-critical; color: get-color(critical, 100);
} }
} }
} }
@@ -118,7 +118,7 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
color: $color-secondary-100; color: get-color(secondary, 100),
} }
} }