feat(calendar-events): add view toggle, source filters, detail dialog, error banner, loading indicator
New stateless components ported from org-mode-agenda-cli reference app: - view-toggle: Grid/Agenda segmented control - source-toggles: colored pill buttons for event source filtering - event-detail-dialog: overlay with title, date, time, tags, source - error-banner: dismissible error bar - loading-indicator: pulsing dot for async state Enhanced existing components: - calendar-event-grid: added :loading?, :header-actions slots - agenda-day-group/agenda-list: added :day-actions slot per day header - Event data format extended with :tags and :source fields CSS: view toggle, loading pulse animation, error banner, source toggles, detail dialog overlay — all using theme tokens with dark mode support. Tests: 126 tests, 794 assertions, 0 failures. Dev demos updated in all 3 targets with full interactive calendar app.
This commit is contained in:
@@ -318,14 +318,14 @@
|
|||||||
(form/form-input {:type :email :error true :value "invalid-email"}))]))
|
(form/form-input {:type :email :error true :value "invalid-email"}))]))
|
||||||
|
|
||||||
(def sample-calendar-events
|
(def sample-calendar-events
|
||||||
[{:title "Team standup" :date "2026-03-29" :time-start "09:00" :time-end "09:30" :color :accent}
|
[{:title "Team standup" :date "2026-03-29" :time-start "09:00" :time-end "09:30" :color :accent :tags ["work"] :source "Work"}
|
||||||
{:title "Lunch with Alex" :date "2026-03-29" :time-start "12:00" :time-end "13:00" :color :success}
|
{:title "Lunch with Alex" :date "2026-03-29" :time-start "12:00" :time-end "13:00" :color :success :tags ["social"] :source "Personal"}
|
||||||
{:title "Deploy v2.0" :date "2026-03-29" :time-start "15:00" :color :danger}
|
{:title "Deploy v2.0" :date "2026-03-29" :time-start "15:00" :color :danger :tags ["ops" "urgent"] :source "Work"}
|
||||||
{:title "Design review" :date "2026-03-30" :time-start "10:00" :color :warning}
|
{:title "Design review" :date "2026-03-30" :time-start "10:00" :color :warning :tags ["design"] :source "Work"}
|
||||||
{:title "All-day planning" :date "2026-03-31" :color nil :done? true}
|
{:title "All-day planning" :date "2026-03-31" :color nil :done? true :source "Work"}
|
||||||
{:title "Sprint retro" :date "2026-04-01" :time-start "14:00" :time-end "15:00" :color :accent}
|
{:title "Sprint retro" :date "2026-04-01" :time-start "14:00" :time-end "15:00" :color :accent :source "Work"}
|
||||||
{:title "1:1 with manager" :date "2026-04-02" :time-start "11:00" :color :success}
|
{:title "1:1 with manager" :date "2026-04-02" :time-start "11:00" :color :success :source "Work"}
|
||||||
{:title "Release party" :date "2026-04-03" :time-start "17:00" :color :danger}])
|
{:title "Release party" :date "2026-04-03" :time-start "17:00" :color :danger :tags ["social"] :source "Personal"}])
|
||||||
|
|
||||||
(defn calendar-demo []
|
(defn calendar-demo []
|
||||||
(section "Calendar"
|
(section "Calendar"
|
||||||
@@ -335,12 +335,25 @@
|
|||||||
:selected-date "2026-03-29"})
|
:selected-date "2026-03-29"})
|
||||||
(calendar/calendar {:year 2026 :month 4 :today-str "2026-03-29"})]
|
(calendar/calendar {:year 2026 :month 4 :today-str "2026-03-29"})]
|
||||||
|
|
||||||
[:h5 "Event Grid"]
|
[:h5 "Full Calendar App"]
|
||||||
|
|
||||||
|
;; Error banner
|
||||||
|
(cal-events/error-banner {:message "Could not fetch events (demo)"})
|
||||||
|
|
||||||
|
;; Source toggles
|
||||||
|
(cal-events/source-toggles
|
||||||
|
{:sources [{:name "Work" :color :accent :active? true}
|
||||||
|
{:name "Personal" :color :success :active? true}
|
||||||
|
{:name "Inbox" :color :warning :active? false}]})
|
||||||
|
|
||||||
|
;; Event grid with view toggle in header
|
||||||
(cal-events/calendar-event-grid {:year 2026 :month 3 :today-str "2026-03-29"
|
(cal-events/calendar-event-grid {:year 2026 :month 3 :today-str "2026-03-29"
|
||||||
:selected-date "2026-03-29"
|
:selected-date "2026-03-29"
|
||||||
:events sample-calendar-events})
|
:events sample-calendar-events
|
||||||
|
:header-actions (cal-events/view-toggle {:view :month})})
|
||||||
|
|
||||||
[:h5 "Day Ticker"]
|
;; Ticker + Agenda
|
||||||
|
[:h5 "Agenda View"]
|
||||||
(cal-events/ticker-strip {:days [{:date "2026-03-27" :day-num 27 :day-label "Fr"}
|
(cal-events/ticker-strip {:days [{:date "2026-03-27" :day-num 27 :day-label "Fr"}
|
||||||
{:date "2026-03-28" :day-num 28 :day-label "Sa"}
|
{:date "2026-03-28" :day-num 28 :day-label "Sa"}
|
||||||
{:date "2026-03-29" :day-num 29 :day-label "Su"}
|
{:date "2026-03-29" :day-num 29 :day-label "Su"}
|
||||||
@@ -352,15 +365,19 @@
|
|||||||
:today-str "2026-03-29"
|
:today-str "2026-03-29"
|
||||||
:selected "2026-03-29"
|
:selected "2026-03-29"
|
||||||
:events sample-calendar-events})
|
:events sample-calendar-events})
|
||||||
|
|
||||||
[:h5 "Agenda List"]
|
|
||||||
(cal-events/agenda-list {:days [{:date "2026-03-29" :label "Today"}
|
(cal-events/agenda-list {:days [{:date "2026-03-29" :label "Today"}
|
||||||
{:date "2026-03-30" :label "Tomorrow"}
|
{:date "2026-03-30" :label "Tomorrow"}
|
||||||
{:date "2026-03-31" :label "Tue"}
|
{:date "2026-03-31" :label "Tue"}
|
||||||
{:date "2026-04-01" :label "Wed"}
|
{:date "2026-04-01" :label "Wed"}
|
||||||
{:date "2026-04-02" :label "Thu"}
|
{:date "2026-04-02" :label "Thu"}
|
||||||
{:date "2026-04-03" :label "Fri"}]
|
{:date "2026-04-03" :label "Fri"}]
|
||||||
:events sample-calendar-events})))
|
:events sample-calendar-events})
|
||||||
|
|
||||||
|
;; Event detail dialog (static demo)
|
||||||
|
[:h5 "Event Detail Dialog"]
|
||||||
|
(cal-events/event-detail-dialog
|
||||||
|
{:event {:title "Deploy v2.0" :date "2026-03-29" :time-start "15:00"
|
||||||
|
:color :danger :tags ["ops" "urgent"] :source "Work"}})))
|
||||||
|
|
||||||
;; ── Pages ───────────────────────────────────────────────────────────
|
;; ── Pages ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -413,9 +430,9 @@
|
|||||||
(defn calendar-page []
|
(defn calendar-page []
|
||||||
[:div
|
[:div
|
||||||
(page-header "Calendar" "Date picker, event grid, ticker strip, and agenda list.")
|
(page-header "Calendar" "Date picker, event grid, ticker strip, and agenda list.")
|
||||||
|
(calendar-demo)
|
||||||
(into [:div {:class "md-docs"}]
|
(into [:div {:class "md-docs"}]
|
||||||
(markdown/markdown->hiccup (slurp "src/ui/calendar.md")))
|
(markdown/markdown->hiccup (slurp "src/ui/calendar.md")))])
|
||||||
(calendar-demo)])
|
|
||||||
|
|
||||||
(defn icons-page []
|
(defn icons-page []
|
||||||
[:div
|
[:div
|
||||||
|
|||||||
@@ -280,75 +280,80 @@
|
|||||||
(form/form-field {:label "Email" :error "Please enter a valid email address."}
|
(form/form-field {:label "Email" :error "Please enter a valid email address."}
|
||||||
(form/form-input {:type :email :error true :value "invalid-email"}))]))
|
(form/form-input {:type :email :error true :value "invalid-email"}))]))
|
||||||
|
|
||||||
(defonce !cal-state (atom {:year 2026 :month 3 :selected-date nil}))
|
(defonce !cal-state (atom {:year 2026 :month 3 :selected-date nil
|
||||||
|
:view :month :selected-event nil :error nil}))
|
||||||
|
|
||||||
(def sample-calendar-events
|
(def sample-calendar-events
|
||||||
[{:title "Team standup" :date "2026-03-29" :time-start "09:00" :time-end "09:30" :color :accent}
|
[{:title "Team standup" :date "2026-03-29" :time-start "09:00" :time-end "09:30" :color :accent :tags ["work"] :source "Work"}
|
||||||
{:title "Lunch with Alex" :date "2026-03-29" :time-start "12:00" :time-end "13:00" :color :success}
|
{:title "Lunch with Alex" :date "2026-03-29" :time-start "12:00" :time-end "13:00" :color :success :tags ["social"] :source "Personal"}
|
||||||
{:title "Deploy v2.0" :date "2026-03-29" :time-start "15:00" :color :danger}
|
{:title "Deploy v2.0" :date "2026-03-29" :time-start "15:00" :color :danger :tags ["ops" "urgent"] :source "Work"}
|
||||||
{:title "Design review" :date "2026-03-30" :time-start "10:00" :color :warning}
|
{:title "Design review" :date "2026-03-30" :time-start "10:00" :color :warning :tags ["design"] :source "Work"}
|
||||||
{:title "All-day planning" :date "2026-03-31" :color nil :done? true}
|
{:title "All-day planning" :date "2026-03-31" :color nil :done? true :source "Work"}
|
||||||
{:title "Sprint retro" :date "2026-04-01" :time-start "14:00" :time-end "15:00" :color :accent}
|
{:title "Sprint retro" :date "2026-04-01" :time-start "14:00" :time-end "15:00" :color :accent :source "Work"}
|
||||||
{:title "1:1 with manager" :date "2026-04-02" :time-start "11:00" :color :success}
|
{:title "1:1 with manager" :date "2026-04-02" :time-start "11:00" :color :success :source "Work"}
|
||||||
{:title "Release party" :date "2026-04-03" :time-start "17:00" :color :danger}])
|
{:title "Release party" :date "2026-04-03" :time-start "17:00" :color :danger :tags ["social"] :source "Personal"}])
|
||||||
|
|
||||||
(defn calendar-demo []
|
(defn calendar-demo []
|
||||||
(let [{:keys [year month selected-date]} @!cal-state
|
(let [{:keys [year month selected-date view selected-event error]} @!cal-state
|
||||||
today-str "2026-03-29"]
|
today-str "2026-03-29"
|
||||||
|
prev-month! (fn [_] (let [[ny nm] (calendar/prev-month year month)]
|
||||||
|
(swap! !cal-state assoc :year ny :month nm)))
|
||||||
|
next-month! (fn [_] (let [[ny nm] (calendar/next-month year month)]
|
||||||
|
(swap! !cal-state assoc :year ny :month nm)))]
|
||||||
(section "Calendar"
|
(section "Calendar"
|
||||||
[:h5 "Date Picker (interactive)"]
|
[:h5 "Date Picker"]
|
||||||
[:div {:style {:display "flex" :gap "1.5rem" :flex-wrap "wrap"}}
|
(calendar/calendar {:year year :month month
|
||||||
(calendar/calendar {:year year :month month
|
:today-str today-str
|
||||||
:today-str today-str
|
:selected-date selected-date
|
||||||
:selected-date selected-date
|
:on-select (fn [d] (swap! !cal-state assoc :selected-date d))
|
||||||
:on-select (fn [d] (swap! !cal-state assoc :selected-date d))
|
:on-prev-month prev-month!
|
||||||
:on-prev-month (fn [_]
|
:on-next-month next-month!})
|
||||||
(let [[ny nm] (calendar/prev-month year month)]
|
|
||||||
(swap! !cal-state assoc :year ny :month nm)))
|
|
||||||
:on-next-month (fn [_]
|
|
||||||
(let [[ny nm] (calendar/next-month year month)]
|
|
||||||
(swap! !cal-state assoc :year ny :month nm)))})]
|
|
||||||
(when selected-date
|
|
||||||
[:p {:style {:color "var(--fg-1)" :font-size "var(--font-sm)"}}
|
|
||||||
(str "Selected: " selected-date)])
|
|
||||||
|
|
||||||
[:h5 "Event Grid"]
|
[:h5 "Full Calendar App"]
|
||||||
(cal-events/calendar-event-grid {:year year :month month
|
(cal-events/error-banner {:message error
|
||||||
:today-str today-str
|
:on-dismiss (fn [_] (swap! !cal-state assoc :error nil))})
|
||||||
:selected-date selected-date
|
|
||||||
:events sample-calendar-events
|
|
||||||
:on-select (fn [d] (swap! !cal-state assoc :selected-date d))
|
|
||||||
:on-prev-month (fn [_]
|
|
||||||
(let [[ny nm] (calendar/prev-month year month)]
|
|
||||||
(swap! !cal-state assoc :year ny :month nm)))
|
|
||||||
:on-next-month (fn [_]
|
|
||||||
(let [[ny nm] (calendar/next-month year month)]
|
|
||||||
(swap! !cal-state assoc :year ny :month nm)))
|
|
||||||
:on-event-click (fn [evt] (js/console.log "Event clicked:" (:title evt)))})
|
|
||||||
|
|
||||||
[:h5 "Day Ticker"]
|
(cal-events/source-toggles
|
||||||
(cal-events/ticker-strip {:days [{:date "2026-03-27" :day-num 27 :day-label "Fr"}
|
{:sources [{:name "Work" :color :accent :active? true}
|
||||||
{:date "2026-03-28" :day-num 28 :day-label "Sa"}
|
{:name "Personal" :color :success :active? true}]
|
||||||
{:date "2026-03-29" :day-num 29 :day-label "Su"}
|
:on-toggle (fn [s] (js/console.log "Toggle source:" s))})
|
||||||
{:date "2026-03-30" :day-num 30 :day-label "Mo"}
|
|
||||||
{:date "2026-03-31" :day-num 31 :day-label "Tu"}
|
|
||||||
{:date "2026-04-01" :day-num 1 :day-label "We"}
|
|
||||||
{:date "2026-04-02" :day-num 2 :day-label "Th"}
|
|
||||||
{:date "2026-04-03" :day-num 3 :day-label "Fr"}]
|
|
||||||
:today-str today-str
|
|
||||||
:selected (or selected-date today-str)
|
|
||||||
:events sample-calendar-events
|
|
||||||
:on-select (fn [d] (swap! !cal-state assoc :selected-date d))})
|
|
||||||
|
|
||||||
[:h5 "Agenda List"]
|
(if (= view :agenda)
|
||||||
(cal-events/agenda-list {:days [{:date "2026-03-29" :label "Today"}
|
[:div
|
||||||
{:date "2026-03-30" :label "Tomorrow"}
|
(cal-events/ticker-strip {:days [{:date "2026-03-27" :day-num 27 :day-label "Fr"}
|
||||||
{:date "2026-03-31" :label "Tue"}
|
{:date "2026-03-28" :day-num 28 :day-label "Sa"}
|
||||||
{:date "2026-04-01" :label "Wed"}
|
{:date "2026-03-29" :day-num 29 :day-label "Su"}
|
||||||
{:date "2026-04-02" :label "Thu"}
|
{:date "2026-03-30" :day-num 30 :day-label "Mo"}
|
||||||
{:date "2026-04-03" :label "Fri"}]
|
{:date "2026-03-31" :day-num 31 :day-label "Tu"}
|
||||||
:events sample-calendar-events
|
{:date "2026-04-01" :day-num 1 :day-label "We"}
|
||||||
:on-event-click (fn [evt] (js/console.log "Agenda event:" (:title evt)))}))))
|
{:date "2026-04-02" :day-num 2 :day-label "Th"}
|
||||||
|
{:date "2026-04-03" :day-num 3 :day-label "Fr"}]
|
||||||
|
:today-str today-str
|
||||||
|
:selected (or selected-date today-str)
|
||||||
|
:events sample-calendar-events
|
||||||
|
:on-select (fn [d] (swap! !cal-state assoc :selected-date d))})
|
||||||
|
(cal-events/agenda-list {:days [{:date "2026-03-29" :label "Today"}
|
||||||
|
{:date "2026-03-30" :label "Tomorrow"}
|
||||||
|
{:date "2026-03-31" :label "Tue"}
|
||||||
|
{:date "2026-04-01" :label "Wed"}
|
||||||
|
{:date "2026-04-02" :label "Thu"}
|
||||||
|
{:date "2026-04-03" :label "Fri"}]
|
||||||
|
:events sample-calendar-events
|
||||||
|
:on-event-click (fn [evt] (swap! !cal-state assoc :selected-event evt))})]
|
||||||
|
(cal-events/calendar-event-grid {:year year :month month
|
||||||
|
:today-str today-str
|
||||||
|
:selected-date selected-date
|
||||||
|
:events sample-calendar-events
|
||||||
|
:on-select (fn [d] (swap! !cal-state assoc :selected-date d))
|
||||||
|
:on-prev-month prev-month!
|
||||||
|
:on-next-month next-month!
|
||||||
|
:on-event-click (fn [evt] (swap! !cal-state assoc :selected-event evt))
|
||||||
|
:header-actions (cal-events/view-toggle
|
||||||
|
{:view view
|
||||||
|
:on-change (fn [v] (swap! !cal-state assoc :view v))})}))
|
||||||
|
|
||||||
|
(cal-events/event-detail-dialog {:event selected-event
|
||||||
|
:on-close (fn [_] (swap! !cal-state assoc :selected-event nil))}))))
|
||||||
|
|
||||||
;; ── Pages ───────────────────────────────────────────────────────────
|
;; ── Pages ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -397,9 +402,9 @@
|
|||||||
(defn calendar-page []
|
(defn calendar-page []
|
||||||
[:div
|
[:div
|
||||||
(page-header "Calendar" "Date picker, event grid, ticker strip, and agenda list.")
|
(page-header "Calendar" "Date picker, event grid, ticker strip, and agenda list.")
|
||||||
|
(calendar-demo)
|
||||||
(into [:div {:class ["md-docs"]}]
|
(into [:div {:class ["md-docs"]}]
|
||||||
(markdown/markdown->hiccup calendar-docs-md))
|
(markdown/markdown->hiccup calendar-docs-md))])
|
||||||
(calendar-demo)])
|
|
||||||
|
|
||||||
(defn icons-page []
|
(defn icons-page []
|
||||||
[:div
|
[:div
|
||||||
|
|||||||
@@ -297,85 +297,99 @@
|
|||||||
(form/form-field {:label "Email" :error "Please enter a valid email address."}
|
(form/form-field {:label "Email" :error "Please enter a valid email address."}
|
||||||
(form/form-input {:type "email" :error true :value "invalid-email"}))]))
|
(form/form-input {:type "email" :error true :value "invalid-email"}))]))
|
||||||
|
|
||||||
(def !cal-state (atom {:year 2026 :month 3 :selected-date nil}))
|
(def !cal-state (atom {:year 2026 :month 3 :selected-date nil
|
||||||
|
:view "month" :selected-event nil :error nil}))
|
||||||
|
|
||||||
(def sample-calendar-events
|
(def sample-calendar-events
|
||||||
[{:title "Team standup" :date "2026-03-29" :time-start "09:00" :time-end "09:30" :color "accent"}
|
[{:title "Team standup" :date "2026-03-29" :time-start "09:00" :time-end "09:30" :color "accent" :tags ["work"] :source "Work"}
|
||||||
{:title "Lunch with Alex" :date "2026-03-29" :time-start "12:00" :time-end "13:00" :color "success"}
|
{:title "Lunch with Alex" :date "2026-03-29" :time-start "12:00" :time-end "13:00" :color "success" :tags ["social"] :source "Personal"}
|
||||||
{:title "Deploy v2.0" :date "2026-03-29" :time-start "15:00" :color "danger"}
|
{:title "Deploy v2.0" :date "2026-03-29" :time-start "15:00" :color "danger" :tags ["ops" "urgent"] :source "Work"}
|
||||||
{:title "Design review" :date "2026-03-30" :time-start "10:00" :color "warning"}
|
{:title "Design review" :date "2026-03-30" :time-start "10:00" :color "warning" :tags ["design"] :source "Work"}
|
||||||
{:title "All-day planning" :date "2026-03-31" :color nil :done? true}
|
{:title "All-day planning" :date "2026-03-31" :color nil :done? true :source "Work"}
|
||||||
{:title "Sprint retro" :date "2026-04-01" :time-start "14:00" :time-end "15:00" :color "accent"}
|
{:title "Sprint retro" :date "2026-04-01" :time-start "14:00" :time-end "15:00" :color "accent" :source "Work"}
|
||||||
{:title "1:1 with manager" :date "2026-04-02" :time-start "11:00" :color "success"}
|
{:title "1:1 with manager" :date "2026-04-02" :time-start "11:00" :color "success" :source "Work"}
|
||||||
{:title "Release party" :date "2026-04-03" :time-start "17:00" :color "danger"}])
|
{:title "Release party" :date "2026-04-03" :time-start "17:00" :color "danger" :tags ["social"] :source "Personal"}])
|
||||||
|
|
||||||
|
(defn- nav! [key val]
|
||||||
|
(fn [_] (swap! !cal-state assoc key val) (render!)))
|
||||||
|
|
||||||
|
(defn- prev-month! [_]
|
||||||
|
(let [{:keys [year month]} @!cal-state
|
||||||
|
[ny nm] (calendar/prev-month year month)]
|
||||||
|
(swap! !cal-state assoc :year ny :month nm)
|
||||||
|
(render!)))
|
||||||
|
|
||||||
|
(defn- next-month! [_]
|
||||||
|
(let [{:keys [year month]} @!cal-state
|
||||||
|
[ny nm] (calendar/next-month year month)]
|
||||||
|
(swap! !cal-state assoc :year ny :month nm)
|
||||||
|
(render!)))
|
||||||
|
|
||||||
(defn calendar-demo []
|
(defn calendar-demo []
|
||||||
(let [{:keys [year month selected-date]} @!cal-state
|
(let [{:keys [year month selected-date view selected-event error]} @!cal-state
|
||||||
today-str "2026-03-29"]
|
today-str "2026-03-29"]
|
||||||
(section "Calendar"
|
(section "Calendar"
|
||||||
[:h5 "Date Picker (interactive)"]
|
[:h5 "Date Picker"]
|
||||||
[:div {:style {"display" "flex" "gap" "1.5rem" "flex-wrap" "wrap"}}
|
(calendar/calendar {:year year :month month
|
||||||
(calendar/calendar {:year year :month month
|
:today-str today-str
|
||||||
:today-str today-str
|
:selected-date selected-date
|
||||||
:selected-date selected-date
|
:on-select (nav! :selected-date)
|
||||||
:on-select (fn [d]
|
:on-prev-month prev-month!
|
||||||
(swap! !cal-state assoc :selected-date d)
|
:on-next-month next-month!})
|
||||||
(render!))
|
|
||||||
:on-prev-month (fn [_]
|
|
||||||
(let [[ny nm] (calendar/prev-month year month)]
|
|
||||||
(swap! !cal-state assoc :year ny :month nm)
|
|
||||||
(render!)))
|
|
||||||
:on-next-month (fn [_]
|
|
||||||
(let [[ny nm] (calendar/next-month year month)]
|
|
||||||
(swap! !cal-state assoc :year ny :month nm)
|
|
||||||
(render!)))})]
|
|
||||||
(when selected-date
|
|
||||||
[:p {:style {"color" "var(--fg-1)" "font-size" "var(--font-sm)"}}
|
|
||||||
(str "Selected: " selected-date)])
|
|
||||||
|
|
||||||
[:h5 "Event Grid"]
|
[:h5 "Full Calendar App"]
|
||||||
(cal-events/calendar-event-grid {:year year :month month
|
;; Error banner
|
||||||
:today-str today-str
|
(cal-events/error-banner {:message error
|
||||||
:selected-date selected-date
|
:on-dismiss (nav! :error nil)})
|
||||||
:events sample-calendar-events
|
|
||||||
:on-select (fn [d]
|
|
||||||
(swap! !cal-state assoc :selected-date d)
|
|
||||||
(render!))
|
|
||||||
:on-prev-month (fn [_]
|
|
||||||
(let [[ny nm] (calendar/prev-month year month)]
|
|
||||||
(swap! !cal-state assoc :year ny :month nm)
|
|
||||||
(render!)))
|
|
||||||
:on-next-month (fn [_]
|
|
||||||
(let [[ny nm] (calendar/next-month year month)]
|
|
||||||
(swap! !cal-state assoc :year ny :month nm)
|
|
||||||
(render!)))
|
|
||||||
:on-event-click (fn [evt] (js/console.log "Event clicked:" (:title evt)))})
|
|
||||||
|
|
||||||
[:h5 "Day Ticker"]
|
;; Source toggles
|
||||||
(cal-events/ticker-strip {:days [{:date "2026-03-27" :day-num 27 :day-label "Fr"}
|
(cal-events/source-toggles
|
||||||
{:date "2026-03-28" :day-num 28 :day-label "Sa"}
|
{:sources [{:name "Work" :color "accent" :active? true}
|
||||||
{:date "2026-03-29" :day-num 29 :day-label "Su"}
|
{:name "Personal" :color "success" :active? true}]
|
||||||
{:date "2026-03-30" :day-num 30 :day-label "Mo"}
|
:on-toggle (fn [s] (js/console.log "Toggle source:" s))})
|
||||||
{:date "2026-03-31" :day-num 31 :day-label "Tu"}
|
|
||||||
{:date "2026-04-01" :day-num 1 :day-label "We"}
|
|
||||||
{:date "2026-04-02" :day-num 2 :day-label "Th"}
|
|
||||||
{:date "2026-04-03" :day-num 3 :day-label "Fr"}]
|
|
||||||
:today-str today-str
|
|
||||||
:selected (or selected-date today-str)
|
|
||||||
:events sample-calendar-events
|
|
||||||
:on-select (fn [d]
|
|
||||||
(swap! !cal-state assoc :selected-date d)
|
|
||||||
(render!))})
|
|
||||||
|
|
||||||
[:h5 "Agenda List"]
|
;; View toggle + event grid / agenda
|
||||||
(cal-events/agenda-list {:days [{:date "2026-03-29" :label "Today"}
|
(if (= view "agenda")
|
||||||
{:date "2026-03-30" :label "Tomorrow"}
|
[:div
|
||||||
{:date "2026-03-31" :label "Tue"}
|
(cal-events/ticker-strip {:days [{:date "2026-03-27" :day-num 27 :day-label "Fr"}
|
||||||
{:date "2026-04-01" :label "Wed"}
|
{:date "2026-03-28" :day-num 28 :day-label "Sa"}
|
||||||
{:date "2026-04-02" :label "Thu"}
|
{:date "2026-03-29" :day-num 29 :day-label "Su"}
|
||||||
{:date "2026-04-03" :label "Fri"}]
|
{:date "2026-03-30" :day-num 30 :day-label "Mo"}
|
||||||
:events sample-calendar-events
|
{:date "2026-03-31" :day-num 31 :day-label "Tu"}
|
||||||
:on-event-click (fn [evt] (js/console.log "Agenda event:" (:title evt)))}))))
|
{:date "2026-04-01" :day-num 1 :day-label "We"}
|
||||||
|
{:date "2026-04-02" :day-num 2 :day-label "Th"}
|
||||||
|
{:date "2026-04-03" :day-num 3 :day-label "Fr"}]
|
||||||
|
:today-str today-str
|
||||||
|
:selected (or selected-date today-str)
|
||||||
|
:events sample-calendar-events
|
||||||
|
:on-select (nav! :selected-date)})
|
||||||
|
(cal-events/agenda-list {:days [{:date "2026-03-29" :label "Today"}
|
||||||
|
{:date "2026-03-30" :label "Tomorrow"}
|
||||||
|
{:date "2026-03-31" :label "Tue"}
|
||||||
|
{:date "2026-04-01" :label "Wed"}
|
||||||
|
{:date "2026-04-02" :label "Thu"}
|
||||||
|
{:date "2026-04-03" :label "Fri"}]
|
||||||
|
:events sample-calendar-events
|
||||||
|
:on-event-click (fn [evt]
|
||||||
|
(swap! !cal-state assoc :selected-event evt)
|
||||||
|
(render!))})]
|
||||||
|
(cal-events/calendar-event-grid {:year year :month month
|
||||||
|
:today-str today-str
|
||||||
|
:selected-date selected-date
|
||||||
|
:events sample-calendar-events
|
||||||
|
:on-select (nav! :selected-date)
|
||||||
|
:on-prev-month prev-month!
|
||||||
|
:on-next-month next-month!
|
||||||
|
:on-event-click (fn [evt]
|
||||||
|
(swap! !cal-state assoc :selected-event evt)
|
||||||
|
(render!))
|
||||||
|
:header-actions (cal-events/view-toggle
|
||||||
|
{:view view
|
||||||
|
:on-change (nav! :view)})}))
|
||||||
|
|
||||||
|
;; Event detail dialog
|
||||||
|
(cal-events/event-detail-dialog {:event selected-event
|
||||||
|
:on-close (fn [] (swap! !cal-state assoc :selected-event nil) (render!))}))))
|
||||||
|
|
||||||
;; ── Pages ───────────────────────────────────────────────────────────
|
;; ── Pages ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -448,10 +462,10 @@
|
|||||||
(load-calendar-docs!)
|
(load-calendar-docs!)
|
||||||
[:div
|
[:div
|
||||||
(page-header "Calendar" "Date picker, event grid, ticker strip, and agenda list.")
|
(page-header "Calendar" "Date picker, event grid, ticker strip, and agenda list.")
|
||||||
|
(calendar-demo)
|
||||||
(when-let [md @!calendar-docs]
|
(when-let [md @!calendar-docs]
|
||||||
(into [:div {:class "md-docs"}]
|
(into [:div {:class "md-docs"}]
|
||||||
(markdown/markdown->hiccup md)))
|
(markdown/markdown->hiccup md)))])
|
||||||
(calendar-demo)])
|
|
||||||
|
|
||||||
(defn icons-page []
|
(defn icons-page []
|
||||||
[:div
|
[:div
|
||||||
|
|||||||
@@ -75,6 +75,60 @@ Vertical list of events grouped by day.
|
|||||||
:on-event-click (fn [event-map] ...)})
|
:on-event-click (fn [event-map] ...)})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## View Toggle
|
||||||
|
|
||||||
|
A segmented control to switch between Grid and Agenda views:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(cal-events/view-toggle
|
||||||
|
{:view :month ;; :month or :agenda
|
||||||
|
:on-change (fn [new-view] ...)})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Source Filter Toggles
|
||||||
|
|
||||||
|
Colored pill buttons to show/hide event sources:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(cal-events/source-toggles
|
||||||
|
{:sources [{:name "Work" :color :accent :active? true}
|
||||||
|
{:name "Personal" :color :success :active? false}]
|
||||||
|
:on-toggle (fn [source-name] ...)})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Detail Dialog
|
||||||
|
|
||||||
|
An overlay dialog showing event details (date, time, tags, source):
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(cal-events/event-detail-dialog
|
||||||
|
{:event {:title "Meeting" :date "2026-03-29" :time-start "10:00"
|
||||||
|
:color :accent :tags ["work"] :source "Work Calendar"}
|
||||||
|
:on-close (fn [] ...)})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Banner
|
||||||
|
|
||||||
|
A dismissible error bar at the top of the calendar:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(cal-events/error-banner
|
||||||
|
{:message "Failed to fetch events"
|
||||||
|
:on-dismiss (fn [_] ...)})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Loading Indicator
|
||||||
|
|
||||||
|
A pulsing dot for loading state, placed inline in the header:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(cal-events/loading-indicator)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Colors
|
||||||
|
|
||||||
|
Colors map to the theme's semantic tokens and support dark mode automatically: `:accent`, `:danger`, `:success`, `:warning`. Pass `nil` for the default gray.
|
||||||
|
|
||||||
## Event Data Format
|
## Event Data Format
|
||||||
|
|
||||||
Events are plain maps:
|
Events are plain maps:
|
||||||
@@ -85,13 +139,11 @@ Events are plain maps:
|
|||||||
:time-start "09:00" ;; HH:MM or nil
|
:time-start "09:00" ;; HH:MM or nil
|
||||||
:time-end "09:30" ;; HH:MM or nil
|
:time-end "09:30" ;; HH:MM or nil
|
||||||
:color :accent ;; :accent :danger :success :warning or nil
|
:color :accent ;; :accent :danger :success :warning or nil
|
||||||
:done? false}
|
:done? false
|
||||||
|
:tags ["work" "daily"] ;; optional, shown in detail dialog
|
||||||
|
:source "Work Calendar"} ;; optional, shown in detail dialog
|
||||||
```
|
```
|
||||||
|
|
||||||
## Event Colors
|
|
||||||
|
|
||||||
Colors map to the theme's semantic tokens and support dark mode automatically: `:accent`, `:danger`, `:success`, `:warning`. Pass `nil` for the default gray.
|
|
||||||
|
|
||||||
## Date Utilities
|
## Date Utilities
|
||||||
|
|
||||||
All date math is pure (no JS Date dependency) and works on all targets:
|
All date math is pure (no JS Date dependency) and works on all targets:
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
(ns ui.calendar-events
|
(ns ui.calendar-events
|
||||||
"Event-aware calendar components. See src/ui/calendar.md for full documentation."
|
"Event-aware calendar components. See src/ui/calendar.md for full documentation."
|
||||||
(:require [clojure.string :as str]
|
(:require [clojure.string :as str]
|
||||||
[ui.calendar :as cal]))
|
[ui.calendar :as cal]
|
||||||
|
[ui.button :as button]
|
||||||
|
[ui.icon :as icon]))
|
||||||
|
|
||||||
;; In squint, keywords are strings — name is identity
|
;; In squint, keywords are strings — name is identity
|
||||||
#?(:squint (defn- kw-name [s] s)
|
#?(:squint (defn- kw-name [s] s)
|
||||||
@@ -16,6 +18,8 @@
|
|||||||
;; :time-end - "HH:MM" string or nil
|
;; :time-end - "HH:MM" string or nil
|
||||||
;; :color - :accent, :danger, :success, :warning, or nil (default)
|
;; :color - :accent, :danger, :success, :warning, or nil (default)
|
||||||
;; :done? - boolean
|
;; :done? - boolean
|
||||||
|
;; :tags - vector of strings (optional)
|
||||||
|
;; :source - string (optional, e.g. file/calendar name)
|
||||||
|
|
||||||
(def event-colors #{"accent" "danger" "success" "warning"})
|
(def event-colors #{"accent" "danger" "success" "warning"})
|
||||||
|
|
||||||
@@ -94,8 +98,260 @@
|
|||||||
[opts]
|
[opts]
|
||||||
(str/join " " (agenda-event-class-list opts)))
|
(str/join " " (agenda-event-class-list opts)))
|
||||||
|
|
||||||
|
(defn view-toggle-class-list
|
||||||
|
"Returns a vector of CSS class strings for a view toggle button.
|
||||||
|
Options:
|
||||||
|
:active? - boolean"
|
||||||
|
[{:keys [active?]}]
|
||||||
|
(cond-> ["cal-view-btn"]
|
||||||
|
active? (conj "cal-view-btn-active")))
|
||||||
|
|
||||||
|
(defn view-toggle-classes [opts]
|
||||||
|
(str/join " " (view-toggle-class-list opts)))
|
||||||
|
|
||||||
|
(defn source-toggle-class-list
|
||||||
|
"Returns a vector of CSS class strings for a source filter toggle.
|
||||||
|
Options:
|
||||||
|
:color - event color keyword
|
||||||
|
:active? - boolean (visible)"
|
||||||
|
[{:keys [color active?]}]
|
||||||
|
(cond-> ["cal-source-toggle" (event-color-class color)]
|
||||||
|
(not active?) (conj "cal-source-inactive")))
|
||||||
|
|
||||||
|
(defn source-toggle-classes [opts]
|
||||||
|
(str/join " " (source-toggle-class-list opts)))
|
||||||
|
|
||||||
;; ── Components ──────────────────────────────────────────────────────
|
;; ── Components ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
;; ── View Toggle ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(defn view-toggle
|
||||||
|
"Render a Grid/Agenda view toggle.
|
||||||
|
|
||||||
|
Props:
|
||||||
|
:view - current view (:month or :agenda)
|
||||||
|
:on-change - callback fn, receives the new view keyword
|
||||||
|
:class - additional CSS classes
|
||||||
|
:attrs - additional HTML attributes"
|
||||||
|
[{:keys [view on-change class attrs]}]
|
||||||
|
(let [v (kw-name (or view #?(:squint "month" :cljs :month :clj :month)))]
|
||||||
|
#?(:squint
|
||||||
|
(let [classes (cond-> "cal-view-toggle" class (str " " class))
|
||||||
|
base-attrs (merge {:class classes} attrs)]
|
||||||
|
[:div base-attrs
|
||||||
|
[:button {:class (view-toggle-classes {:active? (= v "month")})
|
||||||
|
:on-click (when on-change (fn [_] (on-change "month")))}
|
||||||
|
"Grid"]
|
||||||
|
[:button {:class (view-toggle-classes {:active? (= v "agenda")})
|
||||||
|
:on-click (when on-change (fn [_] (on-change "agenda")))}
|
||||||
|
"Agenda"]])
|
||||||
|
|
||||||
|
:cljs
|
||||||
|
(let [classes (cond-> ["cal-view-toggle"] class (conj class))
|
||||||
|
base-attrs (merge {:class classes} attrs)]
|
||||||
|
[:div base-attrs
|
||||||
|
[:button {:class (view-toggle-class-list {:active? (= v "month")})
|
||||||
|
:on (when on-change {:click (fn [_] (on-change :month))})}
|
||||||
|
"Grid"]
|
||||||
|
[:button {:class (view-toggle-class-list {:active? (= v "agenda")})
|
||||||
|
:on (when on-change {:click (fn [_] (on-change :agenda))})}
|
||||||
|
"Agenda"]])
|
||||||
|
|
||||||
|
:clj
|
||||||
|
(let [classes (cond-> "cal-view-toggle" class (str " " class))
|
||||||
|
base-attrs (merge {:class classes} attrs)]
|
||||||
|
[:div base-attrs
|
||||||
|
[:button {:class (view-toggle-classes {:active? (= v "month")})} "Grid"]
|
||||||
|
[:button {:class (view-toggle-classes {:active? (= v "agenda")})} "Agenda"]]))))
|
||||||
|
|
||||||
|
;; ── Loading Indicator ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
(defn loading-indicator
|
||||||
|
"Render a small pulsing dot to indicate loading state."
|
||||||
|
[]
|
||||||
|
#?(:squint [:div {:class "cal-loading"}]
|
||||||
|
:cljs [:div {:class ["cal-loading"]}]
|
||||||
|
:clj [:div {:class "cal-loading"}]))
|
||||||
|
|
||||||
|
;; ── Error Banner ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(defn error-banner
|
||||||
|
"Render an error banner at the top of the calendar.
|
||||||
|
|
||||||
|
Props:
|
||||||
|
:message - error message string
|
||||||
|
:on-dismiss - callback to dismiss the error"
|
||||||
|
[{:keys [message on-dismiss]}]
|
||||||
|
(when message
|
||||||
|
#?(:squint
|
||||||
|
[:div {:class "cal-error-banner"}
|
||||||
|
[:span (str "Error: " message)]
|
||||||
|
(when on-dismiss
|
||||||
|
[:button {:class "cal-error-dismiss" :on-click on-dismiss} "\u00d7"])]
|
||||||
|
|
||||||
|
:cljs
|
||||||
|
[:div {:class ["cal-error-banner"]}
|
||||||
|
[:span (str "Error: " message)]
|
||||||
|
(when on-dismiss
|
||||||
|
[:button {:class ["cal-error-dismiss"]
|
||||||
|
:on {:click on-dismiss}} "\u00d7"])]
|
||||||
|
|
||||||
|
:clj
|
||||||
|
[:div {:class "cal-error-banner"}
|
||||||
|
[:span (str "Error: " message)]
|
||||||
|
[:button {:class "cal-error-dismiss"} "\u00d7"]])))
|
||||||
|
|
||||||
|
;; ── Source Filter Toggles ───────────────────────────────────────────
|
||||||
|
|
||||||
|
(defn source-toggles
|
||||||
|
"Render a row of source filter toggle buttons.
|
||||||
|
|
||||||
|
Props:
|
||||||
|
:sources - vector of {:name :color :active?} maps
|
||||||
|
:on-toggle - callback fn, receives source name string
|
||||||
|
:class - additional CSS classes
|
||||||
|
:attrs - additional HTML attributes"
|
||||||
|
[{:keys [sources on-toggle class attrs]}]
|
||||||
|
(when (seq sources)
|
||||||
|
#?(:squint
|
||||||
|
(let [classes (cond-> "cal-source-toggles" class (str " " class))
|
||||||
|
base-attrs (merge {:class classes} attrs)]
|
||||||
|
(into [:div base-attrs]
|
||||||
|
(map (fn [src]
|
||||||
|
[:button {:class (source-toggle-classes {:color (:color src) :active? (:active? src)})
|
||||||
|
:on-click (when on-toggle (fn [_] (on-toggle (:name src))))}
|
||||||
|
(:name src)])
|
||||||
|
sources)))
|
||||||
|
|
||||||
|
:cljs
|
||||||
|
(let [classes (cond-> ["cal-source-toggles"] class (conj class))
|
||||||
|
base-attrs (merge {:class classes} attrs)]
|
||||||
|
(into [:div base-attrs]
|
||||||
|
(map (fn [src]
|
||||||
|
[:button {:class (source-toggle-class-list {:color (:color src) :active? (:active? src)})
|
||||||
|
:on (when on-toggle {:click (fn [_] (on-toggle (:name src)))})}
|
||||||
|
(:name src)])
|
||||||
|
sources)))
|
||||||
|
|
||||||
|
:clj
|
||||||
|
(let [classes (cond-> "cal-source-toggles" class (str " " class))
|
||||||
|
base-attrs (merge {:class classes} attrs)]
|
||||||
|
(into [:div base-attrs]
|
||||||
|
(map (fn [src]
|
||||||
|
[:button {:class (source-toggle-classes {:color (:color src) :active? (:active? src)})}
|
||||||
|
(:name src)])
|
||||||
|
sources))))))
|
||||||
|
|
||||||
|
;; ── Event Detail Dialog ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
(defn event-detail-dialog
|
||||||
|
"Render an event detail overlay/dialog.
|
||||||
|
|
||||||
|
Props:
|
||||||
|
:event - event map (nil to hide)
|
||||||
|
:on-close - callback to close the dialog
|
||||||
|
:class - additional CSS classes
|
||||||
|
:attrs - additional HTML attributes"
|
||||||
|
[{:keys [event on-close class attrs]}]
|
||||||
|
(when event
|
||||||
|
(let [title (:title event)
|
||||||
|
done? (:done? event)
|
||||||
|
tags (or (:tags event) [])
|
||||||
|
source (:source event)
|
||||||
|
color (:color event)
|
||||||
|
time-str (event-time-display event)
|
||||||
|
date (:date event)]
|
||||||
|
#?(:squint
|
||||||
|
[:div {:class "cal-detail-overlay"
|
||||||
|
:on-click on-close}
|
||||||
|
[:div {:class (str "cal-detail " (event-color-class color)
|
||||||
|
(when class (str " " class)))
|
||||||
|
:on-click (fn [e] (.stopPropagation e))}
|
||||||
|
[:div {:class "cal-detail-header"}
|
||||||
|
[:div {:class "cal-detail-title"}
|
||||||
|
(when done? [:span {:class "cal-detail-badge"} "DONE"])
|
||||||
|
title]
|
||||||
|
[:button {:class "cal-detail-close" :on-click on-close} "\u00d7"]]
|
||||||
|
[:div {:class "cal-detail-body"}
|
||||||
|
(when date
|
||||||
|
[:div {:class "cal-detail-row"}
|
||||||
|
[:span {:class "cal-detail-label"} "Date"]
|
||||||
|
[:span {:class "cal-detail-value"} (str/replace date "-" "/")]])
|
||||||
|
(when time-str
|
||||||
|
[:div {:class "cal-detail-row"}
|
||||||
|
[:span {:class "cal-detail-label"} "Time"]
|
||||||
|
[:span {:class "cal-detail-value"} time-str]])
|
||||||
|
(when (seq tags)
|
||||||
|
[:div {:class "cal-detail-row"}
|
||||||
|
[:span {:class "cal-detail-label"} "Tags"]
|
||||||
|
(into [:div {:class "cal-detail-tags"}]
|
||||||
|
(map (fn [t] [:span {:class "cal-detail-tag"} t]) tags))])
|
||||||
|
(when source
|
||||||
|
[:div {:class "cal-detail-row"}
|
||||||
|
[:span {:class "cal-detail-label"} "Source"]
|
||||||
|
[:span {:class "cal-detail-value"} source]])]]]
|
||||||
|
|
||||||
|
:cljs
|
||||||
|
[:div {:class ["cal-detail-overlay"]
|
||||||
|
:on {:click on-close}}
|
||||||
|
[:div {:class [(str "cal-detail " (event-color-class color))
|
||||||
|
(when class class)]
|
||||||
|
:on {:click (fn [e] (.stopPropagation e))}}
|
||||||
|
[:div {:class ["cal-detail-header"]}
|
||||||
|
[:div {:class ["cal-detail-title"]}
|
||||||
|
(when done? [:span {:class ["cal-detail-badge"]} "DONE"])
|
||||||
|
title]
|
||||||
|
[:button {:class ["cal-detail-close"]
|
||||||
|
:on {:click on-close}} "\u00d7"]]
|
||||||
|
[:div {:class ["cal-detail-body"]}
|
||||||
|
(when date
|
||||||
|
[:div {:class ["cal-detail-row"]}
|
||||||
|
[:span {:class ["cal-detail-label"]} "Date"]
|
||||||
|
[:span {:class ["cal-detail-value"]} (str/replace date "-" "/")]])
|
||||||
|
(when time-str
|
||||||
|
[:div {:class ["cal-detail-row"]}
|
||||||
|
[:span {:class ["cal-detail-label"]} "Time"]
|
||||||
|
[:span {:class ["cal-detail-value"]} time-str]])
|
||||||
|
(when (seq tags)
|
||||||
|
[:div {:class ["cal-detail-row"]}
|
||||||
|
[:span {:class ["cal-detail-label"]} "Tags"]
|
||||||
|
(into [:div {:class ["cal-detail-tags"]}]
|
||||||
|
(map (fn [t] [:span {:class ["cal-detail-tag"]} t]) tags))])
|
||||||
|
(when source
|
||||||
|
[:div {:class ["cal-detail-row"]}
|
||||||
|
[:span {:class ["cal-detail-label"]} "Source"]
|
||||||
|
[:span {:class ["cal-detail-value"]} source]])]]]
|
||||||
|
|
||||||
|
:clj
|
||||||
|
[:div {:class "cal-detail-overlay"}
|
||||||
|
[:div {:class (str "cal-detail " (event-color-class color)
|
||||||
|
(when class (str " " class)))}
|
||||||
|
[:div {:class "cal-detail-header"}
|
||||||
|
[:div {:class "cal-detail-title"}
|
||||||
|
(when done? [:span {:class "cal-detail-badge"} "DONE"])
|
||||||
|
title]
|
||||||
|
[:button {:class "cal-detail-close"} "\u00d7"]]
|
||||||
|
[:div {:class "cal-detail-body"}
|
||||||
|
(when date
|
||||||
|
[:div {:class "cal-detail-row"}
|
||||||
|
[:span {:class "cal-detail-label"} "Date"]
|
||||||
|
[:span {:class "cal-detail-value"} (str/replace date "-" "/")]])
|
||||||
|
(when time-str
|
||||||
|
[:div {:class "cal-detail-row"}
|
||||||
|
[:span {:class "cal-detail-label"} "Time"]
|
||||||
|
[:span {:class "cal-detail-value"} time-str]])
|
||||||
|
(when (seq tags)
|
||||||
|
[:div {:class "cal-detail-row"}
|
||||||
|
[:span {:class "cal-detail-label"} "Tags"]
|
||||||
|
(into [:div {:class "cal-detail-tags"}]
|
||||||
|
(map (fn [t] [:span {:class "cal-detail-tag"} t]) tags))])
|
||||||
|
(when source
|
||||||
|
[:div {:class "cal-detail-row"}
|
||||||
|
[:span {:class "cal-detail-label"} "Source"]
|
||||||
|
[:span {:class "cal-detail-value"} source]])]]]))))
|
||||||
|
|
||||||
|
;; ── Event Pill ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
(defn event-pill
|
(defn event-pill
|
||||||
"Render a small event chip for use inside calendar day cells.
|
"Render a small event chip for use inside calendar day cells.
|
||||||
|
|
||||||
@@ -133,6 +389,8 @@
|
|||||||
[:span {:class "cal-event-time"} time-str])
|
[:span {:class "cal-event-time"} time-str])
|
||||||
[:span {:class "cal-event-title"} title]])))
|
[:span {:class "cal-event-title"} title]])))
|
||||||
|
|
||||||
|
;; ── Event Day Cell ──────────────────────────────────────────────────
|
||||||
|
|
||||||
(defn event-day-cell
|
(defn event-day-cell
|
||||||
"Render a day cell with event pills for the calendar event grid.
|
"Render a day cell with event pills for the calendar event grid.
|
||||||
|
|
||||||
@@ -193,6 +451,8 @@
|
|||||||
(when (pos? overflow)
|
(when (pos? overflow)
|
||||||
[[:div {:class "cal-event-more"} (str "+" overflow " more")]])))])))
|
[[:div {:class "cal-event-more"} (str "+" overflow " more")]])))])))
|
||||||
|
|
||||||
|
;; ── Calendar Event Grid ─────────────────────────────────────────────
|
||||||
|
|
||||||
(defn calendar-event-grid
|
(defn calendar-event-grid
|
||||||
"Render a month grid calendar with events displayed in day cells.
|
"Render a month grid calendar with events displayed in day cells.
|
||||||
|
|
||||||
@@ -207,19 +467,32 @@
|
|||||||
:on-next-month - callback for next month nav
|
:on-next-month - callback for next month nav
|
||||||
:on-event-click - callback for event click
|
:on-event-click - callback for event click
|
||||||
:max-visible - max events per cell (default 3)
|
:max-visible - max events per cell (default 3)
|
||||||
|
:loading? - show loading indicator
|
||||||
|
:header-actions - extra hiccup to render in header right side
|
||||||
:class - additional CSS classes
|
:class - additional CSS classes
|
||||||
:attrs - additional HTML attributes"
|
:attrs - additional HTML attributes"
|
||||||
[{:keys [year month today-str selected-date events on-select
|
[{:keys [year month today-str selected-date events on-select
|
||||||
on-prev-month on-next-month on-event-click max-visible
|
on-prev-month on-next-month on-event-click max-visible
|
||||||
class attrs]}]
|
loading? header-actions class attrs]}]
|
||||||
(let [days (cal/calendar-days year month)]
|
(let [days (cal/calendar-days year month)]
|
||||||
#?(:squint
|
#?(:squint
|
||||||
(let [classes (cond-> "cal cal-has-events" class (str " " class))
|
(let [classes (cond-> "cal cal-has-events" class (str " " class))
|
||||||
base-attrs (merge {:class classes} attrs)]
|
base-attrs (merge {:class classes} attrs)]
|
||||||
[:div base-attrs
|
[:div base-attrs
|
||||||
(cal/calendar-header {:year year :month month
|
[:div {:class "cal-nav"}
|
||||||
:on-prev-month on-prev-month
|
(button/button {:variant "ghost" :icon "chevron-left" :size "sm"
|
||||||
:on-next-month on-next-month})
|
:on-click on-prev-month
|
||||||
|
:class "cal-nav-btn"
|
||||||
|
:attrs {:aria-label "Previous month"}})
|
||||||
|
[:div {:class "cal-nav-title"}
|
||||||
|
(str (cal/month-name month) " " year)
|
||||||
|
(when loading? (loading-indicator))]
|
||||||
|
[:div {:class "cal-nav-actions"}
|
||||||
|
header-actions
|
||||||
|
(button/button {:variant "ghost" :icon "chevron-right" :size "sm"
|
||||||
|
:on-click on-next-month
|
||||||
|
:class "cal-nav-btn"
|
||||||
|
:attrs {:aria-label "Next month"}})]]
|
||||||
(cal/calendar-weekdays {})
|
(cal/calendar-weekdays {})
|
||||||
(into [:div {:class "cal-grid cal-grid-events"}]
|
(into [:div {:class "cal-grid cal-grid-events"}]
|
||||||
(map (fn [day-info]
|
(map (fn [day-info]
|
||||||
@@ -237,9 +510,20 @@
|
|||||||
classes (cond-> cls class (conj class))
|
classes (cond-> cls class (conj class))
|
||||||
base-attrs (merge {:class classes} attrs)]
|
base-attrs (merge {:class classes} attrs)]
|
||||||
[:div base-attrs
|
[:div base-attrs
|
||||||
(cal/calendar-header {:year year :month month
|
[:div {:class ["cal-nav"]}
|
||||||
:on-prev-month on-prev-month
|
(button/button {:variant :ghost :icon :chevron-left :size :sm
|
||||||
:on-next-month on-next-month})
|
:on-click on-prev-month
|
||||||
|
:class "cal-nav-btn"
|
||||||
|
:attrs {:aria-label "Previous month"}})
|
||||||
|
[:div {:class ["cal-nav-title"]}
|
||||||
|
(str (cal/month-name month) " " year)
|
||||||
|
(when loading? (loading-indicator))]
|
||||||
|
[:div {:class ["cal-nav-actions"]}
|
||||||
|
header-actions
|
||||||
|
(button/button {:variant :ghost :icon :chevron-right :size :sm
|
||||||
|
:on-click on-next-month
|
||||||
|
:class "cal-nav-btn"
|
||||||
|
:attrs {:aria-label "Next month"}})]]
|
||||||
(cal/calendar-weekdays {})
|
(cal/calendar-weekdays {})
|
||||||
(into [:div {:class ["cal-grid" "cal-grid-events"]}]
|
(into [:div {:class ["cal-grid" "cal-grid-events"]}]
|
||||||
(map (fn [day-info]
|
(map (fn [day-info]
|
||||||
@@ -256,7 +540,18 @@
|
|||||||
(let [classes (cond-> "cal cal-has-events" class (str " " class))
|
(let [classes (cond-> "cal cal-has-events" class (str " " class))
|
||||||
base-attrs (merge {:class classes} attrs)]
|
base-attrs (merge {:class classes} attrs)]
|
||||||
[:div base-attrs
|
[:div base-attrs
|
||||||
(cal/calendar-header {:year year :month month})
|
[:div {:class "cal-nav"}
|
||||||
|
(button/button {:variant :ghost :icon :chevron-left :size :sm
|
||||||
|
:class "cal-nav-btn"
|
||||||
|
:attrs {:aria-label "Previous month"}})
|
||||||
|
[:div {:class "cal-nav-title"}
|
||||||
|
(str (cal/month-name month) " " year)
|
||||||
|
(when loading? (loading-indicator))]
|
||||||
|
[:div {:class "cal-nav-actions"}
|
||||||
|
header-actions
|
||||||
|
(button/button {:variant :ghost :icon :chevron-right :size :sm
|
||||||
|
:class "cal-nav-btn"
|
||||||
|
:attrs {:aria-label "Next month"}})]]
|
||||||
(cal/calendar-weekdays {})
|
(cal/calendar-weekdays {})
|
||||||
(into [:div {:class "cal-grid cal-grid-events"}]
|
(into [:div {:class "cal-grid cal-grid-events"}]
|
||||||
(map (fn [day-info]
|
(map (fn [day-info]
|
||||||
@@ -321,12 +616,8 @@
|
|||||||
(map (fn [evt] (ticker-dot {:event evt}))
|
(map (fn [evt] (ticker-dot {:event evt}))
|
||||||
(take 4 day-evts)))])))
|
(take 4 day-evts)))])))
|
||||||
|
|
||||||
(def ^:private weekday-short-names
|
|
||||||
["Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun"])
|
|
||||||
|
|
||||||
(defn ticker-strip
|
(defn ticker-strip
|
||||||
"Render a horizontal scrollable day ticker strip.
|
"Render a horizontal scrollable day ticker strip.
|
||||||
Shows days from the given list with event dot indicators.
|
|
||||||
|
|
||||||
Props:
|
Props:
|
||||||
:days - vector of {:date :day-num :day-label} maps
|
:days - vector of {:date :day-num :day-label} maps
|
||||||
@@ -424,18 +715,21 @@
|
|||||||
"Render a day group in the agenda list with header and event rows.
|
"Render a day group in the agenda list with header and event rows.
|
||||||
|
|
||||||
Props:
|
Props:
|
||||||
:date - YYYY-MM-DD string
|
:date - YYYY-MM-DD string
|
||||||
:label - display label (e.g. 'Today', 'Tomorrow', 'Mon')
|
:label - display label (e.g. 'Today', 'Tomorrow', 'Mon')
|
||||||
:events - all events (filtered internally)
|
:events - all events (filtered internally)
|
||||||
:on-event-click - callback for event click"
|
:on-event-click - callback for event click
|
||||||
[{:keys [date label events on-event-click]}]
|
:day-actions - extra hiccup to render in the day header (e.g. add button)"
|
||||||
|
[{:keys [date label events on-event-click day-actions]}]
|
||||||
(let [day-evts (events-for-date events date)]
|
(let [day-evts (events-for-date events date)]
|
||||||
(when (seq day-evts)
|
(when (seq day-evts)
|
||||||
#?(:squint
|
#?(:squint
|
||||||
[:div {:class "cal-agenda-day-group"}
|
[:div {:class "cal-agenda-day-group"}
|
||||||
[:div {:class "cal-agenda-day-header"}
|
[:div {:class "cal-agenda-day-header"}
|
||||||
[:span {:class "cal-agenda-day-label"} label]
|
[:div {:class "cal-agenda-day-header-left"}
|
||||||
[:span {:class "cal-agenda-day-date"} (str/replace date "-" "/")]]
|
[:span {:class "cal-agenda-day-label"} label]
|
||||||
|
[:span {:class "cal-agenda-day-date"} (str/replace date "-" "/")]]
|
||||||
|
(when day-actions day-actions)]
|
||||||
(into [:div {:class "cal-agenda-day-events"}]
|
(into [:div {:class "cal-agenda-day-events"}]
|
||||||
(map (fn [evt]
|
(map (fn [evt]
|
||||||
(agenda-event-row {:event evt :on-click on-event-click}))
|
(agenda-event-row {:event evt :on-click on-event-click}))
|
||||||
@@ -444,8 +738,10 @@
|
|||||||
:cljs
|
:cljs
|
||||||
[:div {:class ["cal-agenda-day-group"]}
|
[:div {:class ["cal-agenda-day-group"]}
|
||||||
[:div {:class ["cal-agenda-day-header"]}
|
[:div {:class ["cal-agenda-day-header"]}
|
||||||
[:span {:class ["cal-agenda-day-label"]} label]
|
[:div {:class ["cal-agenda-day-header-left"]}
|
||||||
[:span {:class ["cal-agenda-day-date"]} (str/replace date "-" "/")]]
|
[:span {:class ["cal-agenda-day-label"]} label]
|
||||||
|
[:span {:class ["cal-agenda-day-date"]} (str/replace date "-" "/")]]
|
||||||
|
(when day-actions day-actions)]
|
||||||
(into [:div {:class ["cal-agenda-day-events"]}]
|
(into [:div {:class ["cal-agenda-day-events"]}]
|
||||||
(map (fn [evt]
|
(map (fn [evt]
|
||||||
(agenda-event-row {:event evt :on-click on-event-click}))
|
(agenda-event-row {:event evt :on-click on-event-click}))
|
||||||
@@ -454,8 +750,10 @@
|
|||||||
:clj
|
:clj
|
||||||
[:div {:class "cal-agenda-day-group"}
|
[:div {:class "cal-agenda-day-group"}
|
||||||
[:div {:class "cal-agenda-day-header"}
|
[:div {:class "cal-agenda-day-header"}
|
||||||
[:span {:class "cal-agenda-day-label"} label]
|
[:div {:class "cal-agenda-day-header-left"}
|
||||||
[:span {:class "cal-agenda-day-date"} (str/replace date "-" "/")]]
|
[:span {:class "cal-agenda-day-label"} label]
|
||||||
|
[:span {:class "cal-agenda-day-date"} (str/replace date "-" "/")]]
|
||||||
|
(when day-actions day-actions)]
|
||||||
(into [:div {:class "cal-agenda-day-events"}]
|
(into [:div {:class "cal-agenda-day-events"}]
|
||||||
(map (fn [evt]
|
(map (fn [evt]
|
||||||
(agenda-event-row {:event evt}))
|
(agenda-event-row {:event evt}))
|
||||||
@@ -468,14 +766,17 @@
|
|||||||
:days - vector of {:date :label} maps for days to show
|
:days - vector of {:date :label} maps for days to show
|
||||||
:events - all events
|
:events - all events
|
||||||
:on-event-click - callback for event click
|
:on-event-click - callback for event click
|
||||||
|
:day-actions-fn - (fn [date-str] hiccup) — optional, renders action slot per day header
|
||||||
:class - additional CSS classes
|
:class - additional CSS classes
|
||||||
:attrs - additional HTML attributes"
|
:attrs - additional HTML attributes"
|
||||||
[{:keys [days events on-event-click class attrs]}]
|
[{:keys [days events on-event-click day-actions-fn class attrs]}]
|
||||||
(let [groups (keep (fn [d]
|
(let [groups (keep (fn [d]
|
||||||
(agenda-day-group {:date (:date d)
|
(agenda-day-group {:date (:date d)
|
||||||
:label (:label d)
|
:label (:label d)
|
||||||
:events events
|
:events events
|
||||||
:on-event-click on-event-click}))
|
:on-event-click on-event-click
|
||||||
|
:day-actions (when day-actions-fn
|
||||||
|
(day-actions-fn (:date d)))}))
|
||||||
days)
|
days)
|
||||||
empty? (not (seq groups))]
|
empty? (not (seq groups))]
|
||||||
#?(:squint
|
#?(:squint
|
||||||
|
|||||||
@@ -439,6 +439,292 @@
|
|||||||
font-size: var(--font-sm);
|
font-size: var(--font-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Nav Actions (header right side slot) ────────────────────────── */
|
||||||
|
|
||||||
|
.cal-nav-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--size-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── View Toggle ─────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.cal-view-toggle {
|
||||||
|
display: flex;
|
||||||
|
background: var(--bg-2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: var(--size-1);
|
||||||
|
gap: var(--size-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-view-btn {
|
||||||
|
appearance: none;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--fg-2);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
padding: var(--size-1) var(--size-2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-view-btn:hover {
|
||||||
|
color: var(--fg-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-view-btn-active {
|
||||||
|
background: var(--bg-1);
|
||||||
|
color: var(--fg-0);
|
||||||
|
box-shadow: var(--shadow-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Loading Indicator ───────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.cal-loading {
|
||||||
|
width: var(--size-2);
|
||||||
|
height: var(--size-2);
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--accent);
|
||||||
|
animation: cal-pulse 0.9s ease-in-out infinite;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: var(--size-2);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cal-pulse {
|
||||||
|
0%, 100% { opacity: 0.3; transform: scale(0.8); }
|
||||||
|
50% { opacity: 1; transform: scale(1.2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Error Banner ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.cal-error-banner {
|
||||||
|
background: var(--danger-100);
|
||||||
|
border-bottom: 1px solid var(--danger-200);
|
||||||
|
color: var(--danger-800);
|
||||||
|
padding: var(--size-2) var(--size-4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-error-dismiss {
|
||||||
|
appearance: none;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--danger-800);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-md);
|
||||||
|
padding: 0 var(--size-1);
|
||||||
|
opacity: 0.6;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-error-dismiss:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .cal-error-banner {
|
||||||
|
background: var(--danger-950);
|
||||||
|
border-bottom-color: var(--danger-900);
|
||||||
|
color: var(--danger-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .cal-error-dismiss {
|
||||||
|
color: var(--danger-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Source Filter Toggles ───────────────────────────────────────── */
|
||||||
|
|
||||||
|
.cal-source-toggles {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--size-1);
|
||||||
|
padding: var(--size-2) var(--size-4);
|
||||||
|
border-bottom: var(--border-0);
|
||||||
|
background: var(--bg-1);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-source-toggle {
|
||||||
|
appearance: none;
|
||||||
|
border: none;
|
||||||
|
padding: var(--size-1) var(--size-2);
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-source-toggle:hover {
|
||||||
|
filter: brightness(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-source-inactive {
|
||||||
|
background: var(--bg-2) !important;
|
||||||
|
color: var(--fg-2) !important;
|
||||||
|
border-left-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Agenda Day Header (with action slot) ────────────────────────── */
|
||||||
|
|
||||||
|
.cal-agenda-day-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--size-2);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-agenda-day-header {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Event Detail Dialog ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.cal-detail-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100;
|
||||||
|
padding: var(--size-4);
|
||||||
|
animation: cal-fade-in 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cal-fade-in {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-detail {
|
||||||
|
background: var(--bg-1);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 28rem;
|
||||||
|
box-shadow: var(--shadow-3);
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
animation: cal-dialog-in 0.15s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cal-dialog-in {
|
||||||
|
from { opacity: 0; transform: scale(0.96) translateY(var(--size-2)); }
|
||||||
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-detail-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--size-4);
|
||||||
|
padding: var(--size-4) var(--size-4) var(--size-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-detail-title {
|
||||||
|
font-size: var(--font-base);
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.35;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--size-2);
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-detail-badge {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: var(--size-1) var(--size-1);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--bg-2);
|
||||||
|
color: var(--fg-1);
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-detail-close {
|
||||||
|
appearance: none;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--fg-2);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-lg);
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-detail-close:hover {
|
||||||
|
color: var(--fg-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-detail-body {
|
||||||
|
padding: 0 var(--size-4) var(--size-4);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--size-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-detail-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--size-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-detail-label {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--fg-2);
|
||||||
|
width: var(--size-16);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-detail-value {
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
color: var(--fg-0);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-detail-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--size-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-detail-tag {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
padding: var(--size-1) var(--size-2);
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--bg-2);
|
||||||
|
color: var(--fg-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .cal-detail {
|
||||||
|
box-shadow: var(--shadow-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .cal-detail-overlay {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Responsive ──────────────────────────────────────────────────── */
|
/* ── Responsive ──────────────────────────────────────────────────── */
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|||||||
@@ -115,3 +115,61 @@
|
|||||||
:label "Wed"
|
:label "Wed"
|
||||||
:events sample-events})]
|
:events sample-events})]
|
||||||
(is (nil? result)))))
|
(is (nil? result)))))
|
||||||
|
|
||||||
|
;; ── New component tests ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
(deftest view-toggle-class-list-test
|
||||||
|
(testing "active"
|
||||||
|
(is (= ["cal-view-btn" "cal-view-btn-active"]
|
||||||
|
(cal-events/view-toggle-class-list {:active? true}))))
|
||||||
|
(testing "inactive"
|
||||||
|
(is (= ["cal-view-btn"]
|
||||||
|
(cal-events/view-toggle-class-list {:active? false})))))
|
||||||
|
|
||||||
|
(deftest source-toggle-class-list-test
|
||||||
|
(testing "active source"
|
||||||
|
(is (= ["cal-source-toggle" "cal-event-accent"]
|
||||||
|
(cal-events/source-toggle-class-list {:color :accent :active? true}))))
|
||||||
|
(testing "inactive source"
|
||||||
|
(is (= ["cal-source-toggle" "cal-event-danger" "cal-source-inactive"]
|
||||||
|
(cal-events/source-toggle-class-list {:color :danger :active? false})))))
|
||||||
|
|
||||||
|
(deftest view-toggle-component-test
|
||||||
|
(testing "renders view toggle (clj target)"
|
||||||
|
(let [result (cal-events/view-toggle {:view :month})]
|
||||||
|
(is (= :div (first result)))
|
||||||
|
(is (= "cal-view-toggle" (get-in result [1 :class]))))))
|
||||||
|
|
||||||
|
(deftest loading-indicator-test
|
||||||
|
(testing "renders loading indicator"
|
||||||
|
(let [result (cal-events/loading-indicator)]
|
||||||
|
(is (= :div (first result)))
|
||||||
|
(is (= "cal-loading" (get-in result [1 :class]))))))
|
||||||
|
|
||||||
|
(deftest error-banner-test
|
||||||
|
(testing "renders error with message"
|
||||||
|
(let [result (cal-events/error-banner {:message "Something broke"})]
|
||||||
|
(is (some? result))
|
||||||
|
(is (= :div (first result)))))
|
||||||
|
(testing "returns nil with no message"
|
||||||
|
(is (nil? (cal-events/error-banner {:message nil})))))
|
||||||
|
|
||||||
|
(deftest source-toggles-test
|
||||||
|
(testing "renders source toggles"
|
||||||
|
(let [result (cal-events/source-toggles
|
||||||
|
{:sources [{:name "Work" :color :accent :active? true}
|
||||||
|
{:name "Personal" :color :success :active? false}]})]
|
||||||
|
(is (some? result))
|
||||||
|
(is (= :div (first result)))))
|
||||||
|
(testing "returns nil with no sources"
|
||||||
|
(is (nil? (cal-events/source-toggles {:sources []})))))
|
||||||
|
|
||||||
|
(deftest event-detail-dialog-test
|
||||||
|
(testing "renders detail dialog with event"
|
||||||
|
(let [evt {:title "Meeting" :date "2026-03-29" :color :accent
|
||||||
|
:time-start "10:00" :tags ["work"] :source "cal:Work"}
|
||||||
|
result (cal-events/event-detail-dialog {:event evt})]
|
||||||
|
(is (some? result))
|
||||||
|
(is (= :div (first result)))))
|
||||||
|
(testing "returns nil with no event"
|
||||||
|
(is (nil? (cal-events/event-detail-dialog {:event nil})))))
|
||||||
|
|||||||
Reference in New Issue
Block a user