Merge branch 'mai/kahn/phase-5i-phase-a-design' (phase 5i slice A: project filter dim + descendants toggle)

# Conflicts:
#	web/dashboard.go
#	web/server.go
#	web/templates/dashboard_section.tmpl
This commit is contained in:
mAi
2026-05-26 13:29:20 +02:00
17 changed files with 1046 additions and 58 deletions

View File

@@ -162,18 +162,24 @@ func New(s *store.Store, logger *slog.Logger) (*Server, error) {
pages[name] = t
}
// tree bundles the tree-section partial so HTMX swaps and the initial
// page render share definitions.
// page render share definitions. project_chip.tmpl is the Phase 5i Slice
// A shared partial that every Views-supporting page includes inside its
// filter strip.
treeTmpl, err := template.New("tree").Funcs(funcs).ParseFS(templatesFS,
"templates/layout.tmpl",
"templates/tree.tmpl",
"templates/tree_section.tmpl",
"templates/project_chip.tmpl",
)
if err != nil {
return nil, fmt.Errorf("parse tree: %w", err)
}
pages["tree"] = treeTmpl
// Standalone tree-section template for HTMX fragment responses.
treeSection, err := template.New("tree_section").Funcs(funcs).ParseFS(templatesFS, "templates/tree_section.tmpl")
treeSection, err := template.New("tree_section").Funcs(funcs).ParseFS(templatesFS,
"templates/tree_section.tmpl",
"templates/project_chip.tmpl",
)
if err != nil {
return nil, fmt.Errorf("parse tree_section: %w", err)
}
@@ -251,6 +257,7 @@ func New(s *store.Store, logger *slog.Logger) (*Server, error) {
"templates/dashboard.tmpl",
"templates/dashboard_section.tmpl",
"templates/dashboard_tiles.tmpl",
"templates/project_chip.tmpl",
)
if err != nil {
return nil, fmt.Errorf("parse dashboard: %w", err)
@@ -259,6 +266,7 @@ func New(s *store.Store, logger *slog.Logger) (*Server, error) {
dashSection, err := template.New("dashboard_section").Funcs(funcs).ParseFS(templatesFS,
"templates/dashboard_section.tmpl",
"templates/dashboard_tiles.tmpl",
"templates/project_chip.tmpl",
)
if err != nil {
return nil, fmt.Errorf("parse dashboard_section: %w", err)
@@ -270,12 +278,16 @@ func New(s *store.Store, logger *slog.Logger) (*Server, error) {
"templates/layout.tmpl",
"templates/timeline.tmpl",
"templates/timeline_section.tmpl",
"templates/project_chip.tmpl",
)
if err != nil {
return nil, fmt.Errorf("parse timeline: %w", err)
}
pages["timeline"] = timelineTmpl
timelineSection, err := template.New("timeline_section").Funcs(funcs).ParseFS(templatesFS, "templates/timeline_section.tmpl")
timelineSection, err := template.New("timeline_section").Funcs(funcs).ParseFS(templatesFS,
"templates/timeline_section.tmpl",
"templates/project_chip.tmpl",
)
if err != nil {
return nil, fmt.Errorf("parse timeline_section: %w", err)
}
@@ -288,12 +300,16 @@ func New(s *store.Store, logger *slog.Logger) (*Server, error) {
"templates/layout.tmpl",
"templates/calendar.tmpl",
"templates/calendar_section.tmpl",
"templates/project_chip.tmpl",
)
if err != nil {
return nil, fmt.Errorf("parse calendar: %w", err)
}
pages["calendar"] = calTmpl
calSection, err := template.New("calendar_section").Funcs(funcs).ParseFS(templatesFS, "templates/calendar_section.tmpl")
calSection, err := template.New("calendar_section").Funcs(funcs).ParseFS(templatesFS,
"templates/calendar_section.tmpl",
"templates/project_chip.tmpl",
)
if err != nil {
return nil, fmt.Errorf("parse calendar_section: %w", err)
}
@@ -427,15 +443,18 @@ func (s *Server) handleTree(w http.ResponseWriter, r *http.Request) {
roots, orphans, total, orphanN, matched := applyTreeFilter(items, filter, linkKinds)
counts := computeChipCounts(items, filter, linkKinds, tags)
data := map[string]any{
"Title": "tree",
"Roots": roots,
"Orphans": orphans,
"Total": total,
"OrphanN": orphanN,
"Matched": matched,
"AllTags": tags,
"Filter": filter,
"Counts": counts,
"Title": "tree",
"Roots": roots,
"Orphans": orphans,
"Total": total,
"OrphanN": orphanN,
"Matched": matched,
"AllTags": tags,
"Filter": filter,
"Counts": counts,
"Projects": parentOptionsFromItems(items),
"BasePath": "/",
"ProjectChipTarget": "#tree-section",
// ActiveTags kept for backwards-compat with the old template path; removed
// after the template migrates fully.
"ActiveTags": filter.Tags,
@@ -880,15 +899,20 @@ func (s *Server) parentOptions(ctx context.Context) ([]ParentOption, error) {
if err != nil {
return nil, err
}
var out []ParentOption
return parentOptionsFromItems(items), nil
}
// parentOptionsFromItems builds the same flat option list parentOptions
// returns, but from an already-loaded items slice. Callers that have already
// fetched items (handleTree, handleDashboard, …) use this to avoid a second
// ListAll round-trip when they only need the picker options.
func parentOptionsFromItems(items []*store.Item) []ParentOption {
out := make([]ParentOption, 0, len(items))
for _, it := range items {
// Surface every primary path as a candidate parent — multi-parent
// items appear once per parent option using their primary path so the
// UI stays unambiguous.
out = append(out, ParentOption{ID: it.ID, Path: it.PrimaryPath()})
}
sort.Slice(out, func(i, j int) bool { return out[i].Path < out[j].Path })
return out, nil
return out
}
// (buildForest + nodeHasAllTags removed in Phase 3b — superseded by