# `PhoenixKitProjects.CalendarDisplay`
[🔗](https://github.com/BeamLabEU/phoenix_kit_projects/blob/v0.21.2/lib/phoenix_kit_projects/calendar_display.ex#L1)

Maps projects — and, for the default Tasks mode, their scheduled tasks — to
`PhoenixLiveCalendar.Event` structs for the Overview dashboard calendar.

Two mapping modes back the Overview calendar's mode toggle:

  * `task_events/3` (the **Tasks** default) — every leaf task across every
    given project, placed on the days its `ScheduleLayout` span covers,
    colored by the OWNING project's identity color so a day reads as "what
    happens today, across projects". Same-day tasks stack as chips,
    multi-day tasks as bars; the LiveView caps the per-day count and the
    grid shows a Google-style "+N more" for the rest.
  * `events/6` (the **Projects** mode — the original view) — each project
    as one all-day "ongoing line": running projects span start →
    planned-end (reaching at least today, so they read as ongoing),
    completed projects span start → completion, and scheduled projects get
    a one-day marker on their planned start.

Bars are colored by **project identity** — a stable color per project, picked
by hashing the project id over a fixed palette of hues spread around the color
wheel — so adjacent and stacked bars stay tellable apart. Lifecycle status is
conveyed by the dashboard's project cards, with one exception: for a running
summary the caller tags `late: true`, only the stretch of the bar PAST its
planned end (planned_end+1 .. today) is highlighted — via `extra.highlight`
with the `pk-overdue` class (it marks that stretch in the INVERSE of the bar's
own colour — diagonal stripes over the identity colour, or a solid fill —
animated per `animation_style/0`, all configured on `/admin/settings/projects`)
— so the length of the marked tail shows how late the project is. The rest
keeps its identity color.

`end` is exclusive in PhoenixLiveCalendar (`[start, end)`), so the inclusive
last day gets `+1`. Projects without a placeable start date are dropped.

# `anim_modes`

```elixir
@spec anim_modes() :: [String.t()]
```

Allowed overdue-animation modes (for the settings form).

# `anim_patterns`

```elixir
@spec anim_patterns() :: [String.t()]
```

Allowed overdue-marker patterns (for the settings form).

# `anim_range`

```elixir
@spec anim_range(String.t()) :: {number(), number()}
```

Clamp range {lo, hi} for a numeric display field (bounds the form input).

# `animation_css`

```elixir
@spec animation_css(map()) :: String.t()
```

CSS body (no surrounding `<style>` tag) for an overdue-animation config map of
the shape `read/0` returns. Public so tests and a settings preview
can render a given config without touching the settings store.

# `animation_style`

```elixir
@spec animation_style(map()) :: String.t()
```

The `<style>` block for the overdue animation, from an already-`read/0`
config map (callers thread the map so one read serves style + legend). Safe to
inject with `Phoenix.HTML.raw/1`: every interpolated value is a validated enum
or clamped number, never free text.

# `color_for`

```elixir
@spec color_for(term()) :: {String.t(), String.t()}
```

# `defaults`

```elixir
@spec defaults() :: map()
```

The pure compile-time defaults of `read/0` (no settings read; core's
`week_start_day` falls back to Monday). Mount-time placeholder so a render
can never dot-access a nil config — `read/0` overwrites it immediately.

# `events`

```elixir
@spec events(
  [map()],
  [PhoenixKitProjects.Schemas.Project.t()],
  [PhoenixKitProjects.Schemas.Project.t()],
  String.t() | nil,
  Date.t(),
  String.t(),
  keyword()
) :: [PhoenixLiveCalendar.Event.t()]
```

Builds the calendar event list from the dashboard's already-loaded data.

  * `active_summaries` — `Projects.project_tree_summary/1` maps for running
    projects (carry `:project` and `:planned_end`), each tagged by the
    caller with `:late` (boolean) to drive the blink
  * `completed` / `upcoming` — `[Project.t()]` (recently completed / scheduled)
  * `lang` — content language for project titles
  * `today` — anchor so a running project's bar always reaches at least today
  * `offset` — viewer timezone offset (e.g. "+3"); UTC datetimes are converted
    to local calendar dates with it, so bars land on the same day the viewer sees

Options:

  * `:late_marker` — how a LATE running project is marked (`"pattern"`
    default): `"pattern"` highlights the overdue stretch
    (planned_end+1 .. today) with the configured overdue pattern, so the
    marked length shows HOW late; `"ring"` rings the whole bar instead —
    the same marker late task chips wear, so one setting dresses
    everything late the same way.

# `events_on`

```elixir
@spec events_on([PhoenixLiveCalendar.Event.t()], Date.t()) :: [
  PhoenixLiveCalendar.Event.t()
]
```

Events whose `[start, end)` span covers `date`, soonest-starting first —
the shared day-popup row source for both calendars.

# `exclusive_end_date`

```elixir
@spec exclusive_end_date(Date.t(), NaiveDateTime.t(), String.t() | nil) :: Date.t()
```

The exclusive end DATE for a span ending at `e` (UTC naive). With a viewer
`offset` (`"+3"`) the end is evaluated in the local frame; `nil` keeps UTC —
the project Calendar tab's Timeline-parity frame. An end falling exactly on
(local) midnight doesn't occupy that day, any later instant does. Floored
one day past the start so a zero-length span still shows as a one-day chip.

# `late_marker_class`

```elixir
@spec late_marker_class(map()) :: String.t() | nil
```

The late-task marker class for a `read/0` config: `"pattern"`
(the default) — the `pk-overdue` overlay, i.e. the same configured
stripes/solid look the Projects-mode overdue stretch uses, so every
calendar marks lateness identically — or `"ring"`, the red inset ring
alternative. Anything else (including maps missing the key) follows the
default. Consumers using the pattern must also inject `animation_style/1`
(and ideally wrap the grid in `SyncAnimations`).

# `late_markers`

```elixir
@spec late_markers() :: [String.t()]
```

Allowed late-task marker styles (for the settings form).

# `loading_class`

```elixir
@spec loading_class() :: String.t()
```

Tailwind classes giving any of OUR clickable controls in-flight feedback:
LiveView tags the clicked element with `phx-click-loading` for the round
trip; these arbitrary variants pulse + dim it and block double-clicks.
Returned from a function (not inlined per call site) so every control
shares one literal for the Tailwind scan.

# `loading_style`

```elixir
@spec loading_style() :: String.t()
```

A `<style>` block giving the calendar LIBRARY's clickable nodes (event
chips, multi-day bars, day cells, "+N more") the same in-flight pulse —
their classes are lib-rendered, so the Tailwind-variant approach above
can't reach them. Static CSS, safe for `Phoenix.HTML.raw/1`.

