diff --git a/dev/hiccup/src/dev/hiccup.clj b/dev/hiccup/src/dev/hiccup.clj index 7a7c3e5..0bcbf54 100644 --- a/dev/hiccup/src/dev/hiccup.clj +++ b/dev/hiccup/src/dev/hiccup.clj @@ -318,14 +318,14 @@ (form/form-input {:type :email :error true :value "invalid-email"}))])) (def sample-calendar-events - [{: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 :tags ["social"] :source "Personal"} - {: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 :tags ["design"] :source "Work"} - {: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 :source "Work"} - {: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 :tags ["social"] :source "Personal"}]) + [{:title "Team standup" :date "2026-03-29" :time-start "09:00" :time-end "09:30" :color :accent} + {:title "Lunch with Alex" :date "2026-03-29" :time-start "12:00" :time-end "13:00" :color :success} + {:title "Deploy v2.0" :date "2026-03-29" :time-start "15:00" :color :danger} + {:title "Design review" :date "2026-03-30" :time-start "10:00" :color :warning} + {:title "All-day planning" :date "2026-03-31" :color nil :done? true} + {:title "Sprint retro" :date "2026-04-01" :time-start "14:00" :time-end "15:00" :color :accent} + {:title "1:1 with manager" :date "2026-04-02" :time-start "11:00" :color :success} + {:title "Release party" :date "2026-04-03" :time-start "17:00" :color :danger}]) (defn calendar-demo [] (section "Calendar" @@ -335,25 +335,12 @@ :selected-date "2026-03-29"}) (calendar/calendar {:year 2026 :month 4 :today-str "2026-03-29"})] - [: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 + [:h5 "Event Grid"] (cal-events/calendar-event-grid {:year 2026 :month 3 :today-str "2026-03-29" :selected-date "2026-03-29" - :events sample-calendar-events - :header-actions (cal-events/view-toggle {:view :month})}) + :events sample-calendar-events}) - ;; Ticker + Agenda - [:h5 "Agenda View"] + [:h5 "Day Ticker"] (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-29" :day-num 29 :day-label "Su"} @@ -365,19 +352,15 @@ :today-str "2026-03-29" :selected "2026-03-29" :events sample-calendar-events}) + + [:h5 "Agenda List"] (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}) - - ;; 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"}}))) + :events sample-calendar-events}))) ;; ── Pages ─────────────────────────────────────────────────────────── @@ -430,9 +413,9 @@ (defn calendar-page [] [:div (page-header "Calendar" "Date picker, event grid, ticker strip, and agenda list.") - (calendar-demo) (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 [] [:div diff --git a/dev/replicant/src/dev/replicant.cljs b/dev/replicant/src/dev/replicant.cljs index 37bf45a..c5ee9b6 100644 --- a/dev/replicant/src/dev/replicant.cljs +++ b/dev/replicant/src/dev/replicant.cljs @@ -280,80 +280,75 @@ (form/form-field {:label "Email" :error "Please enter a valid email address."} (form/form-input {:type :email :error true :value "invalid-email"}))])) -(defonce !cal-state (atom {:year 2026 :month 3 :selected-date nil - :view :month :selected-event nil :error nil})) +(defonce !cal-state (atom {:year 2026 :month 3 :selected-date nil})) (def sample-calendar-events - [{: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 :tags ["social"] :source "Personal"} - {: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 :tags ["design"] :source "Work"} - {: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 :source "Work"} - {: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 :tags ["social"] :source "Personal"}]) + [{:title "Team standup" :date "2026-03-29" :time-start "09:00" :time-end "09:30" :color :accent} + {:title "Lunch with Alex" :date "2026-03-29" :time-start "12:00" :time-end "13:00" :color :success} + {:title "Deploy v2.0" :date "2026-03-29" :time-start "15:00" :color :danger} + {:title "Design review" :date "2026-03-30" :time-start "10:00" :color :warning} + {:title "All-day planning" :date "2026-03-31" :color nil :done? true} + {:title "Sprint retro" :date "2026-04-01" :time-start "14:00" :time-end "15:00" :color :accent} + {:title "1:1 with manager" :date "2026-04-02" :time-start "11:00" :color :success} + {:title "Release party" :date "2026-04-03" :time-start "17:00" :color :danger}]) (defn calendar-demo [] - (let [{:keys [year month selected-date view selected-event error]} @!cal-state - 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)))] + (let [{:keys [year month selected-date]} @!cal-state + today-str "2026-03-29"] (section "Calendar" - [:h5 "Date Picker"] - (calendar/calendar {:year year :month month - :today-str today-str - :selected-date selected-date - :on-select (fn [d] (swap! !cal-state assoc :selected-date d)) - :on-prev-month prev-month! - :on-next-month next-month!}) + [:h5 "Date Picker (interactive)"] + [:div {:style {:display "flex" :gap "1.5rem" :flex-wrap "wrap"}} + (calendar/calendar {:year year :month month + :today-str today-str + :selected-date selected-date + :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)))})] + (when selected-date + [:p {:style {:color "var(--fg-1)" :font-size "var(--font-sm)"}} + (str "Selected: " selected-date)]) - [:h5 "Full Calendar App"] - (cal-events/error-banner {:message error - :on-dismiss (fn [_] (swap! !cal-state assoc :error nil))}) + [:h5 "Event Grid"] + (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 (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)))}) - (cal-events/source-toggles - {:sources [{:name "Work" :color :accent :active? true} - {:name "Personal" :color :success :active? true}] - :on-toggle (fn [s] (js/console.log "Toggle source:" s))}) + [:h5 "Day Ticker"] + (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-29" :day-num 29 :day-label "Su"} + {: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))}) - (if (= view :agenda) - [:div - (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-29" :day-num 29 :day-label "Su"} - {: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))}) - (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))})))) + [:h5 "Agenda List"] + (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] (js/console.log "Agenda event:" (:title evt)))})))) ;; ── Pages ─────────────────────────────────────────────────────────── @@ -402,9 +397,9 @@ (defn calendar-page [] [:div (page-header "Calendar" "Date picker, event grid, ticker strip, and agenda list.") - (calendar-demo) (into [:div {:class ["md-docs"]}] - (markdown/markdown->hiccup calendar-docs-md))]) + (markdown/markdown->hiccup calendar-docs-md)) + (calendar-demo)]) (defn icons-page [] [:div diff --git a/dev/squint/src/dev/squint.cljs b/dev/squint/src/dev/squint.cljs index e806d7f..d762f98 100644 --- a/dev/squint/src/dev/squint.cljs +++ b/dev/squint/src/dev/squint.cljs @@ -297,99 +297,85 @@ (form/form-field {:label "Email" :error "Please enter a valid email address."} (form/form-input {:type "email" :error true :value "invalid-email"}))])) -(def !cal-state (atom {:year 2026 :month 3 :selected-date nil - :view "month" :selected-event nil :error nil})) +(def !cal-state (atom {:year 2026 :month 3 :selected-date nil})) (def sample-calendar-events - [{: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" :tags ["social"] :source "Personal"} - {: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" :tags ["design"] :source "Work"} - {: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" :source "Work"} - {: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" :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!))) + [{:title "Team standup" :date "2026-03-29" :time-start "09:00" :time-end "09:30" :color "accent"} + {:title "Lunch with Alex" :date "2026-03-29" :time-start "12:00" :time-end "13:00" :color "success"} + {:title "Deploy v2.0" :date "2026-03-29" :time-start "15:00" :color "danger"} + {:title "Design review" :date "2026-03-30" :time-start "10:00" :color "warning"} + {:title "All-day planning" :date "2026-03-31" :color nil :done? true} + {:title "Sprint retro" :date "2026-04-01" :time-start "14:00" :time-end "15:00" :color "accent"} + {:title "1:1 with manager" :date "2026-04-02" :time-start "11:00" :color "success"} + {:title "Release party" :date "2026-04-03" :time-start "17:00" :color "danger"}]) (defn calendar-demo [] - (let [{:keys [year month selected-date view selected-event error]} @!cal-state + (let [{:keys [year month selected-date]} @!cal-state today-str "2026-03-29"] (section "Calendar" - [:h5 "Date Picker"] - (calendar/calendar {:year year :month month - :today-str today-str - :selected-date selected-date - :on-select (nav! :selected-date) - :on-prev-month prev-month! - :on-next-month next-month!}) + [:h5 "Date Picker (interactive)"] + [:div {:style {"display" "flex" "gap" "1.5rem" "flex-wrap" "wrap"}} + (calendar/calendar {:year year :month month + :today-str today-str + :selected-date selected-date + :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!)))})] + (when selected-date + [:p {:style {"color" "var(--fg-1)" "font-size" "var(--font-sm)"}} + (str "Selected: " selected-date)]) - [:h5 "Full Calendar App"] - ;; Error banner - (cal-events/error-banner {:message error - :on-dismiss (nav! :error nil)}) + [:h5 "Event Grid"] + (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) + (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)))}) - ;; Source toggles - (cal-events/source-toggles - {:sources [{:name "Work" :color "accent" :active? true} - {:name "Personal" :color "success" :active? true}] - :on-toggle (fn [s] (js/console.log "Toggle source:" s))}) + [:h5 "Day Ticker"] + (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-29" :day-num 29 :day-label "Su"} + {: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) + (render!))}) - ;; View toggle + event grid / agenda - (if (= view "agenda") - [:div - (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-29" :day-num 29 :day-label "Su"} - {: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 (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!))})))) + [:h5 "Agenda List"] + (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] (js/console.log "Agenda event:" (:title evt)))})))) ;; ── Pages ─────────────────────────────────────────────────────────── @@ -462,10 +448,10 @@ (load-calendar-docs!) [:div (page-header "Calendar" "Date picker, event grid, ticker strip, and agenda list.") - (calendar-demo) (when-let [md @!calendar-docs] (into [:div {:class "md-docs"}] - (markdown/markdown->hiccup md)))]) + (markdown/markdown->hiccup md))) + (calendar-demo)]) (defn icons-page [] [:div diff --git a/src/ui/calendar.md b/src/ui/calendar.md index 40860ef..be12908 100644 --- a/src/ui/calendar.md +++ b/src/ui/calendar.md @@ -75,60 +75,6 @@ Vertical list of events grouped by day. :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 Events are plain maps: @@ -139,11 +85,13 @@ Events are plain maps: :time-start "09:00" ;; HH:MM or nil :time-end "09:30" ;; HH:MM or nil :color :accent ;; :accent :danger :success :warning or nil - :done? false - :tags ["work" "daily"] ;; optional, shown in detail dialog - :source "Work Calendar"} ;; optional, shown in detail dialog + :done? false} ``` +## 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 All date math is pure (no JS Date dependency) and works on all targets: diff --git a/src/ui/calendar_events.cljc b/src/ui/calendar_events.cljc index 6383492..0591e53 100644 --- a/src/ui/calendar_events.cljc +++ b/src/ui/calendar_events.cljc @@ -1,9 +1,7 @@ (ns ui.calendar-events "Event-aware calendar components. See src/ui/calendar.md for full documentation." (:require [clojure.string :as str] - [ui.calendar :as cal] - [ui.button :as button] - [ui.icon :as icon])) + [ui.calendar :as cal])) ;; In squint, keywords are strings — name is identity #?(:squint (defn- kw-name [s] s) @@ -18,8 +16,6 @@ ;; :time-end - "HH:MM" string or nil ;; :color - :accent, :danger, :success, :warning, or nil (default) ;; :done? - boolean -;; :tags - vector of strings (optional) -;; :source - string (optional, e.g. file/calendar name) (def event-colors #{"accent" "danger" "success" "warning"}) @@ -98,260 +94,8 @@ [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 ────────────────────────────────────────────────────── -;; ── 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 "Render a small event chip for use inside calendar day cells. @@ -389,8 +133,6 @@ [:span {:class "cal-event-time"} time-str]) [:span {:class "cal-event-title"} title]]))) -;; ── Event Day Cell ────────────────────────────────────────────────── - (defn event-day-cell "Render a day cell with event pills for the calendar event grid. @@ -451,8 +193,6 @@ (when (pos? overflow) [[:div {:class "cal-event-more"} (str "+" overflow " more")]])))]))) -;; ── Calendar Event Grid ───────────────────────────────────────────── - (defn calendar-event-grid "Render a month grid calendar with events displayed in day cells. @@ -467,32 +207,19 @@ :on-next-month - callback for next month nav :on-event-click - callback for event click :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 :attrs - additional HTML attributes" [{:keys [year month today-str selected-date events on-select on-prev-month on-next-month on-event-click max-visible - loading? header-actions class attrs]}] + class attrs]}] (let [days (cal/calendar-days year month)] #?(:squint (let [classes (cond-> "cal cal-has-events" class (str " " class)) base-attrs (merge {:class classes} attrs)] [:div base-attrs - [:div {:class "cal-nav"} - (button/button {:variant "ghost" :icon "chevron-left" :size "sm" - :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-header {:year year :month month + :on-prev-month on-prev-month + :on-next-month on-next-month}) (cal/calendar-weekdays {}) (into [:div {:class "cal-grid cal-grid-events"}] (map (fn [day-info] @@ -510,20 +237,9 @@ classes (cond-> cls class (conj class)) base-attrs (merge {:class classes} attrs)] [:div base-attrs - [:div {:class ["cal-nav"]} - (button/button {:variant :ghost :icon :chevron-left :size :sm - :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-header {:year year :month month + :on-prev-month on-prev-month + :on-next-month on-next-month}) (cal/calendar-weekdays {}) (into [:div {:class ["cal-grid" "cal-grid-events"]}] (map (fn [day-info] @@ -540,18 +256,7 @@ (let [classes (cond-> "cal cal-has-events" class (str " " class)) base-attrs (merge {:class classes} attrs)] [:div base-attrs - [: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-header {:year year :month month}) (cal/calendar-weekdays {}) (into [:div {:class "cal-grid cal-grid-events"}] (map (fn [day-info] @@ -616,8 +321,12 @@ (map (fn [evt] (ticker-dot {:event evt})) (take 4 day-evts)))]))) +(def ^:private weekday-short-names + ["Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun"]) + (defn ticker-strip "Render a horizontal scrollable day ticker strip. + Shows days from the given list with event dot indicators. Props: :days - vector of {:date :day-num :day-label} maps @@ -715,21 +424,18 @@ "Render a day group in the agenda list with header and event rows. Props: - :date - YYYY-MM-DD string - :label - display label (e.g. 'Today', 'Tomorrow', 'Mon') - :events - all events (filtered internally) - :on-event-click - callback for 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]}] + :date - YYYY-MM-DD string + :label - display label (e.g. 'Today', 'Tomorrow', 'Mon') + :events - all events (filtered internally) + :on-event-click - callback for event click" + [{:keys [date label events on-event-click]}] (let [day-evts (events-for-date events date)] (when (seq day-evts) #?(:squint [:div {:class "cal-agenda-day-group"} [:div {:class "cal-agenda-day-header"} - [:div {:class "cal-agenda-day-header-left"} - [:span {:class "cal-agenda-day-label"} label] - [:span {:class "cal-agenda-day-date"} (str/replace date "-" "/")]] - (when day-actions day-actions)] + [:span {:class "cal-agenda-day-label"} label] + [:span {:class "cal-agenda-day-date"} (str/replace date "-" "/")]] (into [:div {:class "cal-agenda-day-events"}] (map (fn [evt] (agenda-event-row {:event evt :on-click on-event-click})) @@ -738,10 +444,8 @@ :cljs [:div {:class ["cal-agenda-day-group"]} [:div {:class ["cal-agenda-day-header"]} - [:div {:class ["cal-agenda-day-header-left"]} - [:span {:class ["cal-agenda-day-label"]} label] - [:span {:class ["cal-agenda-day-date"]} (str/replace date "-" "/")]] - (when day-actions day-actions)] + [:span {:class ["cal-agenda-day-label"]} label] + [:span {:class ["cal-agenda-day-date"]} (str/replace date "-" "/")]] (into [:div {:class ["cal-agenda-day-events"]}] (map (fn [evt] (agenda-event-row {:event evt :on-click on-event-click})) @@ -750,10 +454,8 @@ :clj [:div {:class "cal-agenda-day-group"} [:div {:class "cal-agenda-day-header"} - [:div {:class "cal-agenda-day-header-left"} - [:span {:class "cal-agenda-day-label"} label] - [:span {:class "cal-agenda-day-date"} (str/replace date "-" "/")]] - (when day-actions day-actions)] + [:span {:class "cal-agenda-day-label"} label] + [:span {:class "cal-agenda-day-date"} (str/replace date "-" "/")]] (into [:div {:class "cal-agenda-day-events"}] (map (fn [evt] (agenda-event-row {:event evt})) @@ -766,17 +468,14 @@ :days - vector of {:date :label} maps for days to show :events - all events :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 :attrs - additional HTML attributes" - [{:keys [days events on-event-click day-actions-fn class attrs]}] + [{:keys [days events on-event-click class attrs]}] (let [groups (keep (fn [d] (agenda-day-group {:date (:date d) :label (:label d) :events events - :on-event-click on-event-click - :day-actions (when day-actions-fn - (day-actions-fn (:date d)))})) + :on-event-click on-event-click})) days) empty? (not (seq groups))] #?(:squint diff --git a/src/ui/calendar_events.css b/src/ui/calendar_events.css index ddd0c18..6dd0509 100644 --- a/src/ui/calendar_events.css +++ b/src/ui/calendar_events.css @@ -439,292 +439,6 @@ 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 ──────────────────────────────────────────────────── */ @media (max-width: 768px) { diff --git a/test/ui/calendar_events_test.clj b/test/ui/calendar_events_test.clj index 39bcd99..795e1cf 100644 --- a/test/ui/calendar_events_test.clj +++ b/test/ui/calendar_events_test.clj @@ -115,61 +115,3 @@ :label "Wed" :events sample-events})] (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})))))