Skip to content

Commit 68a1fa6

Browse files
cpcloudcursoragent
andcommitted
feat(ui): add calendar date picker for inline date editing
Inline editing a date column now opens a visual calendar widget instead of a plain text input. The calendar shows the month grid with today, selected date, and cursor highlighted. Navigate with h/l (day), j/k (week), H/L (month), enter to pick, esc to cancel. - New calendarState type with cursor, selected, field pointer, and callback - calendarGrid renders a bordered month view with Wong palette styles (CalCursor, CalSelected, CalToday) - Calendar overlay composited via bubbletea-overlay, stacks with other overlays - openDatePicker helper wires calendar confirm to form submit + reload - All inline date edits (projects, quotes, maintenance, appliances, service logs) route through the calendar instead of huh.Input - Full forms retain text inputs for dates (calendar is inline-only) - Help overlay includes Date Picker section - Keybindings doc updated with date picker and note preview sections - Extracted keyEsc constant to satisfy goconst lint Tests: - 12 new tests: grid rendering, day/week/month movement, month boundary crossing, daysIn, sameDay, key navigation, confirm writes date, esc cancels, renders in view, month nav, empty value defaults to today [DATEPICKER] Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 590a6c0 commit 68a1fa6

File tree

8 files changed

+540
-33
lines changed

8 files changed

+540
-33
lines changed

AGENTS.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,3 +1029,15 @@ in case things crash or otherwise go haywire, be diligent about this.
10291029
## Questions
10301030
- Why are some values pointers to numbers instead of just the number? E.g.,
10311031
HOAFeeCents and PropertyTaxCents. Why aren't those just plain int64s?
1032+
1033+
## Moar
1034+
- for the calendar widget, let's make ctrl+shift+h/l moves years
1035+
- let's make sure that deleting, even soft deleting doesn't break the model,
1036+
e.g., if i try to delete a quote that's linked to a project, i get
1037+
a reasonable error message, probably in the status bar but open to thoughts
1038+
on where to show it
1039+
- would be nice to have a way to hide completed projects easily. we'll get to
1040+
the generic way to do that when we implement filter, but i think it will
1041+
still be useful as a standalone feature
1042+
- seems like vendors should have a drilldown link to quotes, that would effectively show the quote history for a vendor
1043+
- so for the house brick animation on the main website, the reanimation of hte fallen bricks kind of snaps back into place at the last step

docs/content/reference/keybindings.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Complete reference of every keybinding in micasa, organized by mode.
5252

5353
| Key | Action |
5454
|---------|--------|
55-
| `enter` | Drilldown into detail view, or follow FK link |
55+
| `enter` | Drilldown into detail view, follow FK link, or preview notes |
5656
| `i` | Enter Edit mode |
5757
| `?` | Open help overlay |
5858
| `q` | Quit (exit code 0) |
@@ -75,7 +75,7 @@ Same as Normal mode, except `d` and `u` are rebound:
7575
| Key | Action |
7676
|-------|--------|
7777
| `a` | Add new entry to current tab |
78-
| `e` | Edit current cell inline, or full form if cell is read-only |
78+
| `e` | Edit current cell inline (date columns open calendar picker), or full form if cell is read-only |
7979
| `d` | Toggle delete/restore on selected row |
8080
| `x` | Toggle visibility of soft-deleted rows |
8181
| `p` | Edit house profile |
@@ -107,6 +107,24 @@ When the dashboard overlay is open:
107107
| `?` | Open help overlay (stacks on dashboard) |
108108
| `q` | Quit |
109109

110+
## Date picker
111+
112+
When inline editing a date column, a calendar widget opens instead of a text
113+
input:
114+
115+
| Key | Action |
116+
|-----------|--------|
117+
| `h`/`l` | Move one day left/right |
118+
| `j`/`k` | Move one week down/up |
119+
| `H`/`L` | Move one month back/forward |
120+
| `enter` | Pick the highlighted date |
121+
| `esc` | Cancel (keep original value) |
122+
123+
## Note preview
124+
125+
Press `enter` on a notes column (e.g., service log Notes) to open a read-only
126+
overlay showing the full text. Any key dismisses it.
127+
110128
## Help overlay
111129

