From e18c83b618ba7e3b1001716b8935ab7c15e89e08 Mon Sep 17 00:00:00 2001 From: arnef Date: Tue, 1 Sep 2026 19:10:17 +0200 Subject: [PATCH] feat(web): add calendar event detail view --- internal/web/calendar.go | 65 ++ internal/web/event_detail_test.go | 209 ++++++ internal/web/server.go | 1 + internal/web/templates/calendar.templ | 130 +++- internal/web/templates/calendar_templ.go | 896 +++++++++++++++-------- internal/web/templates/calendar_test.go | 48 ++ web/static/app.css | 2 +- 7 files changed, 1025 insertions(+), 326 deletions(-) create mode 100644 internal/web/event_detail_test.go diff --git a/internal/web/calendar.go b/internal/web/calendar.go index 728b519..fb19c32 100644 --- a/internal/web/calendar.go +++ b/internal/web/calendar.go @@ -18,6 +18,7 @@ import ( "git.arnef.de/arnef/nidus/internal/birthdays" "git.arnef.de/arnef/nidus/internal/db" "git.arnef.de/arnef/nidus/internal/icalfix" + "git.arnef.de/arnef/nidus/internal/store" "git.arnef.de/arnef/nidus/internal/web/templates" ical "github.com/emersion/go-ical" ) @@ -636,6 +637,70 @@ func newEventFormDefaults(calRef, dateParam string) templates.EventFormData { } } +// handleEventView renders the read-only detail view for a single event. It +// is the landing page when a user clicks an event in the month/week grid; +// writable calendars link from here to the edit page. +func (s *Server) handleEventView(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.Header().Set("Allow", "GET") + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + username := userFromContext(r.Context()) + ref := r.PathValue("ref") + id := r.PathValue("id") + + if !eventIDRe.MatchString(id) { + http.NotFound(w, r) + return + } + color, form, err := s.eventForDisplay(username, ref, id) + if errors.Is(err, store.ErrNotFound) { + http.NotFound(w, r) + return + } + if err != nil { + s.handleCalRefError(w, r, err) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _ = templates.EventDetail(username, templates.EventDetailData{Form: form, Color: color}).Render(context.Background(), w) +} + +// eventForDisplay resolves a calendar reference for read access, loads and +// decodes the event with the given id, populates the form's display fields +// (CalRef, CalendarLabel, Writable), and returns the calendar's color for +// the detail view's header dot. +func (s *Server) eventForDisplay(username, ref, id string) (color string, form templates.EventFormData, err error) { + owner, name, err := s.resolveCalRef(username, ref, false) + if err != nil { + return "", form, err + } + data, err := s.store.GetObject(owner, "cal-"+name, id) + if err != nil { + return "", form, err + } + form, err = eventFormFromICS(id, data) + if err != nil { + s.logger.Error("decoding event", "error", err) + return "", form, err + } + form.CalRef = ref + label := name + if owner != username { + label = name + " (" + s.dbase.DisplayName(owner) + ")" + } + form.CalendarLabel = label + form.Writable = owner == username + if !form.Writable { + if share, err := s.dbase.CalendarShareFor(owner, name, username); err == nil { + form.Writable = share.Permission == db.PermWrite + } + } + color, _ = s.dbase.GetCalendarColor(owner, name) + return color, form, nil +} + func (s *Server) handleEventEdit(w http.ResponseWriter, r *http.Request) { username := userFromContext(r.Context()) ref := r.PathValue("ref") diff --git a/internal/web/event_detail_test.go b/internal/web/event_detail_test.go new file mode 100644 index 0000000..9608f42 --- /dev/null +++ b/internal/web/event_detail_test.go @@ -0,0 +1,209 @@ +package web + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "git.arnef.de/arnef/nidus/internal/db" +) + +// seedEvent stores an all-day event (one-day span) in owner's calendar cal +// under object id, with the given summary/location/description. Used to set +// up events that handleEventView should render. +func seedEvent(t *testing.T, s *Server, owner, cal, id, summary, location, description string) { + t.Helper() + data := "BEGIN:VCALENDAR\r\n" + + "VERSION:2.0\r\n" + + "PRODID:-//nidus//test//EN\r\n" + + "BEGIN:VEVENT\r\n" + + "UID:" + strings.TrimSuffix(id, ".ics") + "\r\n" + + "DTSTAMP:20260101T000000Z\r\n" + + "SUMMARY:" + summary + "\r\n" + + "LOCATION:" + location + "\r\n" + + "DESCRIPTION:" + description + "\r\n" + + "DTSTART;VALUE=DATE:20260805\r\n" + + "DTEND;VALUE=DATE:20260806\r\n" + + "END:VEVENT\r\n" + + "END:VCALENDAR\r\n" + if err := s.store.PutObject(owner, "cal-"+cal, id, []byte(data)); err != nil { + t.Fatalf("PutObject: %v", err) + } +} + +// getEventDetail issues GET /calendar/{ref}/{id} as the session identified by +// cookie and returns the recorded response. +func getEventDetail(t *testing.T, handler http.Handler, cookie *http.Cookie, ref, id string) *httptest.ResponseRecorder { + t.Helper() + path := "/calendar/" + url.PathEscape(ref) + "/" + url.PathEscape(id) + req := httptest.NewRequest(http.MethodGet, path, nil) + req.AddCookie(cookie) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + return rr +} + +func TestEventDetailShowsOwnEvent(t *testing.T) { + s := newTestServer(t) + handler := s.Handler(emptyStaticFS{}) + cookie := loginAs(t, handler, "alice", "password") + + id := "aabbccddeeff.ics" + seedEvent(t, s, "alice", "work", id, "Team standup", "Meetroom A", "Daily sync with the team") + + rr := getEventDetail(t, handler, cookie, "work", id) + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String()) + } + body := rr.Body.String() + for _, want := range []string{ + "Team standup", // summary / title + "Meetroom A", // location + "Daily sync with the team", // description + "work", // calendar label + ">Edit", // writable → Edit link present + "Export .ics", + } { + if !strings.Contains(body, want) { + t.Errorf("expected body to contain %q, got:\n%s", want, body) + } + } +} + +func TestEventDetailReadOnlySharedHasNoEdit(t *testing.T) { + s := newTestServer(t) + handler := s.Handler(emptyStaticFS{}) + aliceCookie := loginAs(t, handler, "alice", "password") + bobCookie := loginAs(t, handler, "bob", "password") + + id := "123456.ics" + seedEvent(t, s, "bob", "personal", id, "Bob lunch", "Cafe", "Lunch plans") + if err := s.dbase.ShareCalendar("bob", "personal", "alice", db.PermRead); err != nil { + t.Fatalf("ShareCalendar: %v", err) + } + + // alice (read share) can view but not edit. + rr := getEventDetail(t, handler, aliceCookie, "bob~personal", id) + if rr.Code != http.StatusOK { + t.Fatalf("alice view: expected 200, got %d: %s", rr.Code, rr.Body.String()) + } + body := rr.Body.String() + if !strings.Contains(body, "Bob lunch") { + t.Errorf("alice: expected body to contain summary, got:\n%s", body) + } + if !strings.Contains(body, "shared with you as read-only") { + t.Errorf("alice: expected read-only notice, got:\n%s", body) + } + if strings.Contains(body, ">Edit") { + t.Errorf("alice: Edit link should not be present on a read-only share, got:\n%s", body) + } + + // bob (owner) can still see the Edit link. + rrBob := getEventDetail(t, handler, bobCookie, "personal", id) + if rrBob.Code != http.StatusOK { + t.Fatalf("bob view own: expected 200, got %d", rrBob.Code) + } + if !strings.Contains(rrBob.Body.String(), ">Edit") { + t.Errorf("bob: expected Edit link, got:\n%s", rrBob.Body.String()) + } +} + +func TestEventDetailWriteShareHasEditLink(t *testing.T) { + s := newTestServer(t) + handler := s.Handler(emptyStaticFS{}) + cookie := loginAs(t, handler, "alice", "password") + + seedEvent(t, s, "bob", "personal", "a1b2c3.ics", "Bob meeting", "Office", "Sync") + if err := s.dbase.ShareCalendar("bob", "personal", "alice", db.PermWrite); err != nil { + t.Fatalf("ShareCalendar: %v", err) + } + rr := getEventDetail(t, handler, cookie, "bob~personal", "a1b2c3.ics") + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String()) + } + body := rr.Body.String() + if !strings.Contains(body, ">Edit") { + t.Errorf("write share should expose Edit link, got:\n%s", body) + } + if strings.Contains(body, "shared with you as read-only") { + t.Errorf("write share should not show read-only notice, got:\n%s", body) + } +} + +func TestEventDetailNotFound(t *testing.T) { + s := newTestServer(t) + handler := s.Handler(emptyStaticFS{}) + cookie := loginAs(t, handler, "alice", "password") + + // Valid calendar, missing object. + if rr := getEventDetail(t, handler, cookie, "work", "doesnotexist.ics"); rr.Code != http.StatusNotFound { + t.Fatalf("missing event: expected 404, got %d", rr.Code) + } + // Unknown calendar ref. + if rr := getEventDetail(t, handler, cookie, "does_not_exist", "0000.ics"); rr.Code != http.StatusNotFound { + t.Fatalf("unknown calendar: expected 404, got %d", rr.Code) + } + // Invalid id shape (rejected by eventIDRe before store access). + if rr := getEventDetail(t, handler, cookie, "work", "bad/../etc/passwd.ics"); rr.Code != http.StatusNotFound { + t.Fatalf("invalid id: expected 404, got %d", rr.Code) + } +} + +func TestEventDetailRequiresLogin(t *testing.T) { + s := newTestServer(t) + handler := s.Handler(emptyStaticFS{}) + req := httptest.NewRequest(http.MethodGet, "/calendar/work/anything.ics", nil) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + if rr.Code != http.StatusSeeOther { + t.Fatalf("expected redirect to login, got %d", rr.Code) + } +} + +func TestEventDetailRejectsNonGet(t *testing.T) { + s := newTestServer(t) + handler := s.Handler(emptyStaticFS{}) + cookie := loginAs(t, handler, "alice", "password") + req := httptest.NewRequest(http.MethodPost, "/calendar/work/anything.ics", nil) + req.AddCookie(cookie) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + if rr.Code != http.StatusMethodNotAllowed { + t.Fatalf("expected 405 for POST, got %d", rr.Code) + } +} + +// TestEditRouteStillResolves guards against the new detail route +// (/calendar/{ref}/{id}) shadowing the more specific edit/delete/export +// routes under the same {ref}+{id} prefix in Go's ServeMux. +func TestEditRouteStillResolves(t *testing.T) { + s := newTestServer(t) + handler := s.Handler(emptyStaticFS{}) + cookie := loginAs(t, handler, "alice", "password") + + id := "cafebabe00.ics" + seedEvent(t, s, "alice", "work", id, "Edit me", "Room", "Note") + + // GET the edit form — must still hit handleEventEdit, not the detail view. + req := httptest.NewRequest(http.MethodGet, "/calendar/work/cafebabe00.ics/edit", nil) + req.AddCookie(cookie) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("edit GET: expected 200, got %d: %s", rr.Code, rr.Body.String()) + } + if !strings.Contains(rr.Body.String(), "Edit event") || !strings.Contains(rr.Body.String(), " @@ -301,7 +310,7 @@ func eventLinkURL(ev EventSummary) string { if ev.LinkURL != "" { return ev.LinkURL } - return "/web/calendar/" + ev.CalRef + "/" + ev.ID + "/edit" + return "/web/calendar/" + ev.CalRef + "/" + ev.ID } // eventTextColor returns a color derived from hex, darkened if needed so @@ -355,6 +364,119 @@ func clampByte(v float64) int { +// EventDetail is the read-only detail view for a single event, opened when +// the user clicks an event in the month or week grid. It shows the +// event's fields and, when the calendar is writable, offers an Edit link. +templ EventDetail(username string, data EventDetailData) { + @Layout("Calendar", username) { +
+ ← Calendar +
+ if data.Form.Writable { + + Edit + + } + + Export .ics + +
+
+ +
+
+ +
+

