feat(calendar): polish grid styling + mobile breakpoint + design doc

Phase 5e slice B. Polish pass on the month grid: HTMX-swappable filter
chip strip, mobile breakpoint that collapses the 7-column table into a
vertical list of days, refined CSS for hover/today/adjacent-month, and
the docs/design.md §17 entry that pins the contract.

Templates:
- web/templates/calendar_section.tmpl (new) — extracted #calendar-section
  partial. Houses the filter chip strip (form with hx-get=/calendar
  hx-target=#calendar-section), counts line, and the grid <table>.
- web/templates/calendar.tmpl trimmed to the page chrome (h1, prev/next
  nav, today link) + {{template "calendar-section" .}}. Chrome stays
  outside the HTMX swap because chip filtering preserves the month
  context.

web/calendar.go:
- handleCalendar now branches on HX-Request: HTMX → calendar_section
  fragment, full GET → calendar (chrome + section). Same pattern as
  /timeline and /dashboard.
- calendarDay gains LongLabel ("Mi., 14. Mai") — populated by new
  formatCalendarLongLabel helper. Hidden on desktop via CSS; revealed at
  the ≤480px breakpoint where the column header drops out.

web/server.go:
- Calendar template now bundles the section partial. New calendar_section
  template registered as a standalone fragment for HTMX swaps. New
  render() entry case "calendar_section" → "calendar-section".

web/static/style.css:
- Refined .calendar-nav (tabular numerals, transition, no surface-alt
  fallback fighting the theme).
- New #calendar-filterbar layout (flex, gap, counts pushed right).
- .calendar-cell hover background, adjacent-month opacity bump (0.4→0.45
  + 0.7 on hover so it doesn't disappear when reading lead-in days).
- .today-pill line-height fix so it sits flush in the cell header.
- .cell-row min-width on .time slot, tighter line-height, 0.82em font.
- @media (max-width: 480px) breakpoint: grid + thead + tbody + tr + th +
  td all → display:block. Thead hidden; .day-label revealed. Adjacent-
  month cells DISPLAY:NONE on mobile (their value on desktop is grid
  rectangularity; on a vertical list they're just confusing). Cell rows
  bump to 0.95em for readability.

docs/design.md:
- New §17 Calendar view (Phase 5e). Documents sources (VEVENT/VTODO/
  dated item_links), what's excluded (creation markers + Gitea + untimed),
  the layout calculation, filter integration via TreeFilter, cache key,
  the mobile breakpoint, and the German register choice.

Tests (additive, all passing):
- TestFormatCalendarLongLabel — pins the German weekday + day + month
  abbreviation (Mo./Di./.../So., 1.–31., Jan/Feb/März/.../Dez).
- TestCalendarFilterChipStripRenders — chip strip present + hx-target +
  hx-get + hidden month input + tag/mgmt/kind multi-selects.
- TestCalendarHTMXReturnsSectionOnly — HX-Request returns #calendar-
  section only (no <body>, no .calendar-nav chrome).
- TestCalendarCellCarriesLongLabel — May 4 cell ("Mo., 4. Mai") present
  in HTML so the mobile breakpoint CSS reveal works.

Net: +315 / -61.
This commit is contained in:
mAi
2026-05-22 12:07:25 +02:00
parent e5dd31144a
commit 28ac919e01
8 changed files with 315 additions and 61 deletions

View File

@@ -274,16 +274,23 @@ func New(s *store.Store, logger *slog.Logger) (*Server, error) {
}
pages["timeline_section"] = timelineSection
// Calendar page — month grid view, Phase 5e. No HTMX fragment yet;
// filter/month changes are full-page nav.
// Calendar page — month grid view, Phase 5e. Bundles the section
// partial so HTMX swaps (filter chip strip) and the full-page render
// share definitions.
calTmpl, err := template.New("calendar").Funcs(funcs).ParseFS(templatesFS,
"templates/layout.tmpl",
"templates/calendar.tmpl",
"templates/calendar_section.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")
if err != nil {
return nil, fmt.Errorf("parse calendar_section: %w", err)
}
pages["calendar_section"] = calSection
// Bulk-edit page + its fragment + per-row chip cells. The chip cells share
// definitions with bulk_section so we parse them together every time.
@@ -903,6 +910,8 @@ func (s *Server) render(w http.ResponseWriter, r *http.Request, name string, dat
entry = "dashboard-section"
case "timeline_section":
entry = "timeline-section"
case "calendar_section":
entry = "calendar-section"
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := t.ExecuteTemplate(w, entry, data); err != nil {