112130
| Key | Action |

internal/app/calendar.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright 2026 Phillip Cloud
2+
// Licensed under the Apache License, Version 2.0
3+
4+
package app
5+
6+
import (
7+
"fmt"
8+
"strings"
9+
"time"
10+
11+
"github.com/charmbracelet/lipgloss"
12+
)
13+
14+
// calendarState tracks the date picker overlay.
15+
type calendarState struct {
16+
Cursor time.Time // the date the cursor is on
17+
Selected time.Time // the date the field currently has (dim highlight)
18+
HasValue bool // whether Selected is meaningful
19+
FieldPtr *string // pointer to the form field's value string
20+
OnConfirm func() // called after writing the picked date to FieldPtr
21+
}
22+
23+
// calendarGrid renders a single month calendar with the cursor highlighted.
24+
func calendarGrid(cal calendarState, styles Styles) string {
25+
cursor := cal.Cursor
26+
year, month := cursor.Year(), cursor.Month()
27+
28+
// Header: month name + year.
29+
header := lipgloss.NewStyle().
30+
Bold(true).
31+
Foreground(accent).
32+
Render(fmt.Sprintf(" %s %d ", month.String(), year))
33+
34+
// Day-of-week labels.
35+
dayLabels := lipgloss.NewStyle().
36+
Foreground(textDim).
37+
Render("Su Mo Tu We Th Fr Sa")
38+
39+
// Build the day grid.
40+
first := time.Date(year, month, 1, 0, 0, 0, 0, time.Local)
41+
startDow := int(first.Weekday()) // 0=Sun
42+
daysInMonth := daysIn(year, month)
43+
44+
var grid strings.Builder
45+
// Leading blanks.
46+
for i := 0; i < startDow; i++ {
47+
grid.WriteString(" ")
48+
}
49+
50+
for day := 1; day <= daysInMonth; day++ {
51+
date := time.Date(year, month, day, 0, 0, 0, 0, time.Local)
52+
label := fmt.Sprintf("%2d", day)
53+
54+
isCursor := sameDay(date, cursor)
55+
isSelected := cal.HasValue && sameDay(date, cal.Selected)
56+
isToday := sameDay(date, time.Now())
57+
58+
var style lipgloss.Style
59+
switch {
60+
case isCursor:
61+
style = styles.CalCursor
62+
case isSelected:
63+
style = styles.CalSelected
64+
case isToday:
65+
style = styles.CalToday
66+
default:
67+
style = lipgloss.NewStyle()
68+
}
69+
70+
grid.WriteString(style.Render(label))
71+
72+
dow := (startDow + day - 1) % 7
73+
if dow == 6 && day < daysInMonth {
74+
grid.WriteString("\n")
75+
} else if day < daysInMonth {
76+
grid.WriteString(" ")
77+
}
78+
}
79+
80+
// Navigation hints.
81+
hints := lipgloss.NewStyle().
82+
Foreground(textDim).
83+
Render("h/l day j/k week H/L month enter pick esc cancel")
84+
85+
return lipgloss.JoinVertical(
86+
lipgloss.Left,
87+
header,
88+
"",
89+
dayLabels,
90+
grid.String(),
91+
"",
92+
hints,
93+
)
94+
}
95+
96+
func daysIn(year int, month time.Month) int {
97+
return time.Date(year, month+1, 0, 0, 0, 0, 0, time.UTC).Day()
98+
}
99+
100+
func sameDay(a, b time.Time) bool {
101+
return a.Year() == b.Year() && a.Month() == b.Month() && a.Day() == b.Day()
102+
}
103+
104+
// calendarMove adjusts the calendar cursor by the given number of days.
105+
func calendarMove(cal *calendarState, days int) {
106+
cal.Cursor = cal.Cursor.AddDate(0, 0, days)
107+
}
108+
109+
// calendarMoveMonth adjusts the calendar cursor by the given number of months.
110+
func calendarMoveMonth(cal *calendarState, months int) {
111+
cal.Cursor = cal.Cursor.AddDate(0, months, 0)
112+
}

0 commit comments

Comments
 (0)