{ data.Form.Summary }

+

{ data.Form.CalendarLabel }

+
+
+ + if !data.Form.Writable { +

+ This calendar was shared with you as read-only — you can view this event but not change it. +

+ } + +
+
+
When
+
{ eventRangeText(data.Form) }
+
+ if data.Form.Location != "" { +
+
Location
+
{ data.Form.Location }
+
+ } + if data.Form.Description != "" { +
+
Description
+
{ data.Form.Description }
+
+ } +
+
+ } +} + +// eventRangeText renders a human-readable "when" string for an event: +// a single timed event reads "Aug 5, 2026, 14:00 – 15:00"; a multi-day +// range reads "Aug 5 – 7, 2026"; a single all-day date reads +// "August 5, 2026". +func eventRangeText(f EventFormData) string { + if f.AllDay { + if f.StartDate != f.EndDate { + s, err := dateOnly(f.StartDate) + if err != nil { + return f.StartDate + } + e, err := dateOnly(f.EndDate) + if err != nil { + return f.EndDate + } + if s.Year() == e.Year() && s.Month() == e.Month() { + return fmt.Sprintf("%s – %s, %d", s.Format("Jan 2"), e.Format("2"), s.Year()) + } + return fmt.Sprintf("%s – %s", s.Format("Jan 2, 2006"), e.Format("Jan 2, 2006")) + } + d, err := dateOnly(f.StartDate) + if err != nil { + return f.StartDate + } + return d.Format("January 2, 2006") + } + + s, err := dateTime(f.StartDate, f.StartTime) + if err != nil { + return f.StartDate + } + e, err := dateTime(f.EndDate, f.EndTime) + if err != nil { + return s.Format("January 2, 2006, 15:04") + } + if s.Day() == e.Day() { + return fmt.Sprintf("%s, %s – %s", s.Format("January 2, 2006"), s.Format("15:04"), e.Format("15:04")) + } + if s.Year() == e.Year() && s.Month() == e.Month() { + return fmt.Sprintf("%s – %s, %d", s.Format("Jan 2, 15:04"), e.Format("15:04"), s.Year()) + } + return fmt.Sprintf("%s – %s", s.Format("Jan 2, 2006, 15:04"), e.Format("Jan 2, 2006, 15:04")) +} + +func dateOnly(ds string) (time.Time, error) { + return time.ParseInLocation("2006-01-02", ds, time.Local) +} + +func dateTime(ds, ts string) (time.Time, error) { + if ts == "" { + return time.ParseInLocation("2006-01-02", ds, time.Local) + } + return time.ParseInLocation("2006-01-02T15:04", ds+"T"+ts, time.Local) +} + templ EventForm(username string, data EventFormData, errMsg string) { @Layout("Calendar", username) { ← Calendar diff --git a/internal/web/templates/calendar_templ.go b/internal/web/templates/calendar_templ.go index 3513fc9..d4e4f70 100644 --- a/internal/web/templates/calendar_templ.go +++ b/internal/web/templates/calendar_templ.go @@ -11,6 +11,7 @@ import templruntime "github.com/a-h/templ/runtime" import "fmt" import "strconv" import "strings" +import "time" // CalendarSummary is one calendar (own or shared) shown in the combined // month view's legend. @@ -32,9 +33,9 @@ type EventSummary struct { Summary string TimeText string // e.g. "14:00" or "" for all-day events AllDay bool - // LinkURL overrides the default "/web/calendar/{CalRef}/{ID}/edit" - // link target, used by virtual/read-only calendars (e.g. birthdays) - // that don't have an editable event object of their own. + // LinkURL overrides the default "/web/calendar/{CalRef}/{ID}" (detail + // view) link target, used by virtual/read-only calendars (e.g. + // birthdays) that don't have an editable event object of their own. LinkURL string } @@ -107,6 +108,14 @@ type EventFormData struct { EndTime string // "HH:MM", empty when AllDay } +// EventDetailData is everything the read-only event detail view needs: the +// decoded event itself (Form, including its ID/CalRef/Writable display +// metadata) plus the calendar's color for the header dot. +type EventDetailData struct { + Form EventFormData + Color string // calendar color, "" if unset +} + func MonthView(username string, data MonthViewData) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context @@ -147,7 +156,7 @@ func MonthView(username string, data MonthViewData) templ.Component { var templ_7745c5c3_Var3 templ.SafeURL templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(data.PrevMonthURL)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 108, Col: 42} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 117, Col: 42} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -160,7 +169,7 @@ func MonthView(username string, data MonthViewData) templ.Component { var templ_7745c5c3_Var4 templ.SafeURL templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(data.TodayURL)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 109, Col: 38} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 118, Col: 38} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -173,7 +182,7 @@ func MonthView(username string, data MonthViewData) templ.Component { var templ_7745c5c3_Var5 templ.SafeURL templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(data.NextMonthURL)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 110, Col: 42} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 119, Col: 42} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -186,7 +195,7 @@ func MonthView(username string, data MonthViewData) templ.Component { var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.MonthLabel) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 111, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 120, Col: 60} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -228,7 +237,7 @@ func MonthView(username string, data MonthViewData) templ.Component { var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(wd) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 131, Col: 92} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 140, Col: 92} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -312,7 +321,7 @@ func viewSwitcher(active, monthURL, weekURL string) templ.Component { var templ_7745c5c3_Var10 templ.SafeURL templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(monthURL)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 156, Col: 29} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 165, Col: 29} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { @@ -347,7 +356,7 @@ func viewSwitcher(active, monthURL, weekURL string) templ.Component { var templ_7745c5c3_Var13 templ.SafeURL templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(weekURL)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 162, Col: 28} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 171, Col: 28} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { @@ -402,7 +411,7 @@ func calendarLegendRow(c CalendarSummary) templ.Component { var templ_7745c5c3_Var16 string templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-color: " + colorOrDefault(c.Color)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 172, Col: 132} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 181, Col: 132} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { @@ -415,7 +424,7 @@ func calendarLegendRow(c CalendarSummary) templ.Component { var templ_7745c5c3_Var17 string templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(c.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 173, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 182, Col: 36} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { @@ -433,7 +442,7 @@ func calendarLegendRow(c CalendarSummary) templ.Component { var templ_7745c5c3_Var18 string templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(c.Owner) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 175, Col: 58} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 184, Col: 58} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { @@ -462,7 +471,7 @@ func calendarLegendRow(c CalendarSummary) templ.Component { var templ_7745c5c3_Var19 templ.SafeURL templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL("/web/calendar/" + c.Ref + "/export")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 182, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 191, Col: 60} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { @@ -481,7 +490,7 @@ func calendarLegendRow(c CalendarSummary) templ.Component { var templ_7745c5c3_Var20 templ.SafeURL templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL("/web/calendar/" + c.Ref + "/import")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 185, Col: 79} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 194, Col: 79} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) if templ_7745c5c3_Err != nil { @@ -555,7 +564,7 @@ func monthDayCell(day MonthDay) templ.Component { var templ_7745c5c3_Var25 templ.SafeURL templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL("/web/calendar/new?date=" + day.Date)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 197, Col: 58} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 206, Col: 58} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) if templ_7745c5c3_Err != nil { @@ -581,7 +590,7 @@ func monthDayCell(day MonthDay) templ.Component { var templ_7745c5c3_Var27 string templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(day.Day)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 201, Col: 25} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 210, Col: 25} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) if templ_7745c5c3_Err != nil { @@ -599,7 +608,7 @@ func monthDayCell(day MonthDay) templ.Component { var templ_7745c5c3_Var28 templ.SafeURL templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(eventLinkURL(ev))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 206, Col: 38} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 215, Col: 38} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) if templ_7745c5c3_Err != nil { @@ -612,7 +621,7 @@ func monthDayCell(day MonthDay) templ.Component { var templ_7745c5c3_Var29 string templ_7745c5c3_Var29, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-color: " + colorOrDefault(ev.Color) + "22; color: " + eventTextColor(ev.Color)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 208, Col: 102} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 217, Col: 102} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) if templ_7745c5c3_Err != nil { @@ -625,7 +634,7 @@ func monthDayCell(day MonthDay) templ.Component { var templ_7745c5c3_Var30 string templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(ev.Summary) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 209, Col: 22} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 218, Col: 22} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30) if templ_7745c5c3_Err != nil { @@ -638,7 +647,7 @@ func monthDayCell(day MonthDay) templ.Component { var templ_7745c5c3_Var31 string templ_7745c5c3_Var31, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-color: " + eventTextColor(ev.Color)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 211, Col: 148} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 220, Col: 148} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { @@ -656,7 +665,7 @@ func monthDayCell(day MonthDay) templ.Component { var templ_7745c5c3_Var32 string templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(ev.TimeText) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 213, Col: 44} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 222, Col: 44} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) if templ_7745c5c3_Err != nil { @@ -670,7 +679,7 @@ func monthDayCell(day MonthDay) templ.Component { var templ_7745c5c3_Var33 string templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(" " + ev.Summary) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 215, Col: 22} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 224, Col: 22} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) if templ_7745c5c3_Err != nil { @@ -731,7 +740,7 @@ func WeekView(username string, data WeekViewData) templ.Component { var templ_7745c5c3_Var36 templ.SafeURL templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(data.PrevWeekURL)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 228, Col: 41} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 237, Col: 41} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) if templ_7745c5c3_Err != nil { @@ -744,7 +753,7 @@ func WeekView(username string, data WeekViewData) templ.Component { var templ_7745c5c3_Var37 templ.SafeURL templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(data.TodayURL)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 229, Col: 38} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 238, Col: 38} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) if templ_7745c5c3_Err != nil { @@ -757,7 +766,7 @@ func WeekView(username string, data WeekViewData) templ.Component { var templ_7745c5c3_Var38 templ.SafeURL templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(data.NextWeekURL)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 230, Col: 41} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 239, Col: 41} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) if templ_7745c5c3_Err != nil { @@ -770,7 +779,7 @@ func WeekView(username string, data WeekViewData) templ.Component { var templ_7745c5c3_Var39 string templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(data.RangeLabel) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 231, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 240, Col: 60} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) if templ_7745c5c3_Err != nil { @@ -887,7 +896,7 @@ func weekDayHeaderCell(day WeekDay) templ.Component { var templ_7745c5c3_Var43 string templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(day.Weekday) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 270, Col: 15} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 279, Col: 15} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) if templ_7745c5c3_Err != nil { @@ -900,7 +909,7 @@ func weekDayHeaderCell(day WeekDay) templ.Component { var templ_7745c5c3_Var44 string templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(day.Month) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 270, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 279, Col: 57} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) if templ_7745c5c3_Err != nil { @@ -913,7 +922,7 @@ func weekDayHeaderCell(day WeekDay) templ.Component { var templ_7745c5c3_Var45 string templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(day.Day)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 270, Col: 88} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 279, Col: 88} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) if templ_7745c5c3_Err != nil { @@ -955,7 +964,7 @@ func weekDayCell(day WeekDay) templ.Component { var templ_7745c5c3_Var47 templ.SafeURL templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL("/web/calendar/new?date=" + day.Date)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 277, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 286, Col: 57} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) if templ_7745c5c3_Err != nil { @@ -973,7 +982,7 @@ func weekDayCell(day WeekDay) templ.Component { var templ_7745c5c3_Var48 templ.SafeURL templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(eventLinkURL(ev))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 285, Col: 38} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 294, Col: 38} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) if templ_7745c5c3_Err != nil { @@ -986,7 +995,7 @@ func weekDayCell(day WeekDay) templ.Component { var templ_7745c5c3_Var49 string templ_7745c5c3_Var49, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-color: " + colorOrDefault(ev.Color) + "22; color: " + eventTextColor(ev.Color)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 287, Col: 102} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 296, Col: 102} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) if templ_7745c5c3_Err != nil { @@ -999,7 +1008,7 @@ func weekDayCell(day WeekDay) templ.Component { var templ_7745c5c3_Var50 string templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.ResolveAttributeValue(ev.Summary) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 288, Col: 22} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 297, Col: 22} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var50) if templ_7745c5c3_Err != nil { @@ -1012,7 +1021,7 @@ func weekDayCell(day WeekDay) templ.Component { var templ_7745c5c3_Var51 string templ_7745c5c3_Var51, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-color: " + eventTextColor(ev.Color)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 290, Col: 148} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 299, Col: 148} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) if templ_7745c5c3_Err != nil { @@ -1030,7 +1039,7 @@ func weekDayCell(day WeekDay) templ.Component { var templ_7745c5c3_Var52 string templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(ev.TimeText) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 292, Col: 44} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 301, Col: 44} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) if templ_7745c5c3_Err != nil { @@ -1044,7 +1053,7 @@ func weekDayCell(day WeekDay) templ.Component { var templ_7745c5c3_Var53 string templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(" " + ev.Summary) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 294, Col: 22} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 303, Col: 22} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53)) if templ_7745c5c3_Err != nil { @@ -1067,7 +1076,7 @@ func eventLinkURL(ev EventSummary) string { if ev.LinkURL != "" { return ev.LinkURL } - return "/web/calendar/" + ev.CalRef + "/" + ev.ID + "/edit" + return "/web/calendar/" + ev.CalRef + "/" + ev.ID } // eventTextColor returns a color derived from hex, darkened if needed so @@ -1119,7 +1128,10 @@ func clampByte(v float64) int { return int(v) } -func EventForm(username string, data EventFormData, errMsg string) templ.Component { +// EventDetail is the read-only detail view for a single event, opened when +// the user clicks an event in the month or week grid. It shows the +// event's fields and, when the calendar is writable, offers an Edit link. +func EventDetail(username string, data EventDetailData) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -1152,345 +1164,147 @@ func EventForm(username string, data EventFormData, errMsg string) templ.Compone }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "← Calendar

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "
← Calendar
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if data.ID == "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "New event") + if data.Form.Writable { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if errMsg != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var56 string - templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 369, Col: 98} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 376, Col: 92} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "\" class=\"bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700\">Edit
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if data.ID != "" && !data.Writable { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "

This calendar was shared with you as read-only — you can view this event but not change it.

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "
Export .ics
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if data.ID == "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var60 string - templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.JoinStringErrs(data.CalendarLabel) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 391, Col: 63} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\">

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var59 string + templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(data.Form.Summary) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 392, Col: 85} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var60 string + templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.JoinStringErrs(data.Form.CalendarLabel) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 393, Col: 68} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !data.Form.Writable { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "

This calendar was shared with you as read-only — you can view this event but not change it.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "
When
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var61 string - templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Summary) + templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(eventRangeText(data.Form)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 397, Col: 67} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 406, Col: 68} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var61) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if data.AllDay { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, " checked") + if data.Form.Location != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "
Location
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var62 string + templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(data.Form.Location) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 411, Col: 74} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "> All-day event
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var63 = []any{templ.KV("hidden", data.AllDay)} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var63...) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var67 = []any{templ.KV("hidden", data.AllDay)} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var67...) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if data.ID == "" || data.Writable { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, " ") + if data.Form.Description != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "
Description
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var63 string + templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(data.Form.Description) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 417, Col: 97} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - if data.ID != "" && data.Writable { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - if data.ID != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "Export .ics") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1504,6 +1318,446 @@ func EventForm(username string, data EventFormData, errMsg string) templ.Compone }) } +// eventRangeText renders a human-readable "when" string for an event: +// a single timed event reads "Aug 5, 2026, 14:00 – 15:00"; a multi-day +// range reads "Aug 5 – 7, 2026"; a single all-day date reads +// "August 5, 2026". +func eventRangeText(f EventFormData) string { + if f.AllDay { + if f.StartDate != f.EndDate { + s, err := dateOnly(f.StartDate) + if err != nil { + return f.StartDate + } + e, err := dateOnly(f.EndDate) + if err != nil { + return f.EndDate + } + if s.Year() == e.Year() && s.Month() == e.Month() { + return fmt.Sprintf("%s – %s, %d", s.Format("Jan 2"), e.Format("2"), s.Year()) + } + return fmt.Sprintf("%s – %s", s.Format("Jan 2, 2006"), e.Format("Jan 2, 2006")) + } + d, err := dateOnly(f.StartDate) + if err != nil { + return f.StartDate + } + return d.Format("January 2, 2006") + } + + s, err := dateTime(f.StartDate, f.StartTime) + if err != nil { + return f.StartDate + } + e, err := dateTime(f.EndDate, f.EndTime) + if err != nil { + return s.Format("January 2, 2006, 15:04") + } + if s.Day() == e.Day() { + return fmt.Sprintf("%s, %s – %s", s.Format("January 2, 2006"), s.Format("15:04"), e.Format("15:04")) + } + if s.Year() == e.Year() && s.Month() == e.Month() { + return fmt.Sprintf("%s – %s, %d", s.Format("Jan 2, 15:04"), e.Format("15:04"), s.Year()) + } + return fmt.Sprintf("%s – %s", s.Format("Jan 2, 2006, 15:04"), e.Format("Jan 2, 2006, 15:04")) +} + +func dateOnly(ds string) (time.Time, error) { + return time.ParseInLocation("2006-01-02", ds, time.Local) +} + +func dateTime(ds, ts string) (time.Time, error) { + if ts == "" { + return time.ParseInLocation("2006-01-02", ds, time.Local) + } + return time.ParseInLocation("2006-01-02T15:04", ds+"T"+ts, time.Local) +} + +func EventForm(username string, data EventFormData, errMsg string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var64 := templ.GetChildren(ctx) + if templ_7745c5c3_Var64 == nil { + templ_7745c5c3_Var64 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var65 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "← Calendar

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.ID == "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "New event") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "Edit event") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if errMsg != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var66 string + templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 491, Col: 98} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.ID != "" && !data.Writable { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "

