feat(views): Phase 5i slice C — kanban view_type with group_by chip strip

m's Q6 pick (2026-05-26): kanban groups the filtered set by `status`
(default) / `area` / `tag` / `management`. Read-only — drag-to-change
is parked. Adds the third view_type render on /tree (alongside list and
card from earlier slices); kanban is now unlocked in PageViewTypes("/").

New web/kanban.go owns BuildKanbanBoard + the per-dimension keyer +
column ordering (status: active/done/archived; management: mai/self/
external/unmanaged; area + tag: alphabetical). Within-column order:
pinned-first → updated_at desc → title.

ParseGroupBy + GroupByChips provide the URL-param hookup and the chip
strip rendered above the board. Multi-tag items appear in every tag
column they belong to (deliberate — the kanban surfaces overlap).

Render:
- handleTree builds the kanban board off the same flatMatchedItems the
  card view consumes; cost is one extra grouping pass, no new DB hits.
- New templates/tree_kanban.tmpl: header chip strip + responsive
  column board (horizontal scroll on overflow). Empty filtered set
  surfaces a friendly nudge.

CSS additions cover the column / card layout; existing chip aesthetics
reused for the group-by toggle.

Test updates:
- view_type_test.go: slice B's "kanban locked on /" assertions tightened
  to "kanban unlocked; calendar + timeline still locked on /" — slice C
  is the unlock event for kanban.
- New kanban_test.go: per-dimension grouping (status, tag, area),
  pinned-first ordering, parser fallback.
- server_test.go: end-to-end render — GET /?view_type=kanban produces
  kanban-board markup + group-by chip strip; forest absent.
This commit is contained in:
mAi
2026-05-26 13:47:03 +02:00
parent 2f47b28f39
commit bbc7867a35
9 changed files with 439 additions and 9 deletions

View File

@@ -229,6 +229,36 @@ table.classify input, table.classify select { width: 100%; }
.tree-card-slug { font-size: 0.78em; }
.tree-card-meta { display: flex; flex-wrap: wrap; gap: 4px; margin: 0; font-size: 0.78em; }
.tree-card-empty { grid-column: 1 / -1; padding: 24px; color: var(--muted); }
/* Phase 5i Slice C — kanban columns + cards. */
.kanban-controls { margin: 8px 0; }
.groupby-chip {
display: inline-block; font-size: 0.78em; padding: 1px 8px; border-radius: 999px;
background: var(--surface); border: 1px solid var(--border); color: var(--muted); text-decoration: none;
}
.groupby-chip:hover { color: var(--fg); border-color: var(--accent); }
.kanban-board {
display: grid; gap: 12px; padding: 12px 0; overflow-x: auto;
grid-auto-flow: column; grid-auto-columns: minmax(220px, 280px);
}
.kanban-column {
background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
padding: 8px 10px; display: flex; flex-direction: column; gap: 8px;
min-height: 120px;
}
.kanban-col-head {
display: flex; justify-content: space-between; align-items: baseline;
border-bottom: 1px solid var(--border); padding-bottom: 6px;
}
.kanban-col-label { font-weight: 500; }
.kanban-cards { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 6px; }
.kanban-card {
background: var(--bg); border: 1px solid var(--border); border-radius: 4px;
padding: 6px 8px;
}
.kanban-card-title { font-weight: 500; color: var(--fg); text-decoration: none; display: block; }
.kanban-card-title:hover { color: var(--accent); }
.kanban-card-meta { display: flex; flex-wrap: wrap; gap: 4px; margin: 4px 0 0; font-size: 0.78em; }
.kanban-empty { padding: 24px; }
#tree-filterbar small { opacity: 0.75; margin-left: 2px; }
.tree-section .empty { padding: 24px; color: var(--muted); }
.tree-section .clear { color: var(--bad); }