3 Commits

Author SHA1 Message Date
0ab6aaac1c Add PasswordTextInput component
The TextInput component has been reworked to factorize some pieces of code with PasswordTextInput.
2024-03-21 21:12:49 +01:00
89b1f10b6e Add Pyramid icon 2024-03-21 21:05:04 +01:00
570a969cee 💄 Fix conflicts regarding the generated CSS class names 2024-03-21 18:32:40 +01:00
18 changed files with 460 additions and 116 deletions

View File

@@ -38,3 +38,6 @@ target = "x86_64-unknown-linux-gnu"
[build-dependencies] [build-dependencies]
regex = "1.10.3" regex = "1.10.3"
[package.metadata.turf.class_names]
template = "<original_name>--<id>"

View File

@@ -69,16 +69,14 @@ $border-radius: 16px;
$geist-font-path: "../fonts/Geist"; $geist-font-path: "../fonts/Geist";
$transition-duration: 300ms;
@font-face { @font-face {
src: url("#{$geist-font-path}/Geist-Bold.woff2") format("woff2"); src: url("#{$geist-font-path}/Geist-Bold.woff2") format("woff2");
font-family: "Geist"; font-family: "Geist";
font-weight: bold; font-weight: bold;
} }
* {
font-family: "Geist";
}
body { body {
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
@@ -90,8 +88,15 @@ body {
#main { #main {
height: 100%; height: 100%;
width: 100%; width: 100%;
font-family: "Geist";
} }
::selection {
background-color: transparent;
}
// TODO: To remove once the design updated. // TODO: To remove once the design updated.
.aeroButton { .aeroButton {
height: 50%; height: 50%;

View File

@@ -7,7 +7,7 @@ pub fn AvatarSelector(cx: Scope) -> Element {
style { STYLE_SHEET }, style { STYLE_SHEET },
div { div {
class: ClassName::SELECTOR, class: ClassName::AVATAR_SELECTOR,
svg { svg {
view_box: "0 0 100 100", view_box: "0 0 100 100",
linearGradient { linearGradient {
@@ -46,7 +46,7 @@ pub fn AvatarSelector(cx: Scope) -> Element {
}, },
}, },
img { img {
class: ClassName::PICTURE, class: ClassName::AVATAR_SELECTOR_PICTURE,
src: "./images/default-avatar.png", src: "./images/default-avatar.png",
}, },
}, },

View File

@@ -1,9 +1,9 @@
.selector { .avatar-selector {
position: relative; position: relative;
height: 100%; height: 100%;
aspect-ratio: 1; aspect-ratio: 1;
.picture { &__picture {
$height: 65%; $height: 65%;
$margin: calc(100% - $height) / 2; $margin: calc(100% - $height) / 2;

View File

@@ -80,7 +80,7 @@ pub fn RegisterButton<'a>(cx: Scope<'a, ButtonProps>) -> Element<'a> {
Button { Button {
id: cx.props.id.unwrap_or(""), id: cx.props.id.unwrap_or(""),
style: ClassName::REGISTER, style: ClassName::REGISTER_BUTTON,
onclick: |event| { onclick: |event| {
if let Some(cb) = &cx.props.onclick { if let Some(cb) = &cx.props.onclick {
@@ -126,7 +126,7 @@ pub fn LoginButton<'a>(cx: Scope<'a, ButtonProps>) -> Element<'a> {
Button { Button {
id: cx.props.id.unwrap_or(""), id: cx.props.id.unwrap_or(""),
style: ClassName::LOGIN, style: ClassName::LOGIN_BUTTON,
onclick: |event| { onclick: |event| {
if let Some(cb) = &cx.props.onclick { if let Some(cb) = &cx.props.onclick {

View File

@@ -1,6 +1,6 @@
@import "../_base.scss" @import "../_base.scss"
.root { %button {
height: 100%; height: 100%;
aspect-ratio: 3.5; aspect-ratio: 3.5;
@@ -18,27 +18,31 @@
} }
} }
.register { .register-button {
@extend .root; @extend %button;
background-color: $color-ternary-90; background-color: $color-ternary-90;
}
.register:hover { &:hover {
background-color: $color-ternary-80; background-color: $color-ternary-80;
} }
.register:active {
background-color: $color-ternary-70; &:active {
background-color: $color-ternary-70;
}
} }
.login { .login-button {
@extend .root; @extend %button;
background-color: $color-secondary-90; background-color: $color-secondary-90;
}
.login:hover { &:hover {
background-color: $color-secondary-80; background-color: $color-secondary-80;
} }
.login:active {
background-color: $color-secondary-70; &:active {
background-color: $color-secondary-70;
}
} }

View File

@@ -1,9 +1,13 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_free_icons::icons::md_navigation_icons::MdArrowDropDown; use dioxus_free_icons::icons::md_navigation_icons::MdArrowDropDown;
use dioxus_free_icons::Icon; use dioxus_free_icons::{Icon, IconShape};
turf::style_sheet!("src/components/icons.scss"); turf::style_sheet!("src/components/icons.scss");
include!(concat!(env!("OUT_DIR"), "/style_vars.rs"));
use style::{COLOR_PRIMARY_100, COLOR_TERNARY_100};
pub fn DownArrowIcon(cx: Scope) -> Element { pub fn DownArrowIcon(cx: Scope) -> Element {
cx.render(rsx! { cx.render(rsx! {
style { STYLE_SHEET }, style { STYLE_SHEET },
@@ -14,3 +18,113 @@ pub fn DownArrowIcon(cx: Scope) -> Element {
} }
}) })
} }
const _PYRAMID_OFFSET_X: f64 = 1.0;
const _PYRAMID_OFFSET_Y: f64 = 2.0;
const _PYRAMID_STROKE_WIDTH: f64 = 2.0;
const _PYRAMID_DIST_FROM_CENTRAL_X: f64 = 65.0;
const _PYRAMID_DIST_FROM_CENTRAL_E1_Y: f64 = 83.0;
const _PYRAMID_EDGES_E1_X: f64 = _PYRAMID_DIST_FROM_CENTRAL_X + _PYRAMID_OFFSET_X;
const _PYRAMID_EDGES_E1_Y: f64 = _PYRAMID_OFFSET_Y;
const _PYRAMID_LEFT_EDGE_E2_X: f64 = _PYRAMID_OFFSET_X;
const _PYRAMID_LEFT_EDGE_E2_Y: f64 = _PYRAMID_DIST_FROM_CENTRAL_E1_Y + _PYRAMID_OFFSET_Y;
const _PYRAMID_CENTRAL_EDGE_E2_X: f64 = _PYRAMID_DIST_FROM_CENTRAL_X + _PYRAMID_OFFSET_X;
const _PYRAMID_CENTRAL_EDGE_E2_Y: f64 = 100.0 + _PYRAMID_OFFSET_Y;
const _PYRAMID_CENTRAL_EDGE_Y_LEN: f64 = _PYRAMID_CENTRAL_EDGE_E2_Y - _PYRAMID_EDGES_E1_Y;
const _PYRAMID_RIGHT_EDGE_E2_X: f64 = 130.0 + _PYRAMID_OFFSET_X;
const _PYRAMID_RIGHT_EDGE_E2_Y: f64 = _PYRAMID_LEFT_EDGE_E2_Y;
struct PyramidShape<'a> {
color: &'a str,
ratio: f64,
progress_color: &'a str,
}
impl<'a> IconShape for PyramidShape<'a> {
fn view_box(&self) -> String {
let height = _PYRAMID_CENTRAL_EDGE_E2_Y + _PYRAMID_STROKE_WIDTH;
let width = _PYRAMID_RIGHT_EDGE_E2_X + _PYRAMID_STROKE_WIDTH;
format!("0 0 {width} {height}")
}
fn xmlns(&self) -> String {
String::from("http://www.w3.org/2000/svg")
}
fn child_elements(&self) -> LazyNodes {
let inverted_ratio = 1.0 - self.ratio;
let central_edge_ratio_e2_y =
_PYRAMID_CENTRAL_EDGE_Y_LEN * inverted_ratio + _PYRAMID_OFFSET_Y;
let left_edge_ratio_e1_x = _PYRAMID_OFFSET_X + (_PYRAMID_DIST_FROM_CENTRAL_X * self.ratio);
let right_edge_ratio_e1_x = _PYRAMID_OFFSET_X
+ _PYRAMID_EDGES_E1_X
+ (_PYRAMID_DIST_FROM_CENTRAL_X * inverted_ratio);
let no_central_edge_ratio_e1_y =
_PYRAMID_OFFSET_Y + (_PYRAMID_DIST_FROM_CENTRAL_E1_Y * inverted_ratio);
rsx! {
g {
stroke: "#fff",
"stroke-linejoin": "round",
"stroke-width": _PYRAMID_STROKE_WIDTH,
fill: "#{self.progress_color}",
path {
fill: "#{self.color}",
d: "\
M {_PYRAMID_EDGES_E1_X} {_PYRAMID_EDGES_E1_Y} \
L {_PYRAMID_RIGHT_EDGE_E2_X} {_PYRAMID_RIGHT_EDGE_E2_Y} \
L {_PYRAMID_EDGES_E1_X} {_PYRAMID_CENTRAL_EDGE_E2_Y} \
M {_PYRAMID_EDGES_E1_X} {_PYRAMID_EDGES_E1_Y} \
L {_PYRAMID_LEFT_EDGE_E2_X} {_PYRAMID_LEFT_EDGE_E2_Y} \
L {_PYRAMID_EDGES_E1_X} {_PYRAMID_CENTRAL_EDGE_E2_Y} \
M {_PYRAMID_EDGES_E1_X} {_PYRAMID_EDGES_E1_Y} \
V {_PYRAMID_CENTRAL_EDGE_Y_LEN}",
},
path {
d: "\
M {_PYRAMID_CENTRAL_EDGE_E2_X} {_PYRAMID_CENTRAL_EDGE_E2_Y} \
V {central_edge_ratio_e2_y} \
L {left_edge_ratio_e1_x} {no_central_edge_ratio_e1_y} \
L {_PYRAMID_LEFT_EDGE_E2_X} {_PYRAMID_LEFT_EDGE_E2_Y} Z",
},
path {
d: "\
M {_PYRAMID_CENTRAL_EDGE_E2_X} {_PYRAMID_CENTRAL_EDGE_E2_Y} \
V {central_edge_ratio_e2_y} \
L {right_edge_ratio_e1_x} {no_central_edge_ratio_e1_y} \
L {_PYRAMID_RIGHT_EDGE_E2_X} {_PYRAMID_RIGHT_EDGE_E2_Y} Z",
}
}
}
}
}
#[derive(Props)]
pub struct PyramidProps<'a> {
#[props(default = 0.5)]
color: Option<&'a str>,
ratio: f64,
progress_color: Option<&'a str>,
}
pub fn Pyramid<'a>(cx: Scope<'a, PyramidProps<'a>>) -> Element<'a> {
let progress_color = cx.props.progress_color.unwrap_or(COLOR_PRIMARY_100);
let color = cx.props.color.unwrap_or(COLOR_TERNARY_100);
cx.render(rsx! {
style { STYLE_SHEET },
Icon {
class: ClassName::PYRAMID_ICON,
icon: PyramidShape { ratio: cx.props.ratio, color: color, progress_color: progress_color },
}
})
}

View File

@@ -5,3 +5,8 @@
fill: white; fill: white;
} }
} }
.pyramid-icon {
height: 100%;
width: 100%;
}

View File

@@ -14,12 +14,12 @@ pub fn LoadingPage(cx: Scope) -> Element {
style { STYLE_SHEET }, style { STYLE_SHEET },
div { div {
class: ClassName::ROOT, class: ClassName::LOADING,
Wallpaper {}, Wallpaper {},
div { div {
class: ClassName::SPINNER, class: ClassName::LOADING_SPINNER,
Spinner {}, Spinner {},
} }
} }

View File

@@ -1,7 +1,7 @@
@import "../_base.scss" @import "../_base.scss"
@import "./spinner.scss" @import "./spinner.scss"
.root { .loading {
height: 100%; height: 100%;
width: 100%; width: 100%;
@@ -9,7 +9,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
.spinner { &__spinner {
height: 5%; height: 5%;
aspect-ratio: $logo-aspect-ratio; aspect-ratio: $logo-aspect-ratio;
@@ -77,5 +77,4 @@
background-color: $greyscale-0; background-color: $greyscale-0;
} }
} }

View File

@@ -375,14 +375,14 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
let avatar = match random_avatar_future.value() { let avatar = match random_avatar_future.value() {
Some(Some(svg)) => { Some(Some(svg)) => {
rsx!(div { rsx!(div {
class: ClassName::CONTENT, class: ClassName::LOGIN_FORM_PHOTO_CONTENT,
dangerous_inner_html: svg.as_str(), dangerous_inner_html: svg.as_str(),
}) })
} }
Some(None) | None => { Some(None) | None => {
warn!("No profile image set or generated, display the placeholder"); warn!("No profile image set or generated, display the placeholder");
rsx!(div { rsx!(div {
class: ClassName::CONTENT, class: ClassName::LOGIN_FORM_PHOTO_CONTENT,
img { img {
src: "./images/login-profile-placeholder.svg" src: "./images/login-profile-placeholder.svg"
} }
@@ -468,13 +468,13 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
Wallpaper {}, Wallpaper {},
div { div {
class: ClassName::ROOT, class: ClassName::LOGIN,
div { div {
class: "{form_classes_str}", class: "{form_classes_str}",
div { div {
class: ClassName::PHOTO, class: ClassName::LOGIN_FORM_PHOTO,
onclick: move |_| { onclick: move |_| {
random_avatar_future.restart() random_avatar_future.restart()
@@ -484,7 +484,7 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
}, },
div { div {
class: ClassName::HOMESERVER, class: ClassName::LOGIN_FORM_HOMESERVER,
TextInput { TextInput {
id: "hs_url", id: "hs_url",
r#type: "text", r#type: "text",
@@ -497,7 +497,7 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
}, },
div { div {
class: ClassName::ID, class: ClassName::LOGIN_FORM_ID,
TextInput { TextInput {
r#type: "text", r#type: "text",
placeholder: "{id_placeholder}", placeholder: "{id_placeholder}",
@@ -533,21 +533,21 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
}, },
div { div {
class: ClassName::SPINNER, class: ClassName::LOGIN_FORM_SPINNER,
Spinner { Spinner {
animate: **spinner_animated, animate: **spinner_animated,
}, },
}, },
div { div {
class: ClassName::REGISTER, class: ClassName::LOGIN_FORM_REGISTER_BUTTON,
RegisterButton { RegisterButton {
onclick: on_register, onclick: on_register,
}, },
}, },
div { div {
class: ClassName::LOGIN, class: ClassName::LOGIN_FORM_LOGIN_BUTTON,
LoginButton { LoginButton {
focus: true, focus: true,
onclick: on_login, onclick: on_login,

View File

@@ -1,7 +1,7 @@
@import "../_base.scss" @import "../_base.scss"
@import "./spinner.scss" @import "./spinner.scss"
.root { .login {
height: 100%; height: 100%;
width: 100%; width: 100%;
@@ -12,7 +12,9 @@
position: relative; position: relative;
top: -100vh; top: -100vh;
.form { margin-bottom: -100vh;
&__form {
$height: 95%; $height: 95%;
height: $height; height: $height;
@@ -64,13 +66,13 @@
". . . . . . . . . . ." ". . . . . . . . . . ."
; ;
transition: 300ms; transition: $transition-duration;
&.register { &.register {
grid-template-rows: $padding-row $profile-img-height auto 5% 5% 5% 5% 5% 5% 5% 5% $spinner-height 5% $button-height $padding-row; grid-template-rows: $padding-row $profile-img-height auto 5% 5% 5% 5% 5% 5% 5% 5% $spinner-height 5% $button-height $padding-row;
} }
.photo { &__photo {
grid-area: photo; grid-area: photo;
display: flex; display: flex;
@@ -82,25 +84,25 @@
overflow: hidden; overflow: hidden;
.content { &__content {
height: calc(100% + (2 * $border-big-width)); height: calc(100% + (2 * $border-big-width));
aspect-ratio: 1; aspect-ratio: 1;
} }
} }
.homeserver { &__homeserver {
grid-area: homeserver; grid-area: homeserver;
} }
.id { &__id {
grid-area: id; grid-area: id;
} }
.password { &__password {
grid-area: password; grid-area: password;
} }
.confirm-password { &__confirm-password {
grid-area: confirm; grid-area: confirm;
display: none; display: none;
@@ -109,7 +111,7 @@
} }
} }
.spinner { &__spinner {
grid-area: spinner; grid-area: spinner;
} }
@@ -117,11 +119,11 @@
width: 100%; width: 100%;
} }
.register { &__register-button {
grid-area: register; grid-area: register;
} }
.login { &__login-button {
grid-area: login; grid-area: login;
} }
} }

View File

@@ -34,7 +34,7 @@ pub fn Spinner(cx: Scope<SpinnerProps>) -> Element {
style { STYLE_SHEET }, style { STYLE_SHEET },
div { div {
class: ClassName::ROOT, class: ClassName::SPINNER,
Icon { Icon {
class: if cx.props.animate { "" } else { ClassName::PAUSED }, class: if cx.props.animate { "" } else { ClassName::PAUSED },

View File

@@ -6,7 +6,7 @@ $logo-height: calc(32px * 2);
$logo-width: calc(64px * 2); $logo-width: calc(64px * 2);
$logo-aspect-ratio: calc($logo-width / $logo-height); $logo-aspect-ratio: calc($logo-width / $logo-height);
.root { .spinner {
height: 100%; height: 100%;
width: 100%; width: 100%;

View File

@@ -1,8 +1,23 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_free_icons::icons::io_icons::IoEye;
use dioxus_free_icons::icons::io_icons::IoEyeOff;
use dioxus_free_icons::Icon;
use super::icons::Pyramid;
turf::style_sheet!("src/components/text_input.scss"); turf::style_sheet!("src/components/text_input.scss");
#[derive(Debug)] pub trait InputPropsData {}
#[derive(Props)]
pub struct InputProps<'a, D: InputPropsData + 'a> {
value: Option<&'a str>,
placeholder: Option<&'a str>,
oninput: Option<EventHandler<'a, Event<FormData>>>,
state: Option<&'a UseRef<D>>,
}
#[derive(PartialEq)]
pub struct TextInputState { pub struct TextInputState {
pub is_valid: bool, pub is_valid: bool,
pub helper_text: Option<String>, pub helper_text: Option<String>,
@@ -27,62 +42,176 @@ impl TextInputState {
} }
} }
#[derive(Props)] impl InputPropsData for TextInputState {}
pub struct TextInputProps<'a> {
id: Option<&'a str>,
r#type: Option<&'a str>,
value: Option<&'a str>,
placeholder: Option<&'a str>,
oninput: Option<EventHandler<'a, Event<FormData>>>,
state: Option<&'a UseRef<TextInputState>>,
}
pub fn TextInput<'a>(cx: Scope<'a, TextInputProps<'a>>) -> Element<'a> { pub fn TextInput<'a>(cx: Scope<'a, InputProps<'a, TextInputState>>) -> Element<'a> {
let mut level_class = ""; let mut criticity_class = "";
let mut helper_text: String = "".to_string(); let mut helper_text = "".to_string();
match cx.props.state { match cx.props.state {
Some(state) => { Some(state) => {
if !state.read().is_valid { let state = state.read();
level_class = ClassName::INVALID; if !state.is_valid {
criticity_class = ClassName::INVALID;
} }
if let Some(text) = &state.read().helper_text { if let Some(text) = &state.helper_text {
helper_text = text.to_string(); helper_text = text.to_string();
} }
} }
None => {} None => {}
} }
let input_classes_str = [ClassName::TEXT_INPUT_INPUT, criticity_class].join(" ");
cx.render(rsx! { cx.render(rsx! {
style { STYLE_SHEET }, style { STYLE_SHEET },
div { div {
class: ClassName::ROOT, class: ClassName::TEXT_INPUT,
input { input {
class: level_class, class: "{input_classes_str}",
r#type: "text",
id: cx.props.id.unwrap_or(""), placeholder: cx.props.placeholder,
value: cx.props.value,
oninput: move |evt| { oninput: move |evt| {
if let Some(cb) = &cx.props.oninput { if let Some(cb) = &cx.props.oninput {
cb.call(evt); cb.call(evt);
} }
}, },
r#type: cx.props.r#type,
placeholder: cx.props.placeholder,
value: cx.props.value,
}, },
div { div {
class: ClassName::HELPER_TEXT, class: ClassName::TEXT_INPUT_HELPER_TEXT,
p { p {
class: level_class, class: criticity_class,
helper_text helper_text
} }
} }
} }
}) })
} }
#[derive(PartialEq, Props)]
pub struct PasswordInputState {
text_input_state: TextInputState,
#[props(default = 0.0)]
pub score: f64,
}
impl PasswordInputState {
pub fn new() -> Self {
PasswordInputState {
text_input_state: TextInputState::new(),
score: 0.0,
}
}
pub fn reset(&mut self) {
self.text_input_state.reset()
}
pub fn invalidate(&mut self, helper_text: String) {
self.text_input_state.invalidate(helper_text)
}
}
impl InputPropsData for PasswordInputState {}
pub fn PasswordTextInput<'a>(cx: Scope<'a, InputProps<'a, PasswordInputState>>) -> Element<'a> {
let mut criticity_class = "";
let mut helper_text: String = "".to_string();
let mut score: Option<f64> = None;
let show_password = use_state(cx, || false);
match cx.props.state {
Some(state) => {
let state = state.read();
if !state.text_input_state.is_valid {
criticity_class = ClassName::INVALID;
}
if let Some(text) = &state.text_input_state.helper_text {
helper_text = text.to_string();
}
score = Some(state.score);
}
None => {}
}
let text_input_classes = [
ClassName::PASSWORD_TEXT_INPUT,
if score.is_none() {
ClassName::NO_STRENGTH
} else {
""
},
]
.join(" ");
let input_classes = [ClassName::PASSWORD_TEXT_INPUT_INPUT, criticity_class].join(" ");
cx.render(rsx! {
style { STYLE_SHEET },
div {
class: "{text_input_classes}",
input {
class: "{input_classes}",
r#type: if **show_password { "text" } else { "password" },
placeholder: cx.props.placeholder,
value: cx.props.value,
oninput: move |evt| {
if let Some(cb) = &cx.props.oninput {
cb.call(evt);
}
},
},
if let Some(score) = score {
rsx!(
div {
class: ClassName::PASSWORD_TEXT_INPUT_STRENGTH_LEVEL,
Pyramid {
ratio: score,
}
}
)
},
div {
class: ClassName::PASSWORD_TEXT_INPUT_SHOW_TOGGLE,
onclick: move |_| {
show_password.set(!**show_password);
},
if **show_password {
rsx!(
Icon {
icon: IoEyeOff,
}
)
}
else {
rsx!(
Icon {
icon: IoEye,
}
)
}
},
div {
class: ClassName::PASSWORD_TEXT_INPUT_HELPER_TEXT,
p {
class: criticity_class,
helper_text
}
},
}
})
}

View File

@@ -1,47 +1,130 @@
@import "../_base.scss" @import "../_base.scss"
.root { %base-text-input {
$horizontal-padding: 1vw;
height: 100%; height: 100%;
width: calc(100% - (2 * $horizontal-padding));
input { border: $border-normal;
$horizontal-padding: 1vw; border-color: $color-primary-90;
border-radius: $border-radius;
padding-left: $horizontal-padding; padding-left: $horizontal-padding;
padding-right: $horizontal-padding; padding-right: $horizontal-padding;
padding-top: 0px; padding-top: 0px;
padding-bottom: 0px; padding-bottom: 0px;
}
height: calc(100% - (2 * ($border-normal-width))); %base-input {
width: calc(100% - (2 * ($border-normal-width + $horizontal-padding))); $horizontal-padding: 1vw;
margin: 0; height: 100%;
width: 100%;
border: $border-normal; margin: 0;
border-color: $color-primary-90; padding: 0;
border-radius: $border-radius; border: 0;
font-size: 2vh; font-size: 2vh;
&.invalid { &:focus {
border-color: $color-critical; outline: none;
}
} }
.helper-text { &.invalid {
border-color: $color-critical;
}
}
%base-helper-text {
margin: 0;
margin-top: 0.3vh;
font-size: 1.2vh;
color: $color-primary-90;
p {
margin: 0; margin: 0;
margin-top: 0.3vh;
font-size: 1.2vh; &.invalid {
color: $color-critical;
color: $color-primary-90;
p {
margin: 0;
padding-left: 1vw;
&.invalid {
color: $color-critical;
}
} }
} }
} }
.text-input {
@extend %base-text-input;
&__input {
@extend %base-input;
}
&__helper-text {
@extend %base-helper-text;
}
}
.password-text-input {
@extend %base-text-input;
display: grid;
grid-template-columns: auto 7.5% 1% 7.5%;
grid-template-rows: 100%;
grid-template-areas:
"input strength . toggle"
"helper helper helper helper"
;
transition: $transition-duration;
&.no-strength {
grid-template-columns: auto 0% 0% 7.5%;
}
&__input {
@extend %base-input;
grid-area: input;
}
%inner {
display: flex;
align-items: center;
justify-content: center;
}
&__strength-level {
@extend %inner;
grid-area: strength;
p {
margin: 0;
text-align: center;
width: 100%;
font-size: 2vh;
}
}
&__show-toggle {
@extend %inner;
grid-area: toggle;
svg {
height: 100%;
width: 100%;
color: $color-secondary-100;
}
}
&__helper-text {
@extend %base-helper-text;
grid-area: helper;
}
}

View File

@@ -7,10 +7,10 @@ pub fn Wallpaper(cx: Scope) -> Element {
cx.render(rsx! { cx.render(rsx! {
style { STYLE_SHEET }, style { STYLE_SHEET },
div { div {
class: ClassName::ROOT, class: ClassName::WALLPAPER,
div { div {
class: ClassName::CONTENT, class: ClassName::WALLPAPER_CONTENT,
} }
} }
}) })

View File

@@ -1,6 +1,6 @@
@import "../_base.scss" @import "../_base.scss"
.root { .wallpaper {
height: 100%; height: 100%;
width: 100%; width: 100%;
z-index: -1; z-index: -1;
@@ -11,7 +11,7 @@
overflow: hidden; overflow: hidden;
.content { &__content {
background-image: url("./images/wallpaper-pattern.svg"); background-image: url("./images/wallpaper-pattern.svg");
background-position: center; background-position: center;