summaryrefslogtreecommitdiff
path: root/Omni
diff options
context:
space:
mode:
authorBen Sima <ben@bensima.com>2025-11-26 13:03:17 -0500
committerBen Sima <ben@bensima.com>2025-11-26 13:03:17 -0500
commit555d5d416e2001d04145b173aa40fd40f656509d (patch)
tree5edcdb5e96f8b0ee59255153ddcb77faa09388aa /Omni
parent75d5716a31ea1d9d1e92d76d8417dd5ae8dcbab6 (diff)
Add mobile-first CSS styling
All checks pass. The Style.hs file was already implemented from a previo - Mobile-first CSS with Clay - Status badges with colored pills (Open=yellow, InProgress=blue, Review - Large touch targets (44px min height) - Single column layout on narrow screens (<600px) - Card-style sections with subtle shadows - Responsive navigation header - Dark mode support - Served at GET /style.css Task-Id: t-1o2g8gugkr1.9
Diffstat (limited to 'Omni')
-rw-r--r--Omni/Jr/Web.hs870
-rw-r--r--Omni/Jr/Web/Style.hs510
2 files changed, 805 insertions, 575 deletions
diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index c32c716..69162aa 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -9,6 +9,7 @@
-- : dep servant-lucid
-- : dep http-api-data
-- : dep process
+-- : dep clay
module Omni.Jr.Web
( run,
defaultPort,
@@ -18,8 +19,11 @@ where
import Alpha
import qualified Data.List as List
import qualified Data.Text as Text
+import qualified Data.Text.Lazy as LazyText
+import qualified Data.Text.Lazy.Encoding as LazyText
import qualified Lucid
import qualified Network.Wai.Handler.Warp as Warp
+import qualified Omni.Jr.Web.Style as Style
import qualified Omni.Task.Core as TaskCore
import Servant
import qualified Servant.HTML.Lucid as Lucid
@@ -41,6 +45,7 @@ data TaskFilters = TaskFilters
type API =
Get '[Lucid.HTML] HomePage
+ :<|> "style.css" :> Get '[CSS] LazyText.Text
:<|> "ready" :> Get '[Lucid.HTML] ReadyQueuePage
:<|> "tasks"
:> QueryParam "status" TaskCore.Status
@@ -53,6 +58,14 @@ type API =
:<|> "tasks" :> Capture "id" Text :> "accept" :> PostRedirect
:<|> "tasks" :> Capture "id" Text :> "reject" :> ReqBody '[FormUrlEncoded] RejectForm :> PostRedirect
+data CSS
+
+instance Accept CSS where
+ contentType _ = "text/css"
+
+instance MimeRender CSS LazyText.Text where
+ mimeRender _ = LazyText.encodeUtf8
+
data HomePage = HomePage TaskCore.TaskStats [TaskCore.Task] [TaskCore.Task]
newtype ReadyQueuePage = ReadyQueuePage [TaskCore.Task]
@@ -86,49 +99,77 @@ instance FromForm StatusForm where
Just s -> Right (StatusForm s)
Nothing -> Left "Invalid status"
+pageHead :: (Monad m) => Text -> Lucid.HtmlT m ()
+pageHead title =
+ Lucid.head_ <| do
+ Lucid.title_ (Lucid.toHtml title)
+ Lucid.meta_ [Lucid.charset_ "utf-8"]
+ Lucid.meta_
+ [ Lucid.name_ "viewport",
+ Lucid.content_ "width=device-width, initial-scale=1"
+ ]
+ Lucid.link_ [Lucid.rel_ "stylesheet", Lucid.href_ "/style.css"]
+
+statusBadge :: (Monad m) => TaskCore.Status -> Lucid.HtmlT m ()
+statusBadge status =
+ let (cls, label) = case status of
+ TaskCore.Open -> ("badge badge-open", "Open")
+ TaskCore.InProgress -> ("badge badge-inprogress", "In Progress")
+ TaskCore.Review -> ("badge badge-review", "Review")
+ TaskCore.Approved -> ("badge badge-approved", "Approved")
+ TaskCore.Done -> ("badge badge-done", "Done")
+ in Lucid.span_ [Lucid.class_ cls] label
+
+renderTaskCard :: (Monad m) => TaskCore.Task -> Lucid.HtmlT m ()
+renderTaskCard t =
+ Lucid.div_ [Lucid.class_ "task-card"] <| do
+ Lucid.div_ [Lucid.class_ "task-header"] <| do
+ Lucid.a_
+ [ Lucid.class_ "task-id",
+ Lucid.href_ ("/tasks/" <> TaskCore.taskId t)
+ ]
+ (Lucid.toHtml (TaskCore.taskId t))
+ statusBadge (TaskCore.taskStatus t)
+ Lucid.span_ [Lucid.class_ "priority"] (Lucid.toHtml (tshow (TaskCore.taskPriority t)))
+ Lucid.p_ [Lucid.class_ "task-title"] (Lucid.toHtml (TaskCore.taskTitle t))
+
instance Lucid.ToHtml HomePage where
toHtmlRaw = Lucid.toHtml
toHtml (HomePage stats readyTasks recentTasks) =
Lucid.doctypehtml_ <| do
- Lucid.head_ <| do
- Lucid.title_ "Jr Dashboard"
- Lucid.meta_ [Lucid.charset_ "utf-8"]
- Lucid.meta_
- [ Lucid.name_ "viewport",
- Lucid.content_ "width=device-width, initial-scale=1"
- ]
- Lucid.style_ homeStyles
+ pageHead "Jr Dashboard"
Lucid.body_ <| do
- Lucid.h1_ "Jr Dashboard"
-
- Lucid.div_ [Lucid.class_ "actions"] <| do
- Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "action-btn"] "View All Tasks"
- Lucid.a_ [Lucid.href_ "/ready", Lucid.class_ "action-btn action-btn-primary"] "View Ready Queue"
-
- Lucid.h2_ "Task Status"
- Lucid.div_ [Lucid.class_ "stats-grid"] <| do
- statCard "Open" (TaskCore.openTasks stats) "badge-open"
- statCard "In Progress" (TaskCore.inProgressTasks stats) "badge-inprogress"
- statCard "Review" (TaskCore.reviewTasks stats) "badge-review"
- statCard "Approved" (TaskCore.approvedTasks stats) "badge-approved"
- statCard "Done" (TaskCore.doneTasks stats) "badge-done"
-
- Lucid.h2_ <| do
- "Ready Queue "
- Lucid.a_ [Lucid.href_ "/ready", Lucid.class_ "ready-link"]
- <| Lucid.toHtml ("(" <> tshow (length readyTasks) <> " tasks)")
- if null readyTasks
- then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks ready for work."
- else
- Lucid.div_ [Lucid.class_ "task-list"]
- <| traverse_ renderTaskCard (take 5 readyTasks)
-
- Lucid.h2_ "Recent Activity"
- if null recentTasks
- then Lucid.p_ [Lucid.class_ "empty-msg"] "No recent tasks."
- else
- Lucid.div_ [Lucid.class_ "task-list"]
- <| traverse_ renderTaskCard recentTasks
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.h1_ "Jr Dashboard"
+
+ Lucid.div_ [Lucid.class_ "actions"] <| do
+ Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "action-btn"] "View All Tasks"
+ Lucid.a_ [Lucid.href_ "/ready", Lucid.class_ "action-btn action-btn-primary"] "View Ready Queue"
+
+ Lucid.h2_ "Task Status"
+ Lucid.div_ [Lucid.class_ "stats-grid"] <| do
+ statCard "Open" (TaskCore.openTasks stats) "badge-open"
+ statCard "In Progress" (TaskCore.inProgressTasks stats) "badge-inprogress"
+ statCard "Review" (TaskCore.reviewTasks stats) "badge-review"
+ statCard "Approved" (TaskCore.approvedTasks stats) "badge-approved"
+ statCard "Done" (TaskCore.doneTasks stats) "badge-done"
+
+ Lucid.h2_ <| do
+ "Ready Queue "
+ Lucid.a_ [Lucid.href_ "/ready", Lucid.class_ "ready-link"]
+ <| Lucid.toHtml ("(" <> tshow (length readyTasks) <> " tasks)")
+ if null readyTasks
+ then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks ready for work."
+ else
+ Lucid.div_ [Lucid.class_ "task-list"]
+ <| traverse_ renderTaskCard (take 5 readyTasks)
+
+ Lucid.h2_ "Recent Activity"
+ if null recentTasks
+ then Lucid.p_ [Lucid.class_ "empty-msg"] "No recent tasks."
+ else
+ Lucid.div_ [Lucid.class_ "task-list"]
+ <| traverse_ renderTaskCard recentTasks
where
statCard :: (Monad m) => Text -> Int -> Text -> Lucid.HtmlT m ()
statCard label count badgeClass =
@@ -136,202 +177,69 @@ instance Lucid.ToHtml HomePage where
Lucid.div_ [Lucid.class_ "stat-count"] (Lucid.toHtml (tshow count))
Lucid.div_ [Lucid.class_ "stat-label"] (Lucid.toHtml label)
- renderTaskCard :: (Monad m) => TaskCore.Task -> Lucid.HtmlT m ()
- renderTaskCard t =
- Lucid.div_ [Lucid.class_ "task-card"] <| do
- Lucid.div_ [Lucid.class_ "task-header"] <| do
- Lucid.a_
- [ Lucid.class_ "task-id",
- Lucid.href_ ("/tasks/" <> TaskCore.taskId t)
- ]
- (Lucid.toHtml (TaskCore.taskId t))
- statusBadge (TaskCore.taskStatus t)
- Lucid.span_ [Lucid.class_ "priority"] (Lucid.toHtml (tshow (TaskCore.taskPriority t)))
- Lucid.p_ [Lucid.class_ "task-title"] (Lucid.toHtml (TaskCore.taskTitle t))
-
- statusBadge :: (Monad m) => TaskCore.Status -> Lucid.HtmlT m ()
- statusBadge status =
- let (cls, label) = case status of
- TaskCore.Open -> ("badge badge-open", "Open")
- TaskCore.InProgress -> ("badge badge-inprogress", "In Progress")
- TaskCore.Review -> ("badge badge-review", "Review")
- TaskCore.Approved -> ("badge badge-approved", "Approved")
- TaskCore.Done -> ("badge badge-done", "Done")
- in Lucid.span_ [Lucid.class_ cls] label
-
-homeStyles :: Text
-homeStyles =
- "* { box-sizing: border-box; } \
- \body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; \
- \ margin: 0; padding: 16px; background: #f5f5f5; max-width: 900px; } \
- \h1 { margin: 0 0 16px 0; } \
- \h2 { margin: 24px 0 12px 0; color: #374151; font-size: 18px; } \
- \.actions { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; } \
- \.action-btn { display: inline-block; padding: 10px 16px; background: white; \
- \ border: 1px solid #d1d5db; border-radius: 6px; color: #374151; \
- \ text-decoration: none; font-size: 14px; font-weight: 500; } \
- \.action-btn:hover { background: #f9fafb; } \
- \.action-btn-primary { background: #0066cc; color: white; border-color: #0066cc; } \
- \.action-btn-primary:hover { background: #0052a3; } \
- \.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 12px; } \
- \.stat-card { background: white; border-radius: 8px; padding: 16px; text-align: center; \
- \ box-shadow: 0 1px 3px rgba(0,0,0,0.1); } \
- \.stat-count { font-size: 28px; font-weight: 700; } \
- \.stat-label { font-size: 12px; color: #6b7280; margin-top: 4px; } \
- \.stat-card.badge-open { border-left: 4px solid #f59e0b; } \
- \.stat-card.badge-open .stat-count { color: #92400e; } \
- \.stat-card.badge-inprogress { border-left: 4px solid #3b82f6; } \
- \.stat-card.badge-inprogress .stat-count { color: #1e40af; } \
- \.stat-card.badge-review { border-left: 4px solid #8b5cf6; } \
- \.stat-card.badge-review .stat-count { color: #6b21a8; } \
- \.stat-card.badge-approved { border-left: 4px solid #10b981; } \
- \.stat-card.badge-approved .stat-count { color: #065f46; } \
- \.stat-card.badge-done { border-left: 4px solid #10b981; } \
- \.stat-card.badge-done .stat-count { color: #065f46; } \
- \.ready-link { font-size: 14px; color: #0066cc; text-decoration: none; } \
- \.ready-link:hover { text-decoration: underline; } \
- \.empty-msg { color: #6b7280; font-style: italic; } \
- \.task-list { display: flex; flex-direction: column; gap: 8px; } \
- \.task-card { background: white; border-radius: 8px; padding: 16px; \
- \ box-shadow: 0 1px 3px rgba(0,0,0,0.1); } \
- \.task-header { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 8px; } \
- \.task-id { font-family: monospace; color: #0066cc; text-decoration: none; \
- \ font-size: 14px; padding: 4px 0; } \
- \.task-id:hover { text-decoration: underline; } \
- \.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; \
- \ font-size: 12px; font-weight: 500; } \
- \.badge-open { background: #fef3c7; color: #92400e; } \
- \.badge-inprogress { background: #dbeafe; color: #1e40af; } \
- \.badge-review { background: #ede9fe; color: #6b21a8; } \
- \.badge-approved { background: #d1fae5; color: #065f46; } \
- \.badge-done { background: #d1fae5; color: #065f46; } \
- \.priority { font-size: 12px; color: #6b7280; } \
- \.task-title { font-size: 16px; margin: 0; }"
-
instance Lucid.ToHtml ReadyQueuePage where
toHtmlRaw = Lucid.toHtml
toHtml (ReadyQueuePage tasks) =
Lucid.doctypehtml_ <| do
- Lucid.head_ <| do
- Lucid.title_ "Ready Queue - Jr Web UI"
- Lucid.meta_ [Lucid.charset_ "utf-8"]
- Lucid.meta_
- [ Lucid.name_ "viewport",
- Lucid.content_ "width=device-width, initial-scale=1"
- ]
- Lucid.style_ readyStyles
+ pageHead "Ready Queue - Jr"
Lucid.body_ <| do
- Lucid.p_ <| Lucid.a_ [Lucid.href_ "/"] "← Back to Dashboard"
- Lucid.h1_ <| Lucid.toHtml ("Ready Queue (" <> tshow (length tasks) <> " tasks)")
- if null tasks
- then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks are ready for work."
- else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderTask tasks
- where
- readyStyles :: Text
- readyStyles =
- "* { box-sizing: border-box; } \
- \body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; \
- \ margin: 0; padding: 16px; background: #f5f5f5; max-width: 900px; } \
- \h1 { margin: 16px 0; } \
- \.count-badge { background: #0066cc; color: white; padding: 4px 10px; \
- \ border-radius: 12px; font-size: 14px; vertical-align: middle; } \
- \.empty-msg { color: #6b7280; font-style: italic; } \
- \.task-list { display: flex; flex-direction: column; gap: 8px; } \
- \.task-card { background: white; border-radius: 8px; padding: 16px; \
- \ box-shadow: 0 1px 3px rgba(0,0,0,0.1); } \
- \.task-header { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 8px; } \
- \.task-id { font-family: monospace; color: #0066cc; text-decoration: none; \
- \ font-size: 14px; padding: 4px 0; } \
- \.task-id:hover { text-decoration: underline; } \
- \.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; \
- \ font-size: 12px; font-weight: 500; } \
- \.badge-open { background: #fef3c7; color: #92400e; } \
- \.badge-inprogress { background: #dbeafe; color: #1e40af; } \
- \.badge-review { background: #ede9fe; color: #6b21a8; } \
- \.badge-approved { background: #d1fae5; color: #065f46; } \
- \.badge-done { background: #d1fae5; color: #065f46; } \
- \.priority { font-size: 12px; color: #6b7280; } \
- \.task-title { font-size: 16px; margin: 0; }"
-
- renderTask :: (Monad m) => TaskCore.Task -> Lucid.HtmlT m ()
- renderTask t =
- Lucid.div_ [Lucid.class_ "task-card"] <| do
- Lucid.div_ [Lucid.class_ "task-header"] <| do
- Lucid.a_
- [ Lucid.class_ "task-id",
- Lucid.href_ ("/tasks/" <> TaskCore.taskId t)
- ]
- (Lucid.toHtml (TaskCore.taskId t))
- statusBadge (TaskCore.taskStatus t)
- Lucid.span_ [Lucid.class_ "priority"] (Lucid.toHtml (tshow (TaskCore.taskPriority t)))
- Lucid.p_ [Lucid.class_ "task-title"] (Lucid.toHtml (TaskCore.taskTitle t))
-
- statusBadge :: (Monad m) => TaskCore.Status -> Lucid.HtmlT m ()
- statusBadge status =
- let (cls, label) = case status of
- TaskCore.Open -> ("badge badge-open", "Open")
- TaskCore.InProgress -> ("badge badge-inprogress", "In Progress")
- TaskCore.Review -> ("badge badge-review", "Review")
- TaskCore.Approved -> ("badge badge-approved", "Approved")
- TaskCore.Done -> ("badge badge-done", "Done")
- in Lucid.span_ [Lucid.class_ cls] label
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.p_ [Lucid.class_ "back-link"] <| Lucid.a_ [Lucid.href_ "/"] "← Back to Dashboard"
+ Lucid.h1_ <| Lucid.toHtml ("Ready Queue (" <> tshow (length tasks) <> " tasks)")
+ if null tasks
+ then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks are ready for work."
+ else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderTaskCard tasks
instance Lucid.ToHtml TaskListPage where
toHtmlRaw = Lucid.toHtml
toHtml (TaskListPage tasks filters) =
Lucid.doctypehtml_ <| do
- Lucid.head_ <| do
- Lucid.title_ "Tasks - Jr Web UI"
- Lucid.meta_ [Lucid.charset_ "utf-8"]
- Lucid.meta_
- [ Lucid.name_ "viewport",
- Lucid.content_ "width=device-width, initial-scale=1"
- ]
- Lucid.style_ styles
+ pageHead "Tasks - Jr"
Lucid.body_ <| do
- Lucid.p_ <| Lucid.a_ [Lucid.href_ "/"] "← Back to Dashboard"
- Lucid.h1_ <| Lucid.toHtml ("Tasks (" <> tshow (length tasks) <> ")")
-
- Lucid.div_ [Lucid.class_ "filter-form"] <| do
- Lucid.form_ [Lucid.method_ "GET", Lucid.action_ "/tasks"] <| do
- Lucid.div_ [Lucid.class_ "filter-row"] <| do
- Lucid.div_ [Lucid.class_ "filter-group"] <| do
- Lucid.label_ [Lucid.for_ "status"] "Status:"
- Lucid.select_ [Lucid.name_ "status", Lucid.id_ "status", Lucid.class_ "filter-select"] <| do
- Lucid.option_ ([Lucid.value_ ""] <> maybeSelected Nothing (filterStatus filters)) "All"
- statusFilterOption TaskCore.Open (filterStatus filters)
- statusFilterOption TaskCore.InProgress (filterStatus filters)
- statusFilterOption TaskCore.Review (filterStatus filters)
- statusFilterOption TaskCore.Approved (filterStatus filters)
- statusFilterOption TaskCore.Done (filterStatus filters)
-
- Lucid.div_ [Lucid.class_ "filter-group"] <| do
- Lucid.label_ [Lucid.for_ "priority"] "Priority:"
- Lucid.select_ [Lucid.name_ "priority", Lucid.id_ "priority", Lucid.class_ "filter-select"] <| do
- Lucid.option_ ([Lucid.value_ ""] <> maybeSelected Nothing (filterPriority filters)) "All"
- priorityFilterOption TaskCore.P0 (filterPriority filters)
- priorityFilterOption TaskCore.P1 (filterPriority filters)
- priorityFilterOption TaskCore.P2 (filterPriority filters)
- priorityFilterOption TaskCore.P3 (filterPriority filters)
- priorityFilterOption TaskCore.P4 (filterPriority filters)
-
- Lucid.div_ [Lucid.class_ "filter-group"] <| do
- Lucid.label_ [Lucid.for_ "namespace"] "Namespace:"
- Lucid.input_
- [ Lucid.type_ "text",
- Lucid.name_ "namespace",
- Lucid.id_ "namespace",
- Lucid.class_ "filter-input",
- Lucid.placeholder_ "e.g. Omni/Jr",
- Lucid.value_ (fromMaybe "" (filterNamespace filters))
- ]
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.p_ [Lucid.class_ "back-link"] <| Lucid.a_ [Lucid.href_ "/"] "← Back to Dashboard"
+ Lucid.h1_ <| Lucid.toHtml ("Tasks (" <> tshow (length tasks) <> ")")
+
+ Lucid.div_ [Lucid.class_ "filter-form"] <| do
+ Lucid.form_ [Lucid.method_ "GET", Lucid.action_ "/tasks"] <| do
+ Lucid.div_ [Lucid.class_ "filter-row"] <| do
+ Lucid.div_ [Lucid.class_ "filter-group"] <| do
+ Lucid.label_ [Lucid.for_ "status"] "Status:"
+ Lucid.select_ [Lucid.name_ "status", Lucid.id_ "status", Lucid.class_ "filter-select"] <| do
+ Lucid.option_ ([Lucid.value_ ""] <> maybeSelected Nothing (filterStatus filters)) "All"
+ statusFilterOption TaskCore.Open (filterStatus filters)
+ statusFilterOption TaskCore.InProgress (filterStatus filters)
+ statusFilterOption TaskCore.Review (filterStatus filters)
+ statusFilterOption TaskCore.Approved (filterStatus filters)
+ statusFilterOption TaskCore.Done (filterStatus filters)
+
+ Lucid.div_ [Lucid.class_ "filter-group"] <| do
+ Lucid.label_ [Lucid.for_ "priority"] "Priority:"
+ Lucid.select_ [Lucid.name_ "priority", Lucid.id_ "priority", Lucid.class_ "filter-select"] <| do
+ Lucid.option_ ([Lucid.value_ ""] <> maybeSelected Nothing (filterPriority filters)) "All"
+ priorityFilterOption TaskCore.P0 (filterPriority filters)
+ priorityFilterOption TaskCore.P1 (filterPriority filters)
+ priorityFilterOption TaskCore.P2 (filterPriority filters)
+ priorityFilterOption TaskCore.P3 (filterPriority filters)
+ priorityFilterOption TaskCore.P4 (filterPriority filters)
+
+ Lucid.div_ [Lucid.class_ "filter-group"] <| do
+ Lucid.label_ [Lucid.for_ "namespace"] "Namespace:"
+ Lucid.input_
+ [ Lucid.type_ "text",
+ Lucid.name_ "namespace",
+ Lucid.id_ "namespace",
+ Lucid.class_ "filter-input",
+ Lucid.placeholder_ "e.g. Omni/Jr",
+ Lucid.value_ (fromMaybe "" (filterNamespace filters))
+ ]
- Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "filter-btn"] "Filter"
- Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "clear-btn"] "Clear"
+ Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "filter-btn"] "Filter"
+ Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "clear-btn"] "Clear"
- if null tasks
- then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks match the current filters."
- else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderTask tasks
+ if null tasks
+ then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks match the current filters."
+ else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderTaskCard tasks
where
maybeSelected :: (Eq a) => Maybe a -> Maybe a -> [Lucid.Attribute]
maybeSelected opt current = [Lucid.selected_ "selected" | opt == current]
@@ -346,181 +254,108 @@ instance Lucid.ToHtml TaskListPage where
let attrs = [Lucid.value_ (tshow p)] <> [Lucid.selected_ "selected" | Just p == current]
in Lucid.option_ attrs (Lucid.toHtml (tshow p))
- styles :: Text
- styles =
- "* { box-sizing: border-box; } \
- \body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; \
- \ margin: 0; padding: 16px; background: #f5f5f5; max-width: 900px; } \
- \h1 { margin: 16px 0; } \
- \.filter-form { background: white; border-radius: 8px; padding: 16px; \
- \ box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 16px; } \
- \.filter-row { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; } \
- \.filter-group { display: flex; flex-direction: column; gap: 4px; } \
- \.filter-group label { font-size: 12px; color: #6b7280; font-weight: 500; } \
- \.filter-select, .filter-input { padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 4px; \
- \ font-size: 14px; min-width: 120px; } \
- \.filter-input { min-width: 150px; } \
- \.filter-btn { padding: 8px 16px; background: #0066cc; color: white; border: none; \
- \ border-radius: 4px; font-size: 14px; cursor: pointer; } \
- \.filter-btn:hover { background: #0052a3; } \
- \.clear-btn { padding: 8px 16px; background: #6b7280; color: white; border: none; \
- \ border-radius: 4px; font-size: 14px; cursor: pointer; text-decoration: none; } \
- \.clear-btn:hover { background: #4b5563; } \
- \.empty-msg { color: #6b7280; font-style: italic; } \
- \.task-list { display: flex; flex-direction: column; gap: 8px; } \
- \.task-card { background: white; border-radius: 8px; padding: 16px; \
- \ box-shadow: 0 1px 3px rgba(0,0,0,0.1); } \
- \.task-header { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 8px; } \
- \.task-id { font-family: monospace; color: #0066cc; text-decoration: none; \
- \ font-size: 14px; padding: 4px 0; } \
- \.task-id:hover { text-decoration: underline; } \
- \.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; \
- \ font-size: 12px; font-weight: 500; } \
- \.badge-open { background: #fef3c7; color: #92400e; } \
- \.badge-inprogress { background: #dbeafe; color: #1e40af; } \
- \.badge-review { background: #ede9fe; color: #6b21a8; } \
- \.badge-approved { background: #d1fae5; color: #065f46; } \
- \.badge-done { background: #d1fae5; color: #065f46; } \
- \.priority { font-size: 12px; color: #6b7280; } \
- \.task-title { font-size: 16px; margin: 0; }"
-
- renderTask :: (Monad m) => TaskCore.Task -> Lucid.HtmlT m ()
- renderTask t =
- Lucid.div_ [Lucid.class_ "task-card"] <| do
- Lucid.div_ [Lucid.class_ "task-header"] <| do
- Lucid.a_
- [ Lucid.class_ "task-id",
- Lucid.href_ ("/tasks/" <> TaskCore.taskId t)
- ]
- (Lucid.toHtml (TaskCore.taskId t))
- statusBadge (TaskCore.taskStatus t)
- Lucid.span_ [Lucid.class_ "priority"] (Lucid.toHtml (tshow (TaskCore.taskPriority t)))
- Lucid.p_ [Lucid.class_ "task-title"] (Lucid.toHtml (TaskCore.taskTitle t))
-
- statusBadge :: (Monad m) => TaskCore.Status -> Lucid.HtmlT m ()
- statusBadge status =
- let (cls, label) = case status of
- TaskCore.Open -> ("badge badge-open", "Open")
- TaskCore.InProgress -> ("badge badge-inprogress", "In Progress")
- TaskCore.Review -> ("badge badge-review", "Review")
- TaskCore.Approved -> ("badge badge-approved", "Approved")
- TaskCore.Done -> ("badge badge-done", "Done")
- in Lucid.span_ [Lucid.class_ cls] label
-
instance Lucid.ToHtml TaskDetailPage where
toHtmlRaw = Lucid.toHtml
toHtml (TaskDetailNotFound tid) =
Lucid.doctypehtml_ <| do
- Lucid.head_ <| do
- Lucid.title_ "Task Not Found - Jr Web UI"
- Lucid.meta_ [Lucid.charset_ "utf-8"]
- Lucid.meta_
- [ Lucid.name_ "viewport",
- Lucid.content_ "width=device-width, initial-scale=1"
- ]
- Lucid.style_ detailStyles
+ pageHead "Task Not Found - Jr"
Lucid.body_ <| do
- Lucid.h1_ "Task Not Found"
- Lucid.p_ <| do
- "The task "
- Lucid.code_ (Lucid.toHtml tid)
- " could not be found."
- Lucid.p_ <| Lucid.a_ [Lucid.href_ "/tasks"] "← Back to Tasks"
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.h1_ "Task Not Found"
+ Lucid.p_ <| do
+ "The task "
+ Lucid.code_ (Lucid.toHtml tid)
+ " could not be found."
+ Lucid.p_ [Lucid.class_ "back-link"] <| Lucid.a_ [Lucid.href_ "/tasks"] "← Back to Tasks"
toHtml (TaskDetailFound task allTasks) =
Lucid.doctypehtml_ <| do
- Lucid.head_ <| do
- Lucid.title_ <| Lucid.toHtml (TaskCore.taskId task <> " - Jr Web UI")
- Lucid.meta_ [Lucid.charset_ "utf-8"]
- Lucid.meta_
- [ Lucid.name_ "viewport",
- Lucid.content_ "width=device-width, initial-scale=1"
- ]
- Lucid.style_ detailStyles
+ pageHead (TaskCore.taskId task <> " - Jr")
Lucid.body_ <| do
- Lucid.p_ <| Lucid.a_ [Lucid.href_ "/tasks"] "← Back to Tasks"
-
- Lucid.h1_ <| Lucid.toHtml (TaskCore.taskTitle task)
-
- Lucid.div_ [Lucid.class_ "task-detail"] <| do
- Lucid.div_ [Lucid.class_ "detail-row"] <| do
- Lucid.span_ [Lucid.class_ "detail-label"] "ID:"
- Lucid.code_ [Lucid.class_ "detail-value"] (Lucid.toHtml (TaskCore.taskId task))
-
- Lucid.div_ [Lucid.class_ "detail-row"] <| do
- Lucid.span_ [Lucid.class_ "detail-label"] "Type:"
- Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.taskType task)))
-
- Lucid.div_ [Lucid.class_ "detail-row"] <| do
- Lucid.span_ [Lucid.class_ "detail-label"] "Status:"
- Lucid.span_ [Lucid.class_ "detail-value"] <| statusBadge (TaskCore.taskStatus task)
-
- Lucid.div_ [Lucid.class_ "detail-row"] <| do
- Lucid.span_ [Lucid.class_ "detail-label"] "Priority:"
- Lucid.span_ [Lucid.class_ "detail-value"] <| do
- Lucid.toHtml (tshow (TaskCore.taskPriority task))
- Lucid.span_ [Lucid.class_ "priority-desc"] (Lucid.toHtml (priorityDesc (TaskCore.taskPriority task)))
-
- case TaskCore.taskNamespace task of
- Nothing -> pure ()
- Just ns ->
- Lucid.div_ [Lucid.class_ "detail-row"] <| do
- Lucid.span_ [Lucid.class_ "detail-label"] "Namespace:"
- Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml ns)
-
- case TaskCore.taskParent task of
- Nothing -> pure ()
- Just pid ->
- Lucid.div_ [Lucid.class_ "detail-row"] <| do
- Lucid.span_ [Lucid.class_ "detail-label"] "Parent:"
- Lucid.a_ [Lucid.href_ ("/tasks/" <> pid), Lucid.class_ "detail-value task-link"] (Lucid.toHtml pid)
-
- Lucid.div_ [Lucid.class_ "detail-row"] <| do
- Lucid.span_ [Lucid.class_ "detail-label"] "Created:"
- Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.taskCreatedAt task)))
-
- Lucid.div_ [Lucid.class_ "detail-row"] <| do
- Lucid.span_ [Lucid.class_ "detail-label"] "Updated:"
- Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.taskUpdatedAt task)))
-
- let deps = TaskCore.taskDependencies task
- unless (null deps) <| do
- Lucid.div_ [Lucid.class_ "detail-section"] <| do
- Lucid.h3_ "Dependencies"
- Lucid.ul_ [Lucid.class_ "dep-list"] <| do
- traverse_ renderDependency deps
-
- case TaskCore.taskDescription task of
- Nothing -> pure ()
- Just desc ->
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.p_ [Lucid.class_ "back-link"] <| Lucid.a_ [Lucid.href_ "/tasks"] "← Back to Tasks"
+
+ Lucid.h1_ <| Lucid.toHtml (TaskCore.taskTitle task)
+
+ Lucid.div_ [Lucid.class_ "task-detail"] <| do
+ Lucid.div_ [Lucid.class_ "detail-row"] <| do
+ Lucid.span_ [Lucid.class_ "detail-label"] "ID:"
+ Lucid.code_ [Lucid.class_ "detail-value"] (Lucid.toHtml (TaskCore.taskId task))
+
+ Lucid.div_ [Lucid.class_ "detail-row"] <| do
+ Lucid.span_ [Lucid.class_ "detail-label"] "Type:"
+ Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.taskType task)))
+
+ Lucid.div_ [Lucid.class_ "detail-row"] <| do
+ Lucid.span_ [Lucid.class_ "detail-label"] "Status:"
+ Lucid.span_ [Lucid.class_ "detail-value"] <| statusBadge (TaskCore.taskStatus task)
+
+ Lucid.div_ [Lucid.class_ "detail-row"] <| do
+ Lucid.span_ [Lucid.class_ "detail-label"] "Priority:"
+ Lucid.span_ [Lucid.class_ "detail-value"] <| do
+ Lucid.toHtml (tshow (TaskCore.taskPriority task))
+ Lucid.span_ [Lucid.class_ "priority-desc"] (Lucid.toHtml (priorityDesc (TaskCore.taskPriority task)))
+
+ case TaskCore.taskNamespace task of
+ Nothing -> pure ()
+ Just ns ->
+ Lucid.div_ [Lucid.class_ "detail-row"] <| do
+ Lucid.span_ [Lucid.class_ "detail-label"] "Namespace:"
+ Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml ns)
+
+ case TaskCore.taskParent task of
+ Nothing -> pure ()
+ Just pid ->
+ Lucid.div_ [Lucid.class_ "detail-row"] <| do
+ Lucid.span_ [Lucid.class_ "detail-label"] "Parent:"
+ Lucid.a_ [Lucid.href_ ("/tasks/" <> pid), Lucid.class_ "detail-value task-link"] (Lucid.toHtml pid)
+
+ Lucid.div_ [Lucid.class_ "detail-row"] <| do
+ Lucid.span_ [Lucid.class_ "detail-label"] "Created:"
+ Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.taskCreatedAt task)))
+
+ Lucid.div_ [Lucid.class_ "detail-row"] <| do
+ Lucid.span_ [Lucid.class_ "detail-label"] "Updated:"
+ Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.taskUpdatedAt task)))
+
+ let deps = TaskCore.taskDependencies task
+ unless (null deps) <| do
+ Lucid.div_ [Lucid.class_ "detail-section"] <| do
+ Lucid.h3_ "Dependencies"
+ Lucid.ul_ [Lucid.class_ "dep-list"] <| do
+ traverse_ renderDependency deps
+
+ case TaskCore.taskDescription task of
+ Nothing -> pure ()
+ Just desc ->
+ Lucid.div_ [Lucid.class_ "detail-section"] <| do
+ Lucid.h3_ "Description"
+ Lucid.pre_ [Lucid.class_ "description"] (Lucid.toHtml desc)
+
+ let children = filter (maybe False (TaskCore.matchesId (TaskCore.taskId task)) <. TaskCore.taskParent) allTasks
+ unless (null children) <| do
Lucid.div_ [Lucid.class_ "detail-section"] <| do
- Lucid.h3_ "Description"
- Lucid.pre_ [Lucid.class_ "description"] (Lucid.toHtml desc)
-
- let children = filter (maybe False (TaskCore.matchesId (TaskCore.taskId task)) <. TaskCore.taskParent) allTasks
- unless (null children) <| do
- Lucid.div_ [Lucid.class_ "detail-section"] <| do
- Lucid.h3_ "Child Tasks"
- Lucid.ul_ [Lucid.class_ "child-list"] <| do
- traverse_ renderChild children
-
- when (TaskCore.taskStatus task == TaskCore.Review) <| do
- Lucid.div_ [Lucid.class_ "review-link-section"] <| do
- Lucid.a_
- [ Lucid.href_ ("/tasks/" <> TaskCore.taskId task <> "/review"),
- Lucid.class_ "review-link-btn"
- ]
- "Review This Task"
-
- Lucid.div_ [Lucid.class_ "status-form"] <| do
- Lucid.h3_ "Update Status"
- Lucid.form_ [Lucid.method_ "POST", Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/status")] <| do
- Lucid.select_ [Lucid.name_ "status", Lucid.class_ "status-select"] <| do
- statusOption TaskCore.Open (TaskCore.taskStatus task)
- statusOption TaskCore.InProgress (TaskCore.taskStatus task)
- statusOption TaskCore.Review (TaskCore.taskStatus task)
- statusOption TaskCore.Approved (TaskCore.taskStatus task)
- statusOption TaskCore.Done (TaskCore.taskStatus task)
- Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "submit-btn"] "Submit"
+ Lucid.h3_ "Child Tasks"
+ Lucid.ul_ [Lucid.class_ "child-list"] <| do
+ traverse_ renderChild children
+
+ when (TaskCore.taskStatus task == TaskCore.Review) <| do
+ Lucid.div_ [Lucid.class_ "review-link-section"] <| do
+ Lucid.a_
+ [ Lucid.href_ ("/tasks/" <> TaskCore.taskId task <> "/review"),
+ Lucid.class_ "review-link-btn"
+ ]
+ "Review This Task"
+
+ Lucid.div_ [Lucid.class_ "status-form"] <| do
+ Lucid.h3_ "Update Status"
+ Lucid.form_ [Lucid.method_ "POST", Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/status")] <| do
+ Lucid.select_ [Lucid.name_ "status", Lucid.class_ "status-select"] <| do
+ statusOption TaskCore.Open (TaskCore.taskStatus task)
+ statusOption TaskCore.InProgress (TaskCore.taskStatus task)
+ statusOption TaskCore.Review (TaskCore.taskStatus task)
+ statusOption TaskCore.Approved (TaskCore.taskStatus task)
+ statusOption TaskCore.Done (TaskCore.taskStatus task)
+ Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "submit-btn"] "Submit"
where
priorityDesc :: TaskCore.Priority -> Text
priorityDesc p = case p of
@@ -530,19 +365,9 @@ instance Lucid.ToHtml TaskDetailPage where
TaskCore.P3 -> " (Low)"
TaskCore.P4 -> " (Backlog)"
- statusBadge :: (Monad m) => TaskCore.Status -> Lucid.HtmlT m ()
- statusBadge status =
- let (cls, label) = case status of
- TaskCore.Open -> ("badge badge-open", "Open")
- TaskCore.InProgress -> ("badge badge-inprogress", "In Progress")
- TaskCore.Review -> ("badge badge-review", "Review")
- TaskCore.Approved -> ("badge badge-approved", "Approved")
- TaskCore.Done -> ("badge badge-done", "Done")
- in Lucid.span_ [Lucid.class_ cls] label
-
statusOption :: (Monad m) => TaskCore.Status -> TaskCore.Status -> Lucid.HtmlT m ()
statusOption opt current =
- let attrs = if opt == current then [Lucid.value_ (tshow opt), Lucid.selected_ "selected"] else [Lucid.value_ (tshow opt)]
+ let attrs = [Lucid.value_ (tshow opt)] <> [Lucid.selected_ "selected" | opt == current]
in Lucid.option_ attrs (Lucid.toHtml (tshow opt))
renderDependency :: (Monad m) => TaskCore.Dependency -> Lucid.HtmlT m ()
@@ -558,192 +383,83 @@ instance Lucid.ToHtml TaskDetailPage where
Lucid.span_ [Lucid.class_ "child-title"] <| Lucid.toHtml (" - " <> TaskCore.taskTitle child)
Lucid.span_ [Lucid.class_ "child-status"] <| Lucid.toHtml (" [" <> tshow (TaskCore.taskStatus child) <> "]")
-detailStyles :: Text
-detailStyles =
- "* { box-sizing: border-box; } \
- \body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; \
- \ margin: 0; padding: 16px; background: #f5f5f5; max-width: 800px; } \
- \h1 { margin: 16px 0; } \
- \h3 { margin: 16px 0 8px 0; color: #374151; } \
- \.task-detail { background: white; border-radius: 8px; padding: 16px; \
- \ box-shadow: 0 1px 3px rgba(0,0,0,0.1); } \
- \.detail-row { display: flex; padding: 8px 0; border-bottom: 1px solid #e5e7eb; } \
- \.detail-row:last-child { border-bottom: none; } \
- \.detail-label { font-weight: 600; width: 120px; color: #6b7280; } \
- \.detail-value { flex: 1; } \
- \.detail-section { margin-top: 16px; padding-top: 16px; border-top: 1px solid #e5e7eb; } \
- \.task-link { color: #0066cc; text-decoration: none; font-family: monospace; } \
- \.task-link:hover { text-decoration: underline; } \
- \.dep-list, .child-list { margin: 8px 0; padding-left: 20px; } \
- \.dep-list li, .child-list li { margin: 4px 0; } \
- \.dep-type { color: #6b7280; font-size: 14px; } \
- \.child-title { color: #374151; } \
- \.child-status { color: #6b7280; font-size: 14px; } \
- \.description { background: #f9fafb; padding: 12px; border-radius: 4px; \
- \ font-family: monospace; font-size: 14px; white-space: pre-wrap; margin: 0; } \
- \.priority-desc { color: #6b7280; margin-left: 4px; } \
- \.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; \
- \ font-size: 12px; font-weight: 500; } \
- \.badge-open { background: #fef3c7; color: #92400e; } \
- \.badge-inprogress { background: #dbeafe; color: #1e40af; } \
- \.badge-review { background: #ede9fe; color: #6b21a8; } \
- \.badge-approved { background: #d1fae5; color: #065f46; } \
- \.badge-done { background: #d1fae5; color: #065f46; } \
- \.status-form { margin-top: 24px; background: white; border-radius: 8px; padding: 16px; \
- \ box-shadow: 0 1px 3px rgba(0,0,0,0.1); } \
- \.status-select { padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 4px; \
- \ font-size: 14px; margin-right: 8px; } \
- \.submit-btn { padding: 8px 16px; background: #0066cc; color: white; border: none; \
- \ border-radius: 4px; font-size: 14px; cursor: pointer; } \
- \.submit-btn:hover { background: #0052a3; } \
- \.review-link-section { margin: 16px 0; } \
- \.review-link-btn { display: inline-block; padding: 12px 24px; background: #8b5cf6; \
- \ color: white; text-decoration: none; border-radius: 6px; \
- \ font-size: 16px; font-weight: 500; } \
- \.review-link-btn:hover { background: #7c3aed; }"
-
instance Lucid.ToHtml TaskReviewPage where
toHtmlRaw = Lucid.toHtml
toHtml (ReviewPageNotFound tid) =
Lucid.doctypehtml_ <| do
- Lucid.head_ <| do
- Lucid.title_ "Task Not Found - Jr Review"
- Lucid.meta_ [Lucid.charset_ "utf-8"]
- Lucid.meta_
- [ Lucid.name_ "viewport",
- Lucid.content_ "width=device-width, initial-scale=1"
- ]
- Lucid.style_ reviewStyles
+ pageHead "Task Not Found - Jr Review"
Lucid.body_ <| do
- Lucid.h1_ "Task Not Found"
- Lucid.p_ <| do
- "The task "
- Lucid.code_ (Lucid.toHtml tid)
- " could not be found."
- Lucid.p_ <| Lucid.a_ [Lucid.href_ "/tasks"] "<- Back to Tasks"
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.h1_ "Task Not Found"
+ Lucid.p_ <| do
+ "The task "
+ Lucid.code_ (Lucid.toHtml tid)
+ " could not be found."
+ Lucid.p_ [Lucid.class_ "back-link"] <| Lucid.a_ [Lucid.href_ "/tasks"] "← Back to Tasks"
toHtml (ReviewPageFound task reviewInfo) =
Lucid.doctypehtml_ <| do
- Lucid.head_ <| do
- Lucid.title_ <| Lucid.toHtml ("Review: " <> TaskCore.taskId task <> " - Jr")
- Lucid.meta_ [Lucid.charset_ "utf-8"]
- Lucid.meta_
- [ Lucid.name_ "viewport",
- Lucid.content_ "width=device-width, initial-scale=1"
- ]
- Lucid.style_ reviewStyles
+ pageHead ("Review: " <> TaskCore.taskId task <> " - Jr")
Lucid.body_ <| do
- Lucid.p_ <| Lucid.a_ [Lucid.href_ ("/tasks/" <> TaskCore.taskId task)] "<- Back to Task"
-
- Lucid.h1_ "Review Task"
-
- Lucid.div_ [Lucid.class_ "task-summary"] <| do
- Lucid.div_ [Lucid.class_ "detail-row"] <| do
- Lucid.span_ [Lucid.class_ "detail-label"] "ID:"
- Lucid.code_ [Lucid.class_ "detail-value"] (Lucid.toHtml (TaskCore.taskId task))
- Lucid.div_ [Lucid.class_ "detail-row"] <| do
- Lucid.span_ [Lucid.class_ "detail-label"] "Title:"
- Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (TaskCore.taskTitle task))
- Lucid.div_ [Lucid.class_ "detail-row"] <| do
- Lucid.span_ [Lucid.class_ "detail-label"] "Status:"
- Lucid.span_ [Lucid.class_ "detail-value"] <| statusBadge (TaskCore.taskStatus task)
-
- case reviewInfo of
- ReviewNoCommit -> do
- Lucid.div_ [Lucid.class_ "no-commit-msg"] <| do
- Lucid.h3_ "No Commit Found"
- Lucid.p_ "No commit with this task ID was found in the git history."
- Lucid.p_ "The worker may not have completed yet, or the commit message doesn't include the task ID."
- ReviewMergeConflict commitSha conflictFiles -> do
- Lucid.div_ [Lucid.class_ "conflict-warning"] <| do
- Lucid.h3_ "Merge Conflict Detected"
- Lucid.p_ <| do
- "Commit "
- Lucid.code_ (Lucid.toHtml (Text.take 8 commitSha))
- " cannot be cleanly merged."
- Lucid.p_ "Conflicting files:"
- Lucid.ul_ <| traverse_ (Lucid.li_ <. Lucid.toHtml) conflictFiles
- ReviewReady commitSha diffText -> do
- Lucid.div_ [Lucid.class_ "diff-section"] <| do
- Lucid.h3_ <| do
- "Commit: "
- Lucid.code_ (Lucid.toHtml (Text.take 8 commitSha))
- Lucid.pre_ [Lucid.class_ "diff-block"] (Lucid.toHtml diffText)
-
- Lucid.div_ [Lucid.class_ "review-actions"] <| do
- Lucid.form_
- [ Lucid.method_ "POST",
- Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/accept"),
- Lucid.class_ "inline-form"
- ]
- <| do
- Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "accept-btn"] "Accept"
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.p_ [Lucid.class_ "back-link"] <| Lucid.a_ [Lucid.href_ ("/tasks/" <> TaskCore.taskId task)] "← Back to Task"
+
+ Lucid.h1_ "Review Task"
+
+ Lucid.div_ [Lucid.class_ "task-summary"] <| do
+ Lucid.div_ [Lucid.class_ "detail-row"] <| do
+ Lucid.span_ [Lucid.class_ "detail-label"] "ID:"
+ Lucid.code_ [Lucid.class_ "detail-value"] (Lucid.toHtml (TaskCore.taskId task))
+ Lucid.div_ [Lucid.class_ "detail-row"] <| do
+ Lucid.span_ [Lucid.class_ "detail-label"] "Title:"
+ Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (TaskCore.taskTitle task))
+ Lucid.div_ [Lucid.class_ "detail-row"] <| do
+ Lucid.span_ [Lucid.class_ "detail-label"] "Status:"
+ Lucid.span_ [Lucid.class_ "detail-value"] <| statusBadge (TaskCore.taskStatus task)
+
+ case reviewInfo of
+ ReviewNoCommit ->
+ Lucid.div_ [Lucid.class_ "no-commit-msg"] <| do
+ Lucid.h3_ "No Commit Found"
+ Lucid.p_ "No commit with this task ID was found in the git history."
+ Lucid.p_ "The worker may not have completed yet, or the commit message doesn't include the task ID."
+ ReviewMergeConflict commitSha conflictFiles ->
+ Lucid.div_ [Lucid.class_ "conflict-warning"] <| do
+ Lucid.h3_ "Merge Conflict Detected"
+ Lucid.p_ <| do
+ "Commit "
+ Lucid.code_ (Lucid.toHtml (Text.take 8 commitSha))
+ " cannot be cleanly merged."
+ Lucid.p_ "Conflicting files:"
+ Lucid.ul_ <| traverse_ (Lucid.li_ <. Lucid.toHtml) conflictFiles
+ ReviewReady commitSha diffText -> do
+ Lucid.div_ [Lucid.class_ "diff-section"] <| do
+ Lucid.h3_ <| do
+ "Commit: "
+ Lucid.code_ (Lucid.toHtml (Text.take 8 commitSha))
+ Lucid.pre_ [Lucid.class_ "diff-block"] (Lucid.toHtml diffText)
+
+ Lucid.div_ [Lucid.class_ "review-actions"] <| do
+ Lucid.form_
+ [ Lucid.method_ "POST",
+ Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/accept"),
+ Lucid.class_ "inline-form"
+ ]
+ <| do
+ Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "accept-btn"] "Accept"
- Lucid.form_
- [ Lucid.method_ "POST",
- Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/reject"),
- Lucid.class_ "reject-form"
- ]
- <| do
- Lucid.textarea_
- [ Lucid.name_ "notes",
- Lucid.class_ "reject-notes",
- Lucid.placeholder_ "Rejection notes (optional)"
- ]
- ""
- Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "reject-btn"] "Reject"
- where
- statusBadge :: (Monad m) => TaskCore.Status -> Lucid.HtmlT m ()
- statusBadge status =
- let (cls, label) = case status of
- TaskCore.Open -> ("badge badge-open", "Open")
- TaskCore.InProgress -> ("badge badge-inprogress", "In Progress")
- TaskCore.Review -> ("badge badge-review", "Review")
- TaskCore.Approved -> ("badge badge-approved", "Approved")
- TaskCore.Done -> ("badge badge-done", "Done")
- in Lucid.span_ [Lucid.class_ cls] label
-
-reviewStyles :: Text
-reviewStyles =
- "* { box-sizing: border-box; } \
- \body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; \
- \ margin: 0; padding: 16px; background: #f5f5f5; max-width: 1000px; } \
- \h1 { margin: 16px 0; } \
- \h3 { margin: 16px 0 8px 0; color: #374151; } \
- \.task-summary { background: white; border-radius: 8px; padding: 16px; \
- \ box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 16px; } \
- \.detail-row { display: flex; padding: 8px 0; border-bottom: 1px solid #e5e7eb; } \
- \.detail-row:last-child { border-bottom: none; } \
- \.detail-label { font-weight: 600; width: 100px; color: #6b7280; } \
- \.detail-value { flex: 1; } \
- \.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; \
- \ font-size: 12px; font-weight: 500; } \
- \.badge-open { background: #fef3c7; color: #92400e; } \
- \.badge-inprogress { background: #dbeafe; color: #1e40af; } \
- \.badge-review { background: #ede9fe; color: #6b21a8; } \
- \.badge-approved { background: #d1fae5; color: #065f46; } \
- \.badge-done { background: #d1fae5; color: #065f46; } \
- \.no-commit-msg { background: #fff3cd; border: 1px solid #ffc107; border-radius: 8px; \
- \ padding: 16px; margin: 16px 0; } \
- \.conflict-warning { background: #f8d7da; border: 1px solid #dc3545; border-radius: 8px; \
- \ padding: 16px; margin: 16px 0; } \
- \.diff-section { background: white; border-radius: 8px; padding: 16px; \
- \ box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin: 16px 0; } \
- \.diff-block { background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 4px; \
- \ font-family: 'SF Mono', Monaco, 'Courier New', monospace; font-size: 13px; \
- \ overflow-x: auto; white-space: pre; margin: 0; max-height: 600px; overflow-y: auto; } \
- \.review-actions { background: white; border-radius: 8px; padding: 16px; \
- \ box-shadow: 0 1px 3px rgba(0,0,0,0.1); display: flex; gap: 16px; \
- \ align-items: flex-start; flex-wrap: wrap; } \
- \.inline-form { display: inline-block; } \
- \.reject-form { display: flex; gap: 8px; flex: 1; min-width: 300px; } \
- \.reject-notes { flex: 1; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px; \
- \ font-size: 14px; resize: vertical; min-height: 38px; } \
- \.accept-btn { padding: 10px 24px; background: #10b981; color: white; border: none; \
- \ border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; } \
- \.accept-btn:hover { background: #059669; } \
- \.reject-btn { padding: 10px 24px; background: #ef4444; color: white; border: none; \
- \ border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; } \
- \.reject-btn:hover { background: #dc2626; }"
+ Lucid.form_
+ [ Lucid.method_ "POST",
+ Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/reject"),
+ Lucid.class_ "reject-form"
+ ]
+ <| do
+ Lucid.textarea_
+ [ Lucid.name_ "notes",
+ Lucid.class_ "reject-notes",
+ Lucid.placeholder_ "Rejection notes (optional)"
+ ]
+ ""
+ Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "reject-btn"] "Reject"
api :: Proxy API
api = Proxy
@@ -751,6 +467,7 @@ api = Proxy
server :: Server API
server =
homeHandler
+ :<|> styleHandler
:<|> readyQueueHandler
:<|> taskListHandler
:<|> taskDetailHandler
@@ -759,6 +476,9 @@ server =
:<|> taskAcceptHandler
:<|> taskRejectHandler
where
+ styleHandler :: Servant.Handler LazyText.Text
+ styleHandler = pure Style.css
+
homeHandler :: Servant.Handler HomePage
homeHandler = do
stats <- liftIO <| TaskCore.getTaskStats Nothing
diff --git a/Omni/Jr/Web/Style.hs b/Omni/Jr/Web/Style.hs
new file mode 100644
index 0000000..e2377b5
--- /dev/null
+++ b/Omni/Jr/Web/Style.hs
@@ -0,0 +1,510 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+-- : dep clay
+module Omni.Jr.Web.Style
+ ( css,
+ statusBadgeClass,
+ priorityBadgeClass,
+ )
+where
+
+import Alpha hiding (wrap, (**), (|>))
+import Clay
+import qualified Clay.Flexbox as Flexbox
+import qualified Clay.Media as Media
+import qualified Clay.Stylesheet as Stylesheet
+import qualified Data.List.NonEmpty as NE
+import qualified Data.Text.Lazy as LazyText
+
+css :: LazyText.Text
+css = render stylesheet
+
+stylesheet :: Css
+stylesheet = do
+ baseStyles
+ layoutStyles
+ navigationStyles
+ cardStyles
+ statusBadges
+ buttonStyles
+ formStyles
+ responsiveStyles
+ darkModeStyles
+
+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)
+ body ? do
+ fontFamily
+ [ "-apple-system",
+ "BlinkMacSystemFont",
+ "Segoe UI",
+ "Roboto",
+ "Helvetica Neue",
+ "Arial",
+ "Noto Sans",
+ "sans-serif"
+ ]
+ [sansSerif]
+ fontSize (px 16)
+ lineHeight (em 1.5)
+ color "#1f2937"
+ backgroundColor "#f3f4f6"
+ minHeight (vh 100)
+ "h1" ? do
+ fontSize (px 24)
+ fontWeight bold
+ margin (px 0) (px 0) (em 0.5) (px 0)
+ "h2" ? do
+ fontSize (px 18)
+ fontWeight (weight 600)
+ color "#374151"
+ margin (em 1.5) (px 0) (em 0.75) (px 0)
+ "h3" ? do
+ fontSize (px 16)
+ fontWeight (weight 600)
+ color "#374151"
+ margin (em 1) (px 0) (em 0.5) (px 0)
+ a ? do
+ color "#0066cc"
+ textDecoration none
+ a # hover ? textDecoration underline
+ code ? do
+ fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
+ fontSize (em 0.9)
+ backgroundColor "#f3f4f6"
+ padding (px 2) (px 6) (px 2) (px 6)
+ borderRadius (px 4) (px 4) (px 4) (px 4)
+ pre ? do
+ fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
+ fontSize (px 13)
+ backgroundColor "#1e1e1e"
+ color "#d4d4d4"
+ padding (px 16) (px 16) (px 16) (px 16)
+ borderRadius (px 6) (px 6) (px 6) (px 6)
+ overflow auto
+ whiteSpace preWrap
+ maxHeight (px 500)
+
+layoutStyles :: Css
+layoutStyles = do
+ ".container" ? do
+ width (pct 100)
+ maxWidth (px 900)
+ margin (px 0) auto (px 0) auto
+ padding (px 16) (px 16) (px 16) (px 16)
+ main_ ? do
+ Stylesheet.key "flex" ("1 0 auto" :: Text)
+ ".page-content" ? do
+ padding (px 0) (px 0) (px 0) (px 0)
+ ".stats-grid" ? do
+ display grid
+ Stylesheet.key "grid-template-columns" ("repeat(auto-fit, minmax(100px, 1fr))" :: Text)
+ Stylesheet.key "gap" ("12px" :: Text)
+ ".task-list" ? do
+ display flex
+ flexDirection column
+ Stylesheet.key "gap" ("8px" :: Text)
+ ".detail-row" ? do
+ display flex
+ flexWrap Flexbox.wrap
+ padding (px 10) (px 0) (px 10) (px 0)
+ borderBottom (px 1) solid "#e5e7eb"
+ ".detail-row" # lastChild ? borderBottom (px 0) none transparent
+ ".detail-label" ? do
+ fontWeight (weight 600)
+ width (px 120)
+ color "#6b7280"
+ minWidth (px 100)
+ ".detail-value" ? do
+ Stylesheet.key "flex" ("1" :: Text)
+ minWidth (px 0)
+ ".detail-section" ? do
+ marginTop (em 1)
+ paddingTop (em 1)
+ borderTop (px 1) solid "#e5e7eb"
+ ".dep-list" <> ".child-list" ? do
+ margin (px 8) (px 0) (px 8) (px 0)
+ paddingLeft (px 24)
+ (".dep-list" ** li) <> (".child-list" ** li) ? margin (px 6) (px 0) (px 6) (px 0)
+ ".dep-type" <> ".child-status" ? do
+ color "#6b7280"
+ fontSize (px 13)
+ ".child-title" ? color "#374151"
+ ".priority-desc" ? do
+ color "#6b7280"
+ marginLeft (px 4)
+
+navigationStyles :: Css
+navigationStyles = do
+ header ? do
+ backgroundColor white
+ padding (px 12) (px 16) (px 12) (px 16)
+ boxShadow (NE.singleton (bsColor (rgba 0 0 0 0.08) (shadow (px 0) (px 2))))
+ marginBottom (px 16)
+ ".nav-content" ? do
+ maxWidth (px 900)
+ margin (px 0) auto (px 0) auto
+ display flex
+ alignItems center
+ justifyContent spaceBetween
+ flexWrap Flexbox.wrap
+ Stylesheet.key "gap" ("12px" :: Text)
+ ".nav-brand" ? do
+ fontSize (px 20)
+ fontWeight bold
+ color "#1f2937"
+ textDecoration none
+ ".nav-brand" # hover ? textDecoration none
+ ".nav-links" ? do
+ display flex
+ Stylesheet.key "gap" ("8px" :: Text)
+ flexWrap Flexbox.wrap
+ ".back-link" ? do
+ display inlineBlock
+ marginBottom (em 0.75)
+ fontSize (px 14)
+ ".actions" ? do
+ display flex
+ flexWrap Flexbox.wrap
+ Stylesheet.key "gap" ("8px" :: Text)
+ marginBottom (px 16)
+
+cardStyles :: Css
+cardStyles = do
+ ".card"
+ <> ".task-card"
+ <> ".stat-card"
+ <> ".task-detail"
+ <> ".task-summary"
+ <> ".filter-form"
+ <> ".status-form"
+ <> ".diff-section"
+ <> ".review-actions"
+ ? do
+ backgroundColor white
+ borderRadius (px 8) (px 8) (px 8) (px 8)
+ padding (px 16) (px 16) (px 16) (px 16)
+ boxShadow (NE.singleton (bsColor (rgba 0 0 0 0.1) (shadow (px 0) (px 1))))
+ ".stat-card" ? textAlign center
+ ".stat-count" ? do
+ fontSize (px 28)
+ fontWeight bold
+ ".stat-label" ? do
+ fontSize (px 12)
+ color "#6b7280"
+ marginTop (px 4)
+ ".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"
+ ".task-card" ? do
+ transition "box-shadow" (ms 150) ease (sec 0)
+ ".task-card" # hover ? do
+ boxShadow (NE.singleton (bsColor (rgba 0 0 0 0.15) (shadow (px 0) (px 4))))
+ ".task-header" ? do
+ display flex
+ flexWrap Flexbox.wrap
+ alignItems center
+ Stylesheet.key "gap" ("8px" :: Text)
+ marginBottom (px 8)
+ ".task-id" ? do
+ fontFamily ["SF Mono", "Monaco", "monospace"] [monospace]
+ color "#0066cc"
+ textDecoration none
+ fontSize (px 14)
+ padding (px 4) (px 0) (px 4) (px 0)
+ ".task-id" # hover ? textDecoration underline
+ ".priority" ? do
+ fontSize (px 12)
+ color "#6b7280"
+ ".task-title" ? do
+ fontSize (px 16)
+ margin (px 0) (px 0) (px 0) (px 0)
+ ".empty-msg" ? do
+ color "#6b7280"
+ fontStyle italic
+ ".ready-link" ? do
+ fontSize (px 14)
+ color "#0066cc"
+ ".count-badge" ? do
+ backgroundColor "#0066cc"
+ color white
+ padding (px 4) (px 10) (px 4) (px 10)
+ borderRadius (px 12) (px 12) (px 12) (px 12)
+ fontSize (px 14)
+ verticalAlign middle
+ ".description" ? do
+ backgroundColor "#f9fafb"
+ padding (px 12) (px 12) (px 12) (px 12)
+ borderRadius (px 4) (px 4) (px 4) (px 4)
+ margin (px 0) (px 0) (px 0) (px 0)
+ ".diff-block" ? do
+ maxHeight (px 600)
+ overflowY auto
+ ".no-commit-msg" ? do
+ backgroundColor "#fff3cd"
+ border (px 1) solid "#ffc107"
+ borderRadius (px 8) (px 8) (px 8) (px 8)
+ padding (px 16) (px 16) (px 16) (px 16)
+ margin (px 16) (px 0) (px 16) (px 0)
+ ".conflict-warning" ? do
+ backgroundColor "#fee2e2"
+ border (px 1) solid "#ef4444"
+ borderRadius (px 8) (px 8) (px 8) (px 8)
+ padding (px 16) (px 16) (px 16) (px 16)
+ margin (px 16) (px 0) (px 16) (px 0)
+
+statusBadges :: Css
+statusBadges = do
+ ".badge" ? do
+ display inlineBlock
+ padding (px 4) (px 10) (px 4) (px 10)
+ borderRadius (px 20) (px 20) (px 20) (px 20)
+ fontSize (px 12)
+ fontWeight (weight 500)
+ whiteSpace nowrap
+ ".badge-open" ? do
+ backgroundColor "#fef3c7"
+ color "#92400e"
+ ".badge-inprogress" ? do
+ backgroundColor "#dbeafe"
+ color "#1e40af"
+ ".badge-review" ? do
+ backgroundColor "#ede9fe"
+ color "#6b21a8"
+ ".badge-approved" ? do
+ backgroundColor "#cffafe"
+ color "#0e7490"
+ ".badge-done" ? do
+ backgroundColor "#d1fae5"
+ color "#065f46"
+
+buttonStyles :: Css
+buttonStyles = do
+ ".btn"
+ <> ".action-btn"
+ <> ".filter-btn"
+ <> ".submit-btn"
+ <> ".accept-btn"
+ <> ".reject-btn"
+ <> ".review-link-btn"
+ ? do
+ display inlineBlock
+ minHeight (px 44)
+ padding (px 10) (px 20) (px 10) (px 20)
+ borderRadius (px 6) (px 6) (px 6) (px 6)
+ border (px 0) none transparent
+ fontSize (px 14)
+ fontWeight (weight 500)
+ textDecoration none
+ cursor pointer
+ textAlign center
+ 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"
+ ".action-btn" # hover ? do
+ backgroundColor "#f9fafb"
+ borderColor "#9ca3af"
+ ".action-btn-primary" <> ".filter-btn" <> ".submit-btn" ? do
+ backgroundColor "#0066cc"
+ color white
+ borderColor "#0066cc"
+ ".action-btn-primary"
+ # hover
+ <> ".filter-btn"
+ # hover
+ <> ".submit-btn"
+ # hover
+ ? do
+ backgroundColor "#0052a3"
+ ".accept-btn" ? do
+ backgroundColor "#10b981"
+ color white
+ ".accept-btn" # hover ? backgroundColor "#059669"
+ ".reject-btn" ? do
+ backgroundColor "#ef4444"
+ color white
+ ".reject-btn" # hover ? backgroundColor "#dc2626"
+ ".clear-btn" ? do
+ display inlineBlock
+ minHeight (px 44)
+ padding (px 10) (px 16) (px 10) (px 16)
+ backgroundColor "#6b7280"
+ color white
+ borderRadius (px 6) (px 6) (px 6) (px 6)
+ textDecoration none
+ fontSize (px 14)
+ cursor pointer
+ ".clear-btn" # hover ? backgroundColor "#4b5563"
+ ".review-link-btn" ? do
+ backgroundColor "#8b5cf6"
+ color white
+ ".review-link-btn" # hover ? backgroundColor "#7c3aed"
+ ".review-link-section" ? margin (px 16) (px 0) (px 16) (px 0)
+
+formStyles :: Css
+formStyles = do
+ ".filter-row" ? do
+ display flex
+ flexWrap Flexbox.wrap
+ Stylesheet.key "gap" ("12px" :: Text)
+ alignItems flexEnd
+ ".filter-group" ? do
+ display flex
+ flexDirection column
+ Stylesheet.key "gap" ("4px" :: Text)
+ (".filter-group" |> label) ? do
+ fontSize (px 12)
+ color "#6b7280"
+ fontWeight (weight 500)
+ ".filter-select" <> ".filter-input" <> ".status-select" ? do
+ minHeight (px 44)
+ padding (px 10) (px 14) (px 10) (px 14)
+ border (px 1) solid "#d1d5db"
+ borderRadius (px 6) (px 6) (px 6) (px 6)
+ fontSize (px 14)
+ minWidth (px 120)
+ ".filter-input" ? minWidth (px 150)
+ ".inline-form" ? display inlineBlock
+ ".reject-form" ? do
+ display flex
+ Stylesheet.key "gap" ("8px" :: Text)
+ Stylesheet.key "flex" ("1" :: Text)
+ minWidth (px 250)
+ flexWrap Flexbox.wrap
+ ".reject-notes" ? do
+ Stylesheet.key "flex" ("1" :: Text)
+ minWidth (px 200)
+ minHeight (px 44)
+ padding (px 10) (px 14) (px 10) (px 14)
+ border (px 1) solid "#d1d5db"
+ borderRadius (px 6) (px 6) (px 6) (px 6)
+ fontSize (px 14)
+ Stylesheet.key "resize" ("vertical" :: Text)
+
+responsiveStyles :: Css
+responsiveStyles = do
+ query Media.screen [Media.maxWidth (px 600)] <| do
+ body ? fontSize (px 15)
+ ".container" ? padding (px 12) (px 12) (px 12) (px 12)
+ ".nav-content" ? do
+ flexDirection column
+ alignItems flexStart
+ ".stats-grid" ? do
+ Stylesheet.key "grid-template-columns" ("repeat(2, 1fr)" :: Text)
+ ".detail-row" ? do
+ flexDirection column
+ Stylesheet.key "gap" ("4px" :: Text)
+ ".detail-label" ? width auto
+ ".filter-row" ? flexDirection column
+ ".filter-group" ? width (pct 100)
+ ".filter-select" <> ".filter-input" ? width (pct 100)
+ ".review-actions" ? do
+ flexDirection column
+ ".reject-form" ? do
+ width (pct 100)
+ flexDirection column
+ ".reject-notes" ? width (pct 100)
+ ".actions" ? flexDirection column
+ ".action-btn" ? width (pct 100)
+
+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"
+ ? do
+ backgroundColor "#1f2937"
+ boxShadow (NE.singleton (bsColor (rgba 0 0 0 0.3) (shadow (px 0) (px 2))))
+ header ? do
+ backgroundColor "#1f2937"
+ boxShadow (NE.singleton (bsColor (rgba 0 0 0 0.3) (shadow (px 0) (px 2))))
+ ".nav-brand" ? color "#f3f4f6"
+ "h2" <> "h3" ? color "#d1d5db"
+ a ? color "#60a5fa"
+ ".detail-row" ? borderBottomColor "#374151"
+ ".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"
+ ".detail-section" ? borderTopColor "#374151"
+ ".description" ? backgroundColor "#374151"
+ ".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"
+ ".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"
+
+prefersDark :: Stylesheet.Feature
+prefersDark =
+ Stylesheet.Feature "prefers-color-scheme" (Just (Clay.value ("dark" :: Text)))
+
+statusBadgeClass :: Text -> Text
+statusBadgeClass status = case status of
+ "Open" -> "badge badge-open"
+ "InProgress" -> "badge badge-inprogress"
+ "Review" -> "badge badge-review"
+ "Approved" -> "badge badge-approved"
+ "Done" -> "badge badge-done"
+ _ -> "badge"
+
+priorityBadgeClass :: Text -> Text
+priorityBadgeClass priority = case priority of
+ "P0" -> "badge badge-p0"
+ "P1" -> "badge badge-p1"
+ "P2" -> "badge badge-p2"
+ "P3" -> "badge badge-p3"
+ "P4" -> "badge badge-p4"
+ _ -> "badge"