refactor(ui): overhaul typography, color palette, and recipe page layout
Build and Deploy / build-and-deploy (push) Successful in 1m35s
Build and Deploy / build-and-deploy (push) Successful in 1m35s
Changes based on design feedback: Typography: Swap Lexend Peta → Fraunces (literary serif) for all headings, preserving Quicksand for body text. Color palette: Add --roux-ochre and --roux-sage vars. Restrict rust accent to step numbers, rules, and hover. Titles and labels → ink. Navbar: 'Roux' logo now uses Fraunces in ink color. Parser bug: Add isMethodSection filter to exclude 'Ingredients' sections from the method column, preventing raw text ingredient listings from leaking into steps. Index page: Titles in ink with rust-on-hover. Add letter dividers (A, B, C...) for alpha mode. Show tags/course metadata under titles. Recipe page: 3-column grid (240px 1fr 180px) with marginalia column for source link and notes. Tag pills → sage outline. Ingredient highlights → ochre underline. Checkboxes → hand-sketched rotation. Source link → muted ink with rust hover.
This commit is contained in:
+122
-61
@@ -140,77 +140,96 @@ page title content =
|
||||
H.style $
|
||||
H.toHtml $
|
||||
T.unlines
|
||||
[ "@import url('https://fonts.googleapis.com/css2?family=Lexend Peta&family=Quicksand:wght@300..700&display=swap');"
|
||||
[ "@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,400;9..144,500;9..144,600&family=Quicksand:wght@300..700&display=swap');"
|
||||
, ""
|
||||
, ":root { --roux-bg: #F5EFE0; --roux-text: #3D2C1E; --roux-accent: #B85C38; }"
|
||||
, ".lexend-peta { font-family: \"Lexend Peta\", sans-serif; font-weight: 400; font-style: normal; }"
|
||||
, ":root { --roux-bg: #F5EFE0; --roux-text: #3D2C1E; --roux-accent: #B85C38; --roux-ochre: #BF9428; --roux-sage: #7A9A7A; --roux-paper: #F5EFE0; }"
|
||||
, ".fraunces { font-family: \"Fraunces\", serif; font-optical-sizing: auto; font-style: normal; }"
|
||||
, ".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; }"
|
||||
, "h1, h3 { font-family: \"Lexend Peta\", sans-serif; font-weight: 400; }"
|
||||
, "h1 { font-size: 2rem; font-weight: 500; margin: 0 0 0.5rem; line-height: 1.15; color: var(--roux-text); }"
|
||||
, "h2 { font-family: \"Quicksand\", sans-serif; font-size: 0.7rem; font-weight: 600; margin: 0 0 1rem; letter-spacing: 0.05em; text-transform: uppercase; color: var(--roux-accent); }"
|
||||
, "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; }"
|
||||
, "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-accent); text-decoration: none; }"
|
||||
, "a:hover { text-decoration: underline; }"
|
||||
, "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); }"
|
||||
, ""
|
||||
, "/* Search input */"
|
||||
, "input[type=search] { background: var(--roux-bg); color: var(--roux-text); border-color: rgba(184, 92, 56, 0.3); }"
|
||||
, "input[type=search]:focus { border-color: var(--roux-accent); box-shadow: 0 0 0 1px var(--roux-accent); }"
|
||||
, ".roux-navbar { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 0; margin-bottom: 1rem; border-bottom: 0.5px solid rgba(184, 92, 56, 0.2); }"
|
||||
, "/* 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); }"
|
||||
, ".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 a { font-size: 0.8rem; letter-spacing: 0.04em; text-transform: uppercase; }"
|
||||
, ".roux-navbar a.active { color: var(--roux-accent); font-weight: 500; }"
|
||||
, ""
|
||||
, "/* Search input */"
|
||||
, "input[type=search] { background: var(--roux-bg); color: var(--roux-text); border-color: rgba(61, 44, 30, 0.2); }"
|
||||
, "input[type=search]:focus { border-color: var(--roux-accent); box-shadow: 0 0 0 1px var(--roux-accent); }"
|
||||
, ""
|
||||
, "/* Recipe list on index page */"
|
||||
, ".roux-recipes { list-style: none; padding: 0; }"
|
||||
, ".roux-recipes li { padding: 0.5rem 0; border-bottom: 0.5px solid rgba(184, 92, 56, 0.1); }"
|
||||
, ".roux-recipes li:last-child { border-bottom: none; }"
|
||||
, ".roux-recipes a { font-size: 1.1rem; display: block; }"
|
||||
, ".roux-recipes li { padding: 0.4rem 0; }"
|
||||
, ".roux-recipes a { font-size: 1rem; display: block; color: var(--roux-text); }"
|
||||
, ".roux-recipes a:hover { color: var(--roux-accent); }"
|
||||
, ".roux-letter-divider { font-family: \"Fraunces\", serif; font-size: 1.1rem; font-weight: 500; color: var(--roux-text); margin: 1.2rem 0 0.25rem; padding-bottom: 0.2rem; border-bottom: 0.5px solid rgba(61, 44, 30, 0.08); }"
|
||||
, ".roux-recipe-meta { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-top: 0.1rem; }"
|
||||
, ".roux-recipe-meta .r-tag { font-size: 0.65rem; color: rgba(61, 44, 30, 0.5); letter-spacing: 0.02em; }"
|
||||
, ".roux-recipes .r-course { font-size: 0.65rem; color: var(--roux-accent); opacity: 0.6; letter-spacing: 0.03em; text-transform: uppercase; }"
|
||||
, ""
|
||||
, "/* Recipe page */"
|
||||
, ".roux-meta { display: flex; gap: 1.5rem; margin-bottom: 1.5rem; padding-bottom: 0.75rem; border-bottom: 0.5px solid rgba(184, 92, 56, 0.2); }"
|
||||
, ".roux-meta { display: flex; gap: 1.5rem; margin-bottom: 1.5rem; padding-bottom: 0.75rem; border-bottom: 1px solid rgba(61, 44, 30, 0.08); }"
|
||||
, ".roux-meta-item { text-align: center; }"
|
||||
, ".roux-meta-item .label { font-size: 0.65rem; color: var(--roux-accent); margin: 0 0 2px; letter-spacing: 0.04em; text-transform: uppercase; font-weight: 500; }"
|
||||
, ".roux-meta-item .label { font-size: 0.65rem; color: rgba(61, 44, 30, 0.5); margin: 0 0 2px; letter-spacing: 0.04em; text-transform: uppercase; font-weight: 500; }"
|
||||
, ".roux-meta-item .value { font-size: 0.95rem; font-weight: 500; margin: 0; color: var(--roux-text); }"
|
||||
, ""
|
||||
, ".roux-grid { display: grid; grid-template-columns: 260px 1fr; gap: 2rem; }"
|
||||
, ".roux-grid { display: grid; grid-template-columns: 240px 1fr 180px; gap: 2rem; }"
|
||||
, "@media (max-width: 1024px) { .roux-grid { grid-template-columns: 240px 1fr; } }"
|
||||
, "@media (max-width: 768px) { .roux-grid { grid-template-columns: 1fr; } }"
|
||||
, ""
|
||||
, ".roux-subsection { font-size: 0.65rem; color: var(--roux-accent); margin: 0 0 6px; letter-spacing: 0.05em; text-transform: uppercase; font-weight: 500; opacity: 0.75; }"
|
||||
, ".roux-marginalia { font-size: 0.8rem; line-height: 1.6; color: rgba(61, 44, 30, 0.6); }"
|
||||
, ".roux-marginalia h2 { font-size: 0.65rem; letter-spacing: 0.03em; text-transform: uppercase; color: rgba(61, 44, 30, 0.4); margin: 0 0 0.75rem; }"
|
||||
, ".roux-marginalia .note { padding: 0.75rem 0; border-bottom: 0.5px solid rgba(61, 44, 30, 0.06); }"
|
||||
, ".roux-marginalia .note:last-child { border-bottom: none; }"
|
||||
, ".roux-marginalia .ornament { color: var(--roux-accent); font-size: 1rem; margin-right: 0.3rem; }"
|
||||
, "@media (max-width: 1024px) { .roux-marginalia { display: none; } }"
|
||||
, ""
|
||||
, ".roux-ingredient-row { display: flex; align-items: flex-start; gap: 8px; padding: 4px 0; }"
|
||||
, ".roux-ingredient-row input { margin-top: 5px; appearance: none; -webkit-appearance: none; width: 16px; height: 16px; border: 1.5px solid rgba(184,92,56,0.5); border-radius: 3px; background: transparent; cursor: pointer; flex-shrink: 0; position: relative; }"
|
||||
, ".roux-ingredient-row input:checked { background: var(--roux-accent); border-color: var(--roux-accent); }"
|
||||
, ".roux-ingredient-row input:checked::after { content: '✓'; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); font-size: 12px; font-weight: 700; color: #3D2C1E; line-height: 1; }"
|
||||
, ".roux-source-link { font-size: 0.8rem; color: var(--roux-text); opacity: 0.5; white-space: nowrap; flex-shrink: 0; transition: opacity 0.15s; }"
|
||||
, ".roux-source-link:hover { opacity: 1; color: var(--roux-accent); }"
|
||||
, ""
|
||||
, ".roux-subsection { font-size: 0.6rem; color: rgba(61, 44, 30, 0.4); margin: 0 0 6px; letter-spacing: 0.05em; text-transform: uppercase; font-weight: 500; }"
|
||||
, ""
|
||||
, ".roux-ingredient-row { display: flex; align-items: flex-start; gap: 8px; padding: 3px 0; }"
|
||||
, ".roux-ingredient-row input { margin-top: 5px; 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; transform: rotate(0.5deg); transition: border-color 0.15s; }"
|
||||
, ".roux-ingredient-row input:checked { border-color: rgba(61, 44, 30, 0.5); background: transparent; }"
|
||||
, ".roux-ingredient-row input:checked::after { content: '✓'; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%) rotate(-0.5deg); font-size: 11px; font-weight: 400; color: var(--roux-text); opacity: 0.5; line-height: 1; }"
|
||||
, ".roux-ingredient-row .name { font-size: 0.9rem; line-height: 1.5; color: var(--roux-text); transition: opacity 0.15s; }"
|
||||
, ".roux-ingredient-row .qty { font-size: 0.8rem; color: var(--roux-accent); margin-right: 4px; }"
|
||||
, ".roux-ingredient-row:has(input:checked) { opacity: 0.45; }"
|
||||
, ".roux-ingredient-row .qty { font-size: 0.8rem; color: var(--roux-text); opacity: 0.55; margin-right: 4px; }"
|
||||
, ".roux-ingredient-row:has(input:checked) .name { opacity: 0.4; }"
|
||||
, ".roux-ingredient-row .r-plain { font-size: 0.8rem; color: rgba(61, 44, 30, 0.55); font-style: italic; }"
|
||||
, ""
|
||||
, ".roux-step { display: flex; align-items: flex-start; gap: 12px; padding: 8px 0; transition: opacity 0.15s; }"
|
||||
, ".roux-step input { margin-top: 6px; appearance: none; -webkit-appearance: none; width: 16px; height: 16px; border: 1.5px solid rgba(184,92,56,0.5); border-radius: 3px; background: transparent; cursor: pointer; flex-shrink: 0; position: relative; }"
|
||||
, ".roux-step input:checked { background: var(--roux-accent); border-color: var(--roux-accent); }"
|
||||
, ".roux-step input:checked::after { content: '✓'; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); font-size: 12px; font-weight: 700; color: #3D2C1E; line-height: 1; }"
|
||||
, ".roux-step .step-label { font-size: 0.75rem; color: var(--roux-accent); font-weight: 500; margin: 0 0 2px; }"
|
||||
, ".roux-step input { margin-top: 6px; 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; transform: rotate(-0.3deg); transition: border-color 0.15s; }"
|
||||
, ".roux-step input:checked { border-color: rgba(61, 44, 30, 0.5); background: transparent; }"
|
||||
, ".roux-step input:checked::after { content: '✓'; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%) rotate(0.3deg); font-size: 11px; font-weight: 400; color: var(--roux-text); opacity: 0.5; line-height: 1; }"
|
||||
, ".roux-step .step-label { font-size: 0.7rem; color: var(--roux-accent); font-weight: 500; margin: 0 0 2px; letter-spacing: 0.01em; }"
|
||||
, ".roux-step .step-text { font-size: 0.9rem; line-height: 1.65; margin: 0; color: var(--roux-text); }"
|
||||
, ".roux-step:has(input:checked) { opacity: 0.45; }"
|
||||
, ".roux-step:has(input:checked) .step-text { opacity: 0.4; }"
|
||||
, ""
|
||||
, ".roux-note { display: flex; gap: 12px; padding: 8px 0; }"
|
||||
, ".roux-note .ornament { font-size: 1.4rem; color: var(--roux-accent); line-height: 1; margin-top: -1px; }"
|
||||
, ".roux-note .text { font-size: 0.9rem; line-height: 1.65; margin: 0; color: var(--roux-text); }"
|
||||
, ""
|
||||
, ".roux-course { font-size: 0.65rem; color: var(--roux-accent); margin: 0 0 0.15rem; letter-spacing: 0.04em; text-transform: uppercase; font-weight: 500; }"
|
||||
, ".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 { color: var(--roux-accent); font-weight: 500; }"
|
||||
, ".roux-cookware-tag { font-style: italic; opacity: 0.85; }"
|
||||
, ".roux-timer-tag { color: var(--roux-accent); font-style: italic; }"
|
||||
, ".roux-comment-tag { opacity: 0.6; font-style: italic; }"
|
||||
, ".tag { display: inline-block; background: rgba(184,92,56,0.1); padding: 0.1rem 0.5rem; margin: 0.1rem; border-radius: 4px; font-size: 0.7rem; color: var(--roux-accent); }"
|
||||
, ".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-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; }"
|
||||
, ".r-section-header:first-child { border-top: none; margin-top: 0; padding-top: 0; }"
|
||||
]
|
||||
|
||||
H.body $ H.main ! A.class_ "container" $ content
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
@@ -263,11 +282,39 @@ searchJs =
|
||||
, " listEl.innerHTML = html;"
|
||||
, "}"
|
||||
, ""
|
||||
, "function renderRecipeEntry(r) {"
|
||||
, " let meta = '';"
|
||||
, " if (r.tags && r.tags.length > 0) {"
|
||||
, " meta += '<span class=\\\'r-tag\\\'>' + r.tags.map(escapeHtml).join(' · ') + '</span>';"
|
||||
, " }"
|
||||
, " if (r.course) {"
|
||||
, " meta += '<span class=\\\'r-course\\\'>' + escapeHtml(r.course) + '</span>';"
|
||||
, " }"
|
||||
, " let metaHtml = meta ? '<div class=\\\'roux-recipe-meta\\\'>' + meta + '</div>' : '';"
|
||||
, " return '<li><a href=\\\"/recipes/' + encodeURIComponent(r.filename) + '\\\">' + escapeHtml(r.title) + '</a>' + metaHtml + '</li>';"
|
||||
, "}"
|
||||
, ""
|
||||
, "function renderFlatList(items) {"
|
||||
, " if (items.length === 0) return '<p>No recipes found.</p>';"
|
||||
, " let html = '<ul class=\\'roux-recipes\\'>';"
|
||||
, " let currentLetter = '';"
|
||||
, " let html = '<ul class=\\\'roux-recipes\\\'>';"
|
||||
, " items.forEach(r => {"
|
||||
, " html += '<li><a href=\"/recipes/' + encodeURIComponent(r.filename) + '\">' + escapeHtml(r.title) + '</a></li>';"
|
||||
, " const letter = r.title.charAt(0).toUpperCase();"
|
||||
, " if (letter !== currentLetter) {"
|
||||
, " currentLetter = letter;"
|
||||
, " html += '<li class=\\\'roux-letter-divider\\\'>' + letter + '</li>';"
|
||||
, " }"
|
||||
, " html += renderRecipeEntry(r);"
|
||||
, " });"
|
||||
, " html += '</ul>';"
|
||||
, " return html;"
|
||||
, "}"
|
||||
, ""
|
||||
, "function renderFlatListNoAlpha(items) {"
|
||||
, " if (items.length === 0) return '<p>No recipes found.</p>';"
|
||||
, " let html = '<ul class=\\\'roux-recipes\\\'>';"
|
||||
, " items.forEach(r => {"
|
||||
, " html += renderRecipeEntry(r);"
|
||||
, " });"
|
||||
, " html += '</ul>';"
|
||||
, " return html;"
|
||||
@@ -288,7 +335,7 @@ searchJs =
|
||||
, " tags.forEach(t => {"
|
||||
, " const sorted = tagMap[t].sort((a,b) => a.title.localeCompare(b.title));"
|
||||
, " html += '<h3>' + escapeHtml(t) + '</h3>';"
|
||||
, " html += renderFlatList(sorted);"
|
||||
, " html += renderFlatListNoAlpha(sorted);"
|
||||
, " });"
|
||||
, " return html;"
|
||||
, "}"
|
||||
@@ -306,10 +353,11 @@ searchJs =
|
||||
, " courses.forEach(c => {"
|
||||
, " const sorted = courseMap[c].sort((a,b) => a.title.localeCompare(b.title));"
|
||||
, " html += '<h3>' + escapeHtml(c) + '</h3>';"
|
||||
, " html += renderFlatList(sorted);"
|
||||
, " html += renderFlatListNoAlpha(sorted);"
|
||||
, " });"
|
||||
, " return html;"
|
||||
, "}"
|
||||
, "}"
|
||||
, ""
|
||||
, "function escapeHtml(s) {"
|
||||
, " const d = document.createElement('div');"
|
||||
@@ -385,7 +433,7 @@ indexPage :: SortMode -> [Idx.RecipeInfo] -> ByteString
|
||||
indexPage _mode recipes =
|
||||
page "Roux — Recipes" $ do
|
||||
H.div ! A.class_ "roux-navbar" $ do
|
||||
H.ul $ H.li $ H.strong "Roux"
|
||||
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"
|
||||
H.li $ H.a ! A.href "#tags" $ "By Tag"
|
||||
@@ -474,7 +522,7 @@ recipePage info =
|
||||
renderRecipe :: Recipe -> Html
|
||||
renderRecipe recipe = do
|
||||
H.div ! A.class_ "roux-navbar" $ do
|
||||
H.ul $ H.li $ H.a ! A.href "/" $ "← Back"
|
||||
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
|
||||
course = metaCourse meta
|
||||
@@ -485,14 +533,15 @@ renderRecipe recipe = do
|
||||
H.div ! A.style "display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;" $ do
|
||||
H.h1 $ H.toHtml (titleText recipe)
|
||||
case metaSource meta of
|
||||
Just url -> H.a ! A.href (H.toValue url) ! A.target "_blank" ! A.rel "noopener noreferrer" ! A.style "white-space: nowrap; font-size: 0.8rem; flex-shrink: 0;" $ do
|
||||
"↗ Original recipe"
|
||||
Just url -> H.a ! A.class_ "roux-source-link" ! A.href (H.toValue url) ! A.target "_blank" ! A.rel "noopener noreferrer" $ do
|
||||
"↗ Original"
|
||||
Nothing -> pure ()
|
||||
case desc of
|
||||
Just d -> H.p ! A.class_ "roux-desc" $ H.toHtml d
|
||||
Nothing -> pure ()
|
||||
renderMetaBar meta
|
||||
let sections = NE.toList (recipeSections recipe)
|
||||
notes = collectNotes recipe
|
||||
H.div ! A.class_ "roux-grid" $ do
|
||||
-- Ingredients column
|
||||
H.div $ do
|
||||
@@ -502,12 +551,17 @@ renderRecipe recipe = do
|
||||
H.div $ do
|
||||
H.h2 "Method"
|
||||
renderMethodSections sections
|
||||
-- Notes
|
||||
let notes = collectNotes recipe
|
||||
unless (null notes) $ do
|
||||
H.div ! A.style "margin-top: 2rem; padding-top: 1.5rem; border-top: 0.5px solid rgba(184,92,56,0.2);" $ do
|
||||
H.h2 "Notes"
|
||||
mapM_ renderNote notes
|
||||
-- Marginalia column
|
||||
H.div ! A.class_ "roux-marginalia" $ 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"
|
||||
Nothing -> pure ()
|
||||
unless (null notes) $ do
|
||||
H.h2 "Notes"
|
||||
mapM_ renderMarginalNote notes
|
||||
|
||||
-- | Extract display title from recipe metadata or fallback.
|
||||
titleText :: Recipe -> Text
|
||||
@@ -613,9 +667,16 @@ dedupFirst (i : rest) acc =
|
||||
-- | Render the method column: all sections with numbered steps.
|
||||
renderMethodSections :: [Section] -> Html
|
||||
renderMethodSections sections = do
|
||||
let allSteps = concatMap sectionMethodSteps sections
|
||||
let allSteps = concatMap sectionMethodSteps (filter isMethodSection sections)
|
||||
mapM_ (uncurry renderMethodStep) (zip [1 ..] allSteps)
|
||||
|
||||
-- | Is this section a method section (not an ingredient listing)?
|
||||
isMethodSection :: Section -> Bool
|
||||
isMethodSection section =
|
||||
case sectionName section of
|
||||
Just n -> not (T.toLower n `elem` ["ingredients", "ingredient"])
|
||||
Nothing -> True
|
||||
|
||||
-- | Extract (section name, body items) from a section for the method column.
|
||||
sectionMethodSteps :: Section -> [(Maybe Text, SectionBodyItem)]
|
||||
sectionMethodSteps section =
|
||||
@@ -674,12 +735,12 @@ renderStepItem StepBreak =
|
||||
-- Notes section
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- | Render a note with a decorative ornament.
|
||||
renderNote :: Text -> Html
|
||||
renderNote t =
|
||||
H.div ! A.class_ "roux-note" $ do
|
||||
H.span ! A.class_ "ornament" $ "\182"
|
||||
H.p ! A.class_ "text" $ H.toHtml t
|
||||
{- | Render a note with a decorative ornament.
|
||||
| Render a note in the marginalia column (compact, no ornament).
|
||||
-}
|
||||
renderMarginalNote :: Text -> Html
|
||||
renderMarginalNote t =
|
||||
H.div ! A.class_ "note" $ H.p $ H.toHtml t
|
||||
|
||||
-- | Collect all note texts from the recipe.
|
||||
collectNotes :: Recipe -> [Text]
|
||||
|
||||
Reference in New Issue
Block a user