← Back to task

Commit ebc281f9

commit ebc281f90389da3b064bb14888e7c4f81ae4df17
Author: Ben Sima <ben@bensima.com>
Date:   Thu Nov 27 18:57:44 2025

    Click badge to show inline dropdown options
    
    The implementation is complete. The changes I made:
    
    1. **Web.hs** - Replaced the `statusBadgeWithForm` function from a
    `<sel
       - `statusBadgeWithForm` now creates a container div with the
       clickabl - `clickableBadge` renders a badge that, when clicked,
       toggles the "o - `statusDropdownOptions` renders all the status
       options as badge-sty - `statusOption` renders each status option
       as an HTMX form that upda
    
    2. **Style.hs** - Added CSS styles for the new dropdown component:
       - `.status-badge-dropdown` - Container with relative positioning
       - `.status-badge-clickable` - Clickable badge styling with
       cursor poi - `.dropdown-arrow` - Arrow indicator styling -
       `.status-dropdown-menu` - Hidden dropdown that appears when parent -
       `.status-option-form` and `.status-dropdown-option` - Styling for
       o - Dark mode support for the dropdown menu
    
    Task-Id: t-157.2

diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index ce77bcaa..c7e2e44b 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -276,31 +276,62 @@ multiColorProgressBar stats =
 
 statusBadgeWithForm :: (Monad m) => TaskCore.Status -> Text -> Lucid.HtmlT m ()
 statusBadgeWithForm status tid =
-  let badgeClass = case status of
-        TaskCore.Open -> "status-badge-select badge-open"
-        TaskCore.InProgress -> "status-badge-select badge-inprogress"
-        TaskCore.Review -> "status-badge-select badge-review"
-        TaskCore.Approved -> "status-badge-select badge-approved"
-        TaskCore.Done -> "status-badge-select badge-done"
-   in Lucid.select_
-        [ Lucid.id_ "status-badge-container",
-          Lucid.name_ "status",
-          Lucid.class_ badgeClass,
+  Lucid.div_
+    [ Lucid.id_ "status-badge-container",
+      Lucid.class_ "status-badge-dropdown"
+    ]
+    <| do
+      clickableBadge status tid
+      statusDropdownOptions status tid
+
+clickableBadge :: (Monad m) => TaskCore.Status -> Text -> Lucid.HtmlT m ()
+clickableBadge status _tid =
+  let (cls, label) = case status of
+        TaskCore.Open -> ("badge badge-open status-badge-clickable", "Open" :: Text)
+        TaskCore.InProgress -> ("badge badge-inprogress status-badge-clickable", "In Progress")
+        TaskCore.Review -> ("badge badge-review status-badge-clickable", "Review")
+        TaskCore.Approved -> ("badge badge-approved status-badge-clickable", "Approved")
+        TaskCore.Done -> ("badge badge-done status-badge-clickable", "Done")
+   in Lucid.span_
+        [ Lucid.class_ cls,
+          Lucid.makeAttribute "onclick" "this.parentElement.classList.toggle('open')"
+        ]
+        <| do
+          Lucid.toHtml label
+          Lucid.span_ [Lucid.class_ "dropdown-arrow"] " ▾"
+
+statusDropdownOptions :: (Monad m) => TaskCore.Status -> Text -> Lucid.HtmlT m ()
+statusDropdownOptions currentStatus tid =
+  Lucid.div_ [Lucid.class_ "status-dropdown-menu"] <| do
+    statusOption TaskCore.Open currentStatus tid
+    statusOption TaskCore.InProgress currentStatus tid
+    statusOption TaskCore.Review currentStatus tid
+    statusOption TaskCore.Approved currentStatus tid
+    statusOption TaskCore.Done currentStatus tid
+
+statusOption :: (Monad m) => TaskCore.Status -> TaskCore.Status -> Text -> Lucid.HtmlT m ()
+statusOption opt currentStatus tid =
+  let (cls, label) = case opt of
+        TaskCore.Open -> ("badge badge-open", "Open" :: Text)
+        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")
+      isSelected = opt == currentStatus
+      optClass = cls <> " status-dropdown-option" <> if isSelected then " selected" else ""
+   in Lucid.form_
+        [ Lucid.class_ "status-option-form",
           Lucid.makeAttribute "hx-post" ("/tasks/" <> tid <> "/status"),
           Lucid.makeAttribute "hx-target" "#status-badge-container",
           Lucid.makeAttribute "hx-swap" "outerHTML"
         ]
         <| do
-          statusOptionHtmx TaskCore.Open status
-          statusOptionHtmx TaskCore.InProgress status
-          statusOptionHtmx TaskCore.Review status
-          statusOptionHtmx TaskCore.Approved status
-          statusOptionHtmx TaskCore.Done status
-  where
-    statusOptionHtmx :: (Monad m2) => TaskCore.Status -> TaskCore.Status -> Lucid.HtmlT m2 ()
-    statusOptionHtmx opt current =
-      let attrs = [Lucid.value_ (tshow opt)] <> [Lucid.selected_ "selected" | opt == current]
-       in Lucid.option_ attrs (Lucid.toHtml (tshow opt))
+          Lucid.input_ [Lucid.type_ "hidden", Lucid.name_ "status", Lucid.value_ (tshow opt)]
+          Lucid.button_
+            [ Lucid.type_ "submit",
+              Lucid.class_ optClass
+            ]
+            (Lucid.toHtml label)
 
 renderTaskCard :: (Monad m) => TaskCore.Task -> Lucid.HtmlT m ()
 renderTaskCard t =
diff --git a/Omni/Jr/Web/Style.hs b/Omni/Jr/Web/Style.hs
index e768cda1..594fb21e 100644
--- a/Omni/Jr/Web/Style.hs
+++ b/Omni/Jr/Web/Style.hs
@@ -519,25 +519,46 @@ statusBadges = do
   ".badge-done" ? do
     backgroundColor "#d1fae5"
     color "#065f46"
-  ".status-badge-select" ? do
-    Stylesheet.key "-webkit-appearance" ("none" :: Text)
-    Stylesheet.key "-moz-appearance" ("none" :: Text)
-    Stylesheet.key "appearance" ("none" :: Text)
+  ".status-badge-dropdown" ? do
+    position relative
     display inlineBlock
-    padding (px 2) (px 18) (px 2) (px 6)
-    borderRadius (px 2) (px 2) (px 2) (px 2)
-    fontSize (px 11)
-    fontWeight (weight 500)
-    whiteSpace nowrap
+  ".status-badge-clickable" ? do
+    cursor pointer
+    Stylesheet.key "user-select" ("none" :: Text)
+  ".status-badge-clickable" # hover ? do
+    opacity 0.85
+  ".dropdown-arrow" ? do
+    fontSize (px 8)
+    marginLeft (px 2)
+    opacity 0.7
+  ".status-dropdown-menu" ? do
+    display none
+    position absolute
+    left (px 0)
+    top (pct 100)
+    marginTop (px 2)
+    backgroundColor white
+    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
+    padding (px 4) (px 4) (px 4) (px 4)
+    minWidth (px 100)
+  ".status-badge-dropdown.open" |> ".status-dropdown-menu" ? do
+    display block
+  ".status-option-form" ? do
+    margin (px 0) (px 0) (px 0) (px 0)
+    padding (px 0) (px 0) (px 0) (px 0)
+  ".status-dropdown-option" ? do
+    display block
+    width (pct 100)
+    textAlign (alignSide sideLeft)
+    margin (px 2) (px 0) (px 2) (px 0)
     border (px 0) none transparent
     cursor pointer
-    Stylesheet.key "background-image" ("url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cpath fill='currentColor' d='M0 2l4 4 4-4z'/%3E%3C/svg%3E\")" :: Text)
-    Stylesheet.key "background-repeat" ("no-repeat" :: Text)
-    Stylesheet.key "background-position" ("right 4px center" :: Text)
-    Stylesheet.key "background-size" ("8px" :: Text)
-  ".status-badge-select" # hover ? do
-    opacity 0.8
-  ".status-badge-select" # focus ? do
+    transition "opacity" (ms 150) ease (sec 0)
+  ".status-dropdown-option" # hover ? do
+    opacity 0.7
+  ".status-dropdown-option.selected" ? do
     Stylesheet.key "outline" ("2px solid #0066cc" :: Text)
     Stylesheet.key "outline-offset" ("1px" :: Text)
 
@@ -1066,6 +1087,9 @@ darkModeStyles =
       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"