diff options
| author | Ben Sima <ben@bensima.com> | 2025-12-01 21:38:17 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bensima.com> | 2025-12-01 21:38:17 -0500 |
| commit | 7da1c7e717af3cadf927b5f6efb253f0d10423a8 (patch) | |
| tree | e6d52326215026a5cc1002a06f68cbd6281b1b6f /Omni/Jr/Web/Components.hs | |
| parent | 437a204017c7b357c3fc4dcc8c23151a288d1a8e (diff) | |
Make complexity badge editable on task detail page
- Add ComplexityForm and ComplexityBadgePartial types
- Add /tasks/:id/complexity POST endpoint
- Add complexityBadgeWithForm component with dropdown
- Add complexity dropdown JS for keyboard navigation
- Add CSS styles for complexity dropdown
- Always show complexity badge (Set Complexity if none)
Task-Id: t-219
Diffstat (limited to 'Omni/Jr/Web/Components.hs')
| -rw-r--r-- | Omni/Jr/Web/Components.hs | 157 |
1 files changed, 157 insertions, 0 deletions
diff --git a/Omni/Jr/Web/Components.hs b/Omni/Jr/Web/Components.hs index 7dd13c7..4429f75 100644 --- a/Omni/Jr/Web/Components.hs +++ b/Omni/Jr/Web/Components.hs @@ -24,6 +24,7 @@ module Omni.Jr.Web.Components navbarDropdownJs, statusDropdownJs, priorityDropdownJs, + complexityDropdownJs, liveToggleJs, -- * Breadcrumbs @@ -44,6 +45,10 @@ module Omni.Jr.Web.Components clickablePriorityBadge, priorityDropdownOptions, priorityOption, + complexityBadgeWithForm, + clickableComplexityBadge, + complexityDropdownOptions, + complexityOption, -- * Sorting sortDropdown, @@ -188,6 +193,7 @@ pageHead title = ("" :: Text) Lucid.script_ [] statusDropdownJs Lucid.script_ [] priorityDropdownJs + Lucid.script_ [] complexityDropdownJs Lucid.script_ [] navbarDropdownJs Lucid.script_ [] liveToggleJs @@ -362,6 +368,77 @@ priorityDropdownJs = "});" ] +complexityDropdownJs :: Text +complexityDropdownJs = + Text.unlines + [ "function toggleComplexityDropdown(el) {", + " var container = el.parentElement;", + " var isOpen = container.classList.toggle('open');", + " el.setAttribute('aria-expanded', isOpen);", + " if (isOpen) {", + " var firstItem = container.querySelector('[role=\"menuitem\"]');", + " if (firstItem) firstItem.focus();", + " }", + "}", + "", + "function closeComplexityDropdown(container) {", + " container.classList.remove('open');", + " var badge = container.querySelector('[role=\"button\"]');", + " if (badge) {", + " badge.setAttribute('aria-expanded', 'false');", + " badge.focus();", + " }", + "}", + "", + "function handleComplexityKeydown(event, el) {", + " if (event.key === 'Enter' || event.key === ' ') {", + " event.preventDefault();", + " toggleComplexityDropdown(el);", + " } else if (event.key === 'Escape') {", + " closeComplexityDropdown(el.parentElement);", + " } else if (event.key === 'ArrowDown') {", + " event.preventDefault();", + " var container = el.parentElement;", + " if (!container.classList.contains('open')) {", + " toggleComplexityDropdown(el);", + " } else {", + " var firstItem = container.querySelector('[role=\"menuitem\"]');", + " if (firstItem) firstItem.focus();", + " }", + " }", + "}", + "", + "function handleComplexityMenuItemKeydown(event) {", + " var container = event.target.closest('.complexity-badge-dropdown');", + " var items = container.querySelectorAll('[role=\"menuitem\"]');", + " var currentIndex = Array.from(items).indexOf(event.target);", + " ", + " if (event.key === 'ArrowDown') {", + " event.preventDefault();", + " var next = (currentIndex + 1) % items.length;", + " items[next].focus();", + " } else if (event.key === 'ArrowUp') {", + " event.preventDefault();", + " var prev = (currentIndex - 1 + items.length) % items.length;", + " items[prev].focus();", + " } else if (event.key === 'Escape') {", + " event.preventDefault();", + " closeComplexityDropdown(container);", + " } else if (event.key === 'Tab') {", + " closeComplexityDropdown(container);", + " }", + "}", + "", + "document.addEventListener('click', function(e) {", + " var dropdowns = document.querySelectorAll('.complexity-badge-dropdown.open');", + " dropdowns.forEach(function(d) {", + " if (!d.contains(e.target)) {", + " closeComplexityDropdown(d);", + " }", + " });", + "});" + ] + liveToggleJs :: Text liveToggleJs = Text.unlines @@ -774,6 +851,86 @@ priorityOption opt currentPriority tid = ] (Lucid.toHtml label) +-- * Complexity badge with form + +complexityBadgeWithForm :: (Monad m) => Maybe Int -> Text -> Lucid.HtmlT m () +complexityBadgeWithForm complexity tid = + Lucid.div_ + [ Lucid.id_ "complexity-badge-container", + Lucid.class_ "complexity-badge-dropdown" + ] + <| do + clickableComplexityBadge complexity tid + complexityDropdownOptions complexity tid + +clickableComplexityBadge :: (Monad m) => Maybe Int -> Text -> Lucid.HtmlT m () +clickableComplexityBadge complexity _tid = + let (cls, label) = case complexity of + Nothing -> ("badge badge-complexity-none complexity-badge-clickable", "Set Complexity" :: Text) + Just 1 -> ("badge badge-complexity-1 complexity-badge-clickable", "ℂ 1") + Just 2 -> ("badge badge-complexity-2 complexity-badge-clickable", "ℂ 2") + Just 3 -> ("badge badge-complexity-3 complexity-badge-clickable", "ℂ 3") + Just 4 -> ("badge badge-complexity-4 complexity-badge-clickable", "ℂ 4") + Just 5 -> ("badge badge-complexity-5 complexity-badge-clickable", "ℂ 5") + Just _ -> ("badge badge-complexity-none complexity-badge-clickable", "Invalid") + in Lucid.span_ + [ Lucid.class_ cls, + Lucid.tabindex_ "0", + Lucid.role_ "button", + Lucid.makeAttribute "aria-haspopup" "true", + Lucid.makeAttribute "aria-expanded" "false", + Lucid.makeAttribute "onclick" "toggleComplexityDropdown(this)", + Lucid.makeAttribute "onkeydown" "handleComplexityKeydown(event, this)" + ] + <| do + Lucid.toHtml label + Lucid.span_ [Lucid.class_ "dropdown-arrow", Lucid.makeAttribute "aria-hidden" "true"] " ▾" + +complexityDropdownOptions :: (Monad m) => Maybe Int -> Text -> Lucid.HtmlT m () +complexityDropdownOptions currentComplexity tid = + Lucid.div_ + [ Lucid.class_ "complexity-dropdown-menu", + Lucid.role_ "menu", + Lucid.makeAttribute "aria-label" "Change task complexity" + ] + <| do + complexityOption Nothing currentComplexity tid + complexityOption (Just 1) currentComplexity tid + complexityOption (Just 2) currentComplexity tid + complexityOption (Just 3) currentComplexity tid + complexityOption (Just 4) currentComplexity tid + complexityOption (Just 5) currentComplexity tid + +complexityOption :: (Monad m) => Maybe Int -> Maybe Int -> Text -> Lucid.HtmlT m () +complexityOption opt currentComplexity tid = + let (cls, label, val) = case opt of + Nothing -> ("badge badge-complexity-none", "None" :: Text, "none" :: Text) + Just 1 -> ("badge badge-complexity-1", "ℂ 1", "1") + Just 2 -> ("badge badge-complexity-2", "ℂ 2", "2") + Just 3 -> ("badge badge-complexity-3", "ℂ 3", "3") + Just 4 -> ("badge badge-complexity-4", "ℂ 4", "4") + Just 5 -> ("badge badge-complexity-5", "ℂ 5", "5") + Just _ -> ("badge badge-complexity-none", "Invalid", "none") + isSelected = opt == currentComplexity + optClass = cls <> " complexity-dropdown-option" <> if isSelected then " selected" else "" + in Lucid.form_ + [ Lucid.class_ "complexity-option-form", + Lucid.role_ "none", + Lucid.makeAttribute "hx-post" ("/tasks/" <> tid <> "/complexity"), + Lucid.makeAttribute "hx-target" "#complexity-badge-container", + Lucid.makeAttribute "hx-swap" "outerHTML" + ] + <| do + Lucid.input_ [Lucid.type_ "hidden", Lucid.name_ "complexity", Lucid.value_ val] + Lucid.button_ + [ Lucid.type_ "submit", + Lucid.class_ optClass, + Lucid.role_ "menuitem", + Lucid.tabindex_ "-1", + Lucid.makeAttribute "onkeydown" "handleComplexityMenuItemKeydown(event)" + ] + (Lucid.toHtml label) + -- * Task rendering renderTaskCard :: (Monad m) => TaskCore.Task -> Lucid.HtmlT m () |
