Compare commits

...

21 Commits

Author SHA1 Message Date
5be85e7b6b build: get rid of wasm-pack
It get's doing naughty things with network access.
2024-06-23 12:50:24 -04:00
24f045e757 build: fix rust-tls resolver issues 2024-06-23 11:23:57 -04:00
e048767773 build: use rustls 2024-03-10 15:13:39 -04:00
b52fdedb58 maint: having the wasm-pack version in the logs is useful 2024-02-20 17:48:17 -05:00
4230eabcae fix: Unsafe recursive object use 2024-02-20 17:12:16 -05:00
40ff02bb46 Alloy models for browser_state 2024-01-28 16:56:36 -05:00
8de0307e44 Add some models 2024-01-28 15:09:24 -05:00
63fec9f6df Display current plan date at the top 2024-01-18 17:13:17 -05:00
bd058150ed NOTE comment. 2024-01-06 10:25:55 -05:00
579a726dd8 cargo fmt 2024-01-05 18:58:48 -05:00
e393047448 Stop using singular for normalization 2024-01-03 15:26:33 -05:00
c00865e633 Have a packaging unit for measures 2024-01-03 15:14:29 -05:00
d97913e676 ui: more layout tweaks 2023-12-25 13:40:42 -06:00
4aaa2b1a06 ui: normalization and font tweaks 2023-12-04 18:54:09 -05:00
388fbc9ee4 ui: Typography tweaks 2023-12-04 15:05:45 -05:00
550d92179b docs: comments for the event handling in login 2023-12-04 14:48:50 -05:00
3cc52a06a3 ui: Menu font sizes 2023-12-04 14:48:31 -05:00
d282da4b76 maint: upgrade wasm-bindgen version 2023-12-02 15:29:47 -05:00
40f0b5db66 maint: Use gloo_net directly 2023-12-02 15:29:24 -05:00
149dc8961e fix: Issue with request blocking occuring on login 2023-12-02 15:28:08 -05:00
4a5efd52de UI: Cleansheet CSS redesign
Initial skeleton and layout is working.
Still needs a bunch of tweaks.
2023-12-02 09:44:19 -05:00
42 changed files with 1182 additions and 354 deletions

107
Cargo.lock generated
View File

@ -224,7 +224,7 @@ checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -383,7 +383,7 @@ dependencies = [
"regex", "regex",
"rustc-hash", "rustc-hash",
"shlex", "shlex",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -750,7 +750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
dependencies = [ dependencies = [
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -777,7 +777,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"scratch", "scratch",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -794,7 +794,7 @@ checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -983,7 +983,7 @@ checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -1056,14 +1056,15 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]] [[package]]
name = "gloo-net" name = "gloo-net"
version = "0.1.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2899cb1a13be9020b010967adc6b2a8a343b6f1428b90238c9d53ca24decc6db" checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"gloo-utils", "gloo-utils",
"http",
"js-sys", "js-sys",
"pin-project", "pin-project",
"serde", "serde",
@ -1088,9 +1089,9 @@ dependencies = [
[[package]] [[package]]
name = "gloo-utils" name = "gloo-utils"
version = "0.1.6" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8e8fc851e9c7b9852508bc6e3f690f452f474417e8545ec9857b7f7377036b5" checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"serde", "serde",
@ -1233,9 +1234,9 @@ dependencies = [
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.8" version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@ -1448,10 +1449,10 @@ dependencies = [
"base64 0.21.0", "base64 0.21.0",
"chrono", "chrono",
"console_error_panic_hook", "console_error_panic_hook",
"gloo-net",
"js-sys", "js-sys",
"maud", "maud",
"recipes", "recipes",
"reqwasm",
"serde", "serde",
"serde_json", "serde_json",
"sycamore", "sycamore",
@ -1602,7 +1603,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -1658,7 +1659,7 @@ checksum = "731f8ecebd9f3a4aa847dfe75455e4757a45da40a7793d2f0b1f9b6ed18b23f3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -1923,7 +1924,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -1989,7 +1990,7 @@ dependencies = [
"proc-macro-error-attr", "proc-macro-error-attr",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
"version_check", "version_check",
] ]
@ -2006,9 +2007,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.49" version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -2044,9 +2045,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.23" version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -2133,15 +2134,6 @@ version = "0.6.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]]
name = "reqwasm"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b89870d729c501fa7a68c43bf4d938bbb3a8c156d333d90faa0e8b3e3212fb"
dependencies = [
"gloo-net",
]
[[package]] [[package]]
name = "ring" name = "ring"
version = "0.16.20" version = "0.16.20"
@ -2186,7 +2178,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rust-embed-utils", "rust-embed-utils",
"syn", "syn 1.0.107",
"walkdir", "walkdir",
] ]
@ -2311,7 +2303,7 @@ checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -2525,7 +2517,7 @@ dependencies = [
"sha2 0.10.6", "sha2 0.10.6",
"sqlx-core", "sqlx-core",
"sqlx-rt", "sqlx-rt",
"syn", "syn 1.0.107",
"url", "url",
] ]
@ -2618,7 +2610,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -2653,7 +2645,7 @@ dependencies = [
"nom", "nom",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
"unicode-xid", "unicode-xid",
] ]
@ -2694,6 +2686,17 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "syn"
version = "2.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]] [[package]]
name = "sync_wrapper" name = "sync_wrapper"
version = "0.1.1" version = "0.1.1"
@ -2732,7 +2735,7 @@ checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -2823,7 +2826,7 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -2938,7 +2941,7 @@ checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -3158,28 +3161,26 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.84" version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"serde",
"serde_json",
"wasm-bindgen-macro", "wasm-bindgen-macro",
] ]
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.84" version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.39",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3197,9 +3198,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.84" version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -3207,22 +3208,22 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.84" version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.39",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.84" version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
[[package]] [[package]]
name = "wasm-bindgen-test" name = "wasm-bindgen-test"
@ -3269,7 +3270,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"str_inflector", "str_inflector",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]

28
flake.lock generated
View File

