diff --git a/src/Roux/Html.hs b/src/Roux/Html.hs index d437cae..cc3afb8 100644 --- a/src/Roux/Html.hs +++ b/src/Roux/Html.hs @@ -147,21 +147,21 @@ page title content = , ".quicksand { font-family: \"Quicksand\", sans-serif; font-optical-sizing: auto; font-style: normal; }" , "" , "body { background: var(--roux-bg); color: var(--roux-text); font-family: \"Quicksand\", sans-serif; }" - , "main.container { padding-top: 1.5rem; }" + , "main.container { padding-top: 1.5rem; max-width: 1120px; }" , "h1, h3, .roux-heading { font-family: \"Fraunces\", serif; font-optical-sizing: auto; }" , "h1 { font-size: 2rem; font-weight: 500; margin: 0 0 0.5rem; line-height: 1.15; color: var(--roux-text); letter-spacing: -0.01em; }" - , "h2 { font-family: \"Fraunces\", serif; font-size: 0.75rem; font-weight: 600; margin: 0 0 1rem; letter-spacing: 0.04em; text-transform: uppercase; color: var(--roux-text); opacity: 0.6; }" + , "h2, .container h2 { font-family: \"Fraunces\", serif; font-size: 0.75rem; font-weight: 600; margin: 0 0 1rem; letter-spacing: 0.04em; text-transform: uppercase; color: var(--roux-text); opacity: 0.6; }" , "h3 { font-size: 1rem; font-weight: 500; margin: 1.5rem 0 0.5rem; color: var(--roux-text); text-transform: uppercase; opacity: 0.75; }" , "a { color: var(--roux-text); text-decoration: none; transition: color 0.15s; }" , "a:hover { color: var(--roux-accent); }" , "a[role=button] { --pico-color: var(--roux-accent); }" , "" , "/* Navbar */" - , ".roux-navbar { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 0; margin-bottom: 1rem; border-bottom: 1px solid rgba(61, 44, 30, 0.1); }" + , "nav.roux-navbar { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 0; margin-bottom: 1rem; border-bottom: 1px solid rgba(61, 44, 30, 0.1); }" , ".roux-navbar .roux-logo { font-family: \"Fraunces\", serif; font-size: 1.1rem; font-weight: 500; color: var(--roux-text); text-decoration: none; }" , ".roux-navbar .roux-logo:hover { color: var(--roux-text); }" , ".roux-navbar ul { display: flex; gap: 1rem; list-style: none; margin: 0; padding: 0; }" - , ".roux-navbar li { margin: 0; padding: 0; }" + , ".roux-navbar li { margin: 0; padding: 0; list-style: none; }" , ".roux-navbar a { font-size: 0.8rem; letter-spacing: 0.04em; text-transform: uppercase; }" , ".roux-navbar a.active { color: var(--roux-accent); font-weight: 500; }" , "" @@ -221,9 +221,9 @@ page title content = , ".roux-course { font-size: 0.6rem; color: var(--roux-accent); opacity: 0.65; margin: 0 0 0.15rem; letter-spacing: 0.04em; text-transform: uppercase; font-weight: 500; }" , ".roux-desc { font-size: 0.85rem; line-height: 1.6; color: var(--roux-text); margin: 0 0 1.5rem; max-width: 640px; opacity: 0.85; }" , "" - , ".roux-ingredient-tag { border-bottom: 1.5px solid rgba(191, 148, 40, 0.35); font-weight: 500; }" - , ".roux-cookware-tag { font-style: italic; opacity: 0.75; }" - , ".roux-timer-tag { border-bottom: 1px dashed rgba(61, 44, 30, 0.25); font-style: italic; }" + , ".roux-ingredient-tag { border-bottom: 1.5px solid rgba(191, 148, 40, 0.35); }" + , ".roux-cookware-tag, .roux-timer-tag { opacity: 0.85; }" + , ".roux-ingredient-tag .qty { font-size: 0.8rem; color: var(--roux-text); opacity: 0.55; }" , ".roux-comment-tag { opacity: 0.5; font-style: italic; font-size: 0.85em; }" , ".tag { display: inline-block; border: 1px solid rgba(122, 154, 122, 0.35); padding: 0.1rem 0.5rem; margin: 0.1rem; border-radius: 4px; font-size: 0.7rem; color: var(--roux-sage); background: transparent; }" , ".r-section-header { display: block; font-family: \"Fraunces\", serif; font-size: 0.85rem; font-weight: 500; color: var(--roux-text); opacity: 0.5; margin: 1rem 0 0.5rem; letter-spacing: 0.01em; border-top: 0.5px solid rgba(61, 44, 30, 0.06); padding-top: 0.75rem; }" @@ -431,7 +431,7 @@ sseIndexJs = indexPage :: SortMode -> [Idx.RecipeInfo] -> ByteString indexPage _mode recipes = page "Roux — Recipes" $ do - H.div ! A.class_ "roux-navbar" $ do + H.nav ! A.class_ "roux-navbar" $ do H.ul $ H.li $ H.a ! A.class_ "roux-logo" ! A.href "/" $ "Roux" H.ul $ do H.li $ H.a ! A.href "#alpha" $ "A-Z" @@ -468,7 +468,7 @@ indexPage _mode recipes = importPage :: Maybe Text -> ByteString importPage merror = page "Roux \8212 Import Recipe" $ do - H.div ! A.class_ "roux-navbar" $ do + H.nav ! A.class_ "roux-navbar" $ do H.ul $ H.li $ H.a ! A.href "/" $ "\8592 Back" H.ul $ H.li $ H.strong "Import Recipe" case merror of @@ -489,7 +489,7 @@ importPage merror = importResultPage :: ImportError -> ByteString importResultPage (ImportError msg) = page "Roux \8212 Import Error" $ do - H.div ! A.class_ "roux-navbar" $ do + H.nav ! A.class_ "roux-navbar" $ do H.ul $ H.li $ H.a ! A.href "/" $ "\8592 Back" H.ul $ H.li $ H.a ! A.href "/import" $ "Try again" H.h2 "Import failed" @@ -520,7 +520,7 @@ recipePage info = -- | Render the body of a recipe page (no page shell). renderRecipe :: Recipe -> Html renderRecipe recipe = do - H.div ! A.class_ "roux-navbar" $ do + H.nav ! A.class_ "roux-navbar" $ do H.ul $ H.li $ H.a ! A.class_ "roux-logo" ! A.href "/" $ "Roux" H.ul $ H.li $ H.a ! A.href "/" $ "Recipes" let meta = recipeMetadata recipe @@ -555,9 +555,13 @@ renderRecipe recipe = do case metaSource meta of Just url -> H.div ! A.class_ "note" $ do H.h2 "Source" - H.a ! A.href (H.toValue url) ! A.target "_blank" ! A.rel "noopener noreferrer" $ do - "↗ Original recipe" + H.p $ H.toHtml url Nothing -> pure () + let recipeTags = metaTags meta + unless (null recipeTags) $ do + H.div ! A.class_ "note" $ do + H.h2 "Tags" + H.p $ H.toHtml (T.intercalate " · " recipeTags) unless (null notes) $ do H.h2 "Notes" mapM_ renderMarginalNote notes diff --git a/src/Roux/Parser.hs b/src/Roux/Parser.hs index c8e2f85..314efe9 100644 --- a/src/Roux/Parser.hs +++ b/src/Roux/Parser.hs @@ -296,7 +296,7 @@ pQuantityBody = do [] -> return Nothing -- empty braces _ -> case parseQuantityText (T.pack content) of Just q -> return (Just q) - Nothing -> P.parserFail ("invalid quantity: " <> content) + Nothing -> return Nothing -- non-numeric quantity, consume silently {- | Parse a quantity text like @2@, @1%kg@, @=1%tsp@, @1\/2%tbsp@, or @1,1\/2cups@ (unit without @%@ separator).