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>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" name="viewport"
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">
</head>

View File

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

View File

@ -211,6 +211,7 @@ where
F: Fn(Event),
{
name: String,
class: String,
on_change: Option<F>,
min: f64,
counter: &'ctx Signal<f64>,
@ -223,6 +224,7 @@ where
{
let NumberProps {
name,
class,
on_change,
min,
counter,
@ -241,7 +243,7 @@ where
});
let id = name.clone();
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 val: f64 = target.get_attribute("val").unwrap().parse().unwrap();
counter.set(val);

View File

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

View File

@ -79,12 +79,14 @@ pub fn Editor<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>)
debug!("creating editor view");
view! {cx,
label(for="recipe_category") { "Category" }
input(name="recipe_category", bind:value=category, on:change=move |_| dirty.set(true))
div(class="grid") {
div {
label(for="recipe_text") { "Recipe" }
textarea(name="recipe_text", bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| {
div {
label(for="recipe_category") { "Category" }
input(name="recipe_category", bind:value=category, on:change=move |_| dirty.set(true))
}
div {
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);
check_recipe_parses(text.get_untracked().as_str(), error_text, aria_hint);
}, 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()) }
}
span(role="button", on:click=move |_| {
let unparsed = text.get_untracked();
if check_recipe_parses(unparsed.as_str(), error_text, aria_hint) {
debug!("triggering a save");
if !*dirty.get_untracked() {
debug!("Recipe text is unchanged");
return;
div {
button(on:click=move |_| {
let unparsed = text.get_untracked();
if check_recipe_parses(unparsed.as_str(), error_text, aria_hint) {
debug!("triggering a save");
if !*dirty.get_untracked() {
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");
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);
}
// 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" } " "
// TODO(jwall): Show error message if trying to save when recipe doesn't parse.
}) { "Save" } " "
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,
h2 { (category) }
div(class="recipe_selector no-print") {
div(class="no-print flex-wrap-start align-stretch") {
(View::new_fragment(
rows.get().iter().cloned().map(|r| {
view ! {cx,
@ -61,7 +61,7 @@ pub fn CategoryGroup<'ctx, G: Html>(
view=move |cx, sig| {
let title = create_memo(cx, move || sig.get().1.title.clone());
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(),
@ -108,13 +108,13 @@ pub fn RecipePlan<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vie
},
key=|(ref cat, _)| cat.clone(),
)
span(role="button", on:click=move |_| {
button(on:click=move |_| {
sh.dispatch(cx, Message::LoadState(None));
}) { "Reset" } " "
span(role="button", on:click=move |_| {
button(on:click=move |_| {
sh.dispatch(cx, Message::ResetRecipeCounts);
}) { "Clear All" } " "
span(role="button", on:click=move |_| {
button(on:click=move |_| {
// Poor man's click event signaling.
sh.dispatch(cx, Message::SaveState(None));
}) { "Save Plan" } " "

View File

@ -65,8 +65,8 @@ pub fn RecipeSelection<'ctx, G: Html>(
let name = format!("recipe_id:{}", id);
let for_id = name.clone();
view! {cx,
label(for=for_id) { a(href=href) { (*title) } }
NumberField(name=name, counter=count, min=0.0, on_change=Some(move |_| {
label(for=for_id, class="flex-item-grow") { a(href=href) { (*title) } }
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");
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));
})
(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");
sh.dispatch(cx, Message::AddExtra(String::new(), String::new()));
}) { "Add Item" } " "
span(role="button", class="no-print", on:click=move |_| {
button(class="no-print", on:click=move |_| {
info!("Registering reset request for inventory");
sh.dispatch(cx, Message::ResetInventory);
}) { "Reset" } " "
span(role="button", class="no-print", on:click=move |_| {
button(class="no-print", on:click=move |_| {
info!("Registering save request for inventory");
sh.dispatch(cx, Message::SaveState(None));
}) { "Save" } " "

View File

@ -72,8 +72,8 @@ pub fn IngredientsEditor<'ctx, G: Html>(
debug!("creating editor view");
view! {cx,
div(class="grid") {
textarea(bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| {
div {
textarea(class="width-third", bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| {
dirty.set(true);
}, on:input=move |_| {
let current_ts = js_lib::get_ms_timestamp();
@ -84,7 +84,7 @@ pub fn IngredientsEditor<'ctx, G: Html>(
})
div(class="parse") { (error_text.get()) }
}
span(role="button", on:click=move |_| {
button(on:click=move |_| {
let unparsed = text.get();
if !*dirty.get_untracked() {
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(),
);
view! {cx,
nav {
ul(class="tabs") {
nav(class="menu-bg expand-height") {
ul(class="tabs pad-left") {
(menu)
}
}
main(class=".conatiner-fluid") {
main {
(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)
label(for="password") { "Password" }
input(type="password", bind:value=password)
span(role="button", on:click=move |_| {
button(on:click=move |_| {
info!("Attempting login request");
let (username, password) = ((*username.get_untracked()).clone(), (*password.get_untracked()).clone());
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()),
) {
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(|| {
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(),
view=move |cx: Scope, route: &ReadSignal<Routes>| {
view!{cx,
div(class="app") {
Header(sh)
div {
Header(sh)
div(class="app row-flex flex-item-grow expand-height align-stretch") {
(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");
* you may not use this file except in compliance with the License.
@ -21,12 +21,33 @@
--unicode-button-size: 2em;
--toast-anim-duration: 3s;
--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;
--border-width: 2px;
--border-width: 3px;
--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 {
.no-print,
@ -39,28 +60,101 @@
}
}
@media (min-width: 768px) {
:root {
--font-size: 35px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--tab-border-color: lightgrey;
}
}
/** Resets **/
body {
padding: 10px;
margin: 10px;
margin: 0px;
padding: 0px;
background-color: var(--header-bg);
font-size: var(--font-size)
}
nav>ul.tabs>li {
border-style: none;
body, body * {
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-bottom-style: var(--tab-border-style);
border-bottom-color: var(--tab-border-color);
@ -74,10 +168,40 @@ nav>h1 {
display: inline;
vertical-align: middle;
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 {
background-color: firebrick !important;
background-color: #CD5C08 !important;
font-weight: bold;
}
.item-count-inc-dec {
@ -129,24 +253,3 @@ nav>h1 {
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;
}