@ -31,21 +31,6 @@
"type": "github" "type": "github"
} }
}, },
"flake-utils_2": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": { "gitignore": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -96,11 +81,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1679174867, "lastModified": 1719152388,
"narHash": "sha256-fFxb8wN3bjOMvHPr63Iyzo3cuHhQzWW03UkckfTeBWU=", "narHash": "sha256-pHg0nzAa2ZM+zFamfsY7ZvVaB19pMr5wl4G5nO0J7eU=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "f5ec87b82832736f1624874fd34eb60c0b68bdd6", "rev": "be54c7d931a68ba6a79f097ce979288e90a74288",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -121,17 +106,16 @@
}, },
"rust-overlay": { "rust-overlay": {
"inputs": { "inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1678397831, "lastModified": 1718681902,
"narHash": "sha256-7xbxSoiht8G+Zgz55R0ILPsTdbnksILCDMIxeg8Buns=", "narHash": "sha256-E/T7Ge6ayEQe7FVKMJqDBoHyLhRhjc6u9CmU8MyYfy0=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "bdf08e2f43488283eeb25b4a7e7ecba9147a955c", "rev": "16c8ad83297c278eebe740dea5491c1708960dd1",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -25,7 +25,7 @@
let let
overlays = [ rust-overlay.overlays.default ]; overlays = [ rust-overlay.overlays.default ];
pkgs = import nixpkgs { inherit system overlays; }; pkgs = import nixpkgs { inherit system overlays; };
rust-wasm = pkgs.rust-bin.stable."1.68.0".default.override { rust-wasm = pkgs.rust-bin.stable."1.77.0".default.override {
extensions = [ "rust-src" ]; extensions = [ "rust-src" ];
# Add wasm32 as an extra target besides the native target. # Add wasm32 as an extra target besides the native target.
targets = [ "wasm32-unknown-unknown" ]; targets = [ "wasm32-unknown-unknown" ];

View File

@ -2,4 +2,4 @@
fn main() { fn main() {
// trigger recompilation when a new migration is added // trigger recompilation when a new migration is added
println!("cargo:rerun-if-changed=migrations"); println!("cargo:rerun-if-changed=migrations");
} }

View File

@ -1,4 +1,3 @@
use std::collections::BTreeMap;
// Copyright 2022 Jeremy Wall // Copyright 2022 Jeremy Wall
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
@ -12,6 +11,7 @@ use std::collections::BTreeMap;
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::collections::BTreeMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::{collections::BTreeSet, net::SocketAddr}; use std::{collections::BTreeSet, net::SocketAddr};

51
models/browser_state.als Normal file
View File

@ -0,0 +1,51 @@
sig Id {}
sig Text {}
sig Recipe {
, id: one Id
, text: one Text
}
fact {
no r1, r2: Recipe | (r1.id = r2.id) and (r1.text != r2.text)
no r1, r2: Recipe | (r1 != r2) and (r1.id = r2.id)
}
sig Ingredient {}
sig Modifier {}
sig Amt {}
sig ModifiedInventory {
, ingredient: one Ingredient
, modifier: lone Modifier
, amt: one Amt
}
fact {
no mi1, mi2: ModifiedInventory | mi1 != mi2 && (mi1.ingredient = mi2.ingredient) and (mi1.modifier = mi2.modifier)
}
sig DeletedInventory {
, ingredient: one Ingredient
, modifier: lone Modifier
}
fact {
no mi1, mi2: DeletedInventory | mi1 != mi2 && (mi1.ingredient = mi2.ingredient) and (mi1.modifier = mi2.modifier)
}
sig ExtraItems {
, ingredient: one Ingredient
, amt: one Amt
}
sig State {
, recipes: some Recipe
, modified: set ModifiedInventory
, deleted: set DeletedInventory
, extras: set ExtraItems
} {
no rs: Recipe | rs not in recipes
}
run { } for 3 but exactly 2 State, 2 Modifier, exactly 3 ModifiedInventory, exactly 9 Ingredient

17
models/planning.d2 Normal file
View File

@ -0,0 +1,17 @@
Meal Planning: {
shape: sequence_diagram
user: Cook; client: Kitchen frontend; kitchen: Kitchen backend
user -> client: Start new meal Plan
client -> kitchen: new plan created
user -> client: Add recipe to meal plan
client -> kitchen: Update meal plan with recipe
client -> client: cache updated meal plan
user -> client: Do inventory
client -> kitchen: Store inventory mutations
client -> client: cache inventory mutations
user -> client: Undo mutation
client -> kitchen: Store inventory mutations
client -> client: cache inventory mutations
user -> user: Cook recipes
}

125
models/planning.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 94 KiB

View File

@ -34,12 +34,20 @@ stdenv.mkDerivation {
''; '';
# TODO(jwall): Build this from the root rather than the src. # TODO(jwall): Build this from the root rather than the src.
buildPhase = '' buildPhase = ''
set -x
echo building with wasm-pack echo building with wasm-pack
wasm-pack --version
mkdir -p $out mkdir -p $out
cd web cd web
cp -r static $out cp -r static $out
RUST_LOG=info wasm-pack build --mode no-install --release --target web --out-dir $out ${features}; cargo build --lib --release --target wasm32-unknown-unknown --target-dir $out --offline
wasm-bindgen $out/wasm32-unknown-unknown/release/kitchen_wasm.wasm --out-dir $out --typescript --target web
wasm-opt $out/kitchen_wasm_bg.wasm -o $out/kitchen_wasm_bg-opt.wasm -O
rm -f $out/kitchen_wasm_bg.wasm
mv $out/kitchen_wasm_bg-opt.wasm $out/kitchen_wasm_bg.wasm
cp -r index.html $out cp -r index.html $out
cp -r favicon.ico $out cp -r favicon.ico $out
rm -rf $out/release
rm -rf $out/wasm32-unknown-unknown
''; '';
} }

View File

@ -20,14 +20,14 @@ rustPlatform.buildRustPackage rec {
pname = "wasm-bindgen-cli"; pname = "wasm-bindgen-cli";
# NOTE(jwall): This must exactly match the version of the wasm-bindgen crate # NOTE(jwall): This must exactly match the version of the wasm-bindgen crate
# we are using. # we are using.
version = "0.2.84"; version = "0.2.89";
src = fetchCrate { src = fetchCrate {
inherit pname version; inherit pname version;
sha256 = "sha256-0rK+Yx4/Jy44Fw5VwJ3tG243ZsyOIBBehYU54XP/JGk="; sha256 = "sha256-IPxP68xtNSpwJjV2yNMeepAS0anzGl02hYlSTvPocz8=";
}; };
cargoSha256 = "sha256-vcpxcRlW1OKoD64owFF6mkxSqmNrvY+y3Ckn5UwEQ50="; cargoSha256 = "sha256-pBeQaG6i65uJrJptZQLuIaCb/WCQMhba1Z1OhYqA8Zc=";
nativeBuildInputs = [ pkg-config ]; nativeBuildInputs = [ pkg-config ];
@ -36,5 +36,5 @@ rustPlatform.buildRustPackage rec {
nativeCheckInputs = [ nodejs ]; nativeCheckInputs = [ nodejs ];
# other tests require it to be ran in the wasm-bindgen monorepo # other tests require it to be ran in the wasm-bindgen monorepo
cargoTestFlags = [ "--test=interface-types" ]; cargoTestFlags = [ "--test=reference" ];
} }

View File

@ -156,16 +156,28 @@ impl IngredientAccumulator {
set.insert(recipe_title.clone()); set.insert(recipe_title.clone());
self.inner.insert(key, (i.clone(), set)); self.inner.insert(key, (i.clone(), set));
} else { } else {
let amt = match (self.inner[&key].0.amt, i.amt) { let amts = match (&self.inner[&key].0.amt, &i.amt) {
(Volume(rvm), Volume(lvm)) => Volume(lvm + rvm), (Volume(rvm), Volume(lvm)) => vec![Volume(lvm + rvm)],
(Count(lqty), Count(rqty)) => Count(lqty + rqty), (Count(lqty), Count(rqty)) => vec![Count(lqty + rqty)],
(Weight(lqty), Weight(rqty)) => Weight(lqty + rqty), (Weight(lqty), Weight(rqty)) => vec![Weight(lqty + rqty)],
(Package(lnm, lqty), Package(rnm, rqty)) => {
if lnm == rnm {
vec![Package(lnm.clone(), lqty + rqty)]
} else {
vec![
Package(lnm.clone(), lqty.clone()),
Package(rnm.clone(), rqty.clone()),
]
}
}
_ => unreachable!(), _ => unreachable!(),
}; };
self.inner.get_mut(&key).map(|(i, set)| { for amt in amts {
i.amt = amt; self.inner.get_mut(&key).map(|(i, set)| {
set.insert(recipe_title.clone()); i.amt = amt;
}); set.insert(recipe_title.clone());
});
}
} }
} }
} }

View File