# `max_events`

```elixir
@spec max_events() :: pos_integer()
```

Per-day caps for the month grids, shared by BOTH calendars — the "+N more"
link / whole-day popup contract depends on the two grids agreeing on when
a day overflows. `max_events/0` caps single-day chips, `max_multiday/0`
caps multi-day bar slots.

# `max_multiday`

```elixir
@spec max_multiday() :: pos_integer()
```

# `put`

```elixir
@spec put(String.t(), term()) :: term()
```

Persist one calendar-display field. An invalid field or value is ignored.
Booleans go through `put_flag/2`; `week_start_day` is core's setting and is
NOT writable from here.

# `put_flag`

```elixir
@spec put_flag(String.t(), boolean()) :: term()
```

Flip one boolean display flag (`"show_weekends"`, `"show_week_numbers"`,
`"fixed_weeks"`). Unknown fields are ignored.

# `read`

```elixir
@spec read() :: %{
  pattern: String.t(),
  mode: String.t(),
  speed: float(),
  brightness_min: float(),
  brightness_max: float(),
  wave_step: float(),
  opacity: float(),
  late_marker: String.t(),
  week_start: 1..7,
  show_weekends: boolean(),
  show_week_numbers: boolean(),
  fixed_weeks: boolean(),
  max_events: pos_integer(),
  max_multiday: pos_integer()
}
```

The full calendar-display config (validated, with safe defaults): the grid
appearance half (weekends / week numbers / fixed weeks / per-day caps, plus
the site-wide `week_start_day` core setting the grids must honor) and the
overdue/late-marker half. One batched, uncached query — this runs on every
Overview reload (each `{:projects, …}` broadcast), not just mount, and
direct reads keep the settings page's live demo fresh.

# `reset`

```elixir
@spec reset() :: :ok
```

Restore every calendar-display setting to its default.

# `task_events`

```elixir
@spec task_events(
  [{map(), %{start: NaiveDateTime.t(), end: NaiveDateTime.t()}}],
  String.t() | nil,
  String.t(),
  keyword()
) :: {[PhoenixLiveCalendar.Event.t()], %{optional(String.t()) =&gt; map()}}
```

Builds the Tasks-mode event list (+ a per-task metadata map) from prepared
`{item, span}` pairs — each a `PhoenixKitProjects.ScheduleLayout` item with
its computed `%{start: NaiveDateTime, end: NaiveDateTime}` span. Pure: the
caller runs the schedule walk (`ScheduleLayout.tree/1` per project) and
passes only the LEAF items (sub-project container assignments excluded —
their child tasks stand for themselves here).

Each task becomes an all-day event spanning its scheduled days in the
viewer's timezone (`offset`, e.g. `"+3"` — same local basis as the rest of
the Overview), colored by the OWNING project's identity color
(`color_for/1`), so tasks group visually by project across the month.
Exclusive ends (`[start, end)`): a span ending exactly at local midnight
doesn't occupy that day, and a zero-length span still shows as a one-day
chip.

Returns `{events, meta}` — `meta` keyed by assignment uuid with
`:project_uuid` / `:project_name` / `:status` / `:late` for the day popup's
rows, the overdue filter, and click navigation.

Options:

  * `:now` — a UTC `NaiveDateTime`; when given, a not-done task whose span
    ended before it is **late**: its chip/bar gets the late marker class
    and its meta entry `late: true`. Without `:now` nothing is flagged
    (pure date math only).
  * `:late_class` — the marker class for late chips/bars (defaults to the
    red inset ring; pass `late_marker_class(read())` to honor
    the configured marker).

# `task_late?`

```elixir
@spec task_late?(
  map(),
  %{:end =&gt; NaiveDateTime.t(), optional(atom()) =&gt; term()},
  NaiveDateTime.t() | nil
) :: boolean()
```

A task is late when it isn't done and its scheduled span ended before
`now` — the schedule says it should already have finished. The shared rule
behind both calendars' red rings, `late` badges, and Overdue-only filters
(a `nil` `now` flags nothing — pure date math only).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
