Fix recipe page: step text contrast and interactive ingredient checkboxes
Build and Deploy / build-and-deploy (push) Successful in 1m55s
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:
+44
-25
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user