@ -334,7 +334,14 @@ make_fn!(unit<StrIter, String>,
text_token!("kg"), text_token!("kg"),
text_token!("grams"), text_token!("grams"),
text_token!("gram"), text_token!("gram"),
text_token!("g")), text_token!("g"),
text_token!("pkg"),
text_token!("package"),
text_token!("bottle"),
text_token!("bot"),
text_token!("bag"),
text_token!("can")
),
_ => ws, _ => ws,
(u.to_lowercase().to_singular()) (u.to_lowercase().to_singular())
) )
@ -393,6 +400,7 @@ pub fn measure(i: StrIter) -> abortable_parser::Result<StrIter, Measure> {
"oz" => Weight(Oz(qty)), "oz" => Weight(Oz(qty)),
"kg" | "kilogram" => Weight(Kilogram(qty)), "kg" | "kilogram" => Weight(Kilogram(qty)),
"g" | "gram" => Weight(Gram(qty)), "g" | "gram" => Weight(Gram(qty)),
"pkg" | "package" | "can" | "bag" | "bottle" | "bot" => Measure::pkg(s, qty),
_u => { _u => {
eprintln!("Invalid unit: {}", _u); eprintln!("Invalid unit: {}", _u);
unreachable!() unreachable!()
@ -418,9 +426,8 @@ pub fn normalize_name(name: &str) -> String {
// NOTE(jwall): The below unwrap is safe because of the length // NOTE(jwall): The below unwrap is safe because of the length
// check above. // check above.
let last = parts.last().unwrap(); let last = parts.last().unwrap();
let normalized = last.to_singular();
prefix.push(' '); prefix.push(' ');
prefix.push_str(&normalized); prefix.push_str(&last.to_string());
return prefix; return prefix;
} }
return name.trim().to_lowercase().to_owned(); return name.trim().to_lowercase().to_owned();

View File

@ -235,32 +235,30 @@ fn test_ingredient_name_parse() {
#[test] #[test]
fn test_ingredient_parse() { fn test_ingredient_parse() {
for (i, expected) in vec![ for (i, expected) in vec![
//( (
// "1 cup flour ", "1 cup flour ",
// Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""), Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1)))),
//), ),
//( (
// "\t1 cup flour ", "\t1 cup flour ",
// Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""), Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1)))),
//), ),
//( (
// "1 cup apple (chopped)", "1 cup apple (chopped)",
// Ingredient::new( Ingredient::new(
// "apple", "apple",
// Some("chopped".to_owned()), Some("chopped".to_owned()),
// Volume(Cup(Quantity::Whole(1))), Volume(Cup(Quantity::Whole(1))),
// "", ),
// ), ),
//), (
//( "1 cup apple (chopped) ",
// "1 cup apple (chopped) ", Ingredient::new(
// Ingredient::new( "apple",
// "apple", Some("chopped".to_owned()),
// Some("chopped".to_owned()), Volume(Cup(Quantity::Whole(1))),
// Volume(Cup(Quantity::Whole(1))), ),
// "", ),
// ),
//),
( (
"1 green bell pepper (chopped) ", "1 green bell pepper (chopped) ",
Ingredient::new( Ingredient::new(
@ -269,6 +267,46 @@ fn test_ingredient_parse() {
Count(Quantity::Whole(1)), Count(Quantity::Whole(1)),
), ),
), ),
(
"1 pkg green onion",
Ingredient::new(
"green onion",
None,
Package("pkg".into(), Quantity::Whole(1)),
),
),
(
"1 bottle green onion",
Ingredient::new(
"green onion",
None,
Package("bottle".into(), Quantity::Whole(1)),
),
),
(
"1 bot green onion",
Ingredient::new(
"green onion",
None,
Package("bot".into(), Quantity::Whole(1)),
),
),
(
"1 bag green onion",
Ingredient::new(
"green onion",
None,
Package("bag".into(), Quantity::Whole(1)),
),
),
(
"1 can baked beans",
Ingredient::new(
"baked beans",
None,
Package("can".into(), Quantity::Whole(1)),
),
),
] { ] {
match parse::ingredient(StrIter::new(i)) { match parse::ingredient(StrIter::new(i)) {
ParseResult::Complete(_, ing) => assert_eq!(ing, expected), ParseResult::Complete(_, ing) => assert_eq!(ing, expected),

View File

@ -22,6 +22,7 @@ use std::{
convert::TryFrom, convert::TryFrom,
fmt::Display, fmt::Display,
ops::{Add, Div, Mul, Sub}, ops::{Add, Div, Mul, Sub},
rc::Rc,
}; };
use num_rational::Ratio; use num_rational::Ratio;
@ -179,6 +180,20 @@ impl VolumeMeasure {
macro_rules! volume_op { macro_rules! volume_op {
($trait:ident, $method:ident) => { ($trait:ident, $method:ident) => {
impl $trait for &VolumeMeasure {
type Output = VolumeMeasure;
fn $method(self, lhs: Self) -> Self::Output {
let (l, r) = (self.get_ml(), lhs.get_ml());
let result = ML($trait::$method(l, r));
if self.metric() {
result.normalize()
} else {
result.into_tsp().normalize()
}
}
}
impl $trait for VolumeMeasure { impl $trait for VolumeMeasure {
type Output = Self; type Output = Self;
@ -293,6 +308,20 @@ impl WeightMeasure {
macro_rules! weight_op { macro_rules! weight_op {
($trait:ident, $method:ident) => { ($trait:ident, $method:ident) => {
impl $trait for &WeightMeasure {
type Output = WeightMeasure;
fn $method(self, lhs: Self) -> Self::Output {
let (l, r) = (self.get_grams(), lhs.get_grams());
let result = WeightMeasure::Gram($trait::$method(l, r));
if self.metric() {
result.normalize()
} else {
result.into_oz().normalize()
}
}
}
impl $trait for WeightMeasure { impl $trait for WeightMeasure {
type Output = Self; type Output = Self;
@ -335,18 +364,19 @@ impl Display for WeightMeasure {
use WeightMeasure::{Gram, Kilogram, Oz, Pound}; use WeightMeasure::{Gram, Kilogram, Oz, Pound};
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord)] #[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
/// Measurements in a Recipe with associated units for them. /// Measurements in a Recipe with associated units for them.
pub enum Measure { pub enum Measure {
/// Volume measurements as meter cubed base unit /// Volume measurements as meter cubed base unit
Volume(VolumeMeasure), Volume(VolumeMeasure),
/// Simple count of items /// Simple count of items
Count(Quantity), Count(Quantity),
Package(Rc<str>, Quantity),
/// Weight measure as Grams base unit /// Weight measure as Grams base unit
Weight(WeightMeasure), Weight(WeightMeasure),
} }
use Measure::{Count, Volume, Weight}; use Measure::{Count, Package, Volume, Weight};
impl Measure { impl Measure {
pub fn tsp(qty: Quantity) -> Self { pub fn tsp(qty: Quantity) -> Self {
@ -407,11 +437,16 @@ impl Measure {
Weight(Oz(qty)) Weight(Oz(qty))
} }
pub fn pkg<S: Into<Rc<str>>>(name: S, qty: Quantity) -> Self {
Package(name.into(), qty)
}
pub fn measure_type(&self) -> String { pub fn measure_type(&self) -> String {
match self { match self {
Volume(_) => "Volume", Volume(_) => "Volume",
Count(_) => "Count", Count(_) => "Count",
Weight(_) => "Weight", Weight(_) => "Weight",
Package(_, _) => "Package",
} }
.to_owned() .to_owned()
} }
@ -421,6 +456,7 @@ impl Measure {
Volume(vm) => vm.plural(), Volume(vm) => vm.plural(),
Count(qty) => qty.plural(), Count(qty) => qty.plural(),
Weight(wm) => wm.plural(), Weight(wm) => wm.plural(),
Package(_, qty) => qty.plural(),
} }
} }
@ -429,6 +465,7 @@ impl Measure {
Volume(vm) => Volume(vm.normalize()), Volume(vm) => Volume(vm.normalize()),
Count(qty) => Count(qty.clone()), Count(qty) => Count(qty.clone()),
Weight(wm) => Weight(wm.normalize()), Weight(wm) => Weight(wm.normalize()),
Package(nm, qty) => Package(nm.clone(), qty.clone()),
} }
} }
} }
@ -439,6 +476,7 @@ impl Display for Measure {
Volume(vm) => write!(w, "{}", vm), Volume(vm) => write!(w, "{}", vm),
Count(qty) => write!(w, "{}", qty), Count(qty) => write!(w, "{}", qty),
Weight(wm) => write!(w, "{}", wm), Weight(wm) => write!(w, "{}", wm),
Package(nm, qty) => write!(w, "{} {}", qty, nm),
} }
} }
} }
@ -533,6 +571,26 @@ impl TryFrom<f32> for Quantity {
macro_rules! quantity_op { macro_rules! quantity_op {
($trait:ident, $method:ident) => { ($trait:ident, $method:ident) => {
impl $trait for &Quantity {
type Output = Quantity;
fn $method(self, lhs: Self) -> Self::Output {
match (self, lhs) {
(Whole(rhs), Whole(lhs)) => Frac($trait::$method(
Ratio::from_integer(*rhs),
Ratio::from_integer(*lhs),
)),
(Frac(rhs), Frac(lhs)) => Frac($trait::$method(rhs, lhs)),
(Whole(rhs), Frac(lhs)) => {
Frac($trait::$method(Ratio::from_integer(*rhs), lhs))
}
(Frac(rhs), Whole(lhs)) => {
Frac($trait::$method(rhs, Ratio::from_integer(*lhs)))
}
}
}
}
impl $trait for Quantity { impl $trait for Quantity {
type Output = Self; type Output = Self;

View File

@ -43,12 +43,12 @@ features = ["fmt", "time"]
version = "0.4.22" version = "0.4.22"
features = ["serde"] features = ["serde"]
[dependencies.reqwasm] [dependencies.gloo-net]
version = "0.5.0" version = "0.4.0"
[dependencies.wasm-bindgen] [dependencies.wasm-bindgen]
# we need wasm-bindgen v0.2.84 exactly # we need wasm-bindgen v0.2.84 exactly
version = "= 0.2.84" version = "= 0.2.89"
[dependencies.web-sys] [dependencies.web-sys]
version = "0.3" version = "0.3"
@ -56,6 +56,7 @@ features = [
"Event", "Event",
"InputEvent", "InputEvent",
"CustomEvent", "CustomEvent",
"CustomEventInit",
"EventTarget", "EventTarget",
"History", "History",
"HtmlAnchorElement", "HtmlAnchorElement",

View File

@ -19,7 +19,7 @@
<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/normalize.css">
<link rel="stylesheet" href="/ui/static/app.css"> <link rel="stylesheet" href="/ui/static/app.css">
</head> </head>
@ -35,4 +35,4 @@
</script> </script>
</body> </body>
</html> </html>

View File

@ -15,7 +15,7 @@ use std::collections::{BTreeMap, BTreeSet};
use base64::{self, Engine}; use base64::{self, Engine};
use chrono::NaiveDate; use chrono::NaiveDate;
use reqwasm; use gloo_net;
use serde_json::{from_str, to_string}; use serde_json::{from_str, to_string};
use sycamore::prelude::*; use sycamore::prelude::*;
use tracing::{debug, error, instrument}; use tracing::{debug, error, instrument};
@ -25,7 +25,10 @@ use recipes::{IngredientKey, RecipeEntry};
use wasm_bindgen::JsValue; use wasm_bindgen::JsValue;
use web_sys::Storage; use web_sys::Storage;
use crate::{app_state::{AppState, parse_recipes}, js_lib}; use crate::{
app_state::{parse_recipes, AppState},
js_lib,
};
#[derive(Debug)] #[derive(Debug)]
pub struct Error(String); pub struct Error(String);
@ -66,8 +69,8 @@ impl From<std::string::FromUtf8Error> for Error {
} }
} }
impl From<reqwasm::Error> for Error { impl From<gloo_net::Error> for Error {
fn from(item: reqwasm::Error) -> Self { fn from(item: gloo_net::Error) -> Self {
Error(format!("{:?}", item)) Error(format!("{:?}", item))
} }
} }
@ -94,8 +97,15 @@ impl LocalStore {
pub fn store_app_state(&self, state: &AppState) { pub fn store_app_state(&self, state: &AppState) {
self.migrate_local_store(); self.migrate_local_store();
let state = match to_string(state) {
Ok(state) => state,
Err(err) => {
error!(?err, ?state, "Error deserializing app_state");
return;
}
};
self.store self.store
.set("app_state", &to_string(state).unwrap()) .set("app_state", &state)
.expect("Failed to set our app state"); .expect("Failed to set our app state");
} }
@ -104,7 +114,8 @@ impl LocalStore {
self.store.get("app_state").map_or(None, |val| { self.store.get("app_state").map_or(None, |val| {
val.map(|s| { val.map(|s| {
debug!("Found an app_state object"); debug!("Found an app_state object");
let mut app_state: AppState = from_str(&s).expect("Failed to deserialize app state"); let mut app_state: AppState =
from_str(&s).expect("Failed to deserialize app state");
let recipes = parse_recipes(&self.get_recipes()).expect("Failed to parse recipes"); let recipes = parse_recipes(&self.get_recipes()).expect("Failed to parse recipes");
if let Some(recipes) = recipes { if let Some(recipes) = recipes {
debug!("Populating recipes"); debug!("Populating recipes");
@ -153,12 +164,12 @@ impl LocalStore {
} }
fn migrate_local_store(&self) { fn migrate_local_store(&self) {
for k in self.get_storage_keys() for k in self.get_storage_keys().into_iter().filter(|k| {
.into_iter() k.starts_with("categor") || k == "inventory" || k.starts_with("plan") || k == "staples"
.filter(|k| k.starts_with("categor") || k == "inventory" || k.starts_with("plan") || k == "staples") { }) {
// Deleting old local store key // Deleting old local store key
debug!("Deleting old local store key {}", k); debug!("Deleting old local store key {}", k);
self.store.delete(&k).expect("Failed to delete storage key"); self.store.delete(&k).expect("Failed to delete storage key");
} }
} }
@ -266,13 +277,17 @@ impl HttpStore {
debug!("attempting login request against api."); debug!("attempting login request against api.");
let mut path = self.v2_path(); let mut path = self.v2_path();
path.push_str("/auth"); path.push_str("/auth");
let result = reqwasm::http::Request::get(&path) let request = gloo_net::http::Request::get(&path)
.header( .header(
"Authorization", "authorization",
format!("Basic {}", token68(user, pass)).as_str(), format!("Basic {}", token68(user, pass)).as_str(),
) )
.send() .mode(web_sys::RequestMode::SameOrigin)
.await; .credentials(web_sys::RequestCredentials::SameOrigin)
.build()
.expect("Failed to build request");
debug!(?request, "Sending auth request");
let result = request.send().await;
if let Ok(resp) = &result { if let Ok(resp) = &result {
if resp.status() == 200 { if resp.status() == 200 {
let user_data = resp let user_data = resp
@ -294,7 +309,7 @@ impl HttpStore {
debug!("Retrieving User Account data"); debug!("Retrieving User Account data");
let mut path = self.v2_path(); let mut path = self.v2_path();
path.push_str("/account"); path.push_str("/account");
let result = reqwasm::http::Request::get(&path).send().await; let result = gloo_net::http::Request::get(&path).send().await;
if let Ok(resp) = &result { if let Ok(resp) = &result {
if resp.status() == 200 { if resp.status() == 200 {
let user_data = resp let user_data = resp
@ -315,9 +330,9 @@ impl HttpStore {
pub async fn fetch_categories(&self) -> Result<Option<Vec<(String, String)>>, Error> { pub async fn fetch_categories(&self) -> Result<Option<Vec<(String, String)>>, Error> {
let mut path = self.v2_path(); let mut path = self.v2_path();
path.push_str("/category_map"); path.push_str("/category_map");
let resp = match reqwasm::http::Request::get(&path).send().await { let resp = match gloo_net::http::Request::get(&path).send().await {
Ok(resp) => resp, Ok(resp) => resp,
Err(reqwasm::Error::JsError(err)) => { Err(gloo_net::Error::JsError(err)) => {
error!(path, ?err, "Error hitting api"); error!(path, ?err, "Error hitting api");
return Ok(None); return Ok(None);
} }
@ -345,9 +360,9 @@ impl HttpStore {
pub async fn fetch_recipes(&self) -> Result<Option<Vec<RecipeEntry>>, Error> { pub async fn fetch_recipes(&self) -> Result<Option<Vec<RecipeEntry>>, Error> {
let mut path = self.v2_path(); let mut path = self.v2_path();
path.push_str("/recipes"); path.push_str("/recipes");
let resp = match reqwasm::http::Request::get(&path).send().await { let resp = match gloo_net::http::Request::get(&path).send().await {
Ok(resp) => resp, Ok(resp) => resp,
Err(reqwasm::Error::JsError(err)) => { Err(gloo_net::Error::JsError(err)) => {
error!(path, ?err, "Error hitting api"); error!(path, ?err, "Error hitting api");
return Ok(self.local_store.get_recipes()); return Ok(self.local_store.get_recipes());
} }
@ -375,9 +390,9 @@ impl HttpStore {
let mut path = self.v2_path(); let mut path = self.v2_path();
path.push_str("/recipe/"); path.push_str("/recipe/");
path.push_str(id.as_ref()); path.push_str(id.as_ref());
let resp = match reqwasm::http::Request::get(&path).send().await { let resp = match gloo_net::http::Request::get(&path).send().await {
Ok(resp) => resp, Ok(resp) => resp,
Err(reqwasm::Error::JsError(err)) => { Err(gloo_net::Error::JsError(err)) => {
error!(path, ?err, "Error hitting api"); error!(path, ?err, "Error hitting api");
return Ok(self.local_store.get_recipe_entry(id.as_ref())); return Ok(self.local_store.get_recipe_entry(id.as_ref()));
} }
@ -413,7 +428,7 @@ impl HttpStore {
let mut path = self.v2_path(); let mut path = self.v2_path();
path.push_str("/recipe"); path.push_str("/recipe");
path.push_str(&format!("/{}", recipe.as_ref())); path.push_str(&format!("/{}", recipe.as_ref()));
let resp = reqwasm::http::Request::delete(&path).send().await?; let resp = gloo_net::http::Request::delete(&path).send().await?;
if resp.status() != 200 { if resp.status() != 200 {
Err(format!("Status: {}", resp.status()).into()) Err(format!("Status: {}", resp.status()).into())
} else { } else {
@ -431,10 +446,9 @@ impl HttpStore {
return Err("Recipe Ids can not be empty".into()); return Err("Recipe Ids can not be empty".into());
} }
} }
let serialized = to_string(&recipes).expect("Unable to serialize recipe entries"); let resp = gloo_net::http::Request::post(&path)
let resp = reqwasm::http::Request::post(&path) .json(&recipes)
.body(&serialized) .expect("Failed to set body")
.header("content-type", "application/json")
.send() .send()
.await?; .await?;
if resp.status() != 200 { if resp.status() != 200 {
@ -449,9 +463,9 @@ impl HttpStore {
pub async fn store_categories(&self, categories: &Vec<(String, String)>) -> Result<(), Error> { pub async fn store_categories(&self, categories: &Vec<(String, String)>) -> Result<(), Error> {
let mut path = self.v2_path(); let mut path = self.v2_path();
path.push_str("/category_map"); path.push_str("/category_map");
let resp = reqwasm::http::Request::post(&path) let resp = gloo_net::http::Request::post(&path)
.body(to_string(&categories).expect("Unable to encode categories as json")) .json(&categories)
.header("content-type", "application/json") .expect("Failed to set body")
.send() .send()
.await?; .await?;
if resp.status() != 200 { if resp.status() != 200 {
@ -503,9 +517,9 @@ impl HttpStore {
pub async fn store_plan(&self, plan: Vec<(String, i32)>) -> Result<(), Error> { pub async fn store_plan(&self, plan: Vec<(String, i32)>) -> Result<(), Error> {
let mut path = self.v2_path(); let mut path = self.v2_path();
path.push_str("/plan"); path.push_str("/plan");
let resp = reqwasm::http::Request::post(&path) let resp = gloo_net::http::Request::post(&path)
.body(to_string(&plan).expect("Unable to encode plan as json")) .json(&plan)
.header("content-type", "application/json") .expect("Failed to set body")
.send() .send()
.await?; .await?;
if resp.status() != 200 { if resp.status() != 200 {
@ -525,9 +539,9 @@ impl HttpStore {
path.push_str("/plan"); path.push_str("/plan");
path.push_str("/at"); path.push_str("/at");
path.push_str(&format!("/{}", date)); path.push_str(&format!("/{}", date));
let resp = reqwasm::http::Request::post(&path) let resp = gloo_net::http::Request::post(&path)
.body(to_string(&plan).expect("Unable to encode plan as json")) .json(&plan)
.header("content-type", "application/json") .expect("Failed to set body")
.send() .send()
.await?; .await?;
if resp.status() != 200 { if resp.status() != 200 {
@ -542,7 +556,7 @@ impl HttpStore {
let mut path = self.v2_path(); let mut path = self.v2_path();
path.push_str("/plan"); path.push_str("/plan");
path.push_str("/all"); path.push_str("/all");
let resp = reqwasm::http::Request::get(&path).send().await?; let resp = gloo_net::http::Request::get(&path).send().await?;
if resp.status() != 200 { if resp.status() != 200 {
Err(format!("Status: {}", resp.status()).into()) Err(format!("Status: {}", resp.status()).into())
} else { } else {
@ -561,7 +575,7 @@ impl HttpStore {
path.push_str("/plan"); path.push_str("/plan");
path.push_str("/at"); path.push_str("/at");
path.push_str(&format!("/{}", date)); path.push_str(&format!("/{}", date));
let resp = reqwasm::http::Request::delete(&path).send().await?; let resp = gloo_net::http::Request::delete(&path).send().await?;
if resp.status() != 200 { if resp.status() != 200 {
Err(format!("Status: {}", resp.status()).into()) Err(format!("Status: {}", resp.status()).into())
} else { } else {
@ -577,7 +591,7 @@ impl HttpStore {
path.push_str("/plan"); path.push_str("/plan");
path.push_str("/at"); path.push_str("/at");
path.push_str(&format!("/{}", date)); path.push_str(&format!("/{}", date));
let resp = reqwasm::http::Request::get(&path).send().await?; let resp = gloo_net::http::Request::get(&path).send().await?;
if resp.status() != 200 { if resp.status() != 200 {
Err(format!("Status: {}", resp.status()).into()) Err(format!("Status: {}", resp.status()).into())
} else { } else {
@ -594,7 +608,7 @@ impl HttpStore {
//pub async fn fetch_plan(&self) -> Result<Option<Vec<(String, i32)>>, Error> { //pub async fn fetch_plan(&self) -> Result<Option<Vec<(String, i32)>>, Error> {
// let mut path = self.v2_path(); // let mut path = self.v2_path();
// path.push_str("/plan"); // path.push_str("/plan");
// let resp = reqwasm::http::Request::get(&path).send().await?; // let resp = gloo_net::http::Request::get(&path).send().await?;
// if resp.status() != 200 { // if resp.status() != 200 {
// Err(format!("Status: {}", resp.status()).into()) // Err(format!("Status: {}", resp.status()).into())
// } else { // } else {
@ -623,7 +637,7 @@ impl HttpStore {
path.push_str("/inventory"); path.push_str("/inventory");
path.push_str("/at"); path.push_str("/at");
path.push_str(&format!("/{}", date)); path.push_str(&format!("/{}", date));
let resp = reqwasm::http::Request::get(&path).send().await?; let resp = gloo_net::http::Request::get(&path).send().await?;
if resp.status() != 200 { if resp.status() != 200 {
Err(format!("Status: {}", resp.status()).into()) Err(format!("Status: {}", resp.status()).into())
} else { } else {
@ -658,7 +672,7 @@ impl HttpStore {
> { > {
let mut path = self.v2_path(); let mut path = self.v2_path();
path.push_str("/inventory"); path.push_str("/inventory");
let resp = reqwasm::http::Request::get(&path).send().await?; let resp = gloo_net::http::Request::get(&path).send().await?;
if resp.status() != 200 { if resp.status() != 200 {
Err(format!("Status: {}", resp.status()).into()) Err(format!("Status: {}", resp.status()).into())
} else { } else {
@ -695,13 +709,10 @@ impl HttpStore {
path.push_str(&format!("/{}", date)); path.push_str(&format!("/{}", date));
let filtered_ingredients: Vec<IngredientKey> = filtered_ingredients.into_iter().collect(); let filtered_ingredients: Vec<IngredientKey> = filtered_ingredients.into_iter().collect();
let modified_amts: Vec<(IngredientKey, String)> = modified_amts.into_iter().collect(); let modified_amts: Vec<(IngredientKey, String)> = modified_amts.into_iter().collect();
debug!("Storing inventory data in cache");
let serialized_inventory = to_string(&(filtered_ingredients, modified_amts, extra_items))
.expect("Unable to encode plan as json");
debug!("Storing inventory data via API"); debug!("Storing inventory data via API");
let resp = reqwasm::http::Request::post(&path) let resp = gloo_net::http::Request::post(&path)
.body(&serialized_inventory) .json(&(filtered_ingredients, modified_amts, extra_items))
.header("content-type", "application/json") .expect("Failed to set body")
.send() .send()
.await?; .await?;
if resp.status() != 200 { if resp.status() != 200 {
@ -724,13 +735,10 @@ impl HttpStore {
path.push_str("/inventory"); path.push_str("/inventory");
let filtered_ingredients: Vec<IngredientKey> = filtered_ingredients.into_iter().collect(); let filtered_ingredients: Vec<IngredientKey> = filtered_ingredients.into_iter().collect();
let modified_amts: Vec<(IngredientKey, String)> = modified_amts.into_iter().collect(); let modified_amts: Vec<(IngredientKey, String)> = modified_amts.into_iter().collect();
debug!("Storing inventory data in cache");
let serialized_inventory = to_string(&(filtered_ingredients, modified_amts, extra_items))
.expect("Unable to encode plan as json");
debug!("Storing inventory data via API"); debug!("Storing inventory data via API");
let resp = reqwasm::http::Request::post(&path) let resp = gloo_net::http::Request::post(&path)
.body(&serialized_inventory) .json(&(filtered_ingredients, modified_amts, extra_items))
.header("content-type", "application/json") .expect("Failed to set body")
.send() .send()
.await?; .await?;
if resp.status() != 200 { if resp.status() != 200 {
@ -745,7 +753,7 @@ impl HttpStore {
pub async fn fetch_staples(&self) -> Result<Option<String>, Error> { pub async fn fetch_staples(&self) -> Result<Option<String>, Error> {
let mut path = self.v2_path(); let mut path = self.v2_path();
path.push_str("/staples"); path.push_str("/staples");
let resp = reqwasm::http::Request::get(&path).send().await?; let resp = gloo_net::http::Request::get(&path).send().await?;
if resp.status() != 200 { if resp.status() != 200 {
debug!("Invalid response back"); debug!("Invalid response back");
Err(format!("Status: {}", resp.status()).into()) Err(format!("Status: {}", resp.status()).into())
@ -759,15 +767,15 @@ impl HttpStore {
} }
} }
pub async fn store_staples<S: AsRef<str>>(&self, content: S) -> Result<(), Error> { pub async fn store_staples<S: AsRef<str> + serde::Serialize>(
&self,
content: S,
) -> Result<(), Error> {
let mut path = self.v2_path(); let mut path = self.v2_path();
path.push_str("/staples"); path.push_str("/staples");
let serialized_staples: String = let resp = gloo_net::http::Request::post(&path)
to_string(content.as_ref()).expect("Failed to serialize staples to json"); .json(&content)
.expect("Failed to set body")
let resp = reqwasm::http::Request::post(&path)
.body(&serialized_staples)
.header("content-type", "application/json")
.send() .send()
.await?; .await?;
if resp.status() != 200 { if resp.status() != 200 {

View File

@ -174,8 +174,9 @@ impl StateMachine {
local_store: &LocalStore, local_store: &LocalStore,
original: &Signal<AppState>, original: &Signal<AppState>,
) -> Result<(), crate::api::Error> { ) -> Result<(), crate::api::Error> {
// TODO(jwall): We use a linear Signal in here to ensure that we only // NOTE(jwall): We use a linear Signal in here to ensure that we only
// call set on the signal once. // call set on the signal once. When the LinearSignal get's dropped it
// will call set on the contained Signal.
let mut original: LinearSignal<AppState> = original.into(); let mut original: LinearSignal<AppState> = original.into();
if let Some(state) = local_store.fetch_app_state() { if let Some(state) = local_store.fetch_app_state() {
original = original.update(state); original = original.update(state);

View File

@ -49,7 +49,7 @@ fn CategoryRow<'ctx, G: Html>(cx: Scope<'ctx>, props: CategoryRowProps<'ctx>) ->
}); });
view! {cx, view! {cx,
tr() { tr() {
td() { td(class="margin-bot-1 border-bottom") {
(ingredient_clone) br() (ingredient_clone) br()
Indexed( Indexed(
iterable=recipes, iterable=recipes,

View File

@ -17,8 +17,8 @@ use sycamore::prelude::*;
#[component] #[component]
pub fn Footer<G: Html>(cx: Scope) -> View<G> { pub fn Footer<G: Html>(cx: Scope) -> View<G> {
view! {cx, view! {cx,
nav(class="no-print") { nav(class="no-print menu-font") {
ul { ul(class="no-list") {
li { a(href="https://github.com/zaphar/kitchen") { "On Github" } } li { a(href="https://github.com/zaphar/kitchen") { "On Github" } }
} }
} }

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 menu-font") {
h1(class="title") { "Kitchen" } h1(class="title") { "Kitchen" }
ul { ul(class="row-flex align-center no-list") {
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

@ -14,9 +14,9 @@
use maud::html; use maud::html;
use sycamore::prelude::*; use sycamore::prelude::*;
use tracing::{debug, error}; use tracing::{debug, error};
use wasm_bindgen::JsCast; use wasm_bindgen::{JsCast, JsValue};
use wasm_web_component::{web_component, WebComponentBinding}; use wasm_web_component::{web_component, WebComponentBinding};
use web_sys::{CustomEvent, Event, HtmlElement, InputEvent, ShadowRoot, window}; use web_sys::{window, CustomEvent, CustomEventInit, Event, HtmlElement, InputEvent, ShadowRoot};
use crate::js_lib::LogFailures; use crate::js_lib::LogFailures;
@ -135,11 +135,10 @@ impl WebComponentBinding for NumberSpinner {
return; return;
} }
}; };
let mut eventDict = CustomEventInit::new();
eventDict.detail(&JsValue::from_f64(self.value as f64));
element element
.set_attribute("val", &format!("{}", self.value)) .dispatch_event(&CustomEvent::new_with_event_init_dict("updated", &eventDict).unwrap())
.swallow_and_log();
element
.dispatch_event(&CustomEvent::new("updated").unwrap())
.unwrap(); .unwrap();
debug!("Dispatched updated event"); debug!("Dispatched updated event");
} }
@ -147,13 +146,18 @@ impl WebComponentBinding for NumberSpinner {
fn attribute_changed_mut( fn attribute_changed_mut(
&mut self, &mut self,
_element: &web_sys::HtmlElement, _element: &web_sys::HtmlElement,
name: wasm_bindgen::JsValue, name: JsValue,
old_value: wasm_bindgen::JsValue, old_value: JsValue,
new_value: wasm_bindgen::JsValue, new_value: JsValue,
) { ) {
let nval_el = self.get_input_el(); let nval_el = self.get_input_el();
let name = name.as_string().unwrap(); let name = name.as_string().unwrap();
debug!(?name, ?old_value, ?new_value, "COUNTS: handling attribute change"); debug!(
?name,
?old_value,
?new_value,
"COUNTS: handling attribute change"
);
match name.as_str() { match name.as_str() {
"val" => { "val" => {
debug!("COUNTS: got an updated value"); debug!("COUNTS: got an updated value");
@ -208,9 +212,10 @@ impl WebComponentBinding for NumberSpinner {
#[derive(Props)] #[derive(Props)]
pub struct NumberProps<'ctx, F> pub struct NumberProps<'ctx, F>
where where
F: Fn(Event), F: Fn(CustomEvent),
{ {
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>,
@ -219,10 +224,11 @@ where
#[component] #[component]
pub fn NumberField<'ctx, F, G: Html>(cx: Scope<'ctx>, props: NumberProps<'ctx, F>) -> View<G> pub fn NumberField<'ctx, F, G: Html>(cx: Scope<'ctx>, props: NumberProps<'ctx, F>) -> View<G>
where where
F: Fn(web_sys::Event) + 'ctx, F: Fn(CustomEvent) + 'ctx,
{ {
let NumberProps { let NumberProps {
name, name,
class,
on_change, on_change,
min, min,
counter, counter,
@ -231,21 +237,13 @@ where
// TODO(jwall): I'm pretty sure this triggers: https://github.com/sycamore-rs/sycamore/issues/602 // TODO(jwall): I'm pretty sure this triggers: https://github.com/sycamore-rs/sycamore/issues/602
// Which means I probably have to wait till v0.9.0 drops or switch to leptos. // Which means I probably have to wait till v0.9.0 drops or switch to leptos.
let id = name.clone(); let id = name.clone();
create_effect(cx, move || { let initial_count = *counter.get();
let new_count = *counter.get();
debug!(new_count, "COUNTS: Updating spinner with new value");
if let Some(el) = window().unwrap().document().unwrap().get_element_by_id(id.as_str()) {
debug!("COUNTS: found element");
el.set_attribute("val", new_count.to_string().as_str()).unwrap();
}
});
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=(initial_count), min=min, on:updated=move |evt: Event| {
let target: HtmlElement = evt.target().unwrap().dyn_into().unwrap(); let event = evt.unchecked_into::<CustomEvent>();
let val: f64 = target.get_attribute("val").unwrap().parse().unwrap(); let val: f64 = event.detail().as_f64().unwrap();
counter.set(val); counter.set(val);
on_change.as_ref().map(|f| f(evt)); on_change.as_ref().map(|f| f(event));
debug!(counter=%(counter.get_untracked()), "set counter to new value"); debug!(counter=%(counter.get_untracked()), "set counter to new value");
}) })
} }

View File

@ -1,4 +1,3 @@
use chrono::NaiveDate;
// Copyright 2023 Jeremy Wall (Jeremy@marzhilsltudios.com) // Copyright 2023 Jeremy Wall (Jeremy@marzhilsltudios.com)
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
@ -12,6 +11,7 @@ use chrono::NaiveDate;
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use chrono::NaiveDate;
use sycamore::prelude::*; use sycamore::prelude::*;
use crate::app_state::{Message, StateHandler}; use crate::app_state::{Message, StateHandler};
@ -23,30 +23,25 @@ pub struct PlanListProps<'ctx> {
list: &'ctx ReadSignal<Vec<NaiveDate>>, list: &'ctx ReadSignal<Vec<NaiveDate>>,
} }
// TODO(jwall): We also need a "new plan button"
#[instrument(skip_all, fields(dates=?props.list))] #[instrument(skip_all, fields(dates=?props.list))]
#[component] #[component]
pub fn PlanList<'ctx, G: Html>(cx: Scope<'ctx>, props: PlanListProps<'ctx>) -> View<G> { pub fn PlanList<'ctx, G: Html>(cx: Scope<'ctx>, props: PlanListProps<'ctx>) -> View<G> {
let PlanListProps { sh, list } = props; let PlanListProps { sh, list } = props;
view! {cx, view! {cx,
div() { div() {
table() { div(class="column-flex") {
Indexed( Indexed(
iterable=list, iterable=list,
view=move |cx, date| { view=move |cx, date| {
let date_display = format!("{}", date); let date_display = format!("{}", date);
view!{cx, view!{cx,
tr() { div(class="row-flex margin-bot-half") {
td() { button(class="outline margin-right-1", on:click=move |_| {
span(role="button", class="outline", on:click=move |_| { sh.dispatch(cx, Message::SelectPlanDate(date, None))
sh.dispatch(cx, Message::SelectPlanDate(date, None)) }) { (date_display) }
}) { (date_display) } button(class="destructive", on:click=move |_| {
} sh.dispatch(cx, Message::DeletePlan(date, None))
td() { }) { "Delete Plan" }
span(role="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"); 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" } " "
} }
} }
@ -142,7 +146,7 @@ fn Steps<G: Html>(cx: Scope, steps: Vec<recipes::Step>) -> View<G> {
view! {cx, view! {cx,
div { div {
h3 { "Step " (idx + 1) } h3 { "Step " (idx + 1) }
ul(class="ingredients") { ul(class="ingredients no-list") {
(ingredient_fragments) (ingredient_fragments)
} }
div(class="instructions") { div(class="instructions") {

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 row-flex 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

@ -109,12 +109,12 @@ fn make_ingredients_rows<'ctx, G: Html>(
view! {cx, view! {cx,
tr { tr {
td { td {
input(bind:value=amt_signal, type="text", on:change=move |_| { input(bind:value=amt_signal, class="width-5", type="text", on:change=move |_| {
sh.dispatch(cx, Message::UpdateAmt(k_clone.clone(), amt_signal.get_untracked().as_ref().clone())); sh.dispatch(cx, Message::UpdateAmt(k_clone.clone(), amt_signal.get_untracked().as_ref().clone()));
}) })
} }
td { td {
input(type="button", class="no-print destructive", value="X", on:click={ input(type="button", class="fit-content no-print destructive", value="X", on:click={
move |_| { move |_| {
sh.dispatch(cx, Message::AddFilteredIngredient(k.clone())); sh.dispatch(cx, Message::AddFilteredIngredient(k.clone()));
}}) }})
@ -143,14 +143,14 @@ fn make_extras_rows<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> V
view! {cx, view! {cx,
tr { tr {
td { td {
input(bind:value=amt_signal, type="text", on:change=move |_| { input(bind:value=amt_signal, class="width-5", type="text", on:change=move |_| {
sh.dispatch(cx, Message::UpdateExtra(idx, sh.dispatch(cx, Message::UpdateExtra(idx,
amt_signal.get_untracked().as_ref().clone(), amt_signal.get_untracked().as_ref().clone(),
name_signal.get_untracked().as_ref().clone())); name_signal.get_untracked().as_ref().clone()));
}) })
} }
td { td {
input(type="button", class="no-print destructive", value="X", on:click=move |_| { input(type="button", class="fit-content no-print destructive", value="X", on:click=move |_| {
sh.dispatch(cx, Message::RemoveExtra(idx)); sh.dispatch(cx, Message::RemoveExtra(idx));
}) })
} }
@ -194,9 +194,7 @@ fn make_shopping_table<'ctx, G: Html>(
#[instrument(skip_all)] #[instrument(skip_all)]
#[component] #[component]
pub fn ShoppingList<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> { pub fn ShoppingList<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> {
let show_staples = sh.get_selector(cx, |state| { let show_staples = sh.get_selector(cx, |state| state.get().use_staples);
state.get().use_staples
});
view! {cx, view! {cx,
h1 { "Shopping List " } h1 { "Shopping List " }
label(for="show_staples_cb") { "Show staples" } label(for="show_staples_cb") { "Show staples" }
@ -205,15 +203,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 menu-font-2 flex-item-shrink") {
ul(class="tabs") { ul(class="tabs pad-left no-list row-flex align-center") {
(menu) (menu)
} }
} }
main(class=".conatiner-fluid") { main(class="flex-item-grow content-font") {
(children) (children)
} }
} }

View File

@ -16,7 +16,8 @@ use tracing::error;
use web_sys::{window, Storage, Window}; use web_sys::{window, Storage, Window};
pub fn get_storage() -> Storage { pub fn get_storage() -> Storage {
get_window().local_storage() get_window()
.local_storage()
.expect("Failed to get storage") .expect("Failed to get storage")
.expect("No storage available") .expect("No storage available")
} }
@ -26,8 +27,7 @@ pub fn get_ms_timestamp() -> u32 {
} }
pub fn get_window() -> Window { pub fn get_window() -> Window {
window() window().expect("No window present")
.expect("No window present")
} }
pub trait LogFailures<V, E> { pub trait LogFailures<V, E> {

View File

@ -15,10 +15,10 @@ mod api;
mod app_state; mod app_state;
mod components; mod components;
mod js_lib; mod js_lib;
mod linear;
mod pages; mod pages;
mod routing; mod routing;
mod web; mod web;
mod linear;
use sycamore::prelude::*; use sycamore::prelude::*;
use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::prelude::wasm_bindgen;

View File

@ -24,7 +24,10 @@ pub struct LinearSignal<'ctx, Payload> {
impl<'ctx, Payload> Into<LinearSignal<'ctx, Payload>> for &'ctx Signal<Payload> { impl<'ctx, Payload> Into<LinearSignal<'ctx, Payload>> for &'ctx Signal<Payload> {
fn into(self) -> LinearSignal<'ctx, Payload> { fn into(self) -> LinearSignal<'ctx, Payload> {
LinearSignal { signal: self, nv: None } LinearSignal {
signal: self,
nv: None,
}
} }
} }

View File

@ -27,9 +27,13 @@ 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 |evt: web_sys::Event| {
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());
// NOTE(jwall): This is required if we want to keep the below auth request from
// failing to send with blocked by browser. This is because it's on a click and
// the form tries to do a submit event and aborts our network request.
evt.prevent_default();
if username != "" && password != "" { if username != "" && password != "" {
spawn_local_scoped(cx, async move { spawn_local_scoped(cx, async move {
let store = crate::api::HttpStore::get_from_context(cx); let store = crate::api::HttpStore::get_from_context(cx);

View File

@ -18,9 +18,13 @@ use crate::{app_state::StateHandler, components::recipe_list::*};
#[component] #[component]
pub fn CookPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> { pub fn CookPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> {
let current_plan = sh.get_selector(cx, |state| {
state.get().selected_plan_date
});
view! {cx, view! {cx,
PlanningPage( PlanningPage(
selected=Some("Cook".to_owned()), selected=Some("Cook".to_owned()),
plan_date = current_plan,
) { RecipeList(sh) } ) { RecipeList(sh) }
} }
} }

View File

@ -18,9 +18,13 @@ use crate::{app_state::StateHandler, components::shopping_list::*};
#[component] #[component]
pub fn InventoryPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> { pub fn InventoryPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> {
let current_plan = sh.get_selector(cx, |state| {
state.get().selected_plan_date
});
view! {cx, view! {cx,
PlanningPage( PlanningPage(
selected=Some("Inventory".to_owned()), selected=Some("Inventory".to_owned()),
plan_date = current_plan,
) { ShoppingList(sh) } ) { ShoppingList(sh) }
} }
} }

View File

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use crate::components::tabs::*; use crate::components::tabs::*;
use chrono::NaiveDate;
use sycamore::prelude::*; use sycamore::prelude::*;
pub mod cook; pub mod cook;
@ -25,14 +26,19 @@ pub use plan::*;
pub use select::*; pub use select::*;
#[derive(Props)] #[derive(Props)]
pub struct PageState<'a, G: Html> { pub struct PageState<'ctx, G: Html> {
pub children: Children<'a, G>, pub children: Children<'ctx, G>,
pub selected: Option<String>, pub selected: Option<String>,
pub plan_date: &'ctx ReadSignal<Option<NaiveDate>>,
} }
#[component] #[component]
pub fn PlanningPage<'a, G: Html>(cx: Scope<'a>, state: PageState<'a, G>) -> View<G> { pub fn PlanningPage<'ctx, G: Html>(cx: Scope<'ctx>, state: PageState<'ctx, G>) -> View<G> {
let PageState { children, selected } = state; let PageState {
children,
selected,
plan_date,
} = state;
let children = children.call(cx); let children = children.call(cx);
let planning_tabs: Vec<(String, &'static str)> = vec![ let planning_tabs: Vec<(String, &'static str)> = vec![
("/ui/planning/select".to_owned(), "Select"), ("/ui/planning/select".to_owned(), "Select"),
@ -45,6 +51,10 @@ pub fn PlanningPage<'a, G: Html>(cx: Scope<'a>, state: PageState<'a, G>) -> View
TabbedView( TabbedView(
selected=selected, selected=selected,
tablist=planning_tabs, tablist=planning_tabs,
) { (children) } ) { div {
"Plan Date: " (plan_date.get().map_or(String::from("Unknown"), |d| format!("{}", d)))
}
(children)
}
} }
} }

View File

@ -18,9 +18,13 @@ use sycamore::prelude::*;
#[component] #[component]
pub fn PlanPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> { pub fn PlanPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> {
let current_plan = sh.get_selector(cx, |state| {
state.get().selected_plan_date
});
view! {cx, view! {cx,
PlanningPage( PlanningPage(
selected=Some("Plan".to_owned()), selected=Some("Plan".to_owned()),
plan_date = current_plan,
) { RecipePlan(sh) } ) { RecipePlan(sh) }
} }
} }

View File

@ -32,12 +32,16 @@ pub fn SelectPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vie
plans.sort_unstable_by(|d1, d2| d2.cmp(d1)); plans.sort_unstable_by(|d1, d2| d2.cmp(d1));
plans plans
}); });
let current_plan = sh.get_selector(cx, |state| {
state.get().selected_plan_date
});
view! {cx, view! {cx,
PlanningPage( PlanningPage(
selected=Some("Select".to_owned()), selected=Some("Select".to_owned()),
plan_date = current_plan.clone(),
) { ) {
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,10 @@ 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(class="column-flex") {
Header(sh) Header(sh)
(route_switch(route.get().as_ref(), cx, sh)) (route_switch(route.get().as_ref(), cx, sh))
Footer { } }
}
} }
}, },
) )

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use sycamore::{futures::spawn_local_scoped, prelude::*}; use sycamore::{futures::spawn_local_scoped, prelude::*};
use tracing::{info, debug, instrument}; use tracing::{debug, info, instrument};
use crate::app_state::Message; use crate::app_state::Message;
use crate::{api, routing::Handler as RouteHandler}; use crate::{api, routing::Handler as RouteHandler};

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,34 @@
--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: 1.5rem;
--menu-font-size: 2em;
--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 +61,138 @@
} }
} }
@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 * {
border-style: none; color: black;
font-size: inherit;
} }
nav>ul.tabs>li.selected { a {
text-decoration: none;
}
/** layout classes **/
.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 {
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;
}
.no-list {
list-style-type: none;
}
.fit-content {
width: fit-content;
}
.width-10 {
width: 10em;
}
.width-5 {
width: 5em;
}
.border-bottom {
border-bottom-style: solid;
border-bottom-color: var(--tab-border-color);
border-bottom-width: var(--tab-border-width);
}
.margin-bot-1 {
margin-bottom: 1em;
}
.margin-bot-half {
margin-bottom: .5em;
}
.margin-right-1 {
margin-right: 1em;
}
/** Typography classes **/
.menu-font {
font-size: var(--menu-font-size);
}
.menu-font-2 {
font-size: calc(var(--menu-font-size) / 1.5);
}
.content-font {
font-size: 1.5em;
}
/** element specific styling **/
nav li {
margin-right: var(--nav-margin);
}
/** color and borders **/
.header-bg {
background-color: var(--header-bg);
}
.heavy-bottom-border {
border-bottom: var(--border-width) solid var(--heavy-accent)
}
/** Situational **/
.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 +206,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 +291,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;
}

349
web/static/normalize.css vendored Normal file
View File

@ -0,0 +1,349 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}