UI: Cleansheet CSS redesign

Initial skeleton and layout is working.
Still needs a bunch of tweaks.
This commit is contained in:
Jeremy Wall 2023-11-27 19:51:46 -05:00
parent 61634cd682
commit 45737f24e4
14 changed files with 212 additions and 103 deletions

View File

@ -19,7 +19,6 @@
<head> <head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" name="viewport" <meta content="text/html;charset=utf-8" http-equiv="Content-Type" name="viewport"
content="width=device-width, initial-scale=1.0" charset="UTF-8"> content="width=device-width, initial-scale=1.0" charset="UTF-8">
<link rel="stylesheet" href="/ui/static/pico.min.css">
<link rel="stylesheet" href="/ui/static/app.css"> <link rel="stylesheet" href="/ui/static/app.css">
</head> </head>
@ -35,4 +34,4 @@
</script> </script>
</body> </body>
</html> </html>

View File

@ -23,9 +23,9 @@ pub fn Header<'ctx, G: Html>(cx: Scope<'ctx>, h: StateHandler<'ctx>) -> View<G>
None => "Login".to_owned(), None => "Login".to_owned(),
}); });
view! {cx, view! {cx,
nav(class="no-print") { nav(class="no-print row-flex align-center header-bg heavy-bottom-border") {
h1(class="title") { "Kitchen" } h1(class="title") { "Kitchen" }
ul { ul(class="row-flex align-center") {
li { a(href="/ui/planning/select") { "MealPlan" } } li { a(href="/ui/planning/select") { "MealPlan" } }
li { a(href="/ui/manage/ingredients") { "Manage" } } li { a(href="/ui/manage/ingredients") { "Manage" } }
li { a(href="/ui/login") { (login.get()) } } li { a(href="/ui/login") { (login.get()) } }

View File

@ -211,6 +211,7 @@ where
F: Fn(Event), F: Fn(Event),
{ {
name: String, name: String,
class: String,
on_change: Option<F>, on_change: Option<F>,
min: f64, min: f64,
counter: &'ctx Signal<f64>, counter: &'ctx Signal<f64>,
@ -223,6 +224,7 @@ where
{ {
let NumberProps { let NumberProps {
name, name,
class,
on_change, on_change,
min, min,
counter, counter,
@ -241,7 +243,7 @@ where
}); });
let id = name.clone(); let id = name.clone();
view! {cx, view! {cx,
number-spinner(id=id, val=*counter.get(), min=min, on:updated=move |evt: Event| { number-spinner(id=id, class=(class), val=*counter.get(), min=min, on:updated=move |evt: Event| {
let target: HtmlElement = evt.target().unwrap().dyn_into().unwrap(); let target: HtmlElement = evt.target().unwrap().dyn_into().unwrap();
let val: f64 = target.get_attribute("val").unwrap().parse().unwrap(); let val: f64 = target.get_attribute("val").unwrap().parse().unwrap();
counter.set(val); counter.set(val);

View File

@ -38,12 +38,12 @@ pub fn PlanList<'ctx, G: Html>(cx: Scope<'ctx>, props: PlanListProps<'ctx>) -> V
view!{cx, view!{cx,
tr() { tr() {
td() { td() {
span(role="button", class="outline", on:click=move |_| { button(class="outline", on:click=move |_| {
sh.dispatch(cx, Message::SelectPlanDate(date, None)) sh.dispatch(cx, Message::SelectPlanDate(date, None))
}) { (date_display) } }) { (date_display) }
} }
td() { td() {
span(role="button", class="destructive", on:click=move |_| { button(class="destructive", on:click=move |_| {
sh.dispatch(cx, Message::DeletePlan(date, None)) sh.dispatch(cx, Message::DeletePlan(date, None))
}) { "Delete Plan" } }) { "Delete Plan" }
} }

View File

@ -79,12 +79,14 @@ pub fn Editor<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>)
debug!("creating editor view"); debug!("creating editor view");
view! {cx, view! {cx,
label(for="recipe_category") { "Category" } div {
input(name="recipe_category", bind:value=category, on:change=move |_| dirty.set(true)) label(for="recipe_category") { "Category" }
div(class="grid") { input(name="recipe_category", bind:value=category, on:change=move |_| dirty.set(true))
div { }
label(for="recipe_text") { "Recipe" } div {
textarea(name="recipe_text", bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| { div(class="row-flex") {
label(for="recipe_text", class="block align-stretch expand-height") { "Recipe: " }
textarea(class="width-third", name="recipe_text", bind:value=text, aria-invalid=aria_hint.get(), cols="50", rows=20, on:change=move |_| {
dirty.set(true); dirty.set(true);
check_recipe_parses(text.get_untracked().as_str(), error_text, aria_hint); check_recipe_parses(text.get_untracked().as_str(), error_text, aria_hint);
}, on:input=move |_| { }, on:input=move |_| {
@ -97,34 +99,36 @@ pub fn Editor<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>)
} }
div(class="parse") { (error_text.get()) } div(class="parse") { (error_text.get()) }
} }
span(role="button", on:click=move |_| { div {
let unparsed = text.get_untracked(); button(on:click=move |_| {
if check_recipe_parses(unparsed.as_str(), error_text, aria_hint) { let unparsed = text.get_untracked();
debug!("triggering a save"); if check_recipe_parses(unparsed.as_str(), error_text, aria_hint) {
if !*dirty.get_untracked() { debug!("triggering a save");
debug!("Recipe text is unchanged"); if !*dirty.get_untracked() {
return; debug!("Recipe text is unchanged");
return;
}
debug!("Recipe text is changed");
let category = category.get_untracked();
let category = if category.is_empty() {
None
} else {
Some(category.as_ref().clone())
};
let recipe_entry = RecipeEntry(
id.get_untracked().as_ref().clone(),
text.get_untracked().as_ref().clone(),
category,
);
sh.dispatch(cx, Message::SaveRecipe(recipe_entry, None));
dirty.set(false);
} }
debug!("Recipe text is changed"); // TODO(jwall): Show error message if trying to save when recipe doesn't parse.
let category = category.get_untracked(); }) { "Save" } " "
let category = if category.is_empty() { button(on:click=move |_| {
None sh.dispatch(cx, Message::RemoveRecipe(id.get_untracked().as_ref().to_owned(), Some(Box::new(|| sycamore_router::navigate("/ui/planning/plan")))));
} else { }) { "delete" } " "
Some(category.as_ref().clone()) }
};
let recipe_entry = RecipeEntry(
id.get_untracked().as_ref().clone(),
text.get_untracked().as_ref().clone(),
category,
);
sh.dispatch(cx, Message::SaveRecipe(recipe_entry, None));
dirty.set(false);
}
// TODO(jwall): Show error message if trying to save when recipe doesn't parse.
}) { "Save" } " "
span(role="button", on:click=move |_| {
sh.dispatch(cx, Message::RemoveRecipe(id.get_untracked().as_ref().to_owned(), Some(Box::new(|| sycamore_router::navigate("/ui/planning/plan")))));
}) { "delete" } " "
} }
} }

View File

@ -52,7 +52,7 @@ pub fn CategoryGroup<'ctx, G: Html>(
}); });
view! {cx, view! {cx,
h2 { (category) } h2 { (category) }
div(class="recipe_selector no-print") { div(class="no-print flex-wrap-start align-stretch") {
(View::new_fragment( (View::new_fragment(
rows.get().iter().cloned().map(|r| { rows.get().iter().cloned().map(|r| {
view ! {cx, view ! {cx,
@ -61,7 +61,7 @@ pub fn CategoryGroup<'ctx, G: Html>(
view=move |cx, sig| { view=move |cx, sig| {
let title = create_memo(cx, move || sig.get().1.title.clone()); let title = create_memo(cx, move || sig.get().1.title.clone());
view! {cx, view! {cx,
div(class="cell") { RecipeSelection(i=sig.get().0.to_owned(), title=title, sh=sh) } div(class="cell column-flex justify-end align-stretch") { RecipeSelection(i=sig.get().0.to_owned(), title=title, sh=sh) }
} }
}, },
key=|sig| sig.get().0.to_owned(), key=|sig| sig.get().0.to_owned(),
@ -108,13 +108,13 @@ pub fn RecipePlan<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vie
}, },
key=|(ref cat, _)| cat.clone(), key=|(ref cat, _)| cat.clone(),
) )
span(role="button", on:click=move |_| { button(on:click=move |_| {
sh.dispatch(cx, Message::LoadState(None)); sh.dispatch(cx, Message::LoadState(None));
}) { "Reset" } " " }) { "Reset" } " "
span(role="button", on:click=move |_| { button(on:click=move |_| {
sh.dispatch(cx, Message::ResetRecipeCounts); sh.dispatch(cx, Message::ResetRecipeCounts);
}) { "Clear All" } " " }) { "Clear All" } " "
span(role="button", on:click=move |_| { button(on:click=move |_| {
// Poor man's click event signaling. // Poor man's click event signaling.
sh.dispatch(cx, Message::SaveState(None)); sh.dispatch(cx, Message::SaveState(None));
}) { "Save Plan" } " " }) { "Save Plan" } " "

View File

@ -65,8 +65,8 @@ pub fn RecipeSelection<'ctx, G: Html>(
let name = format!("recipe_id:{}", id); let name = format!("recipe_id:{}", id);
let for_id = name.clone(); let for_id = name.clone();
view! {cx, view! {cx,
label(for=for_id) { a(href=href) { (*title) } } label(for=for_id, class="flex-item-grow") { a(href=href) { (*title) } }
NumberField(name=name, counter=count, min=0.0, on_change=Some(move |_| { NumberField(name=name, class="flex-item-shrink".to_string(), counter=count, min=0.0, on_change=Some(move |_| {
debug!(idx=%id, count=%(*count.get_untracked()), "setting recipe count"); debug!(idx=%id, count=%(*count.get_untracked()), "setting recipe count");
sh.dispatch(cx, Message::UpdateRecipeCount(id.as_ref().clone(), *count.get_untracked() as usize)); sh.dispatch(cx, Message::UpdateRecipeCount(id.as_ref().clone(), *count.get_untracked() as usize));
})) }))

View File

@ -205,15 +205,15 @@ pub fn ShoppingList<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> V
sh.dispatch(cx, Message::UpdateUseStaples(value)); sh.dispatch(cx, Message::UpdateUseStaples(value));
}) })
(make_shopping_table(cx, sh, show_staples)) (make_shopping_table(cx, sh, show_staples))
span(role="button", class="no-print", on:click=move |_| { button(class="no-print", on:click=move |_| {
info!("Registering add item request for inventory"); info!("Registering add item request for inventory");
sh.dispatch(cx, Message::AddExtra(String::new(), String::new())); sh.dispatch(cx, Message::AddExtra(String::new(), String::new()));
}) { "Add Item" } " " }) { "Add Item" } " "
span(role="button", class="no-print", on:click=move |_| { button(class="no-print", on:click=move |_| {
info!("Registering reset request for inventory"); info!("Registering reset request for inventory");
sh.dispatch(cx, Message::ResetInventory); sh.dispatch(cx, Message::ResetInventory);
}) { "Reset" } " " }) { "Reset" } " "
span(role="button", class="no-print", on:click=move |_| { button(class="no-print", on:click=move |_| {
info!("Registering save request for inventory"); info!("Registering save request for inventory");
sh.dispatch(cx, Message::SaveState(None)); sh.dispatch(cx, Message::SaveState(None));
}) { "Save" } " " }) { "Save" } " "

View File

@ -72,8 +72,8 @@ pub fn IngredientsEditor<'ctx, G: Html>(
debug!("creating editor view"); debug!("creating editor view");
view! {cx, view! {cx,
div(class="grid") { div {
textarea(bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| { textarea(class="width-third", bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| {
dirty.set(true); dirty.set(true);
}, on:input=move |_| { }, on:input=move |_| {
let current_ts = js_lib::get_ms_timestamp(); let current_ts = js_lib::get_ms_timestamp();
@ -84,7 +84,7 @@ pub fn IngredientsEditor<'ctx, G: Html>(
}) })
div(class="parse") { (error_text.get()) } div(class="parse") { (error_text.get()) }
} }
span(role="button", on:click=move |_| { button(on:click=move |_| {
let unparsed = text.get(); let unparsed = text.get();
if !*dirty.get_untracked() { if !*dirty.get_untracked() {
debug!("Staples text is unchanged"); debug!("Staples text is unchanged");

View File

@ -47,12 +47,12 @@ pub fn TabbedView<'a, G: Html>(cx: Scope<'a>, state: TabState<'a, G>) -> View<G>
.collect(), .collect(),
); );
view! {cx, view! {cx,
nav { nav(class="menu-bg expand-height") {
ul(class="tabs") { ul(class="tabs pad-left") {
(menu) (menu)
} }
} }
main(class=".conatiner-fluid") { main {
(children) (children)
} }
} }

View File

@ -27,7 +27,7 @@ pub fn LoginForm<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View
input(type="text", id="username", bind:value=username) input(type="text", id="username", bind:value=username)
label(for="password") { "Password" } label(for="password") { "Password" }
input(type="password", bind:value=password) input(type="password", bind:value=password)
span(role="button", on:click=move |_| { button(on:click=move |_| {
info!("Attempting login request"); info!("Attempting login request");
let (username, password) = ((*username.get_untracked()).clone(), (*password.get_untracked()).clone()); let (username, password) = ((*username.get_untracked()).clone(), (*password.get_untracked()).clone());
if username != "" && password != "" { if username != "" && password != "" {

View File

@ -37,7 +37,7 @@ pub fn SelectPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vie
selected=Some("Select".to_owned()), selected=Some("Select".to_owned()),
) { ) {
PlanList(sh=sh, list=plan_dates) PlanList(sh=sh, list=plan_dates)
span(role="button", on:click=move |_| { button(on:click=move |_| {
sh.dispatch(cx, Message::SelectPlanDate(chrono::offset::Local::now().naive_local().date(), Some(Box::new(|| { sh.dispatch(cx, Message::SelectPlanDate(chrono::offset::Local::now().naive_local().date(), Some(Box::new(|| {
sycamore_router::navigate("/ui/planning/plan"); sycamore_router::navigate("/ui/planning/plan");
})))) }))))

View File

@ -136,11 +136,12 @@ pub fn Handler<'ctx, G: Html>(cx: Scope<'ctx>, props: HandlerProps<'ctx>) -> Vie
integration=HistoryIntegration::new(), integration=HistoryIntegration::new(),
view=move |cx: Scope, route: &ReadSignal<Routes>| { view=move |cx: Scope, route: &ReadSignal<Routes>| {
view!{cx, view!{cx,
div(class="app") { div {
Header(sh) Header(sh)
div(class="app row-flex flex-item-grow expand-height align-stretch") {
(route_switch(route.get().as_ref(), cx, sh)) (route_switch(route.get().as_ref(), cx, sh))
Footer { }
} }
}
} }
}, },
) )

View File

@ -1,5 +1,5 @@
/** /**
* Copyright 2022 Jeremy Wall * Copyright 2023 Jeremy Wall
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -21,12 +21,33 @@
--unicode-button-size: 2em; --unicode-button-size: 2em;
--toast-anim-duration: 3s; --toast-anim-duration: 3s;
--notification-font-size: calc(var(--font-size) / 2); --notification-font-size: calc(var(--font-size) / 2);
--error-message-color: rgba(255, 98, 0, 0.797); --error-message-color: #CD5C08;
--error-message-bg: grey; --error-message-bg: grey;
--border-width: 2px; --border-width: 3px;
--cell-margin: 1em; --cell-margin: 1em;
--nav-margin: 2em;
--main-color: #A9907E;
--light-accent: #F3DEBA;
--dark-accent: #ABC4AA;
--heavy-accent: #675D50;
--text-color: black;
--menu-bg: var(--main-color);
--header-bg: var(--light-accent);
--font-size: 20px;
--cell-target: 30%;
} }
/** TODO(jwall): Dark color scheme?
@media (prefers-color-scheme: dark) {
:root {
--text-color: white;
--menu-bg: var(--main-color);
--header-bg: var(--dark-accent);
}
}
**/
/** TODO(jwall): Seperate these out into composable classes **/
@media print { @media print {
.no-print, .no-print,
@ -39,28 +60,101 @@
} }
} }
@media (min-width: 768px) { /** Resets **/
:root {
--font-size: 35px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--tab-border-color: lightgrey;
}
}
body { body {
padding: 10px; margin: 0px;
margin: 10px; padding: 0px;
background-color: var(--header-bg);
font-size: var(--font-size)
} }
nav>ul.tabs>li { body, body * {
border-style: none; color: black;
} }
nav>ul.tabs>li.selected { a {
text-decoration: none;
}
/** Our specific page elements **/
/** TODO(jwall): Move these onto the html elements themselves. **/
.column-flex {
display: flex;
flex-direction: column;
}
.row-flex {
display: flex;
flex-direction: row;
}
.flex-item-grow {
flex: 1 0 auto;
}
.flex-item-shrink {
flex: 0 1 auto;
}
.flex-wrap-start {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
}
.expand-height {
height: 100%;
min-height: fit-content;
}
.align-center {
align-items: center;
}
.align-stretch {
align-items: stretch;
}
.width-third {
min-width: fit-content;
width: 33%;
}
.inline-block {
display: inline-block;
}
.block {
display: block;
}
/** nav elements **/
nav ul {
list-style-type: none;
}
nav li {
margin-right: var(--nav-margin);
}
nav li a::after {
content: '| ";
}
nav li a {
color: black;
}
.header-bg {
background-color: var(--header-bg);
}
.heavy-bottom-border {
border-bottom: var(--border-width) solid var(--heavy-accent)
}
.selected {
border-style: none; border-style: none;
border-bottom-style: var(--tab-border-style); border-bottom-style: var(--tab-border-style);
border-bottom-color: var(--tab-border-color); border-bottom-color: var(--tab-border-color);
@ -74,10 +168,40 @@ nav>h1 {
display: inline; display: inline;
vertical-align: middle; vertical-align: middle;
text-align: left; text-align: left;
color: black;
}
main {
border-bottom-left-radius: 1em;
padding: 1em;
width: 100%;
overflow-block: scroll;
}
.cell {
margin: 1em;
width: var(--cell-target);
}
.justify-end {
justify-content: flex-end;
}
.menu-bg {
background-color: var(--menu-bg);
}
.pad-left {
padding-left: .5em;
}
.app nav li {
margin-bottom: var(--nav-margin);
} }
.destructive { .destructive {
background-color: firebrick !important; background-color: #CD5C08 !important;
font-weight: bold;
} }
.item-count-inc-dec { .item-count-inc-dec {
@ -129,24 +253,3 @@ nav>h1 {
opacity: 0 opacity: 0
} }
} }
.recipe_selector {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
align-items: stretch;
align-content: stretch;
}
.recipe_selector .cell {
margin: 1em;
width: calc(100% / 5);
}
.cell {
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: stretch;
align-content: stretch;
}