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

28
flake.lock generated
View File

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

View File

@ -25,7 +25,7 @@
let
overlays = [ rust-overlay.overlays.default ];
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" ];
# Add wasm32 as an extra target besides the native target.
targets = [ "wasm32-unknown-unknown" ];

View File

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

View File

@ -1,4 +1,3 @@
use std::collections::BTreeMap;
// Copyright 2022 Jeremy Wall
//
// 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.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;
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.
buildPhase = ''
set -x
echo building with wasm-pack
wasm-pack --version
mkdir -p $out
cd web
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 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";
# NOTE(jwall): This must exactly match the version of the wasm-bindgen crate
# we are using.
version = "0.2.84";
version = "0.2.89";
src = fetchCrate {
inherit pname version;
sha256 = "sha256-0rK+Yx4/Jy44Fw5VwJ3tG243ZsyOIBBehYU54XP/JGk=";
sha256 = "sha256-IPxP68xtNSpwJjV2yNMeepAS0anzGl02hYlSTvPocz8=";
};
cargoSha256 = "sha256-vcpxcRlW1OKoD64owFF6mkxSqmNrvY+y3Ckn5UwEQ50=";
cargoSha256 = "sha256-pBeQaG6i65uJrJptZQLuIaCb/WCQMhba1Z1OhYqA8Zc=";
nativeBuildInputs = [ pkg-config ];
@ -36,5 +36,5 @@ rustPlatform.buildRustPackage rec {
nativeCheckInputs = [ nodejs ];
# 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());
self.inner.insert(key, (i.clone(), set));
} else {
let amt = match (self.inner[&key].0.amt, i.amt) {
(Volume(rvm), Volume(lvm)) => Volume(lvm + rvm),
(Count(lqty), Count(rqty)) => Count(lqty + rqty),
(Weight(lqty), Weight(rqty)) => Weight(lqty + rqty),
let amts = match (&self.inner[&key].0.amt, &i.amt) {
(Volume(rvm), Volume(lvm)) => vec![Volume(lvm + rvm)],
(Count(lqty), Count(rqty)) => vec![Count(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!(),
};
self.inner.get_mut(&key).map(|(i, set)| {
i.amt = amt;
set.insert(recipe_title.clone());
});
for amt in amts {
self.inner.get_mut(&key).map(|(i, set)| {
i.amt = amt;
set.insert(recipe_title.clone());
});
}
}
}
}

View File

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

View File

@ -235,32 +235,30 @@ fn test_ingredient_name_parse() {
#[test]
fn test_ingredient_parse() {
for (i, expected) in vec![
//(
// "1 cup flour ",
// Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""),
//),
//(
// "\t1 cup flour ",
// Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""),
//),
//(
// "1 cup apple (chopped)",
// Ingredient::new(
// "apple",
// Some("chopped".to_owned()),
// Volume(Cup(Quantity::Whole(1))),
// "",
// ),
//),
//(
// "1 cup apple (chopped) ",
// Ingredient::new(
// "apple",
// Some("chopped".to_owned()),
// Volume(Cup(Quantity::Whole(1))),
// "",
// ),
//),
(
"1 cup flour ",
Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1)))),
),
(
"\t1 cup flour ",
Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1)))),
),
(
"1 cup apple (chopped)",
Ingredient::new(
"apple",
Some("chopped".to_owned()),
Volume(Cup(Quantity::Whole(1))),
),
),
(
"1 cup apple (chopped) ",
Ingredient::new(
"apple",
Some("chopped".to_owned()),
Volume(Cup(Quantity::Whole(1))),
),
),
(
"1 green bell pepper (chopped) ",
Ingredient::new(
@ -269,6 +267,46 @@ fn test_ingredient_parse() {
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)) {
ParseResult::Complete(_, ing) => assert_eq!(ing, expected),

View File

@ -22,6 +22,7 @@ use std::{
convert::TryFrom,
fmt::Display,
ops::{Add, Div, Mul, Sub},
rc::Rc,
};
use num_rational::Ratio;
@ -179,6 +180,20 @@ impl VolumeMeasure {
macro_rules! volume_op {
($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 {
type Output = Self;
@ -293,6 +308,20 @@ impl WeightMeasure {
macro_rules! weight_op {
($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 {
type Output = Self;
@ -335,18 +364,19 @@ impl Display for WeightMeasure {
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.
pub enum Measure {
/// Volume measurements as meter cubed base unit
Volume(VolumeMeasure),
/// Simple count of items
Count(Quantity),
Package(Rc<str>, Quantity),
/// Weight measure as Grams base unit
Weight(WeightMeasure),
}
use Measure::{Count, Volume, Weight};
use Measure::{Count, Package, Volume, Weight};
impl Measure {
pub fn tsp(qty: Quantity) -> Self {
@ -407,11 +437,16 @@ impl Measure {
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 {
match self {
Volume(_) => "Volume",
Count(_) => "Count",
Weight(_) => "Weight",
Package(_, _) => "Package",
}
.to_owned()
}
@ -421,6 +456,7 @@ impl Measure {
Volume(vm) => vm.plural(),
Count(qty) => qty.plural(),
Weight(wm) => wm.plural(),
Package(_, qty) => qty.plural(),
}
}
@ -429,6 +465,7 @@ impl Measure {
Volume(vm) => Volume(vm.normalize()),
Count(qty) => Count(qty.clone()),
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),
Count(qty) => write!(w, "{}", qty),
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 {
($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 {
type Output = Self;

View File

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

View File

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

View File

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

View File

@ -174,8 +174,9 @@ impl StateMachine {
local_store: &LocalStore,
original: &Signal<AppState>,
) -> Result<(), crate::api::Error> {
// TODO(jwall): We use a linear Signal in here to ensure that we only
// call set on the signal once.
// NOTE(jwall): We use a linear Signal in here to ensure that we only
// 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();
if let Some(state) = local_store.fetch_app_state() {
original = original.update(state);

View File

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

View File

@ -17,8 +17,8 @@ use sycamore::prelude::*;
#[component]
pub fn Footer<G: Html>(cx: Scope) -> View<G> {
view! {cx,
nav(class="no-print") {
ul {
nav(class="no-print menu-font") {
ul(class="no-list") {
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(),
});
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" }
ul {
ul(class="row-flex align-center no-list") {
li { a(href="/ui/planning/select") { "MealPlan" } }
li { a(href="/ui/manage/ingredients") { "Manage" } }
li { a(href="/ui/login") { (login.get()) } }

View File

@ -14,9 +14,9 @@
use maud::html;
use sycamore::prelude::*;
use tracing::{debug, error};
use wasm_bindgen::JsCast;
use wasm_bindgen::{JsCast, JsValue};
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;
@ -135,11 +135,10 @@ impl WebComponentBinding for NumberSpinner {
return;
}
};
let mut eventDict = CustomEventInit::new();
eventDict.detail(&JsValue::from_f64(self.value as f64));
element
.set_attribute("val", &format!("{}", self.value))
.swallow_and_log();
element
.dispatch_event(&CustomEvent::new("updated").unwrap())
.dispatch_event(&CustomEvent::new_with_event_init_dict("updated", &eventDict).unwrap())
.unwrap();
debug!("Dispatched updated event");
}
@ -147,13 +146,18 @@ impl WebComponentBinding for NumberSpinner {
fn attribute_changed_mut(
&mut self,
_element: &web_sys::HtmlElement,
name: wasm_bindgen::JsValue,
old_value: wasm_bindgen::JsValue,
new_value: wasm_bindgen::JsValue,
name: JsValue,
old_value: JsValue,
new_value: JsValue,
) {
let nval_el = self.get_input_el();
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() {
"val" => {
debug!("COUNTS: got an updated value");
@ -208,9 +212,10 @@ impl WebComponentBinding for NumberSpinner {
#[derive(Props)]
pub struct NumberProps<'ctx, F>
where
F: Fn(Event),
F: Fn(CustomEvent),
{
name: String,
class: String,
on_change: Option<F>,
min: f64,
counter: &'ctx Signal<f64>,
@ -219,10 +224,11 @@ where
#[component]
pub fn NumberField<'ctx, F, G: Html>(cx: Scope<'ctx>, props: NumberProps<'ctx, F>) -> View<G>
where
F: Fn(web_sys::Event) + 'ctx,
F: Fn(CustomEvent) + 'ctx,
{
let NumberProps {
name,
class,
on_change,
min,
counter,
@ -231,21 +237,13 @@ where
// 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.
let id = name.clone();
create_effect(cx, move || {
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();
let initial_count = *counter.get();
view! {cx,
number-spinner(id=id, 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();
number-spinner(id=id, class=(class), val=(initial_count), min=min, on:updated=move |evt: Event| {
let event = evt.unchecked_into::<CustomEvent>();
let val: f64 = event.detail().as_f64().unwrap();
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");
})
}

View File

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

View File

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

View File

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

View File

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

View File

@ -109,12 +109,12 @@ fn make_ingredients_rows<'ctx, G: Html>(
view! {cx,
tr {
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()));
})
}
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 |_| {
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,
tr {
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,
amt_signal.get_untracked().as_ref().clone(),
name_signal.get_untracked().as_ref().clone()));
})
}
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));
})
}
@ -194,9 +194,7 @@ fn make_shopping_table<'ctx, G: Html>(
#[instrument(skip_all)]
#[component]
pub fn ShoppingList<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> {
let show_staples = sh.get_selector(cx, |state| {
state.get().use_staples
});
let show_staples = sh.get_selector(cx, |state| state.get().use_staples);
view! {cx,
h1 { "Shopping List " }
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));
})
(make_shopping_table(cx, sh, show_staples))
span(role="button", class="no-print", on:click=move |_| {
button(class="no-print", on:click=move |_| {
info!("Registering add item request for inventory");
sh.dispatch(cx, Message::AddExtra(String::new(), String::new()));
}) { "Add Item" } " "
span(role="button", class="no-print", on:click=move |_| {
button(class="no-print", on:click=move |_| {
info!("Registering reset request for inventory");
sh.dispatch(cx, Message::ResetInventory);
}) { "Reset" } " "
span(role="button", class="no-print", on:click=move |_| {
button(class="no-print", on:click=move |_| {
info!("Registering save request for inventory");
sh.dispatch(cx, Message::SaveState(None));
}) { "Save" } " "

View File

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

View File

@ -47,12 +47,12 @@ pub fn TabbedView<'a, G: Html>(cx: Scope<'a>, state: TabState<'a, G>) -> View<G>
.collect(),
);
view! {cx,
nav {
ul(class="tabs") {
nav(class="menu-bg menu-font-2 flex-item-shrink") {
ul(class="tabs pad-left no-list row-flex align-center") {
(menu)
}
}
main(class=".conatiner-fluid") {
main(class="flex-item-grow content-font") {
(children)
}
}

View File

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

View File

@ -15,10 +15,10 @@ mod api;
mod app_state;
mod components;
mod js_lib;
mod linear;
mod pages;
mod routing;
mod web;
mod linear;
use sycamore::prelude::*;
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> {
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)
label(for="password") { "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");
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 != "" {
spawn_local_scoped(cx, async move {
let store = crate::api::HttpStore::get_from_context(cx);

View File

@ -18,9 +18,13 @@ use crate::{app_state::StateHandler, components::recipe_list::*};
#[component]
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,
PlanningPage(
selected=Some("Cook".to_owned()),
plan_date = current_plan,
) { RecipeList(sh) }
}
}

View File

@ -18,9 +18,13 @@ use crate::{app_state::StateHandler, components::shopping_list::*};
#[component]
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,
PlanningPage(
selected=Some("Inventory".to_owned()),
plan_date = current_plan,
) { ShoppingList(sh) }
}
}

View File

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::components::tabs::*;
use chrono::NaiveDate;
use sycamore::prelude::*;
pub mod cook;
@ -25,14 +26,19 @@ pub use plan::*;
pub use select::*;
#[derive(Props)]
pub struct PageState<'a, G: Html> {
pub children: Children<'a, G>,
pub struct PageState<'ctx, G: Html> {
pub children: Children<'ctx, G>,
pub selected: Option<String>,
pub plan_date: &'ctx ReadSignal<Option<NaiveDate>>,
}
#[component]
pub fn PlanningPage<'a, G: Html>(cx: Scope<'a>, state: PageState<'a, G>) -> View<G> {
let PageState { children, selected } = state;
pub fn PlanningPage<'ctx, G: Html>(cx: Scope<'ctx>, state: PageState<'ctx, G>) -> View<G> {
let PageState {
children,
selected,
plan_date,
} = state;
let children = children.call(cx);
let planning_tabs: Vec<(String, &'static str)> = vec![
("/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(
selected=selected,
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]
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,
PlanningPage(
selected=Some("Plan".to_owned()),
plan_date = current_plan,
) { 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
});
let current_plan = sh.get_selector(cx, |state| {
state.get().selected_plan_date
});
view! {cx,
PlanningPage(
selected=Some("Select".to_owned()),
plan_date = current_plan.clone(),
) {
PlanList(sh=sh, list=plan_dates)
span(role="button", on:click=move |_| {
button(on:click=move |_| {
sh.dispatch(cx, Message::SelectPlanDate(chrono::offset::Local::now().naive_local().date(), Some(Box::new(|| {
sycamore_router::navigate("/ui/planning/plan");
}))))

View File

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

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use sycamore::{futures::spawn_local_scoped, prelude::*};
use tracing::{info, debug, instrument};
use tracing::{debug, info, instrument};
use crate::app_state::Message;
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");
* you may not use this file except in compliance with the License.
@ -21,12 +21,34 @@
--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: 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 {
.no-print,
@ -39,28 +61,138 @@
}
}
@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 * {
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-bottom-style: var(--tab-border-style);
border-bottom-color: var(--tab-border-color);
@ -74,10 +206,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 +291,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;
}

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;
}