Merge branch 'look_and_feel'

This commit is contained in:
Jeremy Wall 2024-07-01 16:02:37 -05:00
commit 6087d31aad
49 changed files with 1228 additions and 361 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]]

View File

@ -1,5 +1,6 @@
[workspace]
members = [ "recipes", "kitchen", "web", "api" ]
resolver = "2"
[patch.crates-io]
# TODO(jwall): When the fix for RcSignal Binding is released we can drop this patch.

View File

@ -14,6 +14,11 @@
mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
mkfile_dir := $(dir $(mkfile_path))
sqlite_url := sqlite://$(mkfile_dir)/.session_store/store.db
out := dist
project := kitchen
export out
export kitchen
kitchen: wasm kitchen/src/*.rs
cd kitchen; cargo build
@ -27,15 +32,18 @@ static-prep: web/index.html web/favicon.ico web/static/*.css
cp -r web/favicon.ico web/dist/
cp -r web/static web/dist/
wasmrelease: wasmrelease-dist static-prep
wasmrelease: wasm-opt static-prep
wasm-opt: wasmrelease-dist
cd web; sh ../scripts/wasm-opt.sh release
wasmrelease-dist: web/src/*.rs web/src/components/*.rs
cd web; wasm-pack build --mode no-install --release --target web --no-typescript --out-name kitchen_wasm --out-dir dist/
cd web; sh ../scripts/wasm-build.sh release
wasm: wasm-dist static-prep
wasm-dist: web/src/*.rs web/src/components/*.rs
cd web; wasm-pack build --mode no-install --target web --no-typescript --out-dir dist/ --features debug_logs
cd web; sh ../scripts/wasm-build.sh debug
clean:
rm -rf web/dist/*
@ -50,5 +58,5 @@ sqlx-add-%:
sqlx-revert:
cd kitchen; cargo sqlx migrate revert --database-url $(sqlite_url)
sqlx-prepare:
sqlx-prepare: kitchen
cd kitchen; cargo sqlx prepare --database-url $(sqlite_url)

13
flake.lock generated
View File

@ -99,11 +99,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": {
@ -124,17 +124,16 @@
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1702001829,
"narHash": "sha256-6gEVidNVqzTb06zIy2Gxhz9m6/jXyAgViRxfgEpZkQ8=",
"lastModified": 1718681902,
"narHash": "sha256-E/T7Ge6ayEQe7FVKMJqDBoHyLhRhjc6u9CmU8MyYfy0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "c2a1dd067a928624c1aab36f976758c0722c79bd",
"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.74.1".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" ];
@ -45,6 +45,14 @@
wasm-bindgen = pkgs.callPackage wasm-bindgenGen { inherit pkgs; };
kitchenWasm = kitchenWasmGen {
inherit pkgs rust-wasm wasm-bindgen version;
lockFile = ./Cargo.lock;
outputHashes = {
# I'm maintaining some patches for these so the lockfile hashes are a little
# incorrect. We override those here.
"wasm-web-component-0.2.0" = "sha256-quuPgzGb2F96blHmD3BAUjsWQYbSyJGZl27PVrwL92k=";
"sycamore-0.8.2" = "sha256-D968+8C5EelGGmot9/LkAlULZOf/Cr+1WYXRCMwb1nQ=";
"sqlx-0.6.2" = "sha256-X/LFvtzRfiOIEZJiVzmFvvULPpjhqvI99pSwH7a//GM=";
};
};
kitchen = (kitchenGen {
inherit pkgs version naersk-lib kitchenWasm rust-wasm;

View File

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

View File

@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE recipes DROP COLUMN serving_count;

View File

@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE recipes ADD column serving_count number;

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

@ -3,23 +3,18 @@
features ? "",
rust-wasm,
wasm-bindgen,
lockFile,
outputHashes,
}:
with pkgs;
let
pname = "kitchen-wasm";
src = ./../..;
lockFile = ./../../Cargo.lock;
# NOTE(jwall): Because we use wasm-pack directly below we need
# the cargo dependencies to already be installed.
cargoDeps = (pkgs.rustPlatform.importCargoLock { inherit lockFile; outputHashes = {
# I'm maintaining some patches for these so the lockfile hashes are a little
# incorrect. We override those here.
"wasm-web-component-0.2.0" = "sha256-quuPgzGb2F96blHmD3BAUjsWQYbSyJGZl27PVrwL92k=";
"sycamore-0.8.2" = "sha256-D968+8C5EelGGmot9/LkAlULZOf/Cr+1WYXRCMwb1nQ=";
"sqlx-0.6.2" = "sha256-X/LFvtzRfiOIEZJiVzmFvvULPpjhqvI99pSwH7a//GM=";
};
});
cargoDeps = (pkgs.rustPlatform.importCargoLock { inherit lockFile outputHashes; });
in
# TODO(zaphar): I should actually be leveraging naersklib.buildPackage with a postInstall for the optimization and bindgen
stdenv.mkDerivation {
inherit src pname;
version = version;
@ -34,12 +29,19 @@ stdenv.mkDerivation {
'';
# TODO(jwall): Build this from the root rather than the src.
buildPhase = ''
echo building with wasm-pack
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};
sh ../scripts/wasm-build.sh release
#cargo build --lib --release --target wasm32-unknown-unknown --target-dir $out ${features} --offline
#wasm-bindgen $out/wasm32-unknown-unknown/release/kitchen_wasm.wasm --out-dir $out --typescript --target web
#sh ../scripts/wasm-opt.sh release
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

@ -8,9 +8,7 @@ in
, nodejs
, pkg-config
, openssl
, stdenv
, curl
, runCommand
}:
# This package is special so we don't use the naersk infrastructure to build it.
@ -20,14 +18,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 +34,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

@ -6,10 +6,6 @@ A web assembly experiment in Meal Planning and Shopping List management.
Ensure you have rust installed with support for the web assembly target. You can see instructions here: [Rust wasm book](https://rustwasm.github.io/docs/book/game-of-life/setup.html).
You will also want to have trunk installed. You can see instructions for that here: [trunk](https://trunkrs.dev/)
Then obtain the source. We do not at this time publish kitchen on [crates.io](https://crates.io/).
```sh
git clone https://github.com/zaphar/kitchen
cd kitchen
@ -23,7 +19,7 @@ make release
# Hacking on kitchen
If you want to hack on kitchen, then you may find it useful to use trunk in dev mode. The run script will run build the app and run trunk with it watching for changes and reloading on demand in your browser.
The run script will run build the app and run it for you.
```sh
./run.sh
@ -37,4 +33,4 @@ If all of the above looks like too much work, and you already use the nix packag
```sh
nix run github:zaphar/kitchen
```
```

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;

9
scripts/wasm-build.sh Normal file
View File

@ -0,0 +1,9 @@
set -x
buildtype=$1;
if [ ${buildtype} = "release" ]; then
buildtype_flag="--release"
fi
cargo build --lib ${buildtype_flag} --target wasm32-unknown-unknown --target-dir $out --features debug_logs
wasm-bindgen $out/wasm32-unknown-unknown/${buildtype}/kitchen_wasm.wasm --out-dir $out --typescript --target web

6
scripts/wasm-opt.sh Normal file
View File

@ -0,0 +1,6 @@
set -x
buildtype=$1;
wasm-opt $out/wasm32-unknown-unkown/${buildtype}/${project}_wasm.wasm --out-dir dist/ -O
rm -f $out/${project}_wasm_bg.wasm
mv $out/${project}_wasm_bg-opt.wasm dist/${project}_wasm_bg.wasm

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