From 5f712c68d4d74292bd8b4a65f85d96d016a1d622 Mon Sep 17 00:00:00 2001 From: mAi Date: Tue, 26 May 2026 13:36:28 +0200 Subject: [PATCH] =?UTF-8?q?feat(views):=20Phase=205i=20slice=20B=20?= =?UTF-8?q?=E2=80=94=20view=5Ftype=20URL=20param=20+=20card=20view=20on=20?= =?UTF-8?q?/tree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit m's Q1+Q3 picks (2026-05-26): five canonical view_types (card/list/calendar/kanban/timeline). Slice B introduces the parameter and the first non-default rendering: card view on /tree shows the filtered set as a flat tile grid alongside the existing tree forest. New web/view_type.go owns the enum, per-route allowed set, parser, and the chip-strip builder. Per the design note, view_type is RENDER state, not filter state — kept off TreeFilter so the same filter can render as card or list. PageViewTypes("/") = {default: list, allowed: [list, card]}. Dashboard / calendar / timeline are LOCKED to their native shape in slice B; switching templates on /dashboard for card vs list is mostly already done via fuller's 5h tabbed-tiles surface and stays as-is for now (the chip strip surfaces card as the only allowed value there). Kanban + cross-page list/card swaps land in slice C onwards. Render: - handleTree parses `?view_type=` with the per-route catalog, builds flatMatchedItems for the card consumer alongside the existing forest. - tree_section.tmpl gains a view-type chip strip (locked entries shown greyed-out with title tooltip) + branches into either `tree-card` or the forest based on .ViewType. - New templates/tree_card.tmpl renders a flat grid of tiles for the matched set; per-item field set mirrors the list rendering. - Hidden `view_type` input added to the search form so chip clicks preserve the view choice. Tests: - view_type_test.go: parser fallback, per-route catalog, chip strip active/locked flags, filter preservation in chip URLs. - server_test.go: end-to-end dispatch — GET /?view_type=card renders tree-card-grid, GET / renders forest, unknown values fall back to list. Chip strip present on both views. --- web/server.go | 11 +++ web/server_test.go | 35 ++++++++ web/static/style.css | 23 +++++ web/templates/tree_card.tmpl | 29 ++++++ web/templates/tree_section.tmpl | 15 ++++ web/tree_filter.go | 14 +++ web/view_type.go | 153 ++++++++++++++++++++++++++++++++ web/view_type_test.go | 93 +++++++++++++++++++ 8 files changed, 373 insertions(+) create mode 100644 web/templates/tree_card.tmpl create mode 100644 web/view_type.go create mode 100644 web/view_type_test.go diff --git a/web/server.go b/web/server.go index 7125d8f..eeba124 100644 --- a/web/server.go +++ b/web/server.go @@ -169,6 +169,7 @@ func New(s *store.Store, logger *slog.Logger) (*Server, error) { "templates/layout.tmpl", "templates/tree.tmpl", "templates/tree_section.tmpl", + "templates/tree_card.tmpl", "templates/project_chip.tmpl", ) if err != nil { @@ -178,6 +179,7 @@ func New(s *store.Store, logger *slog.Logger) (*Server, error) { // Standalone tree-section template for HTMX fragment responses. treeSection, err := template.New("tree_section").Funcs(funcs).ParseFS(templatesFS, "templates/tree_section.tmpl", + "templates/tree_card.tmpl", "templates/project_chip.tmpl", ) if err != nil { @@ -440,8 +442,14 @@ func (s *Server) handleTree(w http.ResponseWriter, r *http.Request) { return } filter := ParseTreeFilter(r.URL.Query()) + viewSet := PageViewTypes("/") + view := ParseViewType(r.URL.Query(), viewSet) roots, orphans, total, orphanN, matched := applyTreeFilter(items, filter, linkKinds) counts := computeChipCounts(items, filter, linkKinds, tags) + // Phase 5i Slice B: the card view renders a flat grid of matched items + // (no tree structure). Build from items + filter directly rather than + // reusing the post-prune `roots` (which still keeps ancestors). + cardItems := flatMatchedItems(items, filter, linkKinds) data := map[string]any{ "Title": "tree", "Roots": roots, @@ -455,6 +463,9 @@ func (s *Server) handleTree(w http.ResponseWriter, r *http.Request) { "Projects": parentOptionsFromItems(items), "BasePath": "/", "ProjectChipTarget": "#tree-section", + "ViewType": view, + "ViewTypeChips": ViewTypeChips("/", filter, view), + "CardItems": cardItems, // ActiveTags kept for backwards-compat with the old template path; removed // after the template migrates fully. "ActiveTags": filter.Tags, diff --git a/web/server_test.go b/web/server_test.go index 6af9aaf..b7c8b85 100644 --- a/web/server_test.go +++ b/web/server_test.go @@ -295,6 +295,41 @@ func TestMultiParentBothPathsRouteToSameRow(t *testing.T) { } } +// TestTreeRendersCardGridWhenViewTypeIsCard verifies Phase 5i Slice B +// dispatch: `?view_type=card` renders the flat tile grid instead of the +// forest, and the view-type chip strip is present in either view. Unknown +// view_type values fall back to list with the chip-strip showing list as +// active. +func TestTreeRendersCardGridWhenViewTypeIsCard(t *testing.T) { + srv, pool := mustServer(t) + defer pool.Close() + h := srv.Routes() + // List view (default): forest markup expected; tree-card-grid absent. + _, listBody := get(t, h, "/") + if !strings.Contains(listBody, `