commit 22d2f1cf2f31a9bdeae40efc1718da332500b3bc
Author: Coder Agent <coder@agents.omni>
Date: Mon Feb 16 21:50:46 2026
Shared design system: dark-first terminal aesthetic
Create Omni/Web/Style.hs as unified Clay-based design system with:
- Dark-first color tokens (cBg, cAccent, cFg, etc.)
- Monospace-forward typography for terminal/hacker aesthetic
- Reading typography (sans-serif, 16px, 1.7 line-height) for articles
- iOS webapp meta tags (standalone, safe-area-inset support)
- Shared nav component and page shell for News/Files
- Semi-transparent RGBA badges that work on dark backgrounds
- Mobile responsive breakpoints at 640px
- Sticky nav with backdrop blur
Migrate all three surfaces to shared tokens:
- Task Web: replace 244 hardcoded colors with design token refs,
remove separate darkModeStyles (now redundant)
- News: use sharedShell + newsreader-specific CSS overlay
- Files: use sharedShell + files-specific CSS overlay
Also: rename Omni/Web.hs -> Omni/Web/Core.hs to allow Omni/Web/ directory,
rebrand 'Junior'/'Jr' -> 'omni' throughout nav/breadcrumbs/titles,
lowercase all nav labels for terminal aesthetic.
Task-Id: t-621
diff --git a/Omni/Ava/Core.hs b/Omni/Ava/Core.hs
index 6c7b8e32..36928e79 100755
--- a/Omni/Ava/Core.hs
+++ b/Omni/Ava/Core.hs
@@ -40,7 +40,7 @@ import qualified Omni.Agent.Memory as Memory
import qualified Omni.Ava.Telegram.Bot as Telegram
import qualified Omni.Cli as Cli
import qualified Omni.Test as Test
-import qualified Omni.Web as Web
+import qualified Omni.Web.Core as Web
import qualified System.Directory as Dir
import qualified System.Environment as Environment
import qualified System.IO as IO
diff --git a/Omni/Newsreader/Web.hs b/Omni/Newsreader/Web.hs
index 1ae74df1..459a1328 100644
--- a/Omni/Newsreader/Web.hs
+++ b/Omni/Newsreader/Web.hs
@@ -42,6 +42,7 @@ import qualified Omni.Newsreader.Cluster as Cluster
import qualified Omni.Newsreader.Feed as Feed
import qualified Omni.Newsreader.Ingest as Ingest
import qualified Omni.Newsreader.Search as Search
+import qualified Omni.Web.Style as WebStyle
-- ============================================================================
-- Application
@@ -95,10 +96,10 @@ riverPage p' conn req respond = do
feeds <- Feed.listFeeds conn
let feedMap = Map.fromList [(fid, f) | f <- feeds, Just fid <- [Feed.feedId f]]
respond <| htmlResponse HTTP.status200 <| shell "newsreader" <| do
- nav p'
- L.div_ [L.class_ "articles"]
+ newsSubNav p'
+ L.div_ [L.class_ "container-narrow"]
<| if null articles
- then L.p_ [L.class_ "empty"] "No articles yet. Add some feeds to get started."
+ then L.p_ [L.class_ "empty-state"] "No articles yet. Add some feeds to get started."
else forM_ articles <| \art -> articleCard p' feedMap art
-- | Topics page: keyword-based clusters.
@@ -109,22 +110,23 @@ topicsPage p' conn respond = do
feeds <- Feed.listFeeds conn
let feedMap = Map.fromList [(fid, f) | f <- feeds, Just fid <- [Feed.feedId f]]
respond <| htmlResponse HTTP.status200 <| shell "topics" <| do
- nav p'
- L.h1_ "Topics"
- if null clusters
- then L.p_ [L.class_ "empty"] "Not enough articles to form topics yet."
- else
- forM_ clusters <| \cluster -> do
- let arts = Cluster.clusterItems cluster
- count = length arts
- L.div_ [L.class_ "topic"] <| do
- L.h2_ <| do
- L.toHtml (Cluster.clusterLabel cluster)
- L.span_ [L.class_ "count"] <| L.toHtml (" (" <> T.pack (show count) <> ")" :: Text)
- forM_ (take 5 arts) <| \art -> articleCard p' feedMap art
- when (count > 5)
- <| L.p_ [L.class_ "muted"]
- <| L.toHtml ("+ " <> T.pack (show (count - 5)) <> " more" :: Text)
+ newsSubNav p'
+ L.div_ [L.class_ "container-narrow"] <| do
+ L.h1_ "Topics"
+ if null clusters
+ then L.p_ [L.class_ "empty-state"] "Not enough articles to form topics yet."
+ else
+ forM_ clusters <| \cluster -> do
+ let arts = Cluster.clusterItems cluster
+ count = length arts
+ L.div_ [L.class_ "nr-topic"] <| do
+ L.h2_ <| do
+ L.toHtml (Cluster.clusterLabel cluster)
+ L.span_ [L.class_ "count faint"] <| L.toHtml (" (" <> T.pack (show count) <> ")" :: Text)
+ forM_ (take 5 arts) <| \art -> articleCard p' feedMap art
+ when (count > 5)
+ <| L.p_ [L.class_ "muted"]
+ <| L.toHtml ("+ " <> T.pack (show (count - 5)) <> " more" :: Text)
-- | Single article view.
articlePage :: Pfx -> SQL.Connection -> Text -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived
@@ -140,16 +142,16 @@ articlePage p' conn aidText respond = do
let feedMap = Map.fromList [(fid, f) | f <- feeds, Just fid <- [Feed.feedId f]]
feedName = maybe "unknown" (fromMaybe "untitled" <. Feed.feedTitle) (Map.lookup (Article.articleFeedId art) feedMap)
respond <| htmlResponse HTTP.status200 <| shell (Article.articleTitle art) <| do
- nav p'
- L.article_ [L.class_ "full-article"] <| do
+ newsSubNav p'
+ L.article_ [L.class_ "nr-article"] <| do
L.h1_ (L.toHtml (Article.articleTitle art))
L.div_ [L.class_ "meta"] <| do
- L.span_ [L.class_ "feed"] (L.toHtml feedName)
+ L.span_ [L.class_ "accent"] (L.toHtml feedName)
L.toHtml (" · " :: Text)
- L.span_ [L.class_ "date"] (L.toHtml (fmtTime (Article.articlePublishedAt art)))
+ L.span_ [] (L.toHtml (fmtTime (Article.articlePublishedAt art)))
L.toHtml (" · " :: Text)
L.a_ [L.href_ (Article.articleUrl art), L.target_ "_blank"] "original ↗"
- L.div_ [L.class_ "content"]
+ L.div_ [L.class_ "content reading"]
<| L.toHtmlRaw (Article.articleContent art)
-- | Feeds management page.
@@ -157,25 +159,26 @@ feedsPage :: Pfx -> SQL.Connection -> (Wai.Response -> IO Wai.ResponseReceived)
feedsPage p' conn respond = do
feeds <- Feed.listFeeds conn
respond <| htmlResponse HTTP.status200 <| shell "feeds" <| do
- nav p'
- L.h1_ "Feeds"
- L.form_ [L.method_ "POST", L.action_ (p' "/api/feeds"), L.class_ "add-feed"] <| do
- L.input_ [L.type_ "url", L.name_ "url", L.placeholder_ "https://example.com/feed.xml", L.required_ ""]
- L.button_ [L.type_ "submit"] "add"
- if null feeds
- then L.p_ [L.class_ "empty"] "No feeds yet."
- else
- L.ul_ [L.class_ "feed-list"]
- <| forM_ feeds
- <| \feed -> do
- let title = fromMaybe (Feed.feedUrl feed) (Feed.feedTitle feed)
- fetched = maybe "never" (T.pack <. TimeF.formatTime TimeF.defaultTimeLocale "%Y-%m-%d %H:%M") (Feed.feedLastFetched feed)
- L.li_ <| do
- L.div_ (L.toHtml title)
- L.div_ [L.class_ "muted small"] <| do
- L.toHtml (Feed.feedUrl feed)
- L.toHtml (" · last fetched: " :: Text)
- L.toHtml fetched
+ newsSubNav p'
+ L.div_ [L.class_ "container-narrow"] <| do
+ L.h1_ "Feeds"
+ L.form_ [L.method_ "POST", L.action_ (p' "/api/feeds"), L.class_ "nr-add-feed"] <| do
+ L.input_ [L.type_ "url", L.name_ "url", L.placeholder_ "https://example.com/feed.xml", L.required_ ""]
+ L.button_ [L.type_ "submit", L.class_ "btn btn-primary"] "add"
+ if null feeds
+ then L.p_ [L.class_ "empty-state"] "No feeds yet."
+ else
+ L.ul_ [L.class_ "nr-feed-list"]
+ <| forM_ feeds
+ <| \feed -> do
+ let title = fromMaybe (Feed.feedUrl feed) (Feed.feedTitle feed)
+ fetched = maybe "never" (T.pack <. TimeF.formatTime TimeF.defaultTimeLocale "%Y-%m-%d %H:%M") (Feed.feedLastFetched feed)
+ L.li_ <| do
+ L.div_ (L.toHtml title)
+ L.div_ [L.class_ "muted text-xs"] <| do
+ L.toHtml (Feed.feedUrl feed)
+ L.toHtml (" · last fetched: " :: Text)
+ L.toHtml fetched
-- | Search page.
searchPage :: Pfx -> SQL.Connection -> Wai.Request -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived
@@ -188,15 +191,16 @@ searchPage p' conn req respond = do
then pure []
else Search.searchArticles conn query 50
respond <| htmlResponse HTTP.status200 <| shell "search" <| do
- nav p'
- L.form_ [L.method_ "GET", L.action_ (p' "/search"), L.class_ "search-form"]
- <| L.input_ [L.type_ "search", L.name_ "q", L.value_ query, L.placeholder_ "search articles...", L.autofocus_]
- unless (T.null query)
- <| if null results
- then L.p_ [L.class_ "empty"] "No results."
- else
- forM_ results <| \sr ->
- articleCard p' feedMap (Search.srArticle sr)
+ newsSubNav p'
+ L.div_ [L.class_ "container-narrow"] <| do
+ L.form_ [L.method_ "GET", L.action_ (p' "/search"), L.class_ "nr-search-form"]
+ <| L.input_ [L.type_ "search", L.name_ "q", L.value_ query, L.placeholder_ "search articles...", L.autofocus_]
+ unless (T.null query)
+ <| if null results
+ then L.p_ [L.class_ "empty-state"] "No results."
+ else
+ forM_ results <| \sr ->
+ articleCard p' feedMap (Search.srArticle sr)
-- ============================================================================
-- JSON API
@@ -305,117 +309,94 @@ articleCard p' feedMap art = do
let feedName = maybe "unknown" (fromMaybe "untitled" <. Feed.feedTitle) (Map.lookup (Article.articleFeedId art) feedMap)
snippet = T.take 280 (stripTags (Article.articleContent art))
artUrl = p' ("/article/" <> maybe "" (T.pack <. show <. Article.unArticleId) (Article.articleId art))
- L.div_ [L.class_ "article-card"] <| do
- L.a_ [L.href_ artUrl, L.class_ "title"] (L.toHtml (Article.articleTitle art))
- L.div_ [L.class_ "meta"] <| do
- L.span_ [L.class_ "feed"] (L.toHtml feedName)
- L.toHtml (" · " :: Text)
- L.span_ [L.class_ "date"] (L.toHtml (fmtTime (Article.articlePublishedAt art)))
+ L.div_ [L.class_ "nr-card"] <| do
+ L.a_ [L.href_ artUrl, L.class_ "nr-card-title"] (L.toHtml (Article.articleTitle art))
+ L.div_ [L.class_ "nr-card-meta"] <| do
+ L.span_ [L.class_ "accent"] (L.toHtml feedName)
+ L.span_ [L.class_ "faint"] " · "
+ L.span_ [] (L.toHtml (fmtTime (Article.articlePublishedAt art)))
unless (T.null snippet)
- <| L.p_ [L.class_ "snippet"] (L.toHtml snippet)
-
--- | Navigation bar.
-nav :: Pfx -> L.Html ()
-nav p' =
- L.nav_ <| do
- L.a_ [L.href_ "/tasks", L.class_ "brand"] "omni"
- L.span_ [L.class_ "nav-links"] <| do
- L.a_ [L.href_ "/tasks"] "tasks"
- L.a_ [L.href_ "/news/"] "news"
- L.a_ [L.href_ "/files/"] "files"
- L.a_ [L.href_ (p' "/topics")] "topics"
- L.a_ [L.href_ (p' "/feeds")] "feeds"
- L.a_ [L.href_ (p' "/search")] "search"
+ <| L.p_ [L.class_ "nr-card-snippet"] (L.toHtml snippet)
+
+-- | Sub-navigation for newsreader sections.
+newsSubNav :: Pfx -> L.Html ()
+newsSubNav p' =
+ L.div_ [L.class_ "nr-subnav"] <| do
+ L.a_ [L.href_ (p' "/topics"), L.class_ "nr-subnav-link"] "topics"
+ L.a_ [L.href_ (p' "/feeds"), L.class_ "nr-subnav-link"] "feeds"
+ L.a_ [L.href_ (p' "/search"), L.class_ "nr-subnav-link"] "search"
-- | Error page.
errorPage :: Text -> L.Html ()
errorPage msg =
shell msg <| do
- L.p_ (L.toHtml msg)
+ L.div_ [L.class_ "container"] <| L.p_ [L.class_ "empty-state"] (L.toHtml msg)
--- | Page shell with CSS.
+-- | Page shell with shared design system CSS.
shell :: Text -> L.Html () -> L.Html ()
-shell title body =
- L.doctypehtml_ <| do
- L.head_ <| do
- L.meta_ [L.charset_ "utf-8"]
- L.meta_ [L.name_ "viewport", L.content_ "width=device-width, initial-scale=1"]
- L.title_ (L.toHtml title)
- L.style_ css
- L.body_ body
+shell title content =
+ WebStyle.sharedShell title "news" <| do
+ L.style_ newsCss
+ content
-- ============================================================================
-- CSS
-- ============================================================================
-css :: Text
-css =
+-- | Newsreader-specific CSS (layered on top of shared design system).
+newsCss :: Text
+newsCss =
T.unlines
- [ ":root { --fg: #1f2937; --bg: #f5f5f5; --muted: #6b7280; --link: #0066cc;",
- " --border: #d0d0d0; --hover: #f3f4f6; --card: #ffffff; }",
- "@media (prefers-color-scheme: dark) {",
- " :root { --fg: #d4d4d4; --bg: #111827; --muted: #9ca3af; --link: #60a5fa;",
- " --border: #374151; --hover: #1f2937; --card: #111827; }",
- "}",
- "* { box-sizing: border-box; margin: 0; padding: 0; }",
- "body { font: 14px/1.3 -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;",
- " max-width: 960px; margin: 0 auto; padding: 0.5rem 0.75rem;",
- " background: var(--bg); color: var(--fg); min-height: 100vh; }",
- "a { color: var(--link); text-decoration: none; }",
- "a:hover { text-decoration: underline; }",
- "",
- "nav { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;",
- " background: var(--card); border-bottom: 1px solid var(--border);",
- " padding: 0.375rem 0.75rem; margin-bottom: 0.5rem; }",
- ".brand { font-weight: 700; font-size: 1rem; color: var(--link); }",
- ".brand:hover { text-decoration: none; }",
- ".nav-links { display: flex; gap: 0.25rem; flex-wrap: wrap; font-size: 0.82rem; }",
- ".nav-links a { color: var(--muted); padding: 0.25rem 0.6rem; border-radius: 2px; }",
- ".nav-links a:hover { background: var(--hover); color: var(--fg); text-decoration: none; }",
- "",
- ".article-card { margin-bottom: 0.45rem; background: var(--card); border: 1px solid #e5e7eb; border-radius: 2px; padding: 0.6rem 0.7rem; }",
- ".article-card .title { font-size: 0.95rem; font-weight: 600; display: block; color: var(--link); }",
- ".article-card .meta { font-size: 0.78rem; color: var(--muted); margin: 0.15rem 0 0.3rem; }",
- ".article-card .snippet { font-size: 0.85rem; color: var(--muted); line-height: 1.4; }",
+ [ -- Sub-navigation
+ ".nr-subnav { display: flex; gap: 2px; padding: 4px 16px; border-bottom: 1px solid var(--c-border-subtle); }",
+ ".nr-subnav-link { font-size: 12px; color: var(--c-fg-faint); padding: 4px 10px; border-radius: 4px; }",
+ ".nr-subnav-link:hover { color: var(--c-fg); background: var(--c-bg-hover); text-decoration: none; }",
"",
- ".full-article h1 { font-size: 1.25rem; line-height: 1.25; margin-bottom: 0.4rem; }",
- ".full-article .meta { font-size: 0.82rem; color: var(--muted); margin-bottom: 1rem; }",
- ".full-article .content p { margin-bottom: 0.85rem; line-height: 1.5; }",
+ -- Article cards
+ ".nr-card { padding: 10px 16px; border-bottom: 1px solid var(--c-border-subtle); transition: background 0.1s; }",
+ ".nr-card:hover { background: var(--c-bg-hover); }",
+ ".nr-card-title { font-size: 14px; font-weight: 600; display: block; color: var(--c-fg); line-height: 1.4; }",
+ ".nr-card-title:hover { color: var(--c-accent); text-decoration: none; }",
+ ".nr-card-meta { font-size: 11px; color: var(--c-fg-muted); margin: 3px 0 4px; }",
+ ".nr-card-snippet { font-size: 13px; color: var(--c-fg-muted); line-height: 1.5; font-family: var(--font-reading); }",
"",
- ".topic { margin-bottom: 1rem; }",
- ".topic h2 { font-size: 1rem; margin-bottom: 0.35rem; }",
- ".topic .count { font-weight: normal; color: var(--muted); }",
+ -- Full article view
+ ".nr-article { padding: 16px; max-width: 680px; margin: 0 auto; }",
+ ".nr-article h1 { font-size: 1.4rem; line-height: 1.3; margin-bottom: 8px; }",
+ ".nr-article .meta { font-size: 12px; color: var(--c-fg-muted); margin-bottom: 20px; padding-bottom: 12px; border-bottom: 1px solid var(--c-border-subtle); }",
+ ".nr-article .content { font-family: var(--font-reading); font-size: 16px; line-height: 1.7; letter-spacing: -0.011em; }",
+ ".nr-article .content p { margin-bottom: 1em; }",
+ ".nr-article .content img { max-width: 100%; height: auto; border-radius: 6px; }",
+ ".nr-article .content blockquote { border-left: 3px solid var(--c-accent-dim); padding-left: 16px; color: var(--c-fg-muted); margin: 1em 0; }",
+ ".nr-article .content a { text-underline-offset: 3px; }",
"",
- ".feed-list { list-style: none; background: var(--card); border: 1px solid #e5e7eb; border-radius: 2px; }",
- ".feed-list li { padding: 0.5rem 0.7rem; border-bottom: 1px solid #e5e7eb; }",
- ".feed-list li:last-child { border-bottom: none; }",
- ".small { font-size: 0.8rem; }",
- ".muted { color: var(--muted); }",
- ".empty { color: var(--muted); font-style: italic; margin: 0.5rem 0; }",
+ -- Topics
+ ".nr-topic { margin-bottom: 20px; }",
+ ".nr-topic h2 { font-size: 1rem; margin-bottom: 6px; }",
+ ".nr-topic .count { font-weight: normal; color: var(--c-fg-faint); }",
"",
- ".add-feed { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; }",
- ".add-feed input { flex: 1; padding: 0.35rem 0.5rem; font-size: 0.85rem;",
- " border: 1px solid #d1d5db; background: var(--card); color: var(--fg); border-radius: 2px; }",
- ".add-feed button { padding: 0.35rem 0.65rem; font-size: 0.82rem;",
- " border: 1px solid #d1d5db; background: var(--hover); color: var(--fg);",
- " border-radius: 2px; cursor: pointer; }",
+ -- Feed list
+ ".nr-feed-list { list-style: none; }",
+ ".nr-feed-list li { padding: 10px 16px; border-bottom: 1px solid var(--c-border-subtle); }",
+ ".nr-feed-list li:last-child { border-bottom: none; }",
"",
- ".search-form { margin-bottom: 0.75rem; }",
- ".search-form input { width: 100%; padding: 0.45rem 0.5rem; font-size: 0.9rem;",
- " border: 1px solid #d1d5db; background: var(--card); color: var(--fg); border-radius: 2px; }",
+ -- Add feed form
+ ".nr-add-feed { display: flex; gap: 8px; margin-bottom: 16px; }",
+ ".nr-add-feed input { flex: 1; }",
+ ".nr-add-feed button { padding: 8px 14px; }",
"",
- "@media (max-width: 700px) {",
- " body { font-size: 15px; padding: 0.5rem; }",
- " nav { padding: 0.5rem; }",
- " .nav-links { width: 100%; gap: 0.2rem; }",
- " .nav-links a { padding: 0.45rem 0.6rem; font-size: 0.9rem; }",
- " .article-card { padding: 0.7rem; }",
- " .article-card .title { font-size: 1rem; line-height: 1.3; }",
- " .article-card .meta { font-size: 0.82rem; }",
- " .article-card .snippet { font-size: 0.9rem; }",
- "}",
+ -- Search
+ ".nr-search-form { margin-bottom: 16px; }",
+ ".nr-search-form input { width: 100%; }",
"",
- "h1 { font-size: 1.15rem; margin-bottom: 0.6rem; }"
+ -- Mobile
+ "@media (max-width: 640px) {",
+ " .nr-card { padding: 10px 8px; }",
+ " .nr-card-title { font-size: 15px; }",
+ " .nr-card-snippet { font-size: 14px; }",
+ " .nr-article .content { font-size: 15px; line-height: 1.65; }",
+ " .nr-subnav { padding: 4px 8px; }",
+ "}"
]
-- ============================================================================
diff --git a/Omni/Serve.hs b/Omni/Serve.hs
index fe1415fd..03f1527e 100755
--- a/Omni/Serve.hs
+++ b/Omni/Serve.hs
@@ -46,6 +46,7 @@ import qualified Network.Wai as Wai
import qualified Network.Wai.Handler.Warp as Warp
import qualified Omni.Cli as Cli
import qualified Omni.Test as Test
+import qualified Omni.Web.Style as WebStyle
import qualified System.Directory as Dir
import qualified System.FilePath as FP
@@ -127,10 +128,11 @@ renderMarkdown p' filename content = do
let title = T.pack (FP.dropExtension filename)
rendered = CMark.commonmarkToHtml [CMark.optSmart, CMark.optUnsafe] content
pageShell title <| do
- L.div_ [L.class_ "breadcrumb"] <| do
- L.a_ [L.href_ (p' "/")] "home"
- L.article_ [L.class_ "markdown-body"]
- <| L.toHtmlRaw rendered
+ L.div_ [L.class_ "container"] <| do
+ L.div_ [L.class_ "breadcrumb"] <| do
+ L.a_ [L.href_ (p' "/")] "home"
+ L.article_ [L.class_ "markdown-body"]
+ <| L.toHtmlRaw rendered
-- | Directory listing page.
dirListing :: (Text -> Text) -> FilePath -> FilePath -> IO (L.Html ())
@@ -149,29 +151,30 @@ dirListing p' root relPath = do
else Just </ Dir.getFileSize entryPath
pure (name, isDir, modTime, size)
pure <| pageShell title <| do
- L.div_ [L.class_ "breadcrumb"] <| breadcrumbs p' relPath
- L.h1_ (L.toHtml title)
- L.table_ [L.class_ "listing"] <| do
- L.thead_ <| L.tr_ <| do
- L.th_ "Name"
- L.th_ [L.class_ "size"] "Size"
- L.th_ [L.class_ "modified"] "Modified"
- L.tbody_ <| do
- -- Parent directory link
- when (relPath /= "" && relPath /= ".")
- <| L.tr_
- <| do
- L.td_ <| L.a_ [L.href_ (p' (T.pack ("/" <> FP.takeDirectory relPath)))] "⬆ .."
- L.td_ ""
- L.td_ ""
- forM_ items <| \(name, isDir, modTime, size) -> do
- let href = p' (T.pack ("/" <> relPath FP.</> name <> if isDir then "/" else ""))
- icon = if isDir then "📁 " else fileIcon name
- L.tr_ <| do
- L.td_ <| do
- L.a_ [L.href_ href] <| L.toHtml (icon <> T.pack name)
- L.td_ [L.class_ "size"] <| L.toHtml (maybe "" formatSize size)
- L.td_ [L.class_ "modified"] <| L.toHtml (formatTime modTime)
+ L.div_ [L.class_ "container"] <| do
+ L.div_ [L.class_ "breadcrumb"] <| breadcrumbs p' relPath
+ L.h1_ (L.toHtml title)
+ L.table_ [L.class_ "listing"] <| do
+ L.thead_ <| L.tr_ <| do
+ L.th_ "Name"
+ L.th_ [L.class_ "size"] "Size"
+ L.th_ [L.class_ "modified"] "Modified"
+ L.tbody_ <| do
+ -- Parent directory link
+ when (relPath /= "" && relPath /= ".")
+ <| L.tr_
+ <| do
+ L.td_ <| L.a_ [L.href_ (p' (T.pack ("/" <> FP.takeDirectory relPath)))] "⬆ .."
+ L.td_ ""
+ L.td_ ""
+ forM_ items <| \(name, isDir, modTime, size) -> do
+ let href = p' (T.pack ("/" <> relPath FP.</> name <> if isDir then "/" else ""))
+ icon = if isDir then "📁 " else fileIcon name
+ L.tr_ <| do
+ L.td_ <| do
+ L.a_ [L.href_ href] <| L.toHtml (icon <> T.pack name)
+ L.td_ [L.class_ "size"] <| L.toHtml (maybe "" formatSize size)
+ L.td_ [L.class_ "modified"] <| L.toHtml (formatTime modTime)
-- | Breadcrumb navigation.
breadcrumbs :: (Text -> Text) -> FilePath -> L.Html ()
@@ -184,82 +187,47 @@ breadcrumbs p' relPath = do
where
parts = filter (/= ".") (FP.splitDirectories relPath)
--- | Shared Omni top navigation.
-topNav :: L.Html ()
-topNav =
- L.nav_ [L.class_ "topnav"] <| do
- L.a_ [L.href_ "/tasks", L.class_ "topnav-link"] "tasks"
- L.a_ [L.href_ "/news/", L.class_ "topnav-link"] "news"
- L.a_ [L.href_ "/files/", L.class_ "topnav-link"] "files"
-
--- | Common page shell with CSS.
+-- | Common page shell with shared design system CSS + files-specific CSS.
pageShell :: Text -> L.Html () -> L.Html ()
-pageShell title body =
- L.doctypehtml_ <| do
- L.head_ <| do
- L.meta_ [L.charset_ "utf-8"]
- L.meta_ [L.name_ "viewport", L.content_ "width=device-width, initial-scale=1"]
- L.title_ (L.toHtml title)
- L.style_ css
- L.body_ <| do
- topNav
- body
+pageShell title content =
+ WebStyle.sharedShell title "files" <| do
+ L.style_ filesCss
+ content
--- | Stylesheet.
-css :: Text
-css =
+-- | Files-specific CSS (layered on top of shared design system).
+filesCss :: Text
+filesCss =
T.unlines
- [ ":root { --bg: #f5f5f5; --fg: #1f2937; --muted: #6b7280; --border: #d0d0d0;",
- " --link: #0066cc; --code-bg: #111827; --code-fg: #d4d4d4; --hover: #f3f4f6; --card: #fff; }",
- "@media (prefers-color-scheme: dark) {",
- " :root { --bg: #111827; --fg: #d4d4d4; --muted: #9ca3af; --border: #374151;",
- " --link: #60a5fa; --code-bg: #0b1220; --code-fg: #d4d4d4; --hover: #1f2937; --card: #111827; }",
- "}",
- "* { box-sizing: border-box; }",
- "body { font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;",
- " font-size: 14px; max-width: 960px; margin: 0 auto; padding: 0.5rem 0.75rem;",
- " background: var(--bg); color: var(--fg); line-height: 1.4; min-height: 100vh; }",
- "a { color: var(--link); text-decoration: none; }",
- "a:hover { text-decoration: underline; }",
- ".topnav { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;",
- " background: var(--card); border-bottom: 1px solid var(--border); margin: 0 0 0.5rem; padding: 0.375rem 0.75rem; }",
- ".topnav-link { color: var(--muted); font-size: 0.82rem; padding: 0.25rem 0.6rem; border-radius: 2px; }",
- ".topnav-link:hover { background: var(--hover); color: var(--fg); text-decoration: none; }",
- ".breadcrumb { font-size: 0.8rem; color: var(--muted); margin-bottom: 0.5rem; }",
- "h1 { font-size: 1.15rem; margin: 0 0 0.6rem; }",
+ [ ".breadcrumb { font-size: 12px; color: var(--c-fg-muted); margin-bottom: 8px; padding: 8px 16px; }",
+ ".breadcrumb a { color: var(--c-fg-faint); }",
+ ".breadcrumb a:hover { color: var(--c-accent); }",
"",
- "@media (max-width: 700px) {",
- " body { font-size: 15px; padding: 0.5rem; }",
- " .topnav { padding: 0.5rem; }",
- " .topnav-link { padding: 0.45rem 0.6rem; font-size: 0.9rem; }",
- " .listing td, .listing th { padding: 0.45rem 0.5rem; }",
- " .listing .size, .listing .modified { font-size: 0.78rem; }",
- " .markdown-body { padding: 0.6rem; font-size: 0.95rem; }",
- "}",
+ ".listing { border: 1px solid var(--c-border); border-radius: 6px; overflow: hidden; }",
+ ".listing .size, .listing .modified {",
+ " text-align: right; font-size: 11px; color: var(--c-fg-faint); white-space: nowrap; }",
"",
- "/* Directory listing */",
- ".listing { width: 100%; border-collapse: collapse; background: var(--card); border: 1px solid #e5e7eb; border-radius: 2px; overflow: hidden; }",
- ".listing th { text-align: left; border-bottom: 1px solid #e5e7eb;",
- " padding: 0.4rem 0.65rem; font-size: 0.8rem; color: var(--muted); }",
- ".listing td { padding: 0.38rem 0.65rem; border-bottom: 1px solid #e5e7eb; font-size: 0.86rem; }",
- ".listing tr:last-child td { border-bottom: none; }",
- ".listing tr:hover { background: var(--hover); }",
- ".listing .size, .listing .modified { text-align: right;",
- " font-size: 0.8rem; color: var(--muted); white-space: nowrap; }",
- "",
- "/* Markdown */",
- ".markdown-body { font-size: 0.92rem; background: var(--card); border: 1px solid #e5e7eb; border-radius: 2px; padding: 0.75rem; }",
- ".markdown-body h1 { font-size: 1.4rem; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.25rem; }",
- ".markdown-body h2 { font-size: 1.2rem; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.15rem; }",
- ".markdown-body h3 { font-size: 1.05rem; }",
- ".markdown-body pre { background: var(--code-bg); color: var(--code-fg); padding: 0.75rem;",
- " border-radius: 2px; overflow-x: auto; font-size: 0.82rem; }",
- ".markdown-body code { background: #eef2f7; padding: 0.12rem 0.28rem; border-radius: 2px; font-size: 0.86em; }",
- ".markdown-body pre code { background: none; padding: 0; }",
- ".markdown-body img { max-width: 100%; height: auto; }",
+ ".markdown-body {",
+ " font-family: var(--font-reading); font-size: 16px; line-height: 1.7;",
+ " letter-spacing: -0.011em; max-width: 680px;",
+ " background: var(--c-bg-raised); border: 1px solid var(--c-border);",
+ " border-radius: 6px; padding: 16px 20px; }",
+ ".markdown-body h1 { font-size: 1.4rem; border-bottom: 1px solid var(--c-border); padding-bottom: 6px; margin: 1em 0 0.5em; }",
+ ".markdown-body h2 { font-size: 1.2rem; border-bottom: 1px solid var(--c-border-subtle); padding-bottom: 4px; margin: 0.8em 0 0.4em; }",
+ ".markdown-body h3 { font-size: 1.05rem; margin: 0.6em 0 0.3em; }",
+ ".markdown-body p { margin-bottom: 1em; }",
+ ".markdown-body pre { background: var(--c-bg); border: 1px solid var(--c-border); }",
+ ".markdown-body code { background: var(--c-bg-hover); border: 1px solid var(--c-border); }",
+ ".markdown-body pre code { background: none; border: none; padding: 0; }",
+ ".markdown-body img { max-width: 100%; height: auto; border-radius: 6px; }",
+ ".markdown-body blockquote { border-left: 3px solid var(--c-accent-dim); margin-left: 0; padding-left: 16px; color: var(--c-fg-muted); }",
".markdown-body table { border-collapse: collapse; width: 100%; }",
- ".markdown-body th, .markdown-body td { border: 1px solid #e5e7eb; padding: 0.3rem 0.55rem; }",
- ".markdown-body blockquote { border-left: 3px solid #d1d5db; margin-left: 0; padding-left: 0.75rem; color: var(--muted); }"
+ ".markdown-body th, .markdown-body td { border: 1px solid var(--c-border); padding: 6px 12px; }",
+ "",
+ "@media (max-width: 640px) {",
+ " .listing td, .listing th { padding: 8px 8px; }",
+ " .listing .size, .listing .modified { font-size: 10px; }",
+ " .markdown-body { padding: 12px; font-size: 15px; }",
+ "}"
]
-- ============================================================================
diff --git a/Omni/Task/Web/Components.hs b/Omni/Task/Web/Components.hs
index 275d542e..e9ea5608 100644
--- a/Omni/Task/Web/Components.hs
+++ b/Omni/Task/Web/Components.hs
@@ -197,8 +197,11 @@ pageHead title =
Lucid.meta_ [Lucid.charset_ "utf-8"]
Lucid.meta_
[ Lucid.name_ "viewport",
- Lucid.content_ "width=device-width, initial-scale=1"
+ Lucid.content_ "width=device-width, initial-scale=1, viewport-fit=cover"
]
+ Lucid.meta_ [Lucid.name_ "apple-mobile-web-app-capable", Lucid.content_ "yes"]
+ Lucid.meta_ [Lucid.name_ "apple-mobile-web-app-status-bar-style", Lucid.content_ "black-translucent"]
+ Lucid.meta_ [Lucid.name_ "theme-color", Lucid.content_ "#0a0a0a"]
Lucid.link_ [Lucid.rel_ "stylesheet", Lucid.href_ "/style.css"]
Lucid.script_
[ Lucid.src_ "https://unpkg.com/htmx.org@2.0.4",
@@ -545,7 +548,7 @@ taskBreadcrumbs allTasks task =
let ancestors = getAncestors allTasks task
taskCrumbs = [Breadcrumb (TaskCore.taskId t) (Just ("/tasks/" <> TaskCore.taskId t)) | t <- List.init ancestors]
currentCrumb = Breadcrumb (TaskCore.taskId task) Nothing
- in [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" (Just "/tasks")]
+ in [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "tasks" (Just "/tasks")]
++ taskCrumbs
++ [currentCrumb]
@@ -554,7 +557,7 @@ taskBreadcrumbs allTasks task =
navbar :: (Monad m) => Lucid.HtmlT m ()
navbar =
Lucid.nav_ [Lucid.class_ "navbar"] <| do
- Lucid.a_ [Lucid.href_ "/", Lucid.class_ "navbar-brand"] "Junior"
+ Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "navbar-brand"] "omni"
Lucid.input_
[ Lucid.type_ "checkbox",
Lucid.id_ "navbar-toggle",
@@ -569,22 +572,22 @@ navbar =
Lucid.span_ [Lucid.class_ "hamburger-line"] ""
Lucid.span_ [Lucid.class_ "hamburger-line"] ""
Lucid.div_ [Lucid.class_ "navbar-links"] <| do
- Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "navbar-link"] "Tasks"
- Lucid.a_ [Lucid.href_ "/news/", Lucid.class_ "navbar-link"] "News"
- Lucid.a_ [Lucid.href_ "/files/", Lucid.class_ "navbar-link"] "Files"
+ Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "navbar-link"] "tasks"
+ Lucid.a_ [Lucid.href_ "/news/", Lucid.class_ "navbar-link"] "news"
+ Lucid.a_ [Lucid.href_ "/files/", Lucid.class_ "navbar-link"] "files"
Lucid.div_ [Lucid.class_ "navbar-dropdown"] <| do
- Lucid.button_ [Lucid.class_ "navbar-dropdown-btn"] "Tasks ▾"
+ Lucid.button_ [Lucid.class_ "navbar-dropdown-btn"] "tasks ▾"
Lucid.div_ [Lucid.class_ "navbar-dropdown-content"] <| do
- Lucid.a_ [Lucid.href_ "/ready", Lucid.class_ "navbar-dropdown-item"] "Ready"
- Lucid.a_ [Lucid.href_ "/blocked", Lucid.class_ "navbar-dropdown-item"] "Blocked"
- Lucid.a_ [Lucid.href_ "/intervention", Lucid.class_ "navbar-dropdown-item"] "Human Action"
- Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "navbar-dropdown-item"] "All"
+ Lucid.a_ [Lucid.href_ "/ready", Lucid.class_ "navbar-dropdown-item"] "ready"
+ Lucid.a_ [Lucid.href_ "/blocked", Lucid.class_ "navbar-dropdown-item"] "blocked"
+ Lucid.a_ [Lucid.href_ "/intervention", Lucid.class_ "navbar-dropdown-item"] "needs help"
+ Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "navbar-dropdown-item"] "all"
Lucid.div_ [Lucid.class_ "navbar-dropdown"] <| do
- Lucid.button_ [Lucid.class_ "navbar-dropdown-btn"] "Plans ▾"
+ Lucid.button_ [Lucid.class_ "navbar-dropdown-btn"] "plans ▾"
Lucid.div_ [Lucid.class_ "navbar-dropdown-content"] <| do
- Lucid.a_ [Lucid.href_ "/epics", Lucid.class_ "navbar-dropdown-item"] "Epics"
- Lucid.a_ [Lucid.href_ "/kb", Lucid.class_ "navbar-dropdown-item"] "KB"
- Lucid.a_ [Lucid.href_ "/stats", Lucid.class_ "navbar-link"] "Stats"
+ Lucid.a_ [Lucid.href_ "/epics", Lucid.class_ "navbar-dropdown-item"] "epics"
+ Lucid.a_ [Lucid.href_ "/kb", Lucid.class_ "navbar-dropdown-item"] "kb"
+ Lucid.a_ [Lucid.href_ "/stats", Lucid.class_ "navbar-link"] "stats"
-- * Badges
diff --git a/Omni/Task/Web/Pages.hs b/Omni/Task/Web/Pages.hs
index dd723990..5c38be90 100644
--- a/Omni/Task/Web/Pages.hs
+++ b/Omni/Task/Web/Pages.hs
@@ -16,6 +16,7 @@ import Data.Time (utctDayTime)
import qualified Lucid
import qualified Lucid.Base as Lucid
import Numeric (showFFloat)
+import qualified Omni.Task.Core as TaskCore
import Omni.Task.Web.Components
( Breadcrumb (..),
complexityBadgeWithForm,
@@ -66,7 +67,6 @@ import Omni.Task.Web.Types
sortTasks,
timeRangeToParam,
)
-import qualified Omni.Task.Core as TaskCore
taskToUnixTs :: TaskCore.Task -> Int
taskToUnixTs t =
@@ -177,9 +177,9 @@ instance Lucid.ToHtml HomePage where
instance Lucid.ToHtml ReadyQueuePage where
toHtmlRaw = Lucid.toHtml
toHtml (ReadyQueuePage tasks currentSort _now) =
- let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Ready Queue" Nothing]
+ let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Ready Queue" Nothing]
in Lucid.doctypehtml_ <| do
- pageHead "Ready Queue - Jr"
+ pageHead "Ready Queue - omni"
pageBodyWithCrumbs crumbs <| do
Lucid.div_ [Lucid.class_ "container"] <| do
Lucid.div_ [Lucid.class_ "page-header-row"] <| do
@@ -192,9 +192,9 @@ instance Lucid.ToHtml ReadyQueuePage where
instance Lucid.ToHtml BlockedPage where
toHtmlRaw = Lucid.toHtml
toHtml (BlockedPage tasksWithImpact currentSort _now) =
- let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Blocked" Nothing]
+ let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Blocked" Nothing]
in Lucid.doctypehtml_ <| do
- pageHead "Blocked Tasks - Jr"
+ pageHead "Blocked Tasks - omni"
pageBodyWithCrumbs crumbs <| do
Lucid.div_ [Lucid.class_ "container"] <| do
Lucid.div_ [Lucid.class_ "page-header-row"] <| do
@@ -208,13 +208,13 @@ instance Lucid.ToHtml BlockedPage where
instance Lucid.ToHtml InterventionPage where
toHtmlRaw = Lucid.toHtml
toHtml (InterventionPage actionItems currentSort _now) =
- let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Needs Human Action" Nothing]
+ let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Needs Human Action" Nothing]
failed = TaskCore.failedTasks actionItems
epicsReady = TaskCore.epicsInReview actionItems
needsHelp = TaskCore.tasksNeedingHelp actionItems
totalCount = length failed + length epicsReady + length needsHelp
in Lucid.doctypehtml_ <| do
- pageHead "Needs Human Action - Jr"
+ pageHead "Needs Human Action - omni"
pageBodyWithCrumbs crumbs <| do
Lucid.div_ [Lucid.class_ "container"] <| do
Lucid.div_ [Lucid.class_ "page-header-row"] <| do
@@ -239,9 +239,9 @@ instance Lucid.ToHtml InterventionPage where
instance Lucid.ToHtml KBPage where
toHtmlRaw = Lucid.toHtml
toHtml (KBPage facts) =
- let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Knowledge Base" Nothing]
+ let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Knowledge Base" Nothing]
in Lucid.doctypehtml_ <| do
- pageHead "Knowledge Base - Jr"
+ pageHead "Knowledge Base - omni"
pageBodyWithCrumbs crumbs <| do
Lucid.div_ [Lucid.class_ "container"] <| do
Lucid.h1_ "Knowledge Base"
@@ -336,9 +336,9 @@ instance Lucid.ToHtml KBPage where
instance Lucid.ToHtml FactDetailPage where
toHtmlRaw = Lucid.toHtml
toHtml (FactDetailNotFound fid) =
- let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Knowledge Base" (Just "/kb"), Breadcrumb ("Fact #" <> tshow fid) Nothing]
+ let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Knowledge Base" (Just "/kb"), Breadcrumb ("Fact #" <> tshow fid) Nothing]
in Lucid.doctypehtml_ <| do
- pageHead "Fact Not Found - Jr"
+ pageHead "Fact Not Found - omni"
pageBodyWithCrumbs crumbs <| do
Lucid.div_ [Lucid.class_ "container"] <| do
Lucid.h1_ "Fact Not Found"
@@ -346,9 +346,9 @@ instance Lucid.ToHtml FactDetailPage where
Lucid.a_ [Lucid.href_ "/kb", Lucid.class_ "btn btn-secondary"] "Back to Knowledge Base"
toHtml (FactDetailFound fact now) =
let fid' = maybe "-" tshow (TaskCore.factId fact)
- crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Knowledge Base" (Just "/kb"), Breadcrumb ("Fact #" <> fid') Nothing]
+ crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Knowledge Base" (Just "/kb"), Breadcrumb ("Fact #" <> fid') Nothing]
in Lucid.doctypehtml_ <| do
- pageHead "Fact Detail - Jr"
+ pageHead "Fact Detail - omni"
pageBodyWithCrumbs crumbs <| do
Lucid.div_ [Lucid.class_ "container"] <| do
Lucid.div_ [Lucid.class_ "task-detail-header"] <| do
@@ -439,9 +439,9 @@ instance Lucid.ToHtml FactDetailPage where
instance Lucid.ToHtml EpicsPage where
toHtmlRaw = Lucid.toHtml
toHtml (EpicsPage epics allTasks currentSort) =
- let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Epics" Nothing]
+ let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Epics" Nothing]
in Lucid.doctypehtml_ <| do
- pageHead "Epics - Jr"
+ pageHead "Epics - omni"
pageBodyWithCrumbs crumbs <| do
Lucid.div_ [Lucid.class_ "container"] <| do
Lucid.div_ [Lucid.class_ "page-header-row"] <| do
@@ -455,9 +455,9 @@ instance Lucid.ToHtml EpicsPage where
instance Lucid.ToHtml TaskListPage where
toHtmlRaw = Lucid.toHtml
toHtml (TaskListPage tasks filters currentSort _now) =
- let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" Nothing]
+ let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Tasks" Nothing]
in Lucid.doctypehtml_ <| do
- pageHead "Tasks - Jr"
+ pageHead "Tasks - omni"
pageBodyWithCrumbs crumbs <| do
Lucid.div_ [Lucid.class_ "container"] <| do
Lucid.div_ [Lucid.class_ "page-header-row"] <| do
@@ -537,9 +537,9 @@ instance Lucid.ToHtml TaskListPage where
instance Lucid.ToHtml TaskDetailPage where
toHtmlRaw = Lucid.toHtml
toHtml (TaskDetailNotFound tid) =
- let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid Nothing]
+ let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid Nothing]
in Lucid.doctypehtml_ <| do
- pageHead "Task Not Found - Jr"
+ pageHead "Task Not Found - omni"
pageBodyWithCrumbs crumbs <| do
Lucid.div_ [Lucid.class_ "container"] <| do
Lucid.h1_ "Task Not Found"
@@ -550,7 +550,7 @@ instance Lucid.ToHtml TaskDetailPage where
toHtml (TaskDetailFound task allTasks _activities maybeRetry commits maybeAggMetrics agentEvents now) =
let crumbs = taskBreadcrumbs allTasks task
in Lucid.doctypehtml_ <| do
- pageHead (TaskCore.taskId task <> " - Jr")
+ pageHead (TaskCore.taskId task <> " - omni")
pageBodyWithCrumbs crumbs <| do
Lucid.div_ [Lucid.class_ "container"] <| do
Lucid.h1_ <| Lucid.toHtml (TaskCore.taskTitle task)
@@ -655,7 +655,7 @@ instance Lucid.ToHtml TaskDetailPage where
instance Lucid.ToHtml TaskReviewPage where
toHtmlRaw = Lucid.toHtml
toHtml (ReviewPageNotFound tid) =
- let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb "Review" Nothing]
+ let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb "Review" Nothing]
in Lucid.doctypehtml_ <| do
pageHead "Task Not Found - Jr Review"
pageBodyWithCrumbs crumbs <| do
@@ -667,9 +667,9 @@ instance Lucid.ToHtml TaskReviewPage where
" could not be found."
toHtml (ReviewPageFound task reviewInfo) =
let tid = TaskCore.taskId task
- crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb "Review" Nothing]
+ crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb "Review" Nothing]
in Lucid.doctypehtml_ <| do
- pageHead ("Review: " <> TaskCore.taskId task <> " - Jr")
+ pageHead ("Review: " <> TaskCore.taskId task <> " - omni")
pageBodyWithCrumbs crumbs <| do
Lucid.div_ [Lucid.class_ "container"] <| do
Lucid.h1_ "Review Task"
@@ -734,9 +734,9 @@ instance Lucid.ToHtml TaskDiffPage where
toHtmlRaw = Lucid.toHtml
toHtml (DiffPageNotFound tid commitHash') =
let shortHash = Text.take 8 commitHash'
- crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb ("Diff " <> shortHash) Nothing]
+ crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb ("Diff " <> shortHash) Nothing]
in Lucid.doctypehtml_ <| do
- pageHead "Commit Not Found - Jr"
+ pageHead "Commit Not Found - omni"
pageBodyWithCrumbs crumbs <| do
Lucid.div_ [Lucid.class_ "container"] <| do
Lucid.h1_ "Commit Not Found"
@@ -746,9 +746,9 @@ instance Lucid.ToHtml TaskDiffPage where
Lucid.a_ [Lucid.href_ ("/tasks/" <> tid), Lucid.class_ "back-link"] "← Back to task"
toHtml (DiffPageFound tid commitHash' diffOutput) =
let shortHash = Text.take 8 commitHash'
- crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb ("Diff " <> shortHash) Nothing]
+ crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb ("Diff " <> shortHash) Nothing]
in Lucid.doctypehtml_ <| do
- pageHead ("Diff " <> shortHash <> " - Jr")
+ pageHead ("Diff " <> shortHash <> " - omni")
pageBodyWithCrumbs crumbs <| do
Lucid.div_ [Lucid.class_ "container"] <| do
Lucid.div_ [Lucid.class_ "diff-header"] <| do
@@ -761,9 +761,9 @@ instance Lucid.ToHtml TaskDiffPage where
instance Lucid.ToHtml StatsPage where
toHtmlRaw = Lucid.toHtml
toHtml (StatsPage stats maybeEpic) =
- let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Stats" Nothing]
+ let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Stats" Nothing]
in Lucid.doctypehtml_ <| do
- pageHead "Task Statistics - Jr"
+ pageHead "Task Statistics - omni"
pageBodyWithCrumbs crumbs <| do
Lucid.div_ [Lucid.class_ "container"] <| do
Lucid.h1_ <| case maybeEpic of
diff --git a/Omni/Task/Web/Style.hs b/Omni/Task/Web/Style.hs
index d89de5f0..e41d996f 100644
--- a/Omni/Task/Web/Style.hs
+++ b/Omni/Task/Web/Style.hs
@@ -9,15 +9,16 @@ module Omni.Task.Web.Style
)
where
-import Alpha hiding (wrap, (**), (|>))
+import Alpha hiding (rem, wrap, (**), (|>))
import Clay
import qualified Clay.Flexbox as Flexbox
import qualified Clay.Media as Media
import qualified Clay.Stylesheet as Stylesheet
import qualified Data.Text.Lazy as LazyText
+import qualified Omni.Web.Style as WebStyle
css :: LazyText.Text
-css = render stylesheet
+css = WebStyle.css <> render stylesheet
stylesheet :: Css
stylesheet = do
@@ -46,26 +47,14 @@ stylesheet = do
baseStyles :: Css
baseStyles = do
- star ? boxSizing borderBox
- html <> body ? do
- margin (px 0) (px 0) (px 0) (px 0)
- padding (px 0) (px 0) (px 0) (px 0)
+ -- The shared Omni.Web.Style reset already covers *, html, body basics.
+ -- Here we layer Task-specific overrides.
body ? do
- fontFamily
- [ "-apple-system",
- "BlinkMacSystemFont",
- "Segoe UI",
- "Roboto",
- "Helvetica Neue",
- "Arial",
- "Noto Sans",
- "sans-serif"
- ]
- [sansSerif]
- fontSize (px 14)
- lineHeight (em 1.3)
- color "#1f2937"
- backgroundColor "#f5f5f5"
+ fontFamily WebStyle.fontMono [monospace]
+ fontSize (px 13)
+ lineHeight (unitless 1.6)
+ color WebStyle.cFg
+ backgroundColor WebStyle.cBg
minHeight (vh 100)
"h1" ? do
fontSize (px 20)
@@ -74,30 +63,34 @@ baseStyles = do
"h2" ? do
fontSize (px 16)
fontWeight (weight 600)
- color "#374151"
+ color WebStyle.cFg
margin (em 1) (px 0) (em 0.5) (px 0)
"h3" ? do
fontSize (px 14)
fontWeight (weight 600)
- color "#374151"
+ color WebStyle.cFg
margin (em 0.75) (px 0) (em 0.25) (px 0)
a ? do
- color "#0066cc"
+ color WebStyle.cLink
textDecoration none
- a # hover ? textDecoration underline
+ a # hover ? do
+ textDecoration underline
+ Stylesheet.key "text-underline-offset" ("2px" :: Text)
code ? do
- fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
+ fontFamily WebStyle.fontMono [monospace]
fontSize (em 0.9)
- backgroundColor "#f3f4f6"
- padding (px 1) (px 4) (px 1) (px 4)
- borderRadius (px 2) (px 2) (px 2) (px 2)
+ backgroundColor WebStyle.cBgHover
+ padding (px 1) (px 5) (px 1) (px 5)
+ borderRadius WebStyle.radiusSm WebStyle.radiusSm WebStyle.radiusSm WebStyle.radiusSm
+ border (px 1) solid WebStyle.cBorder
pre ? do
- fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
+ fontFamily WebStyle.fontMono [monospace]
fontSize (px 12)
- backgroundColor "#1e1e1e"
- color "#d4d4d4"
- padding (px 8) (px 8) (px 8) (px 8)
- borderRadius (px 2) (px 2) (px 2) (px 2)
+ backgroundColor WebStyle.cBg
+ color WebStyle.cFg
+ border (px 1) solid WebStyle.cBorder
+ padding (px 8) (px 16) (px 8) (px 16)
+ borderRadius WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd
overflow auto
whiteSpace preWrap
maxHeight (px 500)
@@ -108,7 +101,7 @@ layoutStyles = do
width (pct 100)
maxWidth (px 960)
margin (px 0) auto (px 0) auto
- padding (px 8) (px 12) (px 8) (px 12)
+ padding (px 8) (px 16) (px 8) (px 16)
main_ ? do
Stylesheet.key "flex" ("1 0 auto" :: Text)
".page-content" ? do
@@ -129,7 +122,7 @@ layoutStyles = do
".detail-label" ? do
fontWeight (weight 600)
width (px 100)
- color "#6b7280"
+ color WebStyle.cFgMuted
minWidth (px 80)
fontSize (px 13)
".detail-value" ? do
@@ -138,37 +131,44 @@ layoutStyles = do
".detail-section" ? do
marginTop (em 0.75)
paddingTop (em 0.75)
- borderTop (px 1) solid "#e5e7eb"
+ borderTop (px 1) solid WebStyle.cBorderSubtle
".dep-list" <> ".child-list" ? do
margin (px 4) (px 0) (px 4) (px 0)
paddingLeft (px 16)
(".dep-list" ** li) <> (".child-list" ** li) ? margin (px 2) (px 0) (px 2) (px 0)
".dep-type" <> ".child-status" ? do
- color "#6b7280"
+ color WebStyle.cFgMuted
fontSize (px 12)
- ".child-title" ? color "#374151"
+ ".child-title" ? color WebStyle.cFg
".priority-desc" ? do
- color "#6b7280"
+ color WebStyle.cFgMuted
marginLeft (px 4)
navigationStyles :: Css
navigationStyles = do
".navbar" ? do
- backgroundColor white
- padding (px 6) (px 12) (px 6) (px 12)
- borderBottom (px 1) solid "#d0d0d0"
- marginBottom (px 8)
+ backgroundColor WebStyle.cBg
+ padding (px 8) (px 16) (px 8) (px 16)
+ borderBottom (px 1) solid WebStyle.cBorder
display flex
alignItems center
justifyContent spaceBetween
flexWrap Flexbox.wrap
Stylesheet.key "gap" ("8px" :: Text)
+ position sticky
+ top nil
+ zIndex 100
+ Stylesheet.key "backdrop-filter" ("blur(12px)" :: Text)
+ Stylesheet.key "-webkit-backdrop-filter" ("blur(12px)" :: Text)
".navbar-brand" ? do
- fontSize (px 18)
+ fontSize (px 14)
fontWeight bold
- color "#0066cc"
+ color WebStyle.cAccent
textDecoration none
- ".navbar-brand" # hover ? textDecoration none
+ Stylesheet.key "letter-spacing" ("0.05em" :: Text)
+ ".navbar-brand" # hover ? do
+ textDecoration none
+ opacity 0.8
".navbar-toggle-checkbox" ? display none
".navbar-hamburger" ? do
display none
@@ -183,7 +183,7 @@ navigationStyles = do
display block
width (px 20)
height (px 2)
- backgroundColor "#374151"
+ backgroundColor WebStyle.cFgMuted
borderRadius (px 1) (px 1) (px 1) (px 1)
transition "all" (ms 200) ease (sec 0)
".navbar-links" ? do
@@ -194,14 +194,15 @@ navigationStyles = do
".navbar-link" ? do
display inlineBlock
padding (px 4) (px 10) (px 4) (px 10)
- color "#374151"
+ color WebStyle.cFgMuted
textDecoration none
- borderRadius (px 2) (px 2) (px 2) (px 2)
- fontSize (px 13)
+ borderRadius WebStyle.radiusSm WebStyle.radiusSm WebStyle.radiusSm WebStyle.radiusSm
+ fontSize (px 12)
fontWeight (weight 500)
- transition "background-color" (ms 150) ease (sec 0)
+ transition "all" (ms 150) ease (sec 0)
".navbar-link" # hover ? do
- backgroundColor "#f3f4f6"
+ backgroundColor WebStyle.cBgHover
+ color WebStyle.cFg
textDecoration none
".navbar-dropdown" ? do
position relative
@@ -209,24 +210,28 @@ navigationStyles = do
".navbar-dropdown-btn" ? do
display inlineBlock
padding (px 4) (px 10) (px 4) (px 10)
- color "#374151"
+ color WebStyle.cFgMuted
backgroundColor transparent
border (px 0) none transparent
- borderRadius (px 2) (px 2) (px 2) (px 2)
- fontSize (px 13)
+ borderRadius WebStyle.radiusSm WebStyle.radiusSm WebStyle.radiusSm WebStyle.radiusSm
+ fontSize (px 12)
fontWeight (weight 500)
cursor pointer
- transition "background-color" (ms 150) ease (sec 0)
- ".navbar-dropdown-btn" # hover ? backgroundColor "#f3f4f6"
+ fontFamily WebStyle.fontMono [monospace]
+ transition "all" (ms 150) ease (sec 0)
+ ".navbar-dropdown-btn" # hover ? do
+ backgroundColor WebStyle.cBgHover
+ color WebStyle.cFg
".navbar-dropdown-content" ? do
display none
position absolute
left (px 0)
top (pct 100)
- backgroundColor white
+ backgroundColor WebStyle.cBgRaised
minWidth (px 120)
- Stylesheet.key "box-shadow" ("0 2px 8px rgba(0,0,0,0.15)" :: Text)
- borderRadius (px 2) (px 2) (px 2) (px 2)
+ Stylesheet.key "box-shadow" ("0 4px 16px rgba(0,0,0,0.4)" :: Text)
+ borderRadius WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd
+ border (px 1) solid WebStyle.cBorder
zIndex 100
Stylesheet.key "overflow" ("hidden" :: Text)
".navbar-dropdown" # hover |> ".navbar-dropdown-content" ? display block
@@ -234,18 +239,18 @@ navigationStyles = do
".navbar-dropdown-item" ? do
display block
padding (px 8) (px 12) (px 8) (px 12)
- color "#374151"
+ color WebStyle.cFgMuted
textDecoration none
- fontSize (px 13)
- transition "background-color" (ms 150) ease (sec 0)
+ fontSize (px 12)
+ transition "all" (ms 150) ease (sec 0)
".navbar-dropdown-item" # hover ? do
- backgroundColor "#f3f4f6"
+ backgroundColor WebStyle.cBgHover
+ color WebStyle.cFg
textDecoration none
header ? do
- backgroundColor white
- padding (px 6) (px 12) (px 6) (px 12)
- borderBottom (px 1) solid "#d0d0d0"
- marginBottom (px 8)
+ backgroundColor WebStyle.cBg
+ padding (px 8) (px 16) (px 8) (px 16)
+ borderBottom (px 1) solid WebStyle.cBorder
".nav-content" ? do
maxWidth (px 960)
margin (px 0) auto (px 0) auto
@@ -255,9 +260,9 @@ navigationStyles = do
flexWrap Flexbox.wrap
Stylesheet.key "gap" ("8px" :: Text)
".nav-brand" ? do
- fontSize (px 16)
+ fontSize (px 14)
fontWeight bold
- color "#1f2937"
+ color WebStyle.cAccent
textDecoration none
".nav-brand" # hover ? textDecoration none
".nav-links" ? do
@@ -290,13 +295,13 @@ breadcrumbStyles = do
alignItems center
Stylesheet.key "gap" ("4px" :: Text)
".breadcrumb-sep" ? do
- color "#9ca3af"
+ color WebStyle.cFgFaint
Stylesheet.key "user-select" ("none" :: Text)
".breadcrumb-current" ? do
- color "#6b7280"
+ color WebStyle.cFgMuted
fontWeight (weight 500)
(".breadcrumb-list" ** a) ? do
- color "#0066cc"
+ color WebStyle.cAccent
textDecoration none
(".breadcrumb-list" ** a) # hover ? textDecoration underline
@@ -312,10 +317,10 @@ cardStyles = do
<> ".diff-section"
<> ".review-actions"
? do
- backgroundColor white
- borderRadius (px 2) (px 2) (px 2) (px 2)
- padding (px 8) (px 10) (px 8) (px 10)
- border (px 1) solid "#d0d0d0"
+ backgroundColor WebStyle.cBgRaised
+ borderRadius WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd
+ padding (px 8) (px 12) (px 8) (px 12)
+ border (px 1) solid WebStyle.cBorder
".review-actions" ? do
display flex
flexDirection row
@@ -328,25 +333,25 @@ cardStyles = do
fontWeight bold
".stat-label" ? do
fontSize (px 11)
- color "#6b7280"
+ color WebStyle.cFgMuted
marginTop (px 2)
".stat-card.badge-open" ? do
- borderLeft (px 4) solid "#f59e0b"
- (".stat-card.badge-open" |> ".stat-count") ? color "#92400e"
- ".stat-card.badge-inprogress" ? borderLeft (px 4) solid "#3b82f6"
- (".stat-card.badge-inprogress" |> ".stat-count") ? color "#1e40af"
- ".stat-card.badge-review" ? borderLeft (px 4) solid "#8b5cf6"
- (".stat-card.badge-review" |> ".stat-count") ? color "#6b21a8"
- ".stat-card.badge-approved" ? borderLeft (px 4) solid "#06b6d4"
- (".stat-card.badge-approved" |> ".stat-count") ? color "#0e7490"
- ".stat-card.badge-done" ? borderLeft (px 4) solid "#10b981"
- (".stat-card.badge-done" |> ".stat-count") ? color "#065f46"
- ".stat-card.badge-neutral" ? borderLeft (px 4) solid "#6b7280"
- (".stat-card.badge-neutral" |> ".stat-count") ? color "#374151"
+ borderLeft (px 4) solid WebStyle.cYellow
+ (".stat-card.badge-open" |> ".stat-count") ? color WebStyle.cYellow
+ ".stat-card.badge-inprogress" ? borderLeft (px 4) solid WebStyle.cBlue
+ (".stat-card.badge-inprogress" |> ".stat-count") ? color WebStyle.cBlue
+ ".stat-card.badge-review" ? borderLeft (px 4) solid WebStyle.cPurple
+ (".stat-card.badge-review" |> ".stat-count") ? color WebStyle.cPurple
+ ".stat-card.badge-approved" ? borderLeft (px 4) solid WebStyle.cAccent
+ (".stat-card.badge-approved" |> ".stat-count") ? color WebStyle.cAccent
+ ".stat-card.badge-done" ? borderLeft (px 4) solid WebStyle.cGreen
+ (".stat-card.badge-done" |> ".stat-count") ? color WebStyle.cGreen
+ ".stat-card.badge-neutral" ? borderLeft (px 4) solid WebStyle.cFgMuted
+ (".stat-card.badge-neutral" |> ".stat-count") ? color WebStyle.cFgMuted
".task-card" ? do
transition "border-color" (ms 150) ease (sec 0)
".task-card" # hover ? do
- borderColor "#999"
+ borderColor WebStyle.cAccentDim
".task-card-link" ? do
display block
textDecoration none
@@ -360,19 +365,19 @@ cardStyles = do
Stylesheet.key "gap" ("6px" :: Text)
marginBottom (px 4)
".task-id" ? do
- fontFamily ["SF Mono", "Monaco", "monospace"] [monospace]
- color "#0066cc"
+ fontFamily WebStyle.fontMono [monospace]
+ color WebStyle.cAccent
textDecoration none
fontSize (px 12)
padding (px 2) (px 0) (px 2) (px 0)
".task-id" # hover ? textDecoration underline
".priority" ? do
fontSize (px 11)
- color "#6b7280"
+ color WebStyle.cFgMuted
".blocking-impact" ? do
fontSize (px 10)
- color "#6b7280"
- backgroundColor "#e5e7eb"
+ color WebStyle.cFgMuted
+ backgroundColor WebStyle.cBorder
padding (px 1) (px 6) (px 1) (px 6)
borderRadius (px 8) (px 8) (px 8) (px 8)
marginLeft auto
@@ -380,33 +385,33 @@ cardStyles = do
fontSize (px 14)
margin (px 0) (px 0) (px 0) (px 0)
".empty-msg" ? do
- color "#6b7280"
+ color WebStyle.cFgMuted
fontStyle italic
".info-msg" ? do
- color "#6b7280"
+ color WebStyle.cFgMuted
marginBottom (px 12)
".kb-preview" ? do
- color "#6b7280"
+ color WebStyle.cFgMuted
fontSize (px 12)
marginTop (px 4)
overflow hidden
Stylesheet.key "text-overflow" ("ellipsis" :: Text)
".ready-link" ? do
fontSize (px 13)
- color "#0066cc"
+ color WebStyle.cAccent
".count-badge" ? do
- backgroundColor "#0066cc"
+ backgroundColor WebStyle.cAccentDim
color white
padding (px 2) (px 8) (px 2) (px 8)
borderRadius (px 10) (px 10) (px 10) (px 10)
fontSize (px 12)
verticalAlign middle
".description" ? do
- backgroundColor "#f9fafb"
+ backgroundColor WebStyle.cBgRaised
padding (px 8) (px 8) (px 8) (px 8)
borderRadius (px 2) (px 2) (px 2) (px 2)
margin (px 0) (px 0) (px 0) (px 0)
- color "#374151"
+ color WebStyle.cFg
fontSize (px 13)
".description-block" ? do
pure ()
@@ -419,9 +424,9 @@ cardStyles = do
margin (px 0) (px 0) (px 0) (px 0)
".edit-link" <> ".cancel-link" ? do
fontSize (px 12)
- color "#0066cc"
+ color WebStyle.cAccent
"button.cancel-link" ? do
- color "#dc2626"
+ color WebStyle.cRed
backgroundColor transparent
border (px 0) solid transparent
padding (px 0) (px 0) (px 0) (px 0)
@@ -432,13 +437,13 @@ cardStyles = do
overflowY auto
".progress-bar" ? do
height (px 6)
- backgroundColor "#e5e7eb"
+ backgroundColor WebStyle.cBorder
borderRadius (px 2) (px 2) (px 2) (px 2)
overflow hidden
marginTop (px 6)
".progress-fill" ? do
height (pct 100)
- backgroundColor "#0066cc"
+ backgroundColor WebStyle.cAccentDim
borderRadius (px 2) (px 2) (px 2) (px 2)
transition "width" (ms 300) ease (sec 0)
".multi-progress-container" ? do
@@ -446,22 +451,22 @@ cardStyles = do
".multi-progress-bar" ? do
display flex
height (px 8)
- backgroundColor "#e5e7eb"
+ backgroundColor WebStyle.cBorder
borderRadius (px 4) (px 4) (px 4) (px 4)
overflow hidden
marginTop (px 6)
".multi-progress-segment" ? do
height (pct 100)
transition "width" (ms 300) ease (sec 0)
- ".progress-done" ? backgroundColor "#10b981"
- ".progress-inprogress" ? backgroundColor "#f59e0b"
- ".progress-open" ? backgroundColor "#3b82f6"
+ ".progress-done" ? backgroundColor WebStyle.cGreen
+ ".progress-inprogress" ? backgroundColor WebStyle.cYellow
+ ".progress-open" ? backgroundColor WebStyle.cBlue
".progress-legend" ? do
display flex
Stylesheet.key "gap" ("16px" :: Text)
marginTop (px 6)
fontSize (px 12)
- color "#6b7280"
+ color WebStyle.cFgMuted
".legend-item" ? do
display flex
alignItems center
@@ -471,14 +476,14 @@ cardStyles = do
width (px 10)
height (px 10)
borderRadius (px 2) (px 2) (px 2) (px 2)
- ".legend-done" ? backgroundColor "#10b981"
- ".legend-inprogress" ? backgroundColor "#f59e0b"
- ".legend-open" ? backgroundColor "#3b82f6"
+ ".legend-done" ? backgroundColor WebStyle.cGreen
+ ".legend-inprogress" ? backgroundColor WebStyle.cYellow
+ ".legend-open" ? backgroundColor WebStyle.cBlue
".stats-section" ? do
- backgroundColor white
+ backgroundColor WebStyle.cBgRaised
borderRadius (px 2) (px 2) (px 2) (px 2)
padding (px 8) (px 10) (px 8) (px 10)
- border (px 1) solid "#d0d0d0"
+ border (px 1) solid WebStyle.cBorder
".stats-row" ? do
display flex
alignItems center
@@ -497,19 +502,19 @@ cardStyles = do
fontWeight (weight 500)
fontSize (px 13)
".summary-section" ? do
- backgroundColor white
+ backgroundColor WebStyle.cBgRaised
borderRadius (px 2) (px 2) (px 2) (px 2)
padding (px 8) (px 10) (px 8) (px 10)
- border (px 1) solid "#d0d0d0"
+ border (px 1) solid WebStyle.cBorder
".no-commit-msg" ? do
- backgroundColor "#fff3cd"
- border (px 1) solid "#ffc107"
+ Stylesheet.key "background" ("rgba(250,204,21,0.1)" :: Text)
+ border (px 1) solid WebStyle.cYellow
borderRadius (px 2) (px 2) (px 2) (px 2)
padding (px 8) (px 10) (px 8) (px 10)
margin (px 8) (px 0) (px 8) (px 0)
".conflict-warning" ? do
- backgroundColor "#fee2e2"
- border (px 1) solid "#ef4444"
+ Stylesheet.key "background" ("rgba(248,113,113,0.1)" :: Text)
+ border (px 1) solid WebStyle.cRed
borderRadius (px 2) (px 2) (px 2) (px 2)
padding (px 8) (px 10) (px 8) (px 10)
margin (px 8) (px 0) (px 8) (px 0)
@@ -519,22 +524,22 @@ listGroupStyles = do
".list-group" ? do
display flex
flexDirection column
- backgroundColor white
- borderRadius (px 2) (px 2) (px 2) (px 2)
- border (px 1) solid "#d0d0d0"
+ backgroundColor WebStyle.cBgRaised
+ borderRadius WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd
+ border (px 1) solid WebStyle.cBorder
overflow hidden
".list-group-item" ? do
display flex
alignItems center
justifyContent spaceBetween
padding (px 8) (px 10) (px 8) (px 10)
- borderBottom (px 1) solid "#e5e7eb"
+ borderBottom (px 1) solid WebStyle.cBorderSubtle
textDecoration none
color inherit
transition "background-color" (ms 150) ease (sec 0)
".list-group-item" # lastChild ? borderBottom (px 0) none transparent
".list-group-item" # hover ? do
- backgroundColor "#f9fafb"
+ backgroundColor WebStyle.cBgHover
textDecoration none
".list-group-item-content" ? do
display flex
@@ -545,12 +550,12 @@ listGroupStyles = do
overflow hidden
".list-group-item-id" ? do
fontFamily ["SF Mono", "Monaco", "monospace"] [monospace]
- color "#0066cc"
+ color WebStyle.cAccent
fontSize (px 12)
flexShrink 0
".list-group-item-title" ? do
fontSize (px 13)
- color "#374151"
+ color WebStyle.cFg
overflow hidden
Stylesheet.key "text-overflow" ("ellipsis" :: Text)
whiteSpace nowrap
@@ -570,23 +575,23 @@ statusBadges = do
fontWeight (weight 500)
whiteSpace nowrap
".badge-open" ? do
- backgroundColor "#fef3c7"
- color "#92400e"
+ Stylesheet.key "background" ("rgba(250,204,21,0.15)" :: Text)
+ color WebStyle.cYellow
".badge-inprogress" ? do
- backgroundColor "#dbeafe"
- color "#1e40af"
+ Stylesheet.key "background" ("rgba(96,165,250,0.15)" :: Text)
+ color WebStyle.cBlue
".badge-review" ? do
- backgroundColor "#ede9fe"
- color "#6b21a8"
+ Stylesheet.key "background" ("rgba(167,139,250,0.15)" :: Text)
+ color WebStyle.cPurple
".badge-approved" ? do
- backgroundColor "#cffafe"
- color "#0e7490"
+ Stylesheet.key "background" ("rgba(34,211,238,0.15)" :: Text)
+ color WebStyle.cAccent
".badge-done" ? do
- backgroundColor "#d1fae5"
- color "#065f46"
+ Stylesheet.key "background" ("rgba(74,222,128,0.15)" :: Text)
+ color WebStyle.cGreen
".badge-needshelp" ? do
- backgroundColor "#fef3c7"
- color "#92400e"
+ Stylesheet.key "background" ("rgba(251,146,60,0.15)" :: Text)
+ color WebStyle.cOrange
".status-badge-dropdown" ? do
position relative
display inlineBlock
@@ -605,7 +610,7 @@ statusBadges = do
left (px 0)
top (pct 100)
marginTop (px 2)
- backgroundColor white
+ backgroundColor WebStyle.cBgRaised
borderRadius (px 4) (px 4) (px 4) (px 4)
Stylesheet.key "box-shadow" ("0 2px 8px rgba(0,0,0,0.15)" :: Text)
zIndex 100
@@ -637,20 +642,20 @@ statusBadges = do
Stylesheet.key "outline" ("2px solid #0066cc" :: Text)
Stylesheet.key "outline-offset" ("2px" :: Text)
".badge-p0" ? do
- backgroundColor "#fee2e2"
- color "#991b1b"
+ Stylesheet.key "background" ("rgba(248,113,113,0.15)" :: Text)
+ color WebStyle.cRed
".badge-p1" ? do
- backgroundColor "#fef3c7"
- color "#92400e"
+ Stylesheet.key "background" ("rgba(251,146,60,0.15)" :: Text)
+ color WebStyle.cOrange
".badge-p2" ? do
- backgroundColor "#dbeafe"
- color "#1e40af"
+ Stylesheet.key "background" ("rgba(96,165,250,0.15)" :: Text)
+ color WebStyle.cBlue
".badge-p3" ? do
- backgroundColor "#e5e7eb"
- color "#4b5563"
+ backgroundColor WebStyle.cBgHover
+ color WebStyle.cFgMuted
".badge-p4" ? do
- backgroundColor "#f3f4f6"
- color "#6b7280"
+ backgroundColor WebStyle.cBgHover
+ color WebStyle.cFgFaint
".priority-badge-dropdown" ? do
position relative
display inlineBlock
@@ -665,7 +670,7 @@ statusBadges = do
left (px 0)
top (pct 100)
marginTop (px 2)
- backgroundColor white
+ backgroundColor WebStyle.cBgRaised
borderRadius (px 4) (px 4) (px 4) (px 4)
Stylesheet.key "box-shadow" ("0 2px 8px rgba(0,0,0,0.15)" :: Text)
zIndex 100
@@ -697,26 +702,26 @@ statusBadges = do
Stylesheet.key "outline" ("2px solid #0066cc" :: Text)
Stylesheet.key "outline-offset" ("2px" :: Text)
".badge-complexity" ? do
- backgroundColor "#f0f9ff"
- color "#0c4a6e"
+ Stylesheet.key "background" ("rgba(96,165,250,0.1)" :: Text)
+ color WebStyle.cAccentDim
".badge-complexity-1" ? do
- backgroundColor "#f0fdf4"
- color "#166534"
+ Stylesheet.key "background" ("rgba(74,222,128,0.1)" :: Text)
+ color WebStyle.cGreen
".badge-complexity-2" ? do
- backgroundColor "#f0f9ff"
- color "#075985"
+ Stylesheet.key "background" ("rgba(96,165,250,0.1)" :: Text)
+ color WebStyle.cBlue
".badge-complexity-3" ? do
- backgroundColor "#fef3c7"
- color "#92400e"
+ Stylesheet.key "background" ("rgba(250,204,21,0.15)" :: Text)
+ color WebStyle.cYellow
".badge-complexity-4" ? do
- backgroundColor "#fef3c7"
- color "#b45309"
+ Stylesheet.key "background" ("rgba(250,204,21,0.15)" :: Text)
+ color WebStyle.cOrange
".badge-complexity-5" ? do
- backgroundColor "#fee2e2"
- color "#991b1b"
+ Stylesheet.key "background" ("rgba(248,113,113,0.1)" :: Text)
+ color WebStyle.cRed
".badge-complexity-none" ? do
- backgroundColor "#f3f4f6"
- color "#6b7280"
+ backgroundColor WebStyle.cBgHover
+ color WebStyle.cFgMuted
".complexity-badge-dropdown" ? do
position relative
display inlineBlock
@@ -731,7 +736,7 @@ statusBadges = do
left (px 0)
top (pct 100)
marginTop (px 2)
- backgroundColor white
+ backgroundColor WebStyle.cBgRaised
borderRadius (px 4) (px 4) (px 4) (px 4)
Stylesheet.key "box-shadow" ("0 2px 8px rgba(0,0,0,0.15)" :: Text)
zIndex 100
@@ -786,16 +791,16 @@ buttonStyles = do
transition "all" (ms 150) ease (sec 0)
Stylesheet.key "touch-action" ("manipulation" :: Text)
".action-btn" ? do
- backgroundColor white
- border (px 1) solid "#d1d5db"
- color "#374151"
+ backgroundColor WebStyle.cBgRaised
+ border (px 1) solid WebStyle.cBorder
+ color WebStyle.cFg
".action-btn" # hover ? do
- backgroundColor "#f9fafb"
- borderColor "#9ca3af"
+ backgroundColor WebStyle.cBgHover
+ borderColor WebStyle.cFgFaint
".action-btn-primary" <> ".filter-btn" <> ".submit-btn" ? do
- backgroundColor "#0066cc"
+ backgroundColor WebStyle.cAccentDim
color white
- borderColor "#0066cc"
+ borderColor WebStyle.cAccentDim
".action-btn-primary"
# hover
<> ".filter-btn"
@@ -803,37 +808,39 @@ buttonStyles = do
<> ".submit-btn"
# hover
? do
- backgroundColor "#0052a3"
+ backgroundColor WebStyle.cAccent
+ color black
".accept-btn" ? do
- backgroundColor "#10b981"
- color white
- ".accept-btn" # hover ? backgroundColor "#059669"
+ Stylesheet.key "background" ("rgba(74,222,128,0.15)" :: Text)
+ color WebStyle.cGreen
+ ".accept-btn" # hover ? Stylesheet.key "background" ("rgba(74,222,128,0.25)" :: Text)
".reject-btn" ? do
- backgroundColor "#ef4444"
- color white
- ".reject-btn" # hover ? backgroundColor "#dc2626"
+ Stylesheet.key "background" ("rgba(248,113,113,0.15)" :: Text)
+ color WebStyle.cRed
+ ".reject-btn" # hover ? Stylesheet.key "background" ("rgba(248,113,113,0.25)" :: Text)
".clear-btn" ? do
display inlineBlock
minHeight (px 36)
padding (px 6) (px 10) (px 6) (px 10)
- backgroundColor "#6b7280"
- color white
- borderRadius (px 2) (px 2) (px 2) (px 2)
+ backgroundColor WebStyle.cBgHover
+ color WebStyle.cFg
+ border (px 1) solid WebStyle.cBorder
+ borderRadius WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd
textDecoration none
fontSize (px 13)
cursor pointer
- ".clear-btn" # hover ? backgroundColor "#4b5563"
+ ".clear-btn" # hover ? backgroundColor WebStyle.cBgActive
".review-link-btn" ? do
- backgroundColor "#8b5cf6"
- color white
- ".review-link-btn" # hover ? backgroundColor "#7c3aed"
+ Stylesheet.key "background" ("rgba(167,139,250,0.15)" :: Text)
+ color WebStyle.cPurple
+ ".review-link-btn" # hover ? Stylesheet.key "background" ("rgba(167,139,250,0.25)" :: Text)
".review-link-section" ? margin (px 8) (px 0) (px 8) (px 0)
".btn-secondary" <> ".load-more-btn" ? do
- backgroundColor "#6b7280"
+ backgroundColor WebStyle.cFgMuted
color white
width (pct 100)
marginTop (px 8)
- ".btn-secondary" # hover <> ".load-more-btn" # hover ? backgroundColor "#4b5563"
+ ".btn-secondary" # hover <> ".load-more-btn" # hover ? backgroundColor WebStyle.cFgFaint
formStyles :: Css
formStyles = do
@@ -849,15 +856,18 @@ formStyles = do
Stylesheet.key "gap" ("4px" :: Text)
(".filter-group" |> label) ? do
fontSize (px 12)
- color "#6b7280"
+ color WebStyle.cFgMuted
fontWeight (weight 500)
whiteSpace nowrap
".filter-select" <> ".filter-input" <> ".status-select" ? do
minHeight (px 36)
padding (px 6) (px 10) (px 6) (px 10)
- border (px 1) solid "#d1d5db"
- borderRadius (px 2) (px 2) (px 2) (px 2)
+ border (px 1) solid WebStyle.cBorder
+ borderRadius WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd
fontSize (px 13)
+ fontFamily WebStyle.fontMono [monospace]
+ backgroundColor WebStyle.cBg
+ color WebStyle.cFg
minWidth (px 100)
".filter-input" ? minWidth (px 120)
".inline-form" ? display inlineBlock
@@ -872,17 +882,17 @@ formStyles = do
minWidth (px 160)
minHeight (px 36)
padding (px 6) (px 10) (px 6) (px 10)
- border (px 1) solid "#d1d5db"
+ border (px 1) solid WebStyle.cBorder
borderRadius (px 2) (px 2) (px 2) (px 2)
fontSize (px 13)
Stylesheet.key "resize" ("vertical" :: Text)
".edit-description" ? do
marginTop (px 8)
padding (px 8) (px 0) (px 0) (px 0)
- borderTop (px 1) solid "#e5e7eb"
+ borderTop (px 1) solid WebStyle.cBorderSubtle
(".edit-description" |> "summary") ? do
cursor pointer
- color "#0066cc"
+ color WebStyle.cAccent
fontSize (px 13)
fontWeight (weight 500)
(".edit-description" |> "summary") # hover ? textDecoration underline
@@ -890,7 +900,7 @@ formStyles = do
width (pct 100)
minHeight (px 250)
padding (px 8) (px 10) (px 8) (px 10)
- border (px 1) solid "#d1d5db"
+ border (px 1) solid WebStyle.cBorder
borderRadius (px 2) (px 2) (px 2) (px 2)
fontSize (px 13)
fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
@@ -912,16 +922,16 @@ formStyles = do
marginBottom (px 4)
fontSize (px 13)
fontWeight (weight 500)
- color "#374151"
+ color WebStyle.cFg
".form-input" <> ".form-textarea" ? do
width (pct 100)
padding (px 8) (px 10) (px 8) (px 10)
- border (px 1) solid "#d1d5db"
+ border (px 1) solid WebStyle.cBorder
borderRadius (px 2) (px 2) (px 2) (px 2)
fontSize (px 14)
lineHeight (em 1.5)
".form-input" # focus <> ".form-textarea" # focus ? do
- borderColor "#0066cc"
+ borderColor WebStyle.cAccentDim
Stylesheet.key "outline" ("none" :: Text)
Stylesheet.key "box-shadow" ("0 0 0 2px rgba(0, 102, 204, 0.2)" :: Text)
".form-textarea" ? do
@@ -948,47 +958,47 @@ formStyles = do
cursor pointer
transition "all" (ms 150) ease (sec 0)
".btn-primary" ? do
- backgroundColor "#0066cc"
+ backgroundColor WebStyle.cAccentDim
color white
- ".btn-primary" # hover ? backgroundColor "#0052a3"
+ ".btn-primary" # hover ? backgroundColor WebStyle.cAccent
".btn-secondary" ? do
- backgroundColor "#6b7280"
+ backgroundColor WebStyle.cFgMuted
color white
- ".btn-secondary" # hover ? backgroundColor "#4b5563"
+ ".btn-secondary" # hover ? backgroundColor WebStyle.cFgFaint
".btn-danger" ? do
- backgroundColor "#dc2626"
+ backgroundColor WebStyle.cRed
color white
- ".btn-danger" # hover ? backgroundColor "#b91c1c"
+ ".btn-danger" # hover ? backgroundColor WebStyle.cRed
".danger-zone" ? do
marginTop (px 24)
padding (px 16) (px 16) (px 16) (px 16)
- backgroundColor "#fef2f2"
- border (px 1) solid "#fecaca"
+ Stylesheet.key "background" ("rgba(248,113,113,0.1)" :: Text)
+ border (px 1) solid WebStyle.cRed
borderRadius (px 4) (px 4) (px 4) (px 4)
(".danger-zone" |> h2) ? do
- color "#dc2626"
+ color WebStyle.cRed
marginBottom (px 12)
".back-link" ? do
marginTop (px 24)
paddingTop (px 16)
- borderTop (px 1) solid "#e5e7eb"
+ borderTop (px 1) solid WebStyle.cBorderSubtle
(".back-link" |> a) ? do
- color "#6b7280"
+ color WebStyle.cFgMuted
textDecoration none
(".back-link" |> a) # hover ? do
- color "#374151"
+ color WebStyle.cFg
textDecoration underline
".task-link" ? do
- color "#0066cc"
+ color WebStyle.cAccent
textDecoration none
fontWeight (weight 500)
".task-link" # hover ? textDecoration underline
".error-msg" ? do
- color "#dc2626"
- backgroundColor "#fef2f2"
+ color WebStyle.cRed
+ Stylesheet.key "background" ("rgba(248,113,113,0.1)" :: Text)
padding (px 16) (px 16) (px 16) (px 16)
borderRadius (px 4) (px 4) (px 4) (px 4)
- border (px 1) solid "#fecaca"
+ border (px 1) solid WebStyle.cRed
".create-fact-section" ? do
marginBottom (px 16)
".create-fact-toggle" ? do
@@ -997,18 +1007,18 @@ formStyles = do
".fact-create-form" ? do
marginTop (px 12)
padding (px 16) (px 16) (px 16) (px 16)
- backgroundColor white
+ backgroundColor WebStyle.cBgRaised
borderRadius (px 4) (px 4) (px 4) (px 4)
- border (px 1) solid "#d1d5db"
+ border (px 1) solid WebStyle.cBorder
executionDetailsStyles :: Css
executionDetailsStyles = do
".execution-section" ? do
marginTop (em 1)
- backgroundColor white
+ backgroundColor WebStyle.cBgRaised
borderRadius (px 2) (px 2) (px 2) (px 2)
padding (px 8) (px 10) (px 8) (px 10)
- border (px 1) solid "#d0d0d0"
+ border (px 1) solid WebStyle.cBorder
".execution-details" ? do
marginTop (px 8)
".metric-row" ? do
@@ -1019,20 +1029,20 @@ executionDetailsStyles = do
".metric-label" ? do
fontWeight (weight 600)
width (px 120)
- color "#6b7280"
+ color WebStyle.cFgMuted
fontSize (px 13)
".metric-value" ? do
Stylesheet.key "flex" ("1" :: Text)
fontSize (px 13)
".amp-link" ? do
- color "#0066cc"
+ color WebStyle.cAccent
textDecoration none
wordBreak breakAll
".amp-link" # hover ? textDecoration underline
".amp-thread-btn" ? do
display inlineBlock
padding (px 4) (px 10) (px 4) (px 10)
- backgroundColor "#7c3aed"
+ backgroundColor WebStyle.cPurple
color white
borderRadius (px 3) (px 3) (px 3) (px 3)
textDecoration none
@@ -1040,23 +1050,23 @@ executionDetailsStyles = do
fontWeight (weight 500)
transition "background-color" (ms 150) ease (sec 0)
".amp-thread-btn" # hover ? do
- backgroundColor "#6d28d9"
+ backgroundColor WebStyle.cPurple
textDecoration none
".retry-count" ? do
- color "#f97316"
+ color WebStyle.cOrange
fontWeight (weight 600)
".attempts-divider" ? do
margin (px 12) (px 0) (px 12) (px 0)
border (px 0) none transparent
- borderTop (px 1) solid "#e5e7eb"
+ borderTop (px 1) solid WebStyle.cBorderSubtle
".attempt-header" ? do
fontWeight (weight 600)
fontSize (px 13)
- color "#374151"
+ color WebStyle.cFg
marginTop (px 8)
marginBottom (px 4)
paddingBottom (px 4)
- borderBottom (px 1) solid "#f3f4f6"
+ borderBottom (px 1) solid WebStyle.cBorderSubtle
".aggregated-metrics" ? do
marginTop (em 0.5)
paddingTop (em 0.75)
@@ -1066,21 +1076,21 @@ executionDetailsStyles = do
Stylesheet.key "gap" ("10px" :: Text)
marginTop (px 8)
".metric-card" ? do
- backgroundColor "#f9fafb"
- border (px 1) solid "#e5e7eb"
+ backgroundColor WebStyle.cBgRaised
+ border (px 1) solid WebStyle.cBorderSubtle
borderRadius (px 4) (px 4) (px 4) (px 4)
padding (px 10) (px 12) (px 10) (px 12)
textAlign center
(".metric-card" |> ".metric-value") ? do
fontSize (px 20)
fontWeight bold
- color "#374151"
+ color WebStyle.cFg
display block
marginBottom (px 2)
width auto
(".metric-card" |> ".metric-label") ? do
fontSize (px 11)
- color "#6b7280"
+ color WebStyle.cFgMuted
fontWeight (weight 400)
width auto
@@ -1088,10 +1098,10 @@ activityTimelineStyles :: Css
activityTimelineStyles = do
".activity-section" ? do
marginTop (em 1)
- backgroundColor white
+ backgroundColor WebStyle.cBgRaised
borderRadius (px 2) (px 2) (px 2) (px 2)
padding (px 8) (px 10) (px 8) (px 10)
- border (px 1) solid "#d0d0d0"
+ border (px 1) solid WebStyle.cBorder
".activity-timeline" ? do
position relative
paddingLeft (px 20)
@@ -1103,7 +1113,7 @@ activityTimelineStyles = do
top (px 0)
bottom (px 0)
width (px 2)
- backgroundColor "#e5e7eb"
+ backgroundColor WebStyle.cBorder
".activity-item" ? do
position relative
display flex
@@ -1122,8 +1132,8 @@ activityTimelineStyles = do
justifyContent center
fontSize (px 8)
fontWeight bold
- backgroundColor white
- border (px 2) solid "#e5e7eb"
+ backgroundColor WebStyle.cBgRaised
+ border (px 2) solid WebStyle.cBorder
".activity-content" ? do
Stylesheet.key "flex" ("1" :: Text)
".activity-header" ? do
@@ -1136,43 +1146,43 @@ activityTimelineStyles = do
fontSize (px 12)
".activity-time" ? do
fontSize (px 11)
- color "#6b7280"
+ color WebStyle.cFgMuted
".activity-message" ? do
margin (px 2) (px 0) (px 0) (px 0)
fontSize (px 12)
- color "#374151"
+ color WebStyle.cFg
".activity-metadata" ? do
marginTop (px 4)
(".activity-metadata" |> "summary") ? do
fontSize (px 11)
- color "#6b7280"
+ color WebStyle.cFgMuted
cursor pointer
".metadata-json" ? do
fontSize (px 10)
- backgroundColor "#f3f4f6"
+ backgroundColor WebStyle.cBgHover
padding (px 4) (px 6) (px 4) (px 6)
borderRadius (px 2) (px 2) (px 2) (px 2)
marginTop (px 2)
maxHeight (px 150)
overflow auto
".stage-claiming" |> ".activity-icon" ? do
- borderColor "#3b82f6"
- color "#3b82f6"
+ borderColor WebStyle.cBlue
+ color WebStyle.cBlue
".stage-running" |> ".activity-icon" ? do
- borderColor "#f59e0b"
- color "#f59e0b"
+ borderColor WebStyle.cYellow
+ color WebStyle.cYellow
".stage-reviewing" |> ".activity-icon" ? do
- borderColor "#8b5cf6"
- color "#8b5cf6"
+ borderColor WebStyle.cPurple
+ color WebStyle.cPurple
".stage-retrying" |> ".activity-icon" ? do
- borderColor "#f97316"
- color "#f97316"
+ borderColor WebStyle.cOrange
+ color WebStyle.cOrange
".stage-completed" |> ".activity-icon" ? do
- borderColor "#10b981"
- color "#10b981"
+ borderColor WebStyle.cGreen
+ color WebStyle.cGreen
".stage-failed" |> ".activity-icon" ? do
- borderColor "#ef4444"
- color "#ef4444"
+ borderColor WebStyle.cRed
+ color WebStyle.cRed
commitStyles :: Css
commitStyles = do
@@ -1183,9 +1193,9 @@ commitStyles = do
marginTop (px 8)
".commit-item" ? do
padding (px 6) (px 8) (px 6) (px 8)
- backgroundColor "#f9fafb"
+ backgroundColor WebStyle.cBgRaised
borderRadius (px 2) (px 2) (px 2) (px 2)
- border (px 1) solid "#e5e7eb"
+ border (px 1) solid WebStyle.cBorderSubtle
".commit-header" ? do
display flex
alignItems center
@@ -1194,24 +1204,24 @@ commitStyles = do
".commit-hash" ? do
fontFamily ["SF Mono", "Monaco", "monospace"] [monospace]
fontSize (px 12)
- color "#0066cc"
+ color WebStyle.cAccent
textDecoration none
- backgroundColor "#e5e7eb"
+ backgroundColor WebStyle.cBorder
padding (px 1) (px 4) (px 1) (px 4)
borderRadius (px 2) (px 2) (px 2) (px 2)
".commit-hash" # hover ? textDecoration underline
".commit-summary" ? do
fontSize (px 13)
- color "#374151"
+ color WebStyle.cFg
fontWeight (weight 500)
".commit-meta" ? do
display flex
Stylesheet.key "gap" ("12px" :: Text)
fontSize (px 11)
- color "#6b7280"
+ color WebStyle.cFgMuted
".commit-author" ? fontWeight (weight 500)
".commit-files" ? do
- color "#9ca3af"
+ color WebStyle.cFgFaint
markdownStyles :: Css
markdownStyles = do
@@ -1219,13 +1229,13 @@ markdownStyles = do
width (pct 100)
lineHeight (em 1.6)
fontSize (px 14)
- color "#374151"
+ color WebStyle.cFg
".md-h1" ? do
fontSize (px 18)
fontWeight bold
margin (em 1) (px 0) (em 0.5) (px 0)
paddingBottom (em 0.3)
- borderBottom (px 1) solid "#e5e7eb"
+ borderBottom (px 1) solid WebStyle.cBorderSubtle
".md-h2" ? do
fontSize (px 16)
fontWeight (weight 600)
@@ -1239,11 +1249,11 @@ markdownStyles = do
".md-code" ? do
fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
fontSize (px 12)
- backgroundColor "#f8f8f8"
- color "#333333"
+ backgroundColor WebStyle.cBgRaised
+ color WebStyle.cFg
padding (px 10) (px 12) (px 10) (px 12)
borderRadius (px 4) (px 4) (px 4) (px 4)
- border (px 1) solid "#e1e4e8"
+ border (px 1) solid WebStyle.cBorder
overflow auto
whiteSpace preWrap
margin (em 0.5) (px 0) (em 0.5) (px 0)
@@ -1255,7 +1265,7 @@ markdownStyles = do
".md-inline-code" ? do
fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
fontSize (em 0.9)
- backgroundColor "#f3f4f6"
+ backgroundColor WebStyle.cBgHover
padding (px 1) (px 4) (px 1) (px 4)
borderRadius (px 2) (px 2) (px 2) (px 2)
@@ -1266,11 +1276,11 @@ retryBannerStyles = do
padding (px 12) (px 16) (px 12) (px 16)
margin (px 0) (px 0) (px 16) (px 0)
".retry-banner-warning" ? do
- backgroundColor "#fef3c7"
- border (px 1) solid "#f59e0b"
+ Stylesheet.key "background" ("rgba(250,204,21,0.15)" :: Text)
+ border (px 1) solid WebStyle.cYellow
".retry-banner-critical" ? do
- backgroundColor "#fee2e2"
- border (px 1) solid "#ef4444"
+ Stylesheet.key "background" ("rgba(248,113,113,0.1)" :: Text)
+ border (px 1) solid WebStyle.cRed
".retry-banner-header" ? do
display flex
alignItems center
@@ -1282,9 +1292,9 @@ retryBannerStyles = do
".retry-attempt" ? do
fontSize (px 14)
fontWeight (weight 600)
- color "#374151"
+ color WebStyle.cFg
".retry-warning-badge" ? do
- backgroundColor "#dc2626"
+ backgroundColor WebStyle.cRed
color white
fontSize (px 11)
fontWeight (weight 600)
@@ -1293,7 +1303,7 @@ retryBannerStyles = do
marginLeft auto
".retry-banner-details" ? do
fontSize (px 13)
- color "#374151"
+ color WebStyle.cFg
".retry-detail-row" ? do
display flex
alignItems flexStart
@@ -1304,11 +1314,11 @@ retryBannerStyles = do
minWidth (px 110)
flexShrink 0
".retry-value" ? do
- color "#4b5563"
+ color WebStyle.cFgMuted
".retry-commit" ? do
fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
fontSize (em 0.9)
- backgroundColor "#f3f4f6"
+ backgroundColor WebStyle.cBgHover
padding (px 1) (px 4) (px 1) (px 4)
borderRadius (px 2) (px 2) (px 2) (px 2)
".retry-conflict-list" ? do
@@ -1321,15 +1331,15 @@ retryBannerStyles = do
".retry-warning-message" ? do
marginTop (px 12)
padding (px 10) (px 12) (px 10) (px 12)
- backgroundColor "#fecaca"
+ Stylesheet.key "background" ("rgba(248,113,113,0.2)" :: Text)
borderRadius (px 2) (px 2) (px 2) (px 2)
fontSize (px 12)
- color "#991b1b"
+ color WebStyle.cRed
fontWeight (weight 500)
".retry-hint" ? do
marginTop (px 8)
fontSize (px 12)
- color "#6b7280"
+ color WebStyle.cFgMuted
fontStyle italic
commentStyles :: Css
@@ -1337,15 +1347,15 @@ commentStyles = do
".comments-section" ? do
marginTop (px 12)
".comment-card" ? do
- backgroundColor "#f9fafb"
- border (px 1) solid "#e5e7eb"
+ backgroundColor WebStyle.cBgRaised
+ border (px 1) solid WebStyle.cBorderSubtle
borderRadius (px 4) (px 4) (px 4) (px 4)
padding (px 10) (px 12) (px 10) (px 12)
marginBottom (px 8)
".comment-text" ? do
margin (px 0) (px 0) (px 6) (px 0)
fontSize (px 13)
- color "#374151"
+ color WebStyle.cFg
whiteSpace preWrap
".comment-meta" ? do
display flex
@@ -1360,14 +1370,14 @@ commentStyles = do
textTransform uppercase
whiteSpace nowrap
".author-human" ? do
- backgroundColor "#dbeafe"
- color "#1e40af"
+ Stylesheet.key "background" ("rgba(96,165,250,0.15)" :: Text)
+ color WebStyle.cBlue
".author-junior" ? do
- backgroundColor "#d1fae5"
- color "#065f46"
+ Stylesheet.key "background" ("rgba(74,222,128,0.15)" :: Text)
+ color WebStyle.cGreen
".comment-time" ? do
fontSize (px 11)
- color "#9ca3af"
+ color WebStyle.cFgFaint
".comment-form" ? do
marginTop (px 12)
display flex
@@ -1377,13 +1387,13 @@ commentStyles = do
width (pct 100)
padding (px 8) (px 10) (px 8) (px 10)
fontSize (px 13)
- border (px 1) solid "#d0d0d0"
+ border (px 1) solid WebStyle.cBorder
borderRadius (px 4) (px 4) (px 4) (px 4)
Stylesheet.key "resize" ("vertical" :: Text)
minHeight (px 60)
".comment-textarea" # focus ? do
Stylesheet.key "outline" ("none" :: Text)
- borderColor "#0066cc"
+ borderColor WebStyle.cAccentDim
timeFilterStyles :: Css
timeFilterStyles = do
@@ -1399,22 +1409,22 @@ timeFilterStyles = do
fontWeight (weight 500)
textDecoration none
borderRadius (px 12) (px 12) (px 12) (px 12)
- border (px 1) solid "#d0d0d0"
- backgroundColor white
- color "#374151"
+ border (px 1) solid WebStyle.cBorder
+ backgroundColor WebStyle.cBgRaised
+ color WebStyle.cFg
transition "all" (ms 150) ease (sec 0)
cursor pointer
".time-filter-btn" # hover ? do
- borderColor "#999"
- backgroundColor "#f3f4f6"
+ borderColor WebStyle.cFgFaint
+ backgroundColor WebStyle.cBgHover
textDecoration none
".time-filter-btn.active" ? do
- backgroundColor "#0066cc"
- borderColor "#0066cc"
+ backgroundColor WebStyle.cAccentDim
+ borderColor WebStyle.cAccentDim
color white
".time-filter-btn.active" # hover ? do
- backgroundColor "#0055aa"
- borderColor "#0055aa"
+ backgroundColor WebStyle.cAccent
+ borderColor WebStyle.cAccent
sortDropdownStyles :: Css
sortDropdownStyles = do
@@ -1433,7 +1443,7 @@ sortDropdownStyles = do
Stylesheet.key "gap" ("6px" :: Text)
fontSize (px 13)
".sort-label" ? do
- color "#6b7280"
+ color WebStyle.cFgMuted
fontWeight (weight 500)
".sort-dropdown-wrapper" ? do
position relative
@@ -1441,16 +1451,16 @@ sortDropdownStyles = do
padding (px 4) (px 10) (px 4) (px 10)
fontSize (px 13)
fontWeight (weight 500)
- border (px 1) solid "#d0d0d0"
+ border (px 1) solid WebStyle.cBorder
borderRadius (px 4) (px 4) (px 4) (px 4)
- backgroundColor white
- color "#374151"
+ backgroundColor WebStyle.cBgRaised
+ color WebStyle.cFg
cursor pointer
transition "all" (ms 150) ease (sec 0)
whiteSpace nowrap
".sort-dropdown-btn" # hover ? do
- borderColor "#999"
- backgroundColor "#f3f4f6"
+ borderColor WebStyle.cFgFaint
+ backgroundColor WebStyle.cBgHover
".sort-dropdown-content" ? do
minWidth (px 160)
right (px 0)
@@ -1459,7 +1469,7 @@ sortDropdownStyles = do
padding (px 8) (px 12) (px 8) (px 12)
fontSize (px 13)
".sort-dropdown-item.active" ? do
- backgroundColor "#e0f2fe"
+ Stylesheet.key "background" ("rgba(96,165,250,0.1)" :: Text)
fontWeight (weight 600)
taskMetaStyles :: Css
@@ -1479,17 +1489,17 @@ taskMetaStyles = do
flexWrap Flexbox.wrap
Stylesheet.key "gap" ("6px" :: Text)
fontSize (px 12)
- color "#6b7280"
+ color WebStyle.cFgMuted
".task-meta-id" ? do
fontFamily ["SF Mono", "Monaco", "monospace"] [monospace]
fontSize (px 13)
- backgroundColor "#f3f4f6"
+ backgroundColor WebStyle.cBgHover
padding (px 1) (px 4) (px 1) (px 4)
borderRadius (px 2) (px 2) (px 2) (px 2)
".task-meta-label" ? do
- color "#6b7280"
+ color WebStyle.cFgMuted
".meta-sep" ? do
- color "#d1d5db"
+ color WebStyle.cFg
Stylesheet.key "user-select" ("none" :: Text)
timelineEventStyles :: Css
@@ -1505,20 +1515,20 @@ timelineEventStyles = do
textAlign center
".event-label" ? do
fontWeight (weight 500)
- color "#374151"
+ color WebStyle.cFg
".event-assistant" ? do
padding (px 0) (px 0) (px 0) (px 0)
".event-bubble" ? do
- backgroundColor "#f3f4f6"
+ backgroundColor WebStyle.cBgHover
padding (px 8) (px 12) (px 8) (px 12)
borderRadius (px 8) (px 8) (px 8) (px 8)
whiteSpace preWrap
lineHeight (em 1.5)
".event-truncated" ? do
- color "#6b7280"
+ color WebStyle.cFgMuted
fontStyle italic
".event-tool-call" ? do
- borderLeft (px 3) solid "#3b82f6"
+ borderLeft (px 3) solid WebStyle.cBlue
paddingLeft (px 8)
".event-tool-call" |> "summary" ? do
cursor pointer
@@ -1529,17 +1539,17 @@ timelineEventStyles = do
".event-tool-call" |> "summary" # before ? do
content (stringContent "▶")
fontSize (px 10)
- color "#6b7280"
+ color WebStyle.cFgMuted
transition "transform" (ms 150) ease (sec 0)
".event-tool-call[open]" |> "summary" # before ? do
Stylesheet.key "transform" ("rotate(90deg)" :: Text)
".tool-name" ? do
fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
- color "#3b82f6"
+ color WebStyle.cBlue
".tool-summary" ? do
fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
fontSize (px 12)
- color "#6b7280"
+ color WebStyle.cFgMuted
marginLeft (px 8)
".tool-args" ? do
marginTop (px 4)
@@ -1547,8 +1557,8 @@ timelineEventStyles = do
".tool-output-pre" ? do
fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
fontSize (px 11)
- backgroundColor "#1e1e1e"
- color "#d4d4d4"
+ backgroundColor WebStyle.cBg
+ color WebStyle.cFg
padding (px 8) (px 10) (px 8) (px 10)
borderRadius (px 4) (px 4) (px 4) (px 4)
overflowX auto
@@ -1556,27 +1566,27 @@ timelineEventStyles = do
maxHeight (px 300)
margin (px 0) (px 0) (px 0) (px 0)
".event-tool-result" ? do
- borderLeft (px 3) solid "#10b981"
+ borderLeft (px 3) solid WebStyle.cGreen
paddingLeft (px 8)
".result-header" ? do
fontSize (px 12)
".line-count" ? do
fontSize (px 11)
- color "#6b7280"
- backgroundColor "#f3f4f6"
+ color WebStyle.cFgMuted
+ backgroundColor WebStyle.cBgHover
padding (px 1) (px 6) (px 1) (px 6)
borderRadius (px 10) (px 10) (px 10) (px 10)
".result-collapsible" |> "summary" ? do
cursor pointer
fontSize (px 12)
- color "#0066cc"
+ color WebStyle.cAccent
marginBottom (px 4)
".result-collapsible" |> "summary" # hover ? textDecoration underline
".tool-output" ? do
fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
fontSize (px 11)
- backgroundColor "#1e1e1e"
- color "#d4d4d4"
+ backgroundColor WebStyle.cBg
+ color WebStyle.cFg
padding (px 8) (px 10) (px 8) (px 10)
borderRadius (px 4) (px 4) (px 4) (px 4)
overflowX auto
@@ -1588,19 +1598,19 @@ timelineEventStyles = do
alignItems center
Stylesheet.key "gap" ("6px" :: Text)
fontSize (px 11)
- color "#6b7280"
+ color WebStyle.cFgMuted
padding (px 4) (px 0) (px 4) (px 0)
".cost-text" ? do
fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
".event-error" ? do
- borderLeft (px 3) solid "#ef4444"
+ borderLeft (px 3) solid WebStyle.cRed
paddingLeft (px 8)
- backgroundColor "#fef2f2"
+ Stylesheet.key "background" ("rgba(248,113,113,0.1)" :: Text)
padding (px 8) (px 8) (px 8) (px 12)
borderRadius (px 4) (px 4) (px 4) (px 4)
- ".event-error" |> ".event-label" ? color "#dc2626"
+ ".event-error" |> ".event-label" ? color WebStyle.cRed
".error-message" ? do
- color "#dc2626"
+ color WebStyle.cRed
fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
fontSize (px 12)
whiteSpace preWrap
@@ -1608,13 +1618,13 @@ timelineEventStyles = do
display flex
alignItems center
Stylesheet.key "gap" ("8px" :: Text)
- color "#10b981"
+ color WebStyle.cGreen
fontWeight (weight 500)
padding (px 8) (px 0) (px 8) (px 0)
".output-collapsible" |> "summary" ? do
cursor pointer
fontSize (px 12)
- color "#0066cc"
+ color WebStyle.cAccent
marginBottom (px 4)
".output-collapsible" |> "summary" # hover ? textDecoration underline
Stylesheet.key "@keyframes pulse" ("0%, 100% { opacity: 1; } 50% { opacity: 0.5; }" :: Text)
@@ -1624,49 +1634,49 @@ unifiedTimelineStyles = do
".unified-timeline-section" ? do
marginTop (em 1.5)
paddingTop (em 1)
- borderTop (px 1) solid "#e5e7eb"
+ borderTop (px 1) solid WebStyle.cBorderSubtle
".timeline-live-toggle" ? do
fontSize (px 10)
fontWeight bold
- color "#10b981"
- backgroundColor "#d1fae5"
+ color WebStyle.cGreen
+ Stylesheet.key "background" ("rgba(74,222,128,0.15)" :: Text)
padding (px 2) (px 6) (px 2) (px 6)
borderRadius (px 10) (px 10) (px 10) (px 10)
marginLeft (px 8)
textTransform uppercase
- border (px 1) solid "#6ee7b7"
+ border (px 1) solid WebStyle.cGreen
cursor pointer
Stylesheet.key "transition" ("all 0.3s ease" :: Text)
Stylesheet.key "animation" ("pulse 2s infinite" :: Text)
".timeline-live-toggle:hover" ? do
Stylesheet.key "box-shadow" ("0 0 8px rgba(16,185,129,0.4)" :: Text)
".timeline-live-toggle.timeline-live-paused" ? do
- color "#6b7280"
- backgroundColor "#f3f4f6"
- border (px 1) solid "#d1d5db"
+ color WebStyle.cFgMuted
+ backgroundColor WebStyle.cBgHover
+ border (px 1) solid WebStyle.cBorder
Stylesheet.key "animation" ("none" :: Text)
".timeline-autoscroll-toggle" ? do
fontSize (px 10)
fontWeight bold
- color "#3b82f6"
- backgroundColor "#dbeafe"
+ color WebStyle.cBlue
+ Stylesheet.key "background" ("rgba(96,165,250,0.15)" :: Text)
padding (px 2) (px 6) (px 2) (px 6)
borderRadius (px 10) (px 10) (px 10) (px 10)
marginLeft (px 4)
- border (px 1) solid "#93c5fd"
+ border (px 1) solid WebStyle.cBlue
cursor pointer
Stylesheet.key "transition" ("all 0.2s ease" :: Text)
".timeline-autoscroll-toggle:hover" ? do
Stylesheet.key "box-shadow" ("0 0 6px rgba(59,130,246,0.3)" :: Text)
".timeline-autoscroll-toggle.timeline-autoscroll-disabled" ? do
- color "#6b7280"
- backgroundColor "#f3f4f6"
- border (px 1) solid "#d1d5db"
+ color WebStyle.cFgMuted
+ backgroundColor WebStyle.cBgHover
+ border (px 1) solid WebStyle.cBorder
".timeline-live" ? do
fontSize (px 10)
fontWeight bold
- color "#10b981"
- backgroundColor "#d1fae5"
+ color WebStyle.cGreen
+ Stylesheet.key "background" ("rgba(74,222,128,0.15)" :: Text)
padding (px 2) (px 6) (px 2) (px 6)
borderRadius (px 10) (px 10) (px 10) (px 10)
marginLeft (px 8)
@@ -1690,19 +1700,19 @@ unifiedTimelineStyles = do
marginLeft (px 4)
marginRight (px 4)
".actor-human" ? do
- color "#7c3aed"
- backgroundColor "#f3e8ff"
+ color WebStyle.cPurple
+ Stylesheet.key "background" ("rgba(167,139,250,0.15)" :: Text)
".actor-junior" ? do
- color "#0369a1"
- backgroundColor "#e0f2fe"
+ color WebStyle.cBlue
+ Stylesheet.key "background" ("rgba(96,165,250,0.1)" :: Text)
".actor-system" ? do
- color "#6b7280"
- backgroundColor "#f3f4f6"
+ color WebStyle.cFgMuted
+ backgroundColor WebStyle.cBgHover
".timeline-comment" ? do
paddingLeft (px 4)
".timeline-comment" |> ".comment-bubble" ? do
- backgroundColor "#f3f4f6"
- color "#1f2937"
+ backgroundColor WebStyle.cBgHover
+ color WebStyle.cFg
padding (px 10) (px 14) (px 10) (px 14)
borderRadius (px 8) (px 8) (px 8) (px 8)
whiteSpace preWrap
@@ -1713,39 +1723,39 @@ unifiedTimelineStyles = do
Stylesheet.key "gap" ("6px" :: Text)
flexWrap Flexbox.wrap
padding (px 6) (px 8) (px 6) (px 8)
- backgroundColor "#f0fdf4"
+ Stylesheet.key "background" ("rgba(74,222,128,0.1)" :: Text)
borderRadius (px 6) (px 6) (px 6) (px 6)
- borderLeft (px 3) solid "#22c55e"
+ borderLeft (px 3) solid WebStyle.cGreen
".status-change-text" ? do
fontWeight (weight 500)
- color "#166534"
+ color WebStyle.cGreen
".timeline-activity" ? do
display flex
alignItems center
Stylesheet.key "gap" ("6px" :: Text)
flexWrap Flexbox.wrap
padding (px 4) (px 0) (px 4) (px 0)
- color "#6b7280"
+ color WebStyle.cFgMuted
".activity-detail" ? do
fontSize (px 11)
- color "#9ca3af"
+ color WebStyle.cFgFaint
fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
".timeline-error" ? do
- borderLeft (px 3) solid "#ef4444"
- backgroundColor "#fef2f2"
+ borderLeft (px 3) solid WebStyle.cRed
+ Stylesheet.key "background" ("rgba(248,113,113,0.1)" :: Text)
padding (px 8) (px 12) (px 8) (px 12)
borderRadius (px 4) (px 4) (px 4) (px 4)
".timeline-error" |> ".error-message" ? do
marginTop (px 6)
- color "#dc2626"
+ color WebStyle.cRed
fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
fontSize (px 12)
whiteSpace preWrap
".timeline-thought" ? do
paddingLeft (px 4)
".timeline-thought" |> ".thought-bubble" ? do
- backgroundColor "#fef3c7"
- color "#78350f"
+ Stylesheet.key "background" ("rgba(250,204,21,0.15)" :: Text)
+ color WebStyle.cYellow
padding (px 8) (px 12) (px 8) (px 12)
borderRadius (px 8) (px 8) (px 8) (px 8)
whiteSpace preWrap
@@ -1753,7 +1763,7 @@ unifiedTimelineStyles = do
fontSize (px 12)
lineHeight (em 1.5)
".timeline-tool-call" ? do
- borderLeft (px 3) solid "#3b82f6"
+ borderLeft (px 3) solid WebStyle.cBlue
paddingLeft (px 8)
".timeline-tool-call" |> "summary" ? do
cursor pointer
@@ -1764,12 +1774,12 @@ unifiedTimelineStyles = do
".timeline-tool-call" |> "summary" # before ? do
content (stringContent "▶")
fontSize (px 10)
- color "#6b7280"
+ color WebStyle.cFgMuted
transition "transform" (ms 150) ease (sec 0)
".timeline-tool-call[open]" |> "summary" # before ? do
Stylesheet.key "transform" ("rotate(90deg)" :: Text)
".timeline-tool-result" ? do
- borderLeft (px 3) solid "#10b981"
+ borderLeft (px 3) solid WebStyle.cGreen
paddingLeft (px 8)
".timeline-tool-result" |> "summary" ? do
cursor pointer
@@ -1782,11 +1792,11 @@ unifiedTimelineStyles = do
alignItems center
Stylesheet.key "gap" ("6px" :: Text)
fontSize (px 11)
- color "#6b7280"
+ color WebStyle.cFgMuted
padding (px 2) (px 0) (px 2) (px 0)
".timeline-checkpoint" ? do
- borderLeft (px 3) solid "#8b5cf6"
- backgroundColor "#faf5ff"
+ borderLeft (px 3) solid WebStyle.cPurple
+ Stylesheet.key "background" ("rgba(167,139,250,0.1)" :: Text)
padding (px 8) (px 12) (px 8) (px 12)
borderRadius (px 4) (px 4) (px 4) (px 4)
".timeline-checkpoint" |> ".checkpoint-content" ? do
@@ -1794,21 +1804,21 @@ unifiedTimelineStyles = do
fontSize (px 12)
whiteSpace preWrap
".timeline-guardrail" ? do
- borderLeft (px 3) solid "#f59e0b"
- backgroundColor "#fffbeb"
+ borderLeft (px 3) solid WebStyle.cYellow
+ Stylesheet.key "background" ("rgba(250,204,21,0.1)" :: Text)
padding (px 8) (px 12) (px 8) (px 12)
borderRadius (px 4) (px 4) (px 4) (px 4)
".timeline-guardrail" |> ".guardrail-content" ? do
marginTop (px 6)
fontSize (px 12)
- color "#92400e"
+ color WebStyle.cYellow
".timeline-generic" ? do
padding (px 4) (px 0) (px 4) (px 0)
- color "#6b7280"
+ color WebStyle.cFgMuted
".formatted-json" ? do
margin (px 0) (px 0) (px 0) (px 0)
padding (px 8) (px 8) (px 8) (px 8)
- backgroundColor "#f9fafb"
+ backgroundColor WebStyle.cBgRaised
borderRadius (px 4) (px 4) (px 4) (px 4)
overflowX auto
fontSize (px 12)
@@ -1827,20 +1837,20 @@ compactToolStyles = do
fontSize (px 12)
padding (px 2) (px 0) (px 2) (px 0)
".tool-check" ? do
- color "#10b981"
+ color WebStyle.cGreen
fontWeight bold
".tool-label" ? do
- color "#6b7280"
+ color WebStyle.cFgMuted
fontWeight (weight 500)
".tool-path" ? do
- color "#3b82f6"
+ color WebStyle.cBlue
".tool-pattern" ? do
- color "#8b5cf6"
- backgroundColor "#f5f3ff"
+ color WebStyle.cPurple
+ Stylesheet.key "background" ("rgba(167,139,250,0.1)" :: Text)
padding (px 1) (px 4) (px 1) (px 4)
borderRadius (px 2) (px 2) (px 2) (px 2)
".tool-path-suffix" ? do
- color "#6b7280"
+ color WebStyle.cFgMuted
fontSize (px 11)
".tool-bash" ? do
display flex
@@ -1850,12 +1860,12 @@ compactToolStyles = do
fontSize (px 12)
padding (px 2) (px 0) (px 2) (px 0)
".tool-bash-prompt" ? do
- color "#f59e0b"
+ color WebStyle.cYellow
fontWeight bold
fontSize (px 14)
".tool-bash-cmd" ? do
- color "#374151"
- backgroundColor "#f3f4f6"
+ color WebStyle.cFg
+ backgroundColor WebStyle.cBgHover
padding (px 2) (px 6) (px 2) (px 6)
borderRadius (px 3) (px 3) (px 3) (px 3)
wordBreak breakAll
@@ -1870,7 +1880,7 @@ compactToolStyles = do
".tool-args-pre" ? do
margin (px 4) (px 0) (px 0) (px 16)
padding (px 6) (px 8) (px 6) (px 8)
- backgroundColor "#f9fafb"
+ backgroundColor WebStyle.cBgRaised
borderRadius (px 3) (px 3) (px 3) (px 3)
fontSize (px 11)
whiteSpace preWrap
@@ -1964,302 +1974,11 @@ responsiveStyles = do
justifyContent spaceBetween
marginTop (px 4)
+-- | Dark mode styles are now handled by the base styles being dark-first.
+-- The shared Omni.Web.Style design system uses dark-first tokens, so this
+-- function is now a no-op. Kept for structural consistency.
darkModeStyles :: Css
-darkModeStyles =
- query Media.screen [prefersDark] <| do
- body ? do
- backgroundColor "#111827"
- color "#f3f4f6"
- ".card"
- <> ".task-card"
- <> ".stat-card"
- <> ".task-detail"
- <> ".task-summary"
- <> ".filter-form"
- <> ".status-form"
- <> ".diff-section"
- <> ".review-actions"
- <> ".list-group"
- ? do
- backgroundColor "#1f2937"
- borderColor "#374151"
- ".list-group-item" ? borderBottomColor "#374151"
- ".list-group-item" # hover ? backgroundColor "#374151"
- ".list-group-item-id" ? color "#60a5fa"
- ".list-group-item-title" ? color "#d1d5db"
- header ? do
- backgroundColor "#1f2937"
- borderColor "#374151"
- ".navbar" ? do
- backgroundColor "#1f2937"
- borderColor "#374151"
- ".navbar-brand" ? color "#60a5fa"
- ".navbar-link" ? color "#d1d5db"
- ".navbar-link" # hover ? backgroundColor "#374151"
- ".navbar-dropdown-btn" ? color "#d1d5db"
- ".navbar-dropdown-btn" # hover ? backgroundColor "#374151"
- ".navbar-dropdown-content" ? do
- backgroundColor "#1f2937"
- Stylesheet.key "box-shadow" ("0 2px 8px rgba(0,0,0,0.3)" :: Text)
- ".navbar-dropdown-item" ? color "#d1d5db"
- ".navbar-dropdown-item" # hover ? backgroundColor "#374151"
- ".status-dropdown-menu" ? do
- backgroundColor "#1f2937"
- Stylesheet.key "box-shadow" ("0 2px 8px rgba(0,0,0,0.3)" :: Text)
- ".hamburger-line" ? backgroundColor "#d1d5db"
- ".nav-brand" ? color "#f3f4f6"
- "h2" <> "h3" ? color "#d1d5db"
- a ? color "#60a5fa"
- ".breadcrumb-container" ? backgroundColor transparent
- ".breadcrumb-sep" ? color "#6b7280"
- ".breadcrumb-current" ? color "#9ca3af"
-
- ".detail-label"
- <> ".priority"
- <> ".dep-type"
- <> ".child-status"
- <> ".empty-msg"
- <> ".stat-label"
- <> ".priority-desc"
- ? color "#9ca3af"
- ".child-title" ? color "#d1d5db"
- code ? do
- backgroundColor "#374151"
- color "#f3f4f6"
- ".task-meta-id" ? do
- backgroundColor "#374151"
- color "#e5e7eb"
- ".task-meta-secondary" ? color "#9ca3af"
- ".meta-sep" ? color "#4b5563"
- ".task-meta-label" ? color "#9ca3af"
- ".detail-section" ? borderTopColor "#374151"
- ".description" ? do
- backgroundColor "#374151"
- color "#e5e7eb"
- ".badge-open" ? do
- backgroundColor "#78350f"
- color "#fcd34d"
- ".badge-inprogress" ? do
- backgroundColor "#1e3a8a"
- color "#93c5fd"
- ".badge-review" ? do
- backgroundColor "#4c1d95"
- color "#c4b5fd"
- ".badge-approved" ? do
- backgroundColor "#164e63"
- color "#67e8f9"
- ".badge-done" ? do
- backgroundColor "#064e3b"
- color "#6ee7b7"
- ".badge-needshelp" ? do
- backgroundColor "#78350f"
- color "#fcd34d"
- ".badge-p0" ? do
- backgroundColor "#7f1d1d"
- color "#fca5a5"
- ".badge-p1" ? do
- backgroundColor "#78350f"
- color "#fcd34d"
- ".badge-p2" ? do
- backgroundColor "#1e3a8a"
- color "#93c5fd"
- ".badge-p3" ? do
- backgroundColor "#374151"
- color "#d1d5db"
- ".badge-p4" ? do
- backgroundColor "#1f2937"
- color "#9ca3af"
- ".blocking-impact" ? do
- backgroundColor "#374151"
- color "#9ca3af"
- ".priority-dropdown-menu" ? do
- backgroundColor "#1f2937"
- Stylesheet.key "box-shadow" ("0 2px 8px rgba(0,0,0,0.3)" :: Text)
- ".action-btn" ? do
- backgroundColor "#374151"
- borderColor "#4b5563"
- color "#f3f4f6"
- ".action-btn" # hover ? backgroundColor "#4b5563"
- ".filter-select" <> ".filter-input" <> ".status-select" <> ".reject-notes" ? do
- backgroundColor "#374151"
- borderColor "#4b5563"
- color "#f3f4f6"
- ".stats-section" <> ".summary-section" ? do
- backgroundColor "#1f2937"
- borderColor "#374151"
-
- (".stat-card.badge-open" |> ".stat-count") ? color "#fbbf24"
- (".stat-card.badge-inprogress" |> ".stat-count") ? color "#60a5fa"
- (".stat-card.badge-review" |> ".stat-count") ? color "#a78bfa"
- (".stat-card.badge-approved" |> ".stat-count") ? color "#22d3ee"
- (".stat-card.badge-done" |> ".stat-count") ? color "#34d399"
- (".stat-card.badge-neutral" |> ".stat-count") ? color "#9ca3af"
-
- ".progress-bar" ? backgroundColor "#374151"
- ".progress-fill" ? backgroundColor "#60a5fa"
- ".multi-progress-bar" ? backgroundColor "#374151"
- ".progress-legend" ? color "#9ca3af"
- ".activity-section" ? do
- backgroundColor "#1f2937"
- borderColor "#374151"
- ".activity-timeline" # before ? backgroundColor "#374151"
- ".activity-icon" ? do
- backgroundColor "#1f2937"
- borderColor "#374151"
- ".activity-time" ? color "#9ca3af"
- ".activity-message" ? color "#d1d5db"
- (".activity-metadata" |> "summary") ? color "#9ca3af"
- ".metadata-json" ? backgroundColor "#374151"
- ".execution-section" ? do
- backgroundColor "#1f2937"
- borderColor "#374151"
-
- ".metric-label" ? color "#9ca3af"
- ".metric-value" ? color "#d1d5db"
- ".metric-card" ? do
- backgroundColor "#374151"
- borderColor "#4b5563"
- (".metric-card" |> ".metric-value") ? color "#f3f4f6"
- (".metric-card" |> ".metric-label") ? color "#9ca3af"
- ".amp-link" ? color "#60a5fa"
- ".amp-thread-btn" ? do
- backgroundColor "#8b5cf6"
- ".amp-thread-btn" # hover ? backgroundColor "#7c3aed"
- ".markdown-content" ? color "#d1d5db"
- ".commit-item" ? do
- backgroundColor "#374151"
- borderColor "#4b5563"
- ".commit-hash" ? do
- backgroundColor "#4b5563"
- color "#60a5fa"
- ".commit-summary" ? color "#d1d5db"
- ".commit-meta" ? color "#9ca3af"
- ".md-h1" ? borderBottomColor "#374151"
- ".md-code" ? do
- backgroundColor "#1e1e1e"
- color "#d4d4d4"
- borderColor "#374151"
- ".md-inline-code" ? do
- backgroundColor "#374151"
- color "#f3f4f6"
- ".edit-description" ? borderTopColor "#374151"
- (".edit-description" |> "summary") ? color "#60a5fa"
- ".edit-link" ? color "#60a5fa"
- "button.cancel-link" ? do
- color "#f87171"
- backgroundColor transparent
- border (px 0) solid transparent
- ".description-textarea" ? do
- backgroundColor "#374151"
- borderColor "#4b5563"
- color "#f3f4f6"
- ".fact-create-form" ? do
- backgroundColor "#1f2937"
- borderColor "#374151"
- ".time-filter-btn" ? do
- backgroundColor "#374151"
- borderColor "#4b5563"
- color "#d1d5db"
- ".time-filter-btn" # hover ? do
- backgroundColor "#4b5563"
- borderColor "#6b7280"
- ".time-filter-btn.active" ? do
- backgroundColor "#3b82f6"
- borderColor "#3b82f6"
- color white
- ".time-filter-btn.active" # hover ? do
- backgroundColor "#2563eb"
- borderColor "#2563eb"
- ".sort-label" ? color "#9ca3af"
- ".sort-dropdown-btn" ? do
- backgroundColor "#374151"
- borderColor "#4b5563"
- color "#d1d5db"
- ".sort-dropdown-btn" # hover ? do
- backgroundColor "#4b5563"
- borderColor "#6b7280"
- ".sort-dropdown-item.active" ? do
- backgroundColor "#1e3a5f"
- ".comment-card" ? do
- backgroundColor "#374151"
- borderColor "#4b5563"
- ".comment-text" ? color "#d1d5db"
- ".author-human" ? do
- backgroundColor "#1e3a8a"
- color "#93c5fd"
- ".author-junior" ? do
- backgroundColor "#064e3b"
- color "#6ee7b7"
- ".comment-time" ? color "#9ca3af"
- ".comment-textarea" ? do
- backgroundColor "#374151"
- borderColor "#4b5563"
- color "#f3f4f6"
- ".form-input" <> ".form-textarea" ? do
- backgroundColor "#374151"
- borderColor "#4b5563"
- color "#f3f4f6"
- (".form-group" |> label) ? color "#d1d5db"
- ".danger-zone" ? do
- backgroundColor "#450a0a"
- borderColor "#991b1b"
- (".danger-zone" |> h2) ? color "#f87171"
- ".retry-banner-warning" ? do
- backgroundColor "#451a03"
- borderColor "#b45309"
- ".retry-banner-critical" ? do
- backgroundColor "#450a0a"
- borderColor "#dc2626"
- ".retry-attempt" ? color "#d1d5db"
- ".retry-banner-details" ? color "#d1d5db"
- ".retry-value" ? color "#9ca3af"
- ".retry-commit" ? backgroundColor "#374151"
- ".event-bubble" ? backgroundColor "#374151"
- ".comment-bubble" ? do
- backgroundColor "#374151"
- color "#d1d5db"
- ".thought-bubble" ? do
- backgroundColor "#292524"
- color "#a8a29e"
- borderRadius (px 2) (px 2) (px 2) (px 2)
- ".event-label" ? color "#d1d5db"
- ".tool-bash-cmd" ? do
- backgroundColor "#292524"
- color "#a8a29e"
- ".tool-label" ? color "#9ca3af"
- ".tool-path" ? color "#60a5fa"
- ".tool-pattern" ? do
- backgroundColor "#3b2f5e"
- color "#c4b5fd"
- ".output-collapsible" |> "summary" ? color "#60a5fa"
- ".timeline-tool-call" |> "summary" # before ? color "#9ca3af"
- ".line-count" ? do
- backgroundColor "#374151"
- color "#9ca3af"
- ".event-error" ? do
- backgroundColor "#450a0a"
- borderColor "#dc2626"
- ".event-error" |> ".event-label" ? color "#f87171"
- ".error-message" ? color "#f87171"
- ".timeline-error" |> ".event-label" ? color "#fca5a5"
- ".timeline-guardrail" |> ".event-label" ? color "#fbbf24"
- ".timeline-guardrail" ? do
- backgroundColor "#451a03"
- borderColor "#f59e0b"
- ".timeline-guardrail" |> ".guardrail-content" ? color "#fcd34d"
- ".formatted-json" ? do
- backgroundColor "#1e1e1e"
- color "#d4d4d4"
- -- Responsive dark mode: dropdown content needs background on mobile
- query Media.screen [Media.maxWidth (px 600)] <| do
- ".navbar-dropdown-content" ? do
- backgroundColor "#1f2937"
- ".navbar-dropdown-item" # hover ? do
- backgroundColor "#374151"
-
-prefersDark :: Stylesheet.Feature
-prefersDark =
- Stylesheet.Feature "prefers-color-scheme" (Just (Clay.value ("dark" :: Text)))
+darkModeStyles = pure ()
statusBadgeClass :: Text -> Text
statusBadgeClass status = case status of
diff --git a/Omni/Web.hs b/Omni/Web/Core.hs
similarity index 99%
rename from Omni/Web.hs
rename to Omni/Web/Core.hs
index 6e4d19c7..99f4a37c 100644
--- a/Omni/Web.hs
+++ b/Omni/Web/Core.hs
@@ -14,7 +14,7 @@
-- : dep sqlite-simple
-- : dep wai
-- : dep warp
-module Omni.Web
+module Omni.Web.Core
( startWebServer,
defaultPort,
app,
diff --git a/Omni/Web/Style.hs b/Omni/Web/Style.hs
new file mode 100644
index 00000000..21050e93
--- /dev/null
+++ b/Omni/Web/Style.hs
@@ -0,0 +1,585 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+-- | Shared design system for all Omni web surfaces.
+--
+-- Modern terminal aesthetic: dark-first, monospace-forward, mobile-optimized.
+-- Designed for iOS webapp usage with focus on reading long content.
+--
+-- Uses Clay for type-checked CSS generation. Import 'css' for the full
+-- stylesheet text, or use individual token values for composition.
+--
+-- : dep clay
+-- : dep lucid
+module Omni.Web.Style
+ ( -- * Full rendered CSS
+ css,
+
+ -- * Design tokens (re-export for other Clay modules)
+ cBg,
+ cBgRaised,
+ cBgHover,
+ cBgActive,
+ cBorder,
+ cBorderSubtle,
+ cFg,
+ cFgMuted,
+ cFgFaint,
+ cAccent,
+ cAccentDim,
+ cLink,
+ cGreen,
+ cYellow,
+ cOrange,
+ cRed,
+ cPurple,
+ cBlue,
+ fontMono,
+ fontSans,
+ fontReading,
+ radiusSm,
+ radiusMd,
+ radiusLg,
+
+ -- * Shared HTML components
+ sharedNav,
+ sharedShell,
+ )
+where
+
+import Alpha hiding (all, filter, head, not, rem, reverse, (**), (|>))
+import Clay
+import qualified Clay.Media as Media
+import qualified Clay.Stylesheet as Stylesheet
+import qualified Data.Text.Lazy as LazyText
+import qualified Lucid
+
+-- ============================================================================
+-- Design Tokens
+-- ============================================================================
+
+-- Dark-first palette. Light mode overrides via @media query below.
+
+cBg, cBgRaised, cBgHover, cBgActive :: Color
+cBg = "#0a0a0a"
+cBgRaised = "#141414"
+cBgHover = "#1a1a1a"
+cBgActive = "#222222"
+
+cBorder, cBorderSubtle :: Color
+cBorder = "#262626"
+cBorderSubtle = "#1c1c1c"
+
+cFg, cFgMuted, cFgFaint :: Color
+cFg = "#e0e0e0"
+cFgMuted = "#737373"
+cFgFaint = "#525252"
+
+cAccent, cAccentDim, cLink :: Color
+cAccent = "#22d3ee"
+cAccentDim = "#0e7490"
+cLink = "#22d3ee"
+
+cGreen, cYellow, cOrange, cRed, cPurple, cBlue :: Color
+cGreen = "#4ade80"
+cYellow = "#facc15"
+cOrange = "#fb923c"
+cRed = "#f87171"
+cPurple = "#a78bfa"
+cBlue = "#60a5fa"
+
+fontMono :: [Text]
+fontMono =
+ [ "SF Mono",
+ "Cascadia Code",
+ "JetBrains Mono",
+ "Fira Code",
+ "Menlo",
+ "Monaco",
+ "Courier New",
+ "monospace"
+ ]
+
+fontSans :: [Text]
+fontSans =
+ [ "-apple-system",
+ "BlinkMacSystemFont",
+ "SF Pro Text",
+ "Inter",
+ "system-ui",
+ "sans-serif"
+ ]
+
+fontReading :: [Text]
+fontReading =
+ [ "SF Pro Text",
+ "Inter",
+ "-apple-system",
+ "BlinkMacSystemFont",
+ "system-ui",
+ "sans-serif"
+ ]
+
+radiusSm, radiusMd, radiusLg :: Size LengthUnit
+radiusSm = px 4
+radiusMd = px 6
+radiusLg = px 10
+
+-- ============================================================================
+-- CSS Generation
+-- ============================================================================
+
+css :: LazyText.Text
+css = render stylesheet
+
+stylesheet :: Css
+stylesheet = do
+ resetStyles
+ baseStyles
+ typographyStyles
+ readingStyles
+ layoutStyles
+ navStyles
+ cardStyles
+ badgeStyles
+ buttonStyles
+ formStyles
+ tableStyles
+ utilityStyles
+ mobileStyles
+ iosStyles
+ scrollbarStyles
+ selectionStyles
+
+-- ── Reset ──────────────────────────────────────────────────────────
+
+resetStyles :: Css
+resetStyles = do
+ star <> star # before <> star # after ? do
+ boxSizing borderBox
+ margin nil nil nil nil
+ padding nil nil nil nil
+ html ? do
+ Stylesheet.key "-webkit-text-size-adjust" ("100%" :: Text)
+ Stylesheet.key "-webkit-font-smoothing" ("antialiased" :: Text)
+ Stylesheet.key "-moz-osx-font-smoothing" ("grayscale" :: Text)
+ Stylesheet.key "text-rendering" ("optimizeLegibility" :: Text)
+
+-- ── Base ───────────────────────────────────────────────────────────
+
+baseStyles :: Css
+baseStyles =
+ body ? do
+ fontFamily fontMono [monospace]
+ fontSize (px 13)
+ lineHeight (unitless 1.6)
+ color cFg
+ backgroundColor cBg
+ minHeight (vh 100)
+ Stylesheet.key "min-height" ("100dvh" :: Text)
+ Stylesheet.key "padding-bottom" ("env(safe-area-inset-bottom, 0)" :: Text)
+
+-- ── Typography ─────────────────────────────────────────────────────
+
+typographyStyles :: Css
+typographyStyles = do
+ a ? do
+ color cLink
+ textDecoration none
+ a # hover ? do
+ textDecoration underline
+ Stylesheet.key "text-underline-offset" ("2px" :: Text)
+
+ (h1 <> h2 <> h3 <> h4) ? do
+ fontFamily fontMono [monospace]
+ fontWeight (weight 600)
+ Stylesheet.key "letter-spacing" ("-0.02em" :: Text)
+ lineHeight (unitless 1.3)
+ h1 ? do
+ fontSize (rem 1.25)
+ marginBottom (px 8)
+ h2 ? do
+ fontSize (rem 1.1)
+ marginBottom (px 8)
+ color cFg
+ h3 ? do
+ fontSize (rem 0.95)
+ marginBottom (px 4)
+
+ code ? do
+ fontSize (em 0.9)
+ backgroundColor cBgHover
+ padding (px 1) (px 5) (px 1) (px 5)
+ borderRadius radiusSm radiusSm radiusSm radiusSm
+ border (px 1) solid cBorder
+ pre ? do
+ fontSize (px 12)
+ backgroundColor cBg
+ border (px 1) solid cBorder
+ padding (px 8) (px 16) (px 8) (px 16)
+ borderRadius radiusMd radiusMd radiusMd radiusMd
+ overflow auto
+ whiteSpace preWrap
+ Stylesheet.key "word-break" ("break-all" :: Text)
+ (pre |> code) ? do
+ backgroundColor transparent
+ border nil none transparent
+ padding nil nil nil nil
+ fontSize inherit
+
+-- ── Reading typography (articles, descriptions) ────────────────────
+
+readingStyles :: Css
+readingStyles = do
+ ".reading" ? do
+ fontFamily fontReading [sansSerif]
+ fontSize (px 16)
+ lineHeight (unitless 1.7)
+ Stylesheet.key "letter-spacing" ("-0.011em" :: Text)
+ maxWidth (px 680)
+ ".reading" |> p ? marginBottom (em 1)
+ ".reading" |> h1 ? do
+ fontSize (rem 1.5)
+ margin (em 1.5) nil (em 0.5) nil
+ fontFamily fontReading [sansSerif]
+ fontWeight bold
+ ".reading" |> h2 ? do
+ fontSize (rem 1.25)
+ margin (em 1.25) nil (em 0.4) nil
+ fontFamily fontReading [sansSerif]
+ fontWeight (weight 600)
+ ".reading" |> h3 ? do
+ fontSize (rem 1.1)
+ margin (em 1) nil (em 0.3) nil
+ fontFamily fontReading [sansSerif]
+ fontWeight (weight 600)
+ (".reading" |> ul) <> (".reading" |> ol) ? do
+ margin (em 0.5) nil (em 1) nil
+ paddingLeft (em 1.5)
+ ".reading" ** li ? marginBottom (em 0.3)
+ ".reading" |> "blockquote" ? do
+ borderLeft (px 3) solid cAccentDim
+ paddingLeft (px 16)
+ color cFgMuted
+ margin (em 1) nil (em 1) nil
+ ".reading" ** img ? do
+ maxWidth (pct 100)
+ height auto
+ borderRadius radiusMd radiusMd radiusMd radiusMd
+ ".reading" ** a ? do
+ Stylesheet.key "text-underline-offset" ("3px" :: Text)
+ Stylesheet.key "text-decoration-thickness" ("1px" :: Text)
+ (".reading" ** a)
+ # hover
+ ? Stylesheet.key "text-decoration-thickness" ("2px" :: Text)
+
+-- ── Layout ─────────────────────────────────────────────────────────
+
+layoutStyles :: Css
+layoutStyles = do
+ ".container" ? do
+ width (pct 100)
+ maxWidth (px 960)
+ margin nil auto nil auto
+ padding (px 8) (px 16) (px 8) (px 16)
+ ".container-narrow" ? do
+ width (pct 100)
+ maxWidth (px 680)
+ margin nil auto nil auto
+ padding (px 8) (px 16) (px 8) (px 16)
+
+-- ── Navigation ─────────────────────────────────────────────────────
+
+navStyles :: Css
+navStyles = do
+ ".on-nav" ? do
+ display flex
+ alignItems center
+ Stylesheet.key "gap" ("4px" :: Text)
+ padding (px 8) (px 16) (px 8) (px 16)
+ borderBottom (px 1) solid cBorder
+ backgroundColor cBg
+ position sticky
+ top nil
+ zIndex 100
+ Stylesheet.key "backdrop-filter" ("blur(12px)" :: Text)
+ Stylesheet.key "-webkit-backdrop-filter" ("blur(12px)" :: Text)
+ ".on-nav-brand" ? do
+ fontWeight bold
+ fontSize (px 14)
+ color cAccent
+ textDecoration none
+ Stylesheet.key "letter-spacing" ("0.05em" :: Text)
+ marginRight (px 8)
+ ".on-nav-brand" # hover ? do
+ textDecoration none
+ opacity 0.8
+ ".on-nav-links" ? do
+ display flex
+ Stylesheet.key "gap" ("2px" :: Text)
+ ".on-nav-link" ? do
+ fontSize (px 12)
+ color cFgMuted
+ padding (px 4) (px 10) (px 4) (px 10)
+ borderRadius radiusSm radiusSm radiusSm radiusSm
+ transition "all" (ms 150) ease (sec 0)
+ ".on-nav-link" # hover ? do
+ backgroundColor cBgHover
+ color cFg
+ textDecoration none
+ ".on-active" ? do
+ color cAccent
+ backgroundColor cBgHover
+
+-- ── Cards ──────────────────────────────────────────────────────────
+
+cardStyles :: Css
+cardStyles = do
+ ".card" ? do
+ backgroundColor cBgRaised
+ border (px 1) solid cBorder
+ borderRadius radiusMd radiusMd radiusMd radiusMd
+ padding (px 8) (px 16) (px 8) (px 16)
+ ".card-link" ? do
+ display block
+ textDecoration none
+ color inherit
+ ".card-link" # hover ? textDecoration none
+ (".card-link" # hover |> ".card") ? borderColor cAccentDim
+
+-- ── Badges ─────────────────────────────────────────────────────────
+
+badgeStyles :: Css
+badgeStyles = do
+ ".badge" ? do
+ display inlineBlock
+ fontSize (px 11)
+ fontFamily fontMono [monospace]
+ fontWeight (weight 500)
+ padding (px 2) (px 8) (px 2) (px 8)
+ borderRadius radiusSm radiusSm radiusSm radiusSm
+ Stylesheet.key "letter-spacing" ("0.03em" :: Text)
+ whiteSpace nowrap
+ ".badge-open" ? do
+ Stylesheet.key "background" ("rgba(250,204,21,0.15)" :: Text)
+ color cYellow
+ ".badge-inprogress" ? do
+ Stylesheet.key "background" ("rgba(96,165,250,0.15)" :: Text)
+ color cBlue
+ ".badge-review" ? do
+ Stylesheet.key "background" ("rgba(167,139,250,0.15)" :: Text)
+ color cPurple
+ ".badge-approved" ? do
+ Stylesheet.key "background" ("rgba(34,211,238,0.15)" :: Text)
+ color cAccent
+ ".badge-done" ? do
+ Stylesheet.key "background" ("rgba(74,222,128,0.15)" :: Text)
+ color cGreen
+ ".badge-needshelp" ? do
+ Stylesheet.key "background" ("rgba(251,146,60,0.15)" :: Text)
+ color cOrange
+ ".badge-p0" ? do
+ Stylesheet.key "background" ("rgba(248,113,113,0.15)" :: Text)
+ color cRed
+ ".badge-p1" ? do
+ Stylesheet.key "background" ("rgba(251,146,60,0.15)" :: Text)
+ color cOrange
+ ".badge-p2" ? do
+ Stylesheet.key "background" ("rgba(96,165,250,0.15)" :: Text)
+ color cBlue
+ ".badge-p3" ? do
+ backgroundColor cBgHover
+ color cFgMuted
+ ".badge-p4" ? do
+ backgroundColor cBgHover
+ color cFgFaint
+
+-- ── Buttons ────────────────────────────────────────────────────────
+
+buttonStyles :: Css
+buttonStyles = do
+ ".btn" ? do
+ display inlineFlex
+ alignItems center
+ justifyContent center
+ Stylesheet.key "gap" ("4px" :: Text)
+ minHeight (px 36)
+ padding (px 6) (px 14) (px 6) (px 14)
+ fontFamily fontMono [monospace]
+ fontSize (px 12)
+ fontWeight (weight 500)
+ border (px 1) solid cBorder
+ borderRadius radiusMd radiusMd radiusMd radiusMd
+ backgroundColor cBgRaised
+ color cFg
+ cursor pointer
+ transition "all" (ms 150) ease (sec 0)
+ textDecoration none
+ Stylesheet.key "-webkit-tap-highlight-color" ("transparent" :: Text)
+ Stylesheet.key "touch-action" ("manipulation" :: Text)
+ ".btn" # hover ? do
+ backgroundColor cBgHover
+ borderColor cFgFaint
+ textDecoration none
+ ".btn" # active ? backgroundColor cBgActive
+ ".btn-primary" ? do
+ backgroundColor cAccentDim
+ color white
+ borderColor cAccentDim
+ ".btn-primary" # hover ? do
+ backgroundColor cAccent
+ borderColor cAccent
+ color black
+ ".btn-danger" ? do
+ Stylesheet.key "background" ("rgba(248,113,113,0.15)" :: Text)
+ color cRed
+ Stylesheet.key "border-color" ("rgba(248,113,113,0.3)" :: Text)
+ ".btn-danger"
+ # hover
+ ? Stylesheet.key "background" ("rgba(248,113,113,0.25)" :: Text)
+ ".btn-ghost" ? do
+ backgroundColor transparent
+ borderColor transparent
+ ".btn-ghost" # hover ? backgroundColor cBgHover
+
+-- ── Forms ──────────────────────────────────────────────────────────
+
+formStyles :: Css
+formStyles = do
+ (input <> select <> textarea) ? do
+ fontFamily fontMono [monospace]
+ fontSize (px 13)
+ color cFg
+ backgroundColor cBg
+ border (px 1) solid cBorder
+ borderRadius radiusMd radiusMd radiusMd radiusMd
+ padding (px 8) (px 12) (px 8) (px 12)
+ Stylesheet.key "outline" ("none" :: Text)
+ transition "border-color" (ms 150) ease (sec 0)
+ (input # focus <> select # focus <> textarea # focus) ? do
+ borderColor cAccentDim
+ Stylesheet.key "box-shadow" ("0 0 0 2px rgba(34,211,238,0.1)" :: Text)
+ textarea ? do
+ Stylesheet.key "resize" ("vertical" :: Text)
+ minHeight (px 80)
+
+-- ── Tables ─────────────────────────────────────────────────────────
+
+tableStyles :: Css
+tableStyles = do
+ table ? do
+ width (pct 100)
+ Stylesheet.key "border-collapse" ("collapse" :: Text)
+ (th <> td) ? do
+ textAlign (alignSide sideLeft)
+ padding (px 8) (px 12) (px 8) (px 12)
+ borderBottom (px 1) solid cBorder
+ th ? do
+ color cFgMuted
+ fontSize (px 11)
+ textTransform uppercase
+ Stylesheet.key "letter-spacing" ("0.06em" :: Text)
+ fontWeight (weight 500)
+ tr # hover ? backgroundColor cBgHover
+ (tr # lastChild |> td) ? borderBottom nil none transparent
+
+-- ── Utility classes ────────────────────────────────────────────────
+
+utilityStyles :: Css
+utilityStyles = do
+ ".muted" ? color cFgMuted
+ ".faint" ? color cFgFaint
+ ".accent" ? color cAccent
+ ".text-sm" ? fontSize (px 12)
+ ".text-xs" ? fontSize (px 11)
+ ".text-sans" ? fontFamily fontSans [sansSerif]
+ ".empty-state" ? do
+ color cFgFaint
+ fontStyle italic
+ padding (px 16) nil (px 16) nil
+ ".divider" ? do
+ border nil none transparent
+ borderTop (px 1) solid cBorder
+ margin (px 16) nil (px 16) nil
+
+-- ── Mobile responsive ──────────────────────────────────────────────
+
+mobileStyles :: Css
+mobileStyles =
+ query Media.screen [Media.maxWidth (px 640)] <| do
+ body ? fontSize (px 14)
+ ".container" <> ".container-narrow" ? padding (px 8) (px 8) (px 8) (px 8)
+ ".on-nav" ? padding (px 8) (px 8) (px 8) (px 8)
+ ".on-nav-link" ? do
+ padding (px 6) (px 8) (px 6) (px 8)
+ fontSize (px 13)
+ ".on-nav-brand" ? fontSize (px 15)
+ ".card" ? padding (px 8) (px 8) (px 8) (px 8)
+ h1 ? fontSize (rem 1.15)
+ ".reading" ? do
+ fontSize (px 15)
+ lineHeight (unitless 1.65)
+ ".hide-mobile" ? display none
+
+-- ── iOS safe areas ─────────────────────────────────────────────────
+
+iosStyles :: Css
+iosStyles = do
+ Stylesheet.key
+ "@supports (padding: env(safe-area-inset-top))"
+ ( "{ .on-nav { padding-top: max(8px, env(safe-area-inset-top)); }"
+ <> " .container, .container-narrow { padding-left: max(16px, env(safe-area-inset-left));"
+ <> " padding-right: max(16px, env(safe-area-inset-right)); } }" ::
+ Text
+ )
+
+-- ── Scrollbar ──────────────────────────────────────────────────────
+
+scrollbarStyles :: Css
+scrollbarStyles = do
+ Stylesheet.key "::-webkit-scrollbar" ("{ width: 6px; height: 6px; }" :: Text)
+ Stylesheet.key "::-webkit-scrollbar-track" ("{ background: transparent; }" :: Text)
+ Stylesheet.key "::-webkit-scrollbar-thumb" ("{ background: #262626; border-radius: 3px; }" :: Text)
+
+-- ── Selection ──────────────────────────────────────────────────────
+
+selectionStyles :: Css
+selectionStyles =
+ Stylesheet.key "::selection" ("{ background: rgba(34,211,238,0.3); }" :: Text)
+
+-- ============================================================================
+-- Shared HTML Components
+-- ============================================================================
+
+-- | Navigation bar shared across all surfaces.
+sharedNav :: Text -> Lucid.Html ()
+sharedNav activePage =
+ Lucid.nav_ [Lucid.class_ "on-nav"] <| do
+ Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "on-nav-brand"] "omni"
+ Lucid.div_ [Lucid.class_ "on-nav-links"] <| do
+ navLink "/tasks" "tasks" (activePage == "tasks")
+ navLink "/news/" "news" (activePage == "news")
+ navLink "/files/" "files" (activePage == "files")
+ where
+ navLink href' label' isActive' =
+ Lucid.a_
+ [ Lucid.href_ href',
+ Lucid.class_ ("on-nav-link" <> if isActive' then " on-active" else "")
+ ]
+ label'
+
+-- | Shared page shell with design-system CSS and iOS webapp meta tags.
+sharedShell :: Text -> Text -> Lucid.Html () -> Lucid.Html ()
+sharedShell title' activePage' content' =
+ Lucid.doctypehtml_ <| do
+ Lucid.head_ <| do
+ Lucid.meta_ [Lucid.charset_ "utf-8"]
+ Lucid.meta_ [Lucid.name_ "viewport", Lucid.content_ "width=device-width, initial-scale=1, viewport-fit=cover"]
+ Lucid.meta_ [Lucid.name_ "apple-mobile-web-app-capable", Lucid.content_ "yes"]
+ Lucid.meta_ [Lucid.name_ "apple-mobile-web-app-status-bar-style", Lucid.content_ "black-translucent"]
+ Lucid.meta_ [Lucid.name_ "theme-color", Lucid.content_ "#0a0a0a"]
+ Lucid.title_ (Lucid.toHtml title')
+ Lucid.style_ (LazyText.toStrict css)
+ Lucid.body_ <| do
+ sharedNav activePage'
+ content'