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:
@@ -75,6 +75,60 @@ 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:
|
||||
@@ -85,13 +139,11 @@ 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}
|
||||
: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
|
||||
|
||||
All date math is pure (no JS Date dependency) and works on all targets:
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
(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.calendar :as cal]
|
||||
[ui.button :as button]
|
||||
[ui.icon :as icon]))
|
||||
|
||||
;; In squint, keywords are strings — name is identity
|
||||
#?(:squint (defn- kw-name [s] s)
|
||||
@@ -16,6 +18,8 @@
|
||||
;; :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"})
|
||||
|
||||
@@ -94,8 +98,260 @@
|
||||
[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.
|
||||
|
||||
@@ -133,6 +389,8 @@
|
||||
[: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.
|
||||
|
||||
@@ -193,6 +451,8 @@
|
||||
(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.
|
||||
|
||||
@@ -207,19 +467,32 @@
|
||||
: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
|
||||
class attrs]}]
|
||||
loading? header-actions 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
|
||||
(cal/calendar-header {:year year :month month
|
||||
:on-prev-month on-prev-month
|
||||
:on-next-month on-next-month})
|
||||
[: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-weekdays {})
|
||||
(into [:div {:class "cal-grid cal-grid-events"}]
|
||||
(map (fn [day-info]
|
||||
@@ -237,9 +510,20 @@
|
||||
classes (cond-> cls class (conj class))
|
||||
base-attrs (merge {:class classes} attrs)]
|
||||
[:div base-attrs
|
||||
(cal/calendar-header {:year year :month month
|
||||
:on-prev-month on-prev-month
|
||||
:on-next-month on-next-month})
|
||||
[: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-weekdays {})
|
||||
(into [:div {:class ["cal-grid" "cal-grid-events"]}]
|
||||
(map (fn [day-info]
|
||||
@@ -256,7 +540,18 @@
|
||||
(let [classes (cond-> "cal cal-has-events" class (str " " class))
|
||||
base-attrs (merge {:class classes} 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 {})
|
||||
(into [:div {:class "cal-grid cal-grid-events"}]
|
||||
(map (fn [day-info]
|
||||
@@ -321,12 +616,8 @@
|
||||
(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
|
||||
@@ -424,18 +715,21 @@
|
||||
"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"
|
||||
[{:keys [date label events on-event-click]}]
|
||||
: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]}]
|
||||
(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"}
|
||||
[:span {:class "cal-agenda-day-label"} label]
|
||||
[:span {:class "cal-agenda-day-date"} (str/replace date "-" "/")]]
|
||||
[: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)]
|
||||
(into [:div {:class "cal-agenda-day-events"}]
|
||||
(map (fn [evt]
|
||||
(agenda-event-row {:event evt :on-click on-event-click}))
|
||||
@@ -444,8 +738,10 @@
|
||||
:cljs
|
||||
[:div {:class ["cal-agenda-day-group"]}
|
||||
[:div {:class ["cal-agenda-day-header"]}
|
||||
[:span {:class ["cal-agenda-day-label"]} label]
|
||||
[:span {:class ["cal-agenda-day-date"]} (str/replace date "-" "/")]]
|
||||
[: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)]
|
||||
(into [:div {:class ["cal-agenda-day-events"]}]
|
||||
(map (fn [evt]
|
||||
(agenda-event-row {:event evt :on-click on-event-click}))
|
||||
@@ -454,8 +750,10 @@
|
||||
:clj
|
||||
[:div {:class "cal-agenda-day-group"}
|
||||
[:div {:class "cal-agenda-day-header"}
|
||||
[:span {:class "cal-agenda-day-label"} label]
|
||||
[:span {:class "cal-agenda-day-date"} (str/replace date "-" "/")]]
|
||||
[: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)]
|
||||
(into [:div {:class "cal-agenda-day-events"}]
|
||||
(map (fn [evt]
|
||||
(agenda-event-row {:event evt}))
|
||||
@@ -468,14 +766,17 @@
|
||||
: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 class attrs]}]
|
||||
[{:keys [days events on-event-click day-actions-fn class attrs]}]
|
||||
(let [groups (keep (fn [d]
|
||||
(agenda-day-group {:date (:date d)
|
||||
:label (:label d)
|
||||
: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)
|
||||
empty? (not (seq groups))]
|
||||
#?(:squint
|
||||
|
||||
@@ -439,6 +439,292 @@
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user