diff --git a/web/index.html b/web/index.html index 19de9be..aa42eaf 100644 --- a/web/index.html +++ b/web/index.html @@ -19,7 +19,6 @@ - @@ -35,4 +34,4 @@ - \ No newline at end of file + diff --git a/web/src/components/header.rs b/web/src/components/header.rs index bda1fc4..7f53e43 100644 --- a/web/src/components/header.rs +++ b/web/src/components/header.rs @@ -23,9 +23,9 @@ pub fn Header<'ctx, G: Html>(cx: Scope<'ctx>, h: StateHandler<'ctx>) -> View 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()) } } diff --git a/web/src/components/number_field.rs b/web/src/components/number_field.rs index e0fd487..92b07b8 100644 --- a/web/src/components/number_field.rs +++ b/web/src/components/number_field.rs @@ -211,6 +211,7 @@ where F: Fn(Event), { name: String, + class: String, on_change: Option, min: f64, counter: &'ctx Signal, @@ -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); diff --git a/web/src/components/plan_list.rs b/web/src/components/plan_list.rs index c7e8fb6..1e44bca 100644 --- a/web/src/components/plan_list.rs +++ b/web/src/components/plan_list.rs @@ -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" } } diff --git a/web/src/components/recipe.rs b/web/src/components/recipe.rs index 2e9ca50..ab9c737 100644 --- a/web/src/components/recipe.rs +++ b/web/src/components/recipe.rs @@ -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" } " " + } } } diff --git a/web/src/components/recipe_plan.rs b/web/src/components/recipe_plan.rs index 31c6968..845e1e0 100644 --- a/web/src/components/recipe_plan.rs +++ b/web/src/components/recipe_plan.rs @@ -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" } " " diff --git a/web/src/components/recipe_selection.rs b/web/src/components/recipe_selection.rs index 4fb66c7..3e6777b 100644 --- a/web/src/components/recipe_selection.rs +++ b/web/src/components/recipe_selection.rs @@ -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)); })) diff --git a/web/src/components/shopping_list.rs b/web/src/components/shopping_list.rs index 772923b..b59cb71 100644 --- a/web/src/components/shopping_list.rs +++ b/web/src/components/shopping_list.rs @@ -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" } " " diff --git a/web/src/components/staples.rs b/web/src/components/staples.rs index 3cb420f..98894e2 100644 --- a/web/src/components/staples.rs +++ b/web/src/components/staples.rs @@ -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"); diff --git a/web/src/components/tabs.rs b/web/src/components/tabs.rs index 2e876ef..b907c58 100644 --- a/web/src/components/tabs.rs +++ b/web/src/components/tabs.rs @@ -47,12 +47,12 @@ pub fn TabbedView<'a, G: Html>(cx: Scope<'a>, state: TabState<'a, G>) -> View .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) } } diff --git a/web/src/pages/login.rs b/web/src/pages/login.rs index 09d3a0d..811ea60 100644 --- a/web/src/pages/login.rs +++ b/web/src/pages/login.rs @@ -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 != "" { diff --git a/web/src/pages/planning/select.rs b/web/src/pages/planning/select.rs index b361879..f037003 100644 --- a/web/src/pages/planning/select.rs +++ b/web/src/pages/planning/select.rs @@ -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"); })))) diff --git a/web/src/routing/mod.rs b/web/src/routing/mod.rs index 5108c27..1bd2033 100644 --- a/web/src/routing/mod.rs +++ b/web/src/routing/mod.rs @@ -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| { 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 { } } + } } }, ) diff --git a/web/static/app.css b/web/static/app.css index 70a6006..eb6be9d 100644 --- a/web/static/app.css +++ b/web/static/app.css @@ -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; -}