Add breadcrumbs navigation for task hierarchy

t-172·WorkTask·
·
·
·Omni/Jr.hs
Created3 months ago·Updated3 months ago

Description

Edit

Overview

Add breadcrumbs navigation below navbar, above page content. Especially useful for navigating deeply nested task hierarchies (epics > subtasks > sub-subtasks).

Breadcrumb Examples

| Page | Breadcrumbs | |------|-------------| | / (homepage) | *none* | | /tasks | Jr > Tasks | | /tasks/t-123 (no parent) | Jr > Tasks > t-123 | | /tasks/t-123 (parent: t-100) | Jr > Tasks > t-100 > t-123 | | /tasks/t-50 (epic with children t-100, t-101) | Jr > Tasks > t-50 | | /tasks/t-123 (grandparent: t-50 > t-100 > t-123) | Jr > Tasks > t-50 > t-100 > t-123 | | /ready | Jr > Ready Queue | | /blocked | Jr > Blocked | | /intervention | Jr > Needs Intervention | | /stats | Jr > Stats | | /kb | Jr > Knowledge Base | | /kb/42 | Jr > Knowledge Base > Fact #42 | | /epics | Jr > Epics | | /tasks/t-50/review | Jr > Tasks > t-50 > Review |

Implementation

Data Structure

data Breadcrumb = Breadcrumb
  { crumbLabel :: Text
  , crumbHref :: Maybe Text  -- Nothing for current page (last crumb)
  }

type Breadcrumbs = [Breadcrumb]

Helper to Build Task Ancestry

-- Build breadcrumbs for a task, including parent chain
taskBreadcrumbs :: [TaskCore.Task] -> TaskCore.Task -> Breadcrumbs
taskBreadcrumbs allTasks task =
  let ancestors = getAncestors allTasks task  -- returns [grandparent, parent, task]
      taskCrumbs = map (\t -> Breadcrumb (TaskCore.taskId t) (Just ("/tasks/" <> TaskCore.taskId t))) (init ancestors)
      currentCrumb = Breadcrumb (TaskCore.taskId task) Nothing
  in [ Breadcrumb "Jr" (Just "/")
     , Breadcrumb "Tasks" (Just "/tasks")
     ] ++ taskCrumbs ++ [currentCrumb]

getAncestors :: [TaskCore.Task] -> TaskCore.Task -> [TaskCore.Task]
getAncestors allTasks task = 
  case TaskCore.taskParent task of
    Nothing -> [task]
    Just pid -> case TaskCore.findTask pid allTasks of
      Nothing -> [task]
      Just parent -> getAncestors allTasks parent ++ [task]

Render Function

renderBreadcrumbs :: (Monad m) => Breadcrumbs -> Lucid.HtmlT m ()
renderBreadcrumbs [] = pure ()
renderBreadcrumbs crumbs =
  Lucid.nav_ [Lucid.class_ "breadcrumbs", Lucid.makeAttribute "aria-label" "Breadcrumb"] <| do
    Lucid.ol_ [Lucid.class_ "breadcrumb-list"] <| do
      traverse_ renderCrumb (zip [0..] crumbs)
  where
    renderCrumb (idx, Breadcrumb label mHref) = do
      let isLast = idx == length crumbs - 1
      Lucid.li_ [Lucid.class_ "breadcrumb-item"] <| do
        when (idx > 0) <| Lucid.span_ [Lucid.class_ "breadcrumb-sep"] ">"
        case mHref of
          Just href -> Lucid.a_ [Lucid.href_ href] (Lucid.toHtml label)
          Nothing -> Lucid.span_ [Lucid.class_ "breadcrumb-current"] (Lucid.toHtml label)

Page Layout Integration

Modify pageBody or create wrapper that accepts optional breadcrumbs:

pageBodyWithCrumbs :: (Monad m) => Breadcrumbs -> Lucid.HtmlT m () -> Lucid.HtmlT m ()
pageBodyWithCrumbs crumbs content =
  Lucid.body_ <| do
    navbar
    unless (null crumbs) <| do
      Lucid.div_ [Lucid.class_ "breadcrumb-container"] <| do
        Lucid.div_ [Lucid.class_ "container"] <| renderBreadcrumbs crumbs
    Lucid.main_ [Lucid.class_ "page-content"] content

CSS

breadcrumbStyles :: Css
breadcrumbStyles = do
  ".breadcrumb-container" ? do
    backgroundColor "#f9fafb"
    borderBottom (px 1) solid "#e5e7eb"
    padding (px 6) (px 0) (px 6) (px 0)
  ".breadcrumb-list" ? do
    display flex
    alignItems center
    flexWrap Flexbox.wrap
    Stylesheet.key "gap" ("4px" :: Text)
    margin (px 0) (px 0) (px 0) (px 0)
    padding (px 0) (px 0) (px 0) (px 0)
    listStyleType none
    fontSize (px 12)
  ".breadcrumb-item" ? do
    display flex
    alignItems center
    Stylesheet.key "gap" ("4px" :: Text)
  ".breadcrumb-sep" ? do
    color "#9ca3af"
    userSelect none
  ".breadcrumb-current" ? do
    color "#6b7280"
    fontWeight (weight 500)
  (".breadcrumb-list" ** a) ? do
    color "#0066cc"
    textDecoration none
  (".breadcrumb-list" ** a) # hover ? textDecoration underline

Dark mode:

".breadcrumb-container" ? do
  backgroundColor "#1f2937"
  borderBottomColor "#374151"
".breadcrumb-sep" ? color "#6b7280"
".breadcrumb-current" ? color "#9ca3af"

Files to Modify

1. Omni/Jr/Web.hs:

  • Add Breadcrumb type and Breadcrumbs alias
  • Add renderBreadcrumbs function
  • Add taskBreadcrumbs, getAncestors helpers
  • Add pageBodyWithCrumbs or modify existing pages
  • Update each page's ToHtml instance to pass appropriate breadcrumbs
  • Homepage uses pageBody (no crumbs), others use pageBodyWithCrumbs

2. Omni/Jr/Web/Style.hs:

  • Add breadcrumbStyles function
  • Add to stylesheet list
  • Add dark mode overrides

Edge Cases

  • Task with deleted parent: Show task without parent in chain
  • Very deep nesting (5+ levels): Allow horizontal scroll or truncate middle with "..."
  • Long task IDs: They're short (t-xxx) so not a concern

Testing

1. Navigate to /tasks - see "Jr > Tasks" 2. Click a task with no parent - see "Jr > Tasks > t-xxx" 3. Click a task with parent - see full chain 4. Click a task with grandparent - see full chain 5. Click breadcrumb links - navigate correctly 6. Check on mobile - wraps gracefully 7. Check dark mode

Timeline (0)

No activity yet.