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:
@@ -695,7 +695,35 @@ flexsiebels' Go (or Deno) backend POSTs to `https://projax.msbls.de/mcp/rpc` wit
|
||||
- Asset hosting for screenshots — projax stores URLs; m hosts images wherever already-deployed (Imgur, S3, static-asset endpoint, …).
|
||||
- A publish workflow with approval stages — single boolean is enough.
|
||||
|
||||
## 9. Phase-1 deliverable checklist
|
||||
## 17. Calendar view (Phase 5e)
|
||||
|
||||
Month grid at `/calendar?month=YYYY-MM` — the fourth dated surface, sibling to `/timeline` (chronological spine), `/dashboard` (today/week buckets), and `/graph` (DAG topology). Same `internal/aggregate.Aggregator` data pipeline as timeline; different presentation.
|
||||
|
||||
**Sources** (per cell, anchor date is local-zone midnight of the row's date):
|
||||
|
||||
1. **CalDAV VEVENTs** in the grid window (`[gridStart, gridEnd)`). Event start used as the anchor; the cell shows `HH:MM Summary` (or just `Summary` for all-day).
|
||||
2. **CalDAV VTODOs** with `DUE` in the window. Open todos anchor on `DUE`; completed/cancelled todos in the last 14 days anchor on `LastModified`. Overdue (DUE before today, still open) renders with a warn-coloured border accent.
|
||||
3. **Dated `projax.item_links`** with `event_date` in the window. Note text is the row summary; ref_id's last path segment is the fallback. Muted border accent.
|
||||
|
||||
Not surfaced: item-creation markers (too noisy for a month grid), Gitea issues (no date anchor), untimed items (calendar is fundamentally date-scoped).
|
||||
|
||||
**Layout** — `web/calendar.go layoutCalendarWeeks` builds the rectangular grid:
|
||||
|
||||
- 7 columns Mon→Sun. `mondayWeekday(t)` converts Go's Sunday=0 default to the German Monday=0 convention.
|
||||
- Leading days from the previous month fill the first row's gap before the 1st. Trailing days from the next month pad to the last row's Sunday. Both carry `IsAdjacent` so CSS can grey them out.
|
||||
- Each cell caps visible rows at `calendarMaxRowsPerCell` (3). Overflow becomes "+N more" linking to `/timeline?from=YYYY-MM-DD&to=YYYY-MM-DD` for a focused single-day view.
|
||||
- Today's cell carries `IsToday` → CSS adds an accent border + "Heute" pill.
|
||||
- Per-cell rows sort: timed first (by `HH:MM`), then by kind rank (event < todo < doc), then by summary.
|
||||
|
||||
**Filter integration** — reuses `TreeFilter` from `web/tree_filter.go`. Same query keys (`q`, `tag`, `mgmt`, `has`) plus a calendar-specific `kind=event,todo,doc` multi-select. The chip strip uses HTMX `hx-target=#calendar-section` for in-place swaps; the page chrome (month label + prev/next nav) stays outside the swap because chip filtering doesn't change month.
|
||||
|
||||
**Cache** — `cache.TTLCache[*calendarPayload]` keyed by `(filter, month, kinds)` at 60s, matching the dashboard's cadence. `?refresh=1` invalidates the entire calendar cache.
|
||||
|
||||
**Mobile breakpoint** (≤480px) — the 7-column grid is unreadable on a 360px-wide phone, so the CSS collapses to a vertical list-of-days. The `LongLabel` field (e.g. "Mi., 14. Mai") is hidden on desktop and revealed at the breakpoint to compensate for the absent weekday column header. Adjacent-month cells drop out entirely on mobile so the list is calendar-scoped.
|
||||
|
||||
**German register** — month and weekday labels in German throughout (`Mai 2026`, `heute`, `Heute`, `Mi., 14. Mai`). Rest of the app stays English; the calendar surface reads more naturally in German for m's usage.
|
||||
|
||||
|
||||
|
||||
- [ ] `projax.items` + `projax.item_links` migrations in `db/migrations/`
|
||||
- [ ] Path trigger + tests
|
||||
|
||||
@@ -2,6 +2,7 @@ package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -48,11 +49,12 @@ type calendarDay struct {
|
||||
Date time.Time
|
||||
DateKey string // "2026-05-15"
|
||||
DayNum int // 1-31
|
||||
LongLabel string // "Mi., 14. Mai" — shown by CSS only at the mobile breakpoint
|
||||
IsToday bool
|
||||
IsAdjacent bool // belongs to prev/next month
|
||||
IsAdjacent bool // belongs to prev/next month
|
||||
Rows []calendarRow // capped at calendarMaxRowsPerCell
|
||||
ExtraCount int // rows hidden under the +N more link
|
||||
TotalRows int // total before capping (Rows + ExtraCount)
|
||||
ExtraCount int // rows hidden under the +N more link
|
||||
TotalRows int // total before capping (Rows + ExtraCount)
|
||||
}
|
||||
|
||||
// calendarRow is one stack-able marker rendered inside a cell. Kind drives
|
||||
@@ -189,6 +191,10 @@ func (s *Server) handleCalendar(w http.ResponseWriter, r *http.Request) {
|
||||
"Query": q,
|
||||
"Now": now,
|
||||
}
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
s.render(w, r, "calendar_section", data)
|
||||
return
|
||||
}
|
||||
s.render(w, r, "calendar", data)
|
||||
}
|
||||
|
||||
@@ -372,6 +378,7 @@ func layoutCalendarWeeks(monthStart, gridStart, gridEnd, today time.Time, byDay
|
||||
Date: day,
|
||||
DateKey: day.Format("2006-01-02"),
|
||||
DayNum: day.Day(),
|
||||
LongLabel: formatCalendarLongLabel(day),
|
||||
IsToday: day.Equal(today),
|
||||
IsAdjacent: day.Before(monthStart) || !day.Before(monthEnd),
|
||||
}
|
||||
@@ -418,6 +425,18 @@ func formatMonthLabel(t time.Time) string {
|
||||
return months[int(t.Month())-1] + " " + t.Format("2006")
|
||||
}
|
||||
|
||||
// formatCalendarLongLabel renders the per-cell long German label —
|
||||
// "Mi., 14. Mai" — used by the mobile breakpoint where each cell becomes a
|
||||
// stacked block and the bare day number no longer carries enough context.
|
||||
func formatCalendarLongLabel(t time.Time) string {
|
||||
weekdays := []string{"So.", "Mo.", "Di.", "Mi.", "Do.", "Fr.", "Sa."}
|
||||
months := []string{
|
||||
"Jan", "Feb", "März", "Apr", "Mai", "Juni",
|
||||
"Juli", "Aug", "Sept", "Okt", "Nov", "Dez",
|
||||
}
|
||||
return fmt.Sprintf("%s, %d. %s", weekdays[int(t.Weekday())], t.Day(), months[int(t.Month())-1])
|
||||
}
|
||||
|
||||
// docSummary picks a human-readable single-line summary for a dated
|
||||
// item_link. Prefers the note, then ref_id's last path segment, then
|
||||
// ref_id verbatim.
|
||||
|
||||
@@ -2,6 +2,8 @@ package web_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -179,3 +181,72 @@ func TestCalendarNavPrevNextLinks(t *testing.T) {
|
||||
t.Errorf("expected next link to 2026-06, body did not include it")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalendarFilterChipStripRenders proves the HTMX filter chip strip
|
||||
// (Phase 5e slice B) is rendered above the grid with the hx-target
|
||||
// pointing at #calendar-section so chip changes swap only the data and
|
||||
// leave the month-label chrome alone.
|
||||
func TestCalendarFilterChipStripRenders(t *testing.T) {
|
||||
srv, pool := mustServer(t)
|
||||
defer pool.Close()
|
||||
h := srv.Routes()
|
||||
_, body := get(t, h, "/calendar?month=2026-05")
|
||||
for _, want := range []string{
|
||||
`id="calendar-filterbar"`,
|
||||
`hx-target="#calendar-section"`,
|
||||
`hx-get="/calendar"`,
|
||||
`<input type="hidden" name="month" value="2026-05">`, // preserves month across chip changes
|
||||
`name="kind"`,
|
||||
`name="tag"`,
|
||||
`name="mgmt"`,
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("calendar body missing %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalendarHTMXReturnsSectionOnly proves an HX-Request returns just
|
||||
// the calendar-section fragment (no layout chrome) so the filter chip
|
||||
// strip can swap the grid in place without re-rendering the page shell.
|
||||
func TestCalendarHTMXReturnsSectionOnly(t *testing.T) {
|
||||
srv, pool := mustServer(t)
|
||||
defer pool.Close()
|
||||
h := srv.Routes()
|
||||
req := httptest.NewRequest("GET", "/calendar?month=2026-05", nil)
|
||||
req.Header.Set("HX-Request", "true")
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
body, _ := io.ReadAll(w.Result().Body)
|
||||
bs := string(body)
|
||||
if w.Result().StatusCode != 200 {
|
||||
t.Fatalf("HTMX /calendar → %d body=%s", w.Result().StatusCode, bs)
|
||||
}
|
||||
if !strings.Contains(bs, `id="calendar-section"`) {
|
||||
t.Errorf("HTMX response missing #calendar-section: %s", bs)
|
||||
}
|
||||
// Layout chrome (e.g. the <nav class="calendar-nav"> month label) lives
|
||||
// OUTSIDE the section in calendar.tmpl, so an HTMX fragment must not
|
||||
// include it. If it does, the chip-strip swap would double up the nav.
|
||||
if strings.Contains(bs, `class="calendar-nav"`) {
|
||||
t.Errorf("HTMX response should not include the page chrome (.calendar-nav)")
|
||||
}
|
||||
if strings.Contains(bs, `<!doctype html>`) || strings.Contains(bs, "<body>") {
|
||||
t.Errorf("HTMX response should be a fragment, not a full document: %s", bs)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalendarCellCarriesLongLabel proves the per-cell long German label
|
||||
// is in the rendered HTML so the mobile breakpoint CSS (≤480px) can
|
||||
// reveal it. The label compensates for the column-header weekday strip
|
||||
// that the mobile breakpoint removes.
|
||||
func TestCalendarCellCarriesLongLabel(t *testing.T) {
|
||||
srv, pool := mustServer(t)
|
||||
defer pool.Close()
|
||||
h := srv.Routes()
|
||||
_, body := get(t, h, "/calendar?month=2026-05")
|
||||
// May 4 2026 is a Monday → "Mo., 4. Mai".
|
||||
if !strings.Contains(body, `Mo., 4. Mai`) {
|
||||
t.Errorf("expected long label 'Mo., 4. Mai' for 2026-05-04 cell, body did not include it")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +156,27 @@ func TestMondayWeekday(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestFormatCalendarLongLabel exercises the per-cell long German label
|
||||
// used by the mobile breakpoint (hidden on desktop via CSS). The label
|
||||
// matters because on a 360px-wide phone the bare day number no longer
|
||||
// carries weekday context — the CSS collapses the column header.
|
||||
func TestFormatCalendarLongLabel(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"2026-05-04": "Mo., 4. Mai", // Monday
|
||||
"2026-05-14": "Do., 14. Mai", // Thursday
|
||||
"2026-03-15": "So., 15. März", // Sunday in March (uses März)
|
||||
"2026-12-31": "Do., 31. Dez",
|
||||
}
|
||||
for in, want := range cases {
|
||||
t.Run(in, func(t *testing.T) {
|
||||
d, _ := time.Parse("2006-01-02", in)
|
||||
if got := formatCalendarLongLabel(d); got != want {
|
||||
t.Errorf("formatCalendarLongLabel(%s) = %q, want %q", in, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFormatMonthLabel confirms the German month names render — m reads
|
||||
// /calendar primarily in the German register and the label is the most
|
||||
// visible piece of chrome.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -761,18 +761,32 @@ details.proj-section > summary.proj-section-summary small { font-weight: 400; }
|
||||
.calendar-header h1 { margin: 0; }
|
||||
.calendar-nav {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
align-items: baseline;
|
||||
}
|
||||
.calendar-nav a {
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
padding: 2px 8px;
|
||||
padding: 2px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
transition: color 0.12s, border-color 0.12s, background 0.12s;
|
||||
}
|
||||
.calendar-nav a:hover { color: var(--accent); border-color: var(--accent); }
|
||||
.calendar-nav .today { background: var(--surface-alt, transparent); }
|
||||
.calendar-nav .today { font-weight: 600; }
|
||||
#calendar-filterbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
margin: 8px 0 12px;
|
||||
}
|
||||
#calendar-filterbar .counts {
|
||||
margin: 0 0 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
.calendar-grid {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -792,8 +806,11 @@ details.proj-section > summary.proj-section-summary small { font-weight: 400; }
|
||||
padding: 4px 6px;
|
||||
height: 110px;
|
||||
width: 14.2857%; /* 1/7 */
|
||||
transition: background 0.12s, border-color 0.12s;
|
||||
}
|
||||
.calendar-cell.adjacent-month { opacity: 0.4; }
|
||||
.calendar-cell:hover { background: var(--surface-alt, rgba(127, 127, 127, 0.06)); }
|
||||
.calendar-cell.adjacent-month { opacity: 0.45; }
|
||||
.calendar-cell.adjacent-month:hover { opacity: 0.7; }
|
||||
.calendar-cell.is-today {
|
||||
border-color: var(--accent);
|
||||
box-shadow: inset 0 0 0 1px var(--accent);
|
||||
@@ -802,35 +819,44 @@ details.proj-section > summary.proj-section-summary small { font-weight: 400; }
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.cell-header .day-num {
|
||||
font-size: 0.95em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cell-header .day-label {
|
||||
/* Hidden on desktop — only the day number is needed when the weekday
|
||||
header runs across the top of the grid. The mobile breakpoint shows
|
||||
it. */
|
||||
display: none;
|
||||
}
|
||||
.today-pill {
|
||||
font-size: 0.7em;
|
||||
font-size: 0.65em;
|
||||
background: var(--accent);
|
||||
color: var(--accent-fg);
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.cell-rows { list-style: none; padding: 0; margin: 0; }
|
||||
.cell-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
font-size: 0.85em;
|
||||
font-size: 0.82em;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.cell-row .time { color: var(--muted); font-variant-numeric: tabular-nums; }
|
||||
.cell-row .time { color: var(--muted); font-variant-numeric: tabular-nums; min-width: 32px; }
|
||||
.cell-row .row-link { color: inherit; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
||||
.cell-row .row-link:hover { color: var(--accent); }
|
||||
.cell-row.row-event { border-left: 3px solid var(--accent); padding-left: 6px; }
|
||||
@@ -844,3 +870,37 @@ details.proj-section > summary.proj-section-summary small { font-weight: 400; }
|
||||
text-decoration: none;
|
||||
}
|
||||
.cell-more:hover { color: var(--accent); text-decoration: underline; }
|
||||
|
||||
/* Mobile breakpoint — collapse the 7-column grid to a vertical list. A
|
||||
horizontal week grid on a 360px-wide phone is unreadable; stacking each
|
||||
day as its own row keeps the rows full-width and surfaces the long
|
||||
weekday label that was hidden on desktop. Adjacent-month cells drop
|
||||
out completely on mobile so the list is calendar-scoped. */
|
||||
@media (max-width: 480px) {
|
||||
.calendar-grid, .calendar-grid thead, .calendar-grid tbody,
|
||||
.calendar-grid tr, .calendar-grid th, .calendar-grid td {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.calendar-grid thead { display: none; }
|
||||
.calendar-week { margin-bottom: 4px; }
|
||||
.calendar-cell {
|
||||
height: auto;
|
||||
min-height: 48px;
|
||||
padding: 6px 10px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.calendar-cell.adjacent-month { display: none; }
|
||||
.cell-header .day-label {
|
||||
display: inline;
|
||||
font-size: 0.95em;
|
||||
color: var(--muted);
|
||||
margin-left: 6px;
|
||||
}
|
||||
.cell-header .day-num { font-size: 1.1em; }
|
||||
.cell-row { font-size: 0.95em; padding: 4px 6px; }
|
||||
.calendar-nav { flex-wrap: wrap; }
|
||||
#calendar-filterbar { gap: 8px; }
|
||||
#calendar-filterbar .counts { margin-left: 0; }
|
||||
}
|
||||
|
||||
@@ -1,58 +1,13 @@
|
||||
{{define "content"}}
|
||||
<section class="calendar" id="calendar-section">
|
||||
<section class="calendar">
|
||||
<header class="calendar-header">
|
||||
<h1>{{.P.MonthLabel}}</h1>
|
||||
<nav class="calendar-nav" aria-label="Monthsnavigation">
|
||||
<nav class="calendar-nav" aria-label="Monatsnavigation">
|
||||
<a class="prev" href="/calendar?month={{.P.PrevMonth}}{{with .Filter.QueryString}}&{{.}}{{end}}">< {{.P.PrevMonth}}</a>
|
||||
<a class="today" href="/calendar{{with .Filter.QueryString}}?{{.}}{{end}}">heute</a>
|
||||
<a class="next" href="/calendar?month={{.P.NextMonth}}{{with .Filter.QueryString}}&{{.}}{{end}}">{{.P.NextMonth}} ></a>
|
||||
</nav>
|
||||
<p class="counts muted">
|
||||
<small>{{.P.TotalRows}} {{if eq .P.TotalRows 1}}row{{else}}rows{{end}} · {{join " · " .P.Kinds}}</small>
|
||||
{{if .P.Cached}}<small title="Served from 60s in-memory cache · built {{.P.BuiltAt.Format "15:04:05"}}">· cached</small>{{else}}<small>· fresh</small>{{end}}
|
||||
{{if .Filter.Active}} · <a href="/calendar?month={{.P.MonthKey}}">clear filters</a>{{end}}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<table class="calendar-grid" role="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Mon</th>
|
||||
<th scope="col">Tue</th>
|
||||
<th scope="col">Wed</th>
|
||||
<th scope="col">Thu</th>
|
||||
<th scope="col">Fri</th>
|
||||
<th scope="col">Sat</th>
|
||||
<th scope="col">Sun</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .P.Weeks}}
|
||||
<tr class="calendar-week">
|
||||
{{range .Days}}
|
||||
<td class="calendar-cell{{if .IsToday}} is-today{{end}}{{if .IsAdjacent}} adjacent-month{{end}}" data-date="{{.DateKey}}">
|
||||
<div class="cell-header">
|
||||
<span class="day-num">{{.DayNum}}</span>
|
||||
{{if .IsToday}}<span class="today-pill">Heute</span>{{end}}
|
||||
</div>
|
||||
{{if .Rows}}
|
||||
<ul class="cell-rows">
|
||||
{{range .Rows}}
|
||||
<li class="cell-row row-{{.Kind}}{{if .Overdue}} overdue{{end}}">
|
||||
{{if .Time}}<span class="time">{{.Time}}</span>{{end}}
|
||||
<a class="row-link" href="/i/{{.ItemPath}}" title="{{.ItemPath}}">{{.Summary}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
{{if gt .ExtraCount 0}}
|
||||
<a class="cell-more muted" href="/timeline?from={{.DateKey}}&to={{.DateKey}}">+{{.ExtraCount}} more</a>
|
||||
{{end}}
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{template "calendar-section" .}}
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
91
web/templates/calendar_section.tmpl
Normal file
91
web/templates/calendar_section.tmpl
Normal file
@@ -0,0 +1,91 @@
|
||||
{{define "calendar-section"}}
|
||||
<div id="calendar-section" class="calendar-section">
|
||||
|
||||
<section class="tagbar" id="calendar-filterbar">
|
||||
<form id="calendar-filter" class="search"
|
||||
hx-get="/calendar"
|
||||
hx-target="#calendar-section"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="change from:select"
|
||||
hx-push-url="true">
|
||||
<input type="hidden" name="month" value="{{.P.MonthKey}}">
|
||||
<label>tag
|
||||
<select name="tag" multiple size="3">
|
||||
{{range $.Filter.Tags}}<option value="{{.}}" selected>{{.}}</option>{{end}}
|
||||
</select>
|
||||
</label>
|
||||
<label>mgmt
|
||||
<select name="mgmt" multiple size="3">
|
||||
{{$selM := .Filter.Management}}
|
||||
<option value="mai" {{if contains $selM "mai"}}selected{{end}}>mai</option>
|
||||
<option value="self" {{if contains $selM "self"}}selected{{end}}>self</option>
|
||||
<option value="external" {{if contains $selM "external"}}selected{{end}}>external</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>has
|
||||
<select name="has" multiple size="2">
|
||||
{{$selH := .Filter.HasLinks}}
|
||||
<option value="caldav-list" {{if contains $selH "caldav-list"}}selected{{end}}>caldav</option>
|
||||
<option value="gitea-repo" {{if contains $selH "gitea-repo"}}selected{{end}}>gitea</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>kind
|
||||
<select name="kind" multiple size="3">
|
||||
{{$selK := .P.Kinds}}
|
||||
<option value="event" {{if contains $selK "event"}}selected{{end}}>event</option>
|
||||
<option value="todo" {{if contains $selK "todo"}}selected{{end}}>todo</option>
|
||||
<option value="doc" {{if contains $selK "doc"}}selected{{end}}>doc</option>
|
||||
</select>
|
||||
</label>
|
||||
{{if .Filter.Active}}<a class="clear" href="/calendar?month={{.P.MonthKey}}">clear filters</a>{{end}}
|
||||
</form>
|
||||
<p class="counts muted">
|
||||
<small>{{.P.TotalRows}} {{if eq .P.TotalRows 1}}row{{else}}rows{{end}}</small>
|
||||
{{if .P.Cached}}<small title="Served from 60s in-memory cache · built {{.P.BuiltAt.Format "15:04:05"}}">· cached</small>{{else}}<small>· fresh</small>{{end}}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<table class="calendar-grid" role="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Mon</th>
|
||||
<th scope="col">Tue</th>
|
||||
<th scope="col">Wed</th>
|
||||
<th scope="col">Thu</th>
|
||||
<th scope="col">Fri</th>
|
||||
<th scope="col">Sat</th>
|
||||
<th scope="col">Sun</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .P.Weeks}}
|
||||
<tr class="calendar-week">
|
||||
{{range .Days}}
|
||||
<td class="calendar-cell{{if .IsToday}} is-today{{end}}{{if .IsAdjacent}} adjacent-month{{end}}" data-date="{{.DateKey}}">
|
||||
<div class="cell-header">
|
||||
<span class="day-num">{{.DayNum}}</span>
|
||||
<span class="day-label">{{.LongLabel}}</span>
|
||||
{{if .IsToday}}<span class="today-pill">Heute</span>{{end}}
|
||||
</div>
|
||||
{{if .Rows}}
|
||||
<ul class="cell-rows">
|
||||
{{range .Rows}}
|
||||
<li class="cell-row row-{{.Kind}}{{if .Overdue}} overdue{{end}}">
|
||||
{{if .Time}}<span class="time">{{.Time}}</span>{{end}}
|
||||
<a class="row-link" href="/i/{{.ItemPath}}" title="{{.ItemPath}}">{{.Summary}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
{{if gt .ExtraCount 0}}
|
||||
<a class="cell-more muted" href="/timeline?from={{.DateKey}}&to={{.DateKey}}">+{{.ExtraCount}} more</a>
|
||||
{{end}}
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user