Fix recipe page: step text contrast and interactive ingredient checkboxes
Build and Deploy / build-and-deploy (push) Successful in 1m55s

- Darken step text: use --roux-text instead of #4A3525, explicit p color
  to override Pico CSS defaults
- Make ingredient list interactive: each ingredient is now a checkbox
  row that shows ✓ checkmark on click with line-through + muted opacity
- Collect ingredients from ALL sections (not just sections named
  'Ingredients'), deduplicated per section, grouped by section name
- Switch from <ul>/<li> to <div>/<label> with checkbox inputs
This commit is contained in:
2026-05-26 09:32:34 -04:00
parent 7b64f9576b
commit 7b77cf2840
+44 -25
View File
@@ -932,14 +932,20 @@ recipePageStyles =
, ".rp-section-label { font-size: 12px; letter-spacing: 0.12em; color: #8A7560; text-transform: uppercase; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: baseline; }"
, ".rp-serves { font-family: \"Fraunces\", serif; font-size: 28px; margin-bottom: 22px; }"
, ""
, ".rp-ingredients { list-style: none; padding: 0; margin: 0 0 28px; font-size: 15px; line-height: 1.9; }"
, ".rp-ingredients li { display: flex; gap: 14px; align-items: baseline; padding: 4px 0; border-bottom: 0.5px dashed rgba(61,40,23,0.15); }"
, ".rp-ingredients li:last-child { border-bottom: none; }"
, ".rp-ingredients .rp-qty { color: #8A7560; min-width: 48px; font-variant-numeric: tabular-nums; }"
, ".rp-ingredients { list-style: none; padding: 0; margin: 0 0 28px; font-size: 15px; }"
, ".rp-ingredient-row { display: flex; align-items: center; gap: 14px; padding: 6px 0; border-bottom: 0.5px dashed rgba(61,40,23,0.15); cursor: pointer; }"
, ".rp-ingredient-row:last-child { border-bottom: none; }"
, ".rp-ingredient-row input[type=checkbox] { appearance: none; -webkit-appearance: none; width: 15px; height: 15px; border: 1.5px solid rgba(61,44,30,0.25); border-radius: 2px; background: transparent; cursor: pointer; flex-shrink: 0; position: relative; transition: border-color 0.15s; margin: 0; }"
, ".rp-ingredient-row input[type=checkbox]:checked { border-color: rgba(61,44,30,0.5); }"
, ".rp-ingredient-row input[type=checkbox]:checked::after { content: '\x2713'; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); font-size: 11px; font-weight: 400; color: var(--roux-text); opacity: 0.5; line-height: 1; }"
, ".rp-ingredient-row .rp-qty { color: #8A7560; min-width: 48px; font-variant-numeric: tabular-nums; transition: opacity 0.15s; }"
, ".rp-ingredient-row .name { font-size: 0.9rem; line-height: 1.5; color: var(--roux-text); transition: opacity 0.15s; }"
, ".rp-ingredient-row:has(input:checked) .rp-qty { opacity: 0.4; }"
, ".rp-ingredient-row:has(input:checked) .name { opacity: 0.4; text-decoration: line-through; }"
, ".rp-ing-section { font-size: 13px; font-weight: 500; color: #6B5847; margin: 16px 0 6px; letter-spacing: 0.03em; }"
, ""
, ".rp-method { font-size: 15px; line-height: 1.7; color: #4A3525; }"
, ".rp-method p { margin: 0 0 14px; }"
, ".rp-method { font-size: 15px; line-height: 1.7; color: var(--roux-text); }"
, ".rp-method p { margin: 0 0 14px; color: var(--roux-text); }"
, ".rp-method .rp-step { font-weight: 500; color: var(--roux-text); margin-right: 4px; }"
, ".rp-method .rp-comment { color: #8A7560; font-size: 14px; }"
, ".rp-method .rp-note { color: #8A7560; font-size: 13px; }"
@@ -1068,30 +1074,36 @@ formatFullDate = T.pack . Time.formatTime Time.defaultTimeLocale "%B %e, %Y"
-- | Render ingredients as a flat list grouped by section (mockup style).
rpIngredientList :: [Section] -> Html
rpIngredientList sections = do
let ingSects = filter isIngSection sections
unnamed = [s | s <- ingSects, isUnnamedIngSection s]
named = [s | s <- ingSects, not (isUnnamedIngSection s)]
-- Collect ingredients from all sections, deduplicated per section
let withIngredients = filter (not . Prelude.null . collectIngredients) sections
unnamed = [s | s <- withIngredients, isUnnamedIngredientSection s]
named = [s | s <- withIngredients, not (isUnnamedIngredientSection s)]
-- Unnamed sections first, then named
unless (null unnamed) $
H.ul ! A.class_ "rp-ingredients" $
mapM_ rpIngredientItems (concatMap collectIngredients unnamed)
H.div ! A.class_ "rp-ingredients" $
mapM_ rpIngredientItems (dedupIngredients (concatMap collectIngredients unnamed))
mapM_ rpNamedIngredientSection named
where
isIngSection s =
case sectionName s of
Just n -> let lower = T.toLower n in lower `elem` ["ingredients", "ingredient"]
Nothing -> not . Prelude.null $ collectIngredients s
isUnnamedIngSection s = Prelude.null (sectionName s)
isUnnamedIngredientSection s = Prelude.null (sectionName s)
-- | Deduplicate ingredients by name, keeping the first occurrence.
dedupIngredients :: [(Text, Maybe Quantity)] -> [(Text, Maybe Quantity)]
dedupIngredients = go []
where
go seen [] = reverse seen
go seen ((name, qty) : rest)
| name `elem` Prelude.map fst seen = go seen rest
| otherwise = go ((name, qty) : seen) rest
-- | Render a named ingredient section with a sub-heading.
rpNamedIngredientSection :: Section -> Html
rpNamedIngredientSection section = do
let ings = collectIngredients section
let ings = dedupIngredients (collectIngredients section)
unless (null ings) $ do
case sectionName section of
Just n -> H.p ! A.class_ "rp-ing-section" $ H.toHtml n
Nothing -> pure ()
H.ul ! A.class_ "rp-ingredients" $ mapM_ rpIngredientItems ings
H.div ! A.class_ "rp-ingredients" $ mapM_ rpIngredientItems ings
-- | Collect ingredient (name, quantity) pairs from a section.
collectIngredients :: Section -> [(Text, Maybe Quantity)]
@@ -1106,24 +1118,31 @@ collectIngredients section =
-- | Render one ingredient item in the mockup-style list.
rpIngredientItems :: (Text, Maybe Quantity) -> Html
rpIngredientItems (name, mQty) =
H.li $ do
H.label ! A.class_ "rp-ingredient-row" $ do
H.input ! A.type_ "checkbox"
H.span ! A.class_ "rp-qty" $
case mQty of
Just q -> H.toHtml (showQuantity q)
Nothing -> H.toHtml ("\x2014" :: Text)
H.span $ H.toHtml name
H.span ! A.class_ "name" $ H.toHtml name
-- | Render numbered method steps as plain paragraphs (mockup style).
rpMethodList :: [Section] -> Html
rpMethodList sections = do
let methodSects = filter (not . isIngSection) sections
allSteps = concatMap extractSteps methodSects
meaningful = filter (not . isEmptyStep) allSteps
H.div ! A.class_ "rp-method" $ mapM_ (uncurry rpRenderMethodStep) (zip [1 ..] meaningful)
allItems = concatMap extractSteps methodSects
(_, html) = Prelude.foldl go (1, mempty) allItems
H.div ! A.class_ "rp-method" $ html
where
extractSteps s = Prelude.map (sectionName s,) (NE.toList (sectionBody s))
isEmptyStep (_, SecStep step) = null (unStep step) || all isBlankText (unStep step)
isEmptyStep _ = False
-- \| Fold through items, incrementing step counter only for non-empty steps.
-- Comments and notes render without consuming a step number, so any
-- horizontal-rule separators or front-matter comments don't throw off
-- the numbering.
go (n, acc) (sname, SecStep step)
| null (unStep step) || all isBlankText (unStep step) = (n, acc)
| otherwise = (n + 1, acc <> rpRenderMethodStep n (sname, SecStep step))
go (n, acc) item = (n, acc <> rpRenderMethodStep 0 item)
isBlankText (StepText t) = T.all (== ' ') t
isBlankText _ = False
isIngSection s =