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