This calendar was shared with you as read-only — you can view this event but not change it.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.ID == "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var70 string + templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(data.CalendarLabel) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 513, Col: 63} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var73 = []any{templ.KV("hidden", data.AllDay)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var73...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var77 = []any{templ.KV("hidden", data.AllDay)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var77...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.ID == "" || data.Writable { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.ID != "" && data.Writable { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.ID != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "Export .ics") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = Layout("Calendar", username).Render(templ.WithChildren(ctx, templ_7745c5c3_Var65), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + func eventFormAction(data EventFormData) templ.SafeURL { if data.ID == "" { return templ.URL("/web/calendar/new") diff --git a/internal/web/templates/calendar_test.go b/internal/web/templates/calendar_test.go index 2c99b7f..8ae8112 100644 --- a/internal/web/templates/calendar_test.go +++ b/internal/web/templates/calendar_test.go @@ -33,6 +33,54 @@ func TestEventTextColorDarkensLightColors(t *testing.T) { } } +// TestEventRangeText pins the human-readable "When" strings shown on the +// event detail view so any change to date formatting is intentional. +func TestEventRangeText(t *testing.T) { + cases := []struct { + name string + form EventFormData + want string + }{ + { + name: "single allday date", + form: EventFormData{AllDay: true, StartDate: "2026-08-05", EndDate: "2026-08-05"}, + want: "August 5, 2026", + }, + { + name: "all-day multi-day same month", + form: EventFormData{AllDay: true, StartDate: "2026-08-05", EndDate: "2026-08-07"}, + want: "Aug 5 – 7, 2026", + }, + { + name: "all-day multi-day different months", + form: EventFormData{AllDay: true, StartDate: "2026-08-30", EndDate: "2026-09-02"}, + want: "Aug 30, 2026 – Sep 2, 2026", + }, + { + name: "timed same day", + form: EventFormData{AllDay: false, StartDate: "2026-08-05", StartTime: "14:00", EndDate: "2026-08-05", EndTime: "15:00"}, + want: "August 5, 2026, 14:00 – 15:00", + }, + { + name: "timed different days same month", + form: EventFormData{AllDay: false, StartDate: "2026-08-05", StartTime: "23:30", EndDate: "2026-08-06", EndTime: "01:00"}, + want: "Aug 5, 23:30 – 01:00, 2026", + }, + { + name: "timed different months", + form: EventFormData{AllDay: false, StartDate: "2026-08-31", StartTime: "10:00", EndDate: "2026-09-01", EndTime: "11:00"}, + want: "Aug 31, 2026, 10:00 – Sep 1, 2026, 11:00", + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if got := eventRangeText(c.form); got != c.want { + t.Errorf("eventRangeText(%+v) = %q, want %q", c.form, got, c.want) + } + }) + } +} + func TestEventTextColorFallsBackForInvalidInput(t *testing.T) { if got := eventTextColor(""); got != colorOrDefault("") { t.Errorf("eventTextColor(\"\") = %s, want the same default colorOrDefault returns", got) diff --git a/web/static/app.css b/web/static/app.css index dabf945..0715016 100644 --- a/web/static/app.css +++ b/web/static/app.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-700:oklch(55.5% .163 48.998);--color-green-50:oklch(98.2% .018 155.826);--color-green-200:oklch(92.5% .084 155.995);--color-green-700:oklch(52.7% .154 150.069);--color-indigo-50:oklch(96.2% .018 272.314);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-500:oklch(58.5% .233 277.117);--color-indigo-600:oklch(51.1% .262 276.966);--color-indigo-700:oklch(45.7% .24 277.023);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-lg:32rem;--container-xl:36rem;--container-4xl:56rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wide:.025em;--radius-md:.375rem;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.invisible{visibility:hidden}.visible{visibility:visible}.relative{position:relative}.static{position:static}.col-span-2{grid-column:span 2/span 2}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.-mx-3{margin-inline:calc(var(--spacing) * -3)}.mx-auto{margin-inline:auto}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mr-1{margin-right:var(--spacing)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-1{margin-left:var(--spacing)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-3{height:calc(var(--spacing) * 3)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-20{height:calc(var(--spacing) * 20)}.h-24{height:calc(var(--spacing) * 24)}.h-full{height:100%}.min-h-\[4\.5rem\]{min-height:4.5rem}.min-h-\[24rem\]{min-height:24rem}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-3{width:calc(var(--spacing) * 3)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-8{width:calc(var(--spacing) * 8)}.w-10{width:calc(var(--spacing) * 10)}.w-12{width:calc(var(--spacing) * 12)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-24{width:calc(var(--spacing) * 24)}.w-36{width:calc(var(--spacing) * 36)}.w-auto{width:auto}.w-full{width:100%}.max-w-4xl{max-width:var(--container-4xl)}.max-w-\[8rem\]{max-width:8rem}.max-w-lg{max-width:var(--container-lg)}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.min-w-\[640px\]{min-width:640px}.flex-1{flex:1}.shrink-0{flex-shrink:0}.table-fixed{table-layout:fixed}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:var(--spacing)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-px{gap:1px}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-y-1{row-gap:var(--spacing)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.self-start{align-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-amber-200{border-color:var(--color-amber-200)}.border-gray-50{border-color:var(--color-gray-50)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-green-200{border-color:var(--color-green-200)}.border-indigo-400{border-color:var(--color-indigo-400)}.border-red-200{border-color:var(--color-red-200)}.bg-amber-50{background-color:var(--color-amber-50)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-green-50{background-color:var(--color-green-50)}.bg-indigo-50{background-color:var(--color-indigo-50)}.bg-indigo-600{background-color:var(--color-indigo-600)}.bg-red-50{background-color:var(--color-red-50)}.bg-white{background-color:var(--color-white)}.object-cover{object-fit:cover}.p-0{padding:0}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-1{padding:var(--spacing)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pl-6{padding-left:calc(var(--spacing) * 6)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.align-top{vertical-align:top}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.text-amber-700{color:var(--color-amber-700)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-900{color:var(--color-gray-900)}.text-green-700{color:var(--color-green-700)}.text-indigo-600{color:var(--color-indigo-600)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-black\/10{--tw-ring-color:#0000001a}@supports (color:color-mix(in lab, red, red)){.ring-black\/10{--tw-ring-color:color-mix(in oklab, var(--color-black) 10%, transparent)}}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ring-inset{--tw-ring-inset:inset}@media (hover:hover){.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-indigo-700:hover{background-color:var(--color-indigo-700)}.hover\:text-indigo-600:hover{color:var(--color-indigo-600)}.hover\:text-red-700:hover{color:var(--color-red-700)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-80:hover{opacity:.8}}.focus\:border-indigo-500:focus{border-color:var(--color-indigo-500)}.focus\:ring-indigo-500:focus{--tw-ring-color:var(--color-indigo-500)}@media (min-width:40rem){.sm\:mx-0{margin-inline:0}.sm\:mr-3{margin-right:calc(var(--spacing) * 3)}.sm\:hidden{display:none}.sm\:table{display:table}.sm\:table-cell{display:table-cell}.sm\:min-h-\[6rem\]{min-height:6rem}.sm\:w-auto{width:auto}.sm\:min-w-0{min-width:0}.sm\:flex-row{flex-direction:row}.sm\:items-end{align-items:flex-end}.sm\:gap-4{gap:calc(var(--spacing) * 4)}.sm\:gap-6{gap:calc(var(--spacing) * 6)}.sm\:p-1\.5{padding:calc(var(--spacing) * 1.5)}.sm\:px-0{padding-inline:0}.sm\:px-3{padding-inline:calc(var(--spacing) * 3)}.sm\:px-4{padding-inline:calc(var(--spacing) * 4)}.sm\:py-8{padding-block:calc(var(--spacing) * 8)}}@media (min-width:48rem){.md\:table-cell{display:table-cell}}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-700:oklch(55.5% .163 48.998);--color-green-50:oklch(98.2% .018 155.826);--color-green-200:oklch(92.5% .084 155.995);--color-green-700:oklch(52.7% .154 150.069);--color-indigo-50:oklch(96.2% .018 272.314);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-500:oklch(58.5% .233 277.117);--color-indigo-600:oklch(51.1% .262 276.966);--color-indigo-700:oklch(45.7% .24 277.023);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-4xl:56rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wide:.025em;--leading-tight:1.25;--radius-md:.375rem;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.invisible{visibility:hidden}.visible{visibility:visible}.relative{position:relative}.static{position:static}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.-mx-3{margin-inline:calc(var(--spacing) * -3)}.mx-auto{margin-inline:auto}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mr-1{margin-right:var(--spacing)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-1{margin-left:var(--spacing)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-3{height:calc(var(--spacing) * 3)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-20{height:calc(var(--spacing) * 20)}.h-24{height:calc(var(--spacing) * 24)}.h-full{height:100%}.min-h-\[4\.5rem\]{min-height:4.5rem}.min-h-\[24rem\]{min-height:24rem}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-3{width:calc(var(--spacing) * 3)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-8{width:calc(var(--spacing) * 8)}.w-10{width:calc(var(--spacing) * 10)}.w-12{width:calc(var(--spacing) * 12)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-24{width:calc(var(--spacing) * 24)}.w-36{width:calc(var(--spacing) * 36)}.w-auto{width:auto}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-\[8rem\]{max-width:8rem}.max-w-lg{max-width:var(--container-lg)}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.min-w-0{min-width:0}.min-w-\[640px\]{min-width:640px}.flex-1{flex:1}.shrink-0{flex-shrink:0}.table-fixed{table-layout:fixed}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:var(--spacing)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-px{gap:1px}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-y-1{row-gap:var(--spacing)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.self-start{align-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-amber-200{border-color:var(--color-amber-200)}.border-gray-50{border-color:var(--color-gray-50)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-green-200{border-color:var(--color-green-200)}.border-indigo-400{border-color:var(--color-indigo-400)}.border-red-200{border-color:var(--color-red-200)}.bg-amber-50{background-color:var(--color-amber-50)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-green-50{background-color:var(--color-green-50)}.bg-indigo-50{background-color:var(--color-indigo-50)}.bg-indigo-600{background-color:var(--color-indigo-600)}.bg-red-50{background-color:var(--color-red-50)}.bg-white{background-color:var(--color-white)}.object-cover{object-fit:cover}.p-0{padding:0}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-1{padding:var(--spacing)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.pl-6{padding-left:calc(var(--spacing) * 6)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.align-top{vertical-align:top}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-700{color:var(--color-amber-700)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-900{color:var(--color-gray-900)}.text-green-700{color:var(--color-green-700)}.text-indigo-600{color:var(--color-indigo-600)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-black\/10{--tw-ring-color:#0000001a}@supports (color:color-mix(in lab, red, red)){.ring-black\/10{--tw-ring-color:color-mix(in oklab, var(--color-black) 10%, transparent)}}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ring-inset{--tw-ring-inset:inset}@media (hover:hover){.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-indigo-700:hover{background-color:var(--color-indigo-700)}.hover\:text-indigo-600:hover{color:var(--color-indigo-600)}.hover\:text-red-700:hover{color:var(--color-red-700)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-80:hover{opacity:.8}}.focus\:border-indigo-500:focus{border-color:var(--color-indigo-500)}.focus\:ring-indigo-500:focus{--tw-ring-color:var(--color-indigo-500)}@media (min-width:40rem){.sm\:mx-0{margin-inline:0}.sm\:mr-3{margin-right:calc(var(--spacing) * 3)}.sm\:hidden{display:none}.sm\:table{display:table}.sm\:table-cell{display:table-cell}.sm\:min-h-\[6rem\]{min-height:6rem}.sm\:w-auto{width:auto}.sm\:min-w-0{min-width:0}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-end{align-items:flex-end}.sm\:gap-4{gap:calc(var(--spacing) * 4)}.sm\:gap-6{gap:calc(var(--spacing) * 6)}.sm\:p-1\.5{padding:calc(var(--spacing) * 1.5)}.sm\:px-0{padding-inline:0}.sm\:px-3{padding-inline:calc(var(--spacing) * 3)}.sm\:px-4{padding-inline:calc(var(--spacing) * 4)}.sm\:py-8{padding-block:calc(var(--spacing) * 8)}}@media (min-width:48rem){.md\:table-cell{display:table-cell}}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} \ No newline at end of file