From 541dc0e7cee3ab536169e98b1b46f24e82dd3f60 Mon Sep 17 00:00:00 2001 From: James Brechtel Date: Wed, 27 May 2026 21:13:48 -0400 Subject: [PATCH] Unify header navigation and page padding across all pages - Add Home link to every page's navigation header (index, recipe, cook history, import) - Move landing page padding (36px 48px 80px) into base main.container CSS so all pages share the same consistent spacing - Remove per-page padding overrides from .lnd-page and .rp-page --- src/Roux/Html.hs | 124 ++++++++++++++++++++++++----------------------- 1 file changed, 63 insertions(+), 61 deletions(-) diff --git a/src/Roux/Html.hs b/src/Roux/Html.hs index 76a097d..9fb7502 100644 --- a/src/Roux/Html.hs +++ b/src/Roux/Html.hs @@ -146,7 +146,7 @@ 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; max-width: 1120px; }" + , "main.container { padding: 36px 48px 80px; 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, .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; }" @@ -155,14 +155,13 @@ page title content = , "a:hover { color: var(--roux-accent); }" , "a[role=button] { --pico-color: var(--roux-accent); }" , "" - , "/* Navbar */" - , "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; 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; }" + , "/* Header navigation */" + , ".roux-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 0.5px solid rgba(61,44,30,0.18); padding-bottom: 12px; margin-bottom: 32px; font-size: 12px; letter-spacing: 0.12em; color: #6B5847; text-transform: uppercase; }" + , ".roux-header-logo { font-family: \"Fraunces\", serif; font-size: 22px; font-weight: 500; letter-spacing: 0.08em; color: var(--roux-text); text-decoration: none; }" + , ".roux-header-links { display: flex; gap: 28px; }" + , ".roux-header-links a { color: #6B5847; text-decoration: none; }" + , ".roux-header-links a:hover { color: var(--roux-accent); }" + , ".roux-header-links a.active { color: var(--roux-accent); }" , "" , "/* Search input */" , "input[type=search] { background: var(--roux-bg); color: var(--roux-text); border-color: rgba(61, 44, 30, 0.2); }" @@ -423,7 +422,7 @@ searchJs = , "// Set active tab style based on hash" , "function updateActiveNav() {" , " const mode = getSortMode();" - , " document.querySelectorAll('.roux-navbar a').forEach(a => {" + , " document.querySelectorAll('.roux-header-links a').forEach(a => {" , " a.classList.toggle('active', a.getAttribute('href') === '#' + mode);" , " });" , "}" @@ -737,18 +736,33 @@ sseIndexJs = , "})();" ] +-- --------------------------------------------------------------------------- +-- Shared header navigation +-- --------------------------------------------------------------------------- + +-- | Render the shared header navigation with the landing-page style. +renderHeader :: [(Text, Text, Bool)] -> Html +renderHeader links = + H.nav ! A.class_ "roux-header" $ do + H.a ! A.class_ "roux-header-logo" ! A.href "/" $ "ROUX" + H.div ! A.class_ "roux-header-links" $ + mapM_ renderLink links + where + renderLink (href, label, True) = H.a ! A.class_ "active" ! A.href (H.toValue href) $ H.toHtml label + renderLink (href, label, False) = H.a ! A.href (H.toValue href) $ H.toHtml label + -- | Render the recipe index page -- shell with embedded JSON + JS rendering. indexPage :: SortMode -> [Idx.RecipeInfo] -> ByteString indexPage _mode recipes = page "Roux — Recipes" $ 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" - H.li $ H.a ! A.href "#tags" $ "By Tag" - H.li $ H.a ! A.href "#course" $ "By Course" - H.li $ H.a ! A.href "/import" $ "Import" - H.li $ H.a ! A.href "/cook-history" $ "Cook History" + renderHeader + [ ("/", "Home", False) + , ("#alpha", "A-Z", True) + , ("#tags", "By Tag", False) + , ("#course", "By Course", False) + , ("/import", "Import", False) + , ("/cook-history", "Cook History", False) + ] -- Search input H.input ! A.type_ "search" @@ -810,12 +824,12 @@ fileUploadJs = importPage :: Maybe Text -> ByteString importPage merror = page "Roux \8212 Import Recipe" $ 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 "/recipes" $ "Recipes" - H.li $ H.a ! A.class_ "active" ! A.href "/import" $ "Import" - H.li $ H.a ! A.href "/cook-history" $ "Cook History" + renderHeader + [ ("/", "Home", False) + , ("/recipes", "Recipes", False) + , ("/import", "Import", True) + , ("/cook-history", "Cook History", False) + ] case merror of Just err -> H.p ! A.style "color: var(--roux-accent);" $ H.toHtml err Nothing -> pure () @@ -899,13 +913,8 @@ formatDay = T.pack . Time.formatTime Time.defaultTimeLocale "%b %e" recipePageStyles :: Text recipePageStyles = T.unlines - [ ".rp-page { max-width: 980px; margin: 0 auto; padding: 36px 48px 80px; }" - , ".rp-nav { display: flex; justify-content: space-between; align-items: center; border-bottom: 0.5px solid rgba(61,44,30,0.18); padding-bottom: 12px; margin-bottom: 20px; font-size: 12px; letter-spacing: 0.12em; color: #6B5847; text-transform: uppercase; }" - , ".rp-logo { font-family: \"Fraunces\", serif; font-size: 22px; font-weight: 500; letter-spacing: 0.08em; color: var(--roux-text); text-decoration: none; }" - , ".rp-nav-links { display: flex; gap: 28px; }" - , ".rp-nav-links a { color: #6B5847; text-decoration: none; }" - , ".rp-nav-links a:hover { color: var(--roux-accent); }" - , "" + [ ".rp-page { max-width: 980px; margin: 0 auto; }" + , ".rp-course { font-size: 11px; letter-spacing: 0.15em; text-transform: uppercase; color: var(--roux-accent); opacity: 0.65; margin: 0 0 4px; }" , ".rp-title { font-size: 38px; line-height: 1.05; font-family: \"Fraunces\", serif; font-weight: 500; margin: 0; }" , ".rp-title.roux-recipe-title:hover::after { content: '\\270E'; font-size: 0.7rem; opacity: 0.3; margin-left: 0.5rem; vertical-align: super; }" @@ -965,7 +974,7 @@ recipePageStyles = , ".rp-tag { font-size: 12px; color: var(--roux-accent); border: 1px solid rgba(184,92,56,0.25); border-radius: 4px; padding: 2px 10px; }" , ".rp-history-section { border-top: 0.5px solid rgba(61,40,23,0.15); padding-top: 24px; margin-top: 0; }" , "" - , "@media (max-width: 720px) { .rp-page { padding: 24px 24px 60px; } .rp-meta-strip { flex-direction: column; align-items: flex-start; } .rp-meta-strip-actions { width: 100%; justify-content: flex-end; } .rp-body-grid { grid-template-columns: 1fr; gap: 32px; } .rp-title { font-size: 30px; } }" + , "@media (max-width: 720px) { .rp-meta-strip { flex-direction: column; align-items: flex-start; } .rp-meta-strip-actions { width: 100%; justify-content: flex-end; } .rp-body-grid { grid-template-columns: 1fr; gap: 32px; } .rp-title { font-size: 30px; } }" ] -- | Render the body of a recipe page (no page shell) -- mockup-compliant layout. @@ -973,13 +982,13 @@ renderRecipe :: FilePath -> Recipe -> [CookLog.CookEntry] -> Html renderRecipe filename recipe entries = do H.style $ H.toHtml recipePageStyles H.div ! A.class_ "rp-page" $ do - -- Nav (matching landing page style) - H.nav ! A.class_ "rp-nav" $ do - H.a ! A.class_ "rp-logo" ! A.href "/" $ "ROUX" - H.div ! A.class_ "rp-nav-links" $ do - H.a ! A.href "/recipes" $ "Recipes" - H.a ! A.href "/import" $ "Import" - H.a ! A.href "/cook-history" $ "Cook History" + -- Nav + renderHeader + [ ("/", "Home", False) + , ("/recipes", "Recipes", False) + , ("/import", "Import", False) + , ("/cook-history", "Cook History", False) + ] let meta = recipeMetadata recipe course = metaCourse meta desc = metaDescription meta @@ -1307,12 +1316,12 @@ isRight = not . isLeft cookHistoryPage :: (Text -> Text) -> [(Text, [CookLog.CookEntry])] -> ByteString cookHistoryPage lookupTitle grouped = page "Cook History" $ 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 "/recipes" $ "Recipes" - H.li $ H.a ! A.href "/import" $ "Import" - H.li $ H.a ! A.class_ "active" ! A.href "/cook-history" $ "Cook History" + renderHeader + [ ("/", "Home", False) + , ("/recipes", "Recipes", False) + , ("/import", "Import", False) + , ("/cook-history", "Cook History", True) + ] H.h1 "Cook History" mapM_ (renderMonthGroup lookupTitle) grouped @@ -1341,14 +1350,8 @@ renderHistoryEntry lookupTitle entry = do landingStyles :: Text landingStyles = T.unlines - [ ".lnd-page { max-width: 980px; margin: 0 auto; padding: 36px 48px 80px; }" - , ".lnd-nav { display: flex; justify-content: space-between; align-items: center; border-bottom: 0.5px solid rgba(61,44,30,0.18); padding-bottom: 12px; margin-bottom: 32px; font-size: 12px; letter-spacing: 0.12em; color: #6B5847; text-transform: uppercase; }" - , ".lnd-logo { font-family: \"Fraunces\", serif; font-size: 22px; font-weight: 500; letter-spacing: 0.08em; }" - , ".lnd-nav-links { display: flex; gap: 28px; }" - , ".lnd-nav-links a { color: #6B5847; text-decoration: none; }" - , ".lnd-nav-links a.active { color: #B85C38; }" - , ".lnd-nav-links a:hover { color: #B85C38; }" - , "" + [ ".lnd-page { max-width: 980px; margin: 0 auto; }" + , ".lnd-shuffle { background: rgba(184,92,56,0.08); border-radius: 12px; padding: 20px 24px; display: flex; justify-content: space-between; align-items: center; gap: 24px; margin-bottom: 36px; }" , ".lnd-shuffle-meta { font-size: 12px; letter-spacing: 0.12em; color: #8A7560; text-transform: uppercase; margin-bottom: 4px; }" , ".lnd-shuffle h3 { font-family: \"Fraunces\", serif; font-size: 20px; font-weight: 500; margin: 0; }" @@ -1379,7 +1382,7 @@ landingStyles = , ".lnd-img-5 { background: linear-gradient(135deg, #C7A878, #8E6B3D 60%, #5A4520); }" , ".lnd-img-6 { background: linear-gradient(135deg, #B5D4A1, #7AA862 60%, #4D7038); }" , "" - , "@media (max-width: 720px) { .lnd-page { padding: 24px 24px 60px; } .lnd-shelf-row { grid-template-columns: 1fr 1fr; } .lnd-shuffle { flex-direction: column; align-items: flex-start; } .lnd-shuffle-actions { width: 100%; justify-content: flex-end; } }" + , "@media (max-width: 720px) { .lnd-shelf-row { grid-template-columns: 1fr 1fr; } .lnd-shuffle { flex-direction: column; align-items: flex-start; } .lnd-shuffle-actions { width: 100%; justify-content: flex-end; } }" ] -- | Inline JavaScript for the landing page shuffle and navigation. @@ -1417,13 +1420,12 @@ landingPage allRecipes recentlyCooked recentlyAdded = H.style $ H.toHtml landingStyles H.div ! A.class_ "lnd-page" $ do -- Nav - H.nav ! A.class_ "lnd-nav" $ do - H.span ! A.class_ "lnd-logo" $ "ROUX" - H.div ! A.class_ "lnd-nav-links" $ do - H.a ! A.class_ "active" $ "Home" - H.a ! A.href "/recipes" $ "Recipes" - H.a ! A.href "/import" $ "Import" - H.a ! A.href "/cook-history" $ "Cook History" + renderHeader + [ ("/", "Home", True) + , ("/recipes", "Recipes", False) + , ("/import", "Import", False) + , ("/cook-history", "Cook History", False) + ] -- Shuffle card H.div ! A.class_ "lnd-shuffle" $ do H.div $ do