Skip to main content

CHANGELOG.md

# Changelog

## [0.1.1] — 2026-06-13

Docs + accessibility. No API changes.

### Added

- Much-expanded README: "Making it interactive" (hooks, the built-in toolbar,
  the `on_*` callback table, and the `extra.actions` / `extra.badges` shapes),
  "Translations" (the chrome `translations` map vs. consumer-resolved content —
  works with gettext, Cldr, or a JSONB multilang column), "Live updates", and
  an "Accessibility" section. New Gotchas: nil/duplicate id raises, `today`
  defaults to UTC, and `window_start`/`window_end` is all-or-nothing.
- `:doc` for the `translations` attr.

### Accessibility

- Sub-project chevrons now expose `aria-expanded` (and an `aria-label`), so
  screen readers announce expand/collapse state.
- The decorative connector + arrowhead SVGs are `aria-hidden`, so a screen
  reader walks the bars rather than the path geometry.

## [0.1.0] — 2026-06-13

Initial release. Extracted from `live_calendar`'s waterfall view into a
standalone package.

### Features

- `PhoenixLiveGantt.gantt/1` component — horizontal bars on a time axis with
  orthogonal connector routing (FS/SS/FF/SF), bar-edge attach modes,
  bus stagger, smart trunk consolidation.
- `PhoenixLiveGantt.Task` struct — Gantt-focused (no calendar/recurrence
  baggage).
- Sub-projects: any task with `extra.parent_id` becomes a child;
  parents roll up over descendants, expand/collapse via chevron or
  popover button, with framed timeline + sidebar treatment.
- Per-bar popover (click to open) with title, assignee/progress
  subtitle, optional custom action buttons (icon + tooltip +
  phx-click + per-event badges).
- Corner badges (notification-style pills with stacking + flash).
- Built-in `PhoenixLiveGantt.Inspector` for HTML → geometry parsing and
  `PhoenixLiveGantt.TestHelpers` for property assertions.
- `mix phoenix_live_gantt.dump` for offline geometry inspection.
- JS hooks `LgBarPopover` + `LgAutoScroll`.
- **`PhoenixLiveGantt.scroll_to_start/2` — scroll the timeline back to its start.** A
  `Phoenix.LiveView.JS` command (composes with `JS.push/2`) that the
  `LgAutoScroll` hook consumes (`lg:scroll-start`) to scroll the chart to its
  leftmost column. Pair it with a "home"/"fit" button whose server handler
  refits the window — the server can't move the scroll, and the built-in
  scroll-to-today only fires when the today marker is in view, so a refit that
  doesn't include today would otherwise leave the timeline parked at a stale
  spot. A pending-flag in the hook makes the scroll authoritative across the
  refit patch even when it moves the today marker (which would otherwise
  re-center on today).
- **`window_start` / `window_end` attrs — sub-day positioning window.** The
  positioning axis is normally `date_range`'s whole-day, midnight-to-midnight
  span. A consumer can now override the ORIGIN and SPAN with a pair of
  `NaiveDateTime`s so the axis starts/ends partway through a day — e.g. ~1 column
  before the first task at `:hour`/`:min15`/`:min5` zoom, instead of a wall of
  empty pre-task columns from midnight. Positioning threads a `view = {origin,
  span_days}` (origin is the whole-day `range.first` Date in the default path, a
  `NaiveDateTime` when overridden) through bars, connector endpoints, the today
  marker, sub-project frames, obstacles, and a new `window_columns/5` column
  builder that walks fixed slot-minute steps from the origin (labels: the date on
  each midnight slot, a bare hour on `:hour`, the `:15` clock boundaries on
  sub-hour zooms). `date_range` still drives event partition / edge counts, so
  keep it covering the same window. Behavior is byte-identical when the override
  is absent (origin = `range.first`, span = `total_days`). Snap `window_start` to
  a slot boundary so column labels land on round clock times.
- **`tiny_bar_px` attr (default `5`) — "too small to see" marker.** A bar whose
  TRUE width renders narrower than this many SCREEN pixels gets a small
  fixed-size down-triangle at the task's start, signalling a task that's there
  but too short to see. The decision is **pure CSS** — each marker lives inside a
  per-task `container-type: inline-size` element whose width tracks the bar's
  rendered width, and an injected container query (`@container (max-width:
  {tiny_bar_px}px)`) reveals it. So it's server-emitted and browser-resolved
  against true screen pixels: correct under the responsive fill + zoom, **instant
  on first paint** (no socket/hook/measurement), and re-resolved on resize by the
  browser with zero JavaScript. The marker is clickable (opens the same popover —
  that part needs `enable_hooks`). Set `0` to disable. Pairs with `min_bar_px: 0`
  (the default) so bars stay honest while hairline tasks remain discoverable.
  Assumes a uniform `tiny_bar_px` across charts sharing a page.
- **`min_bar_px` attr (default `0`) — bars reflect their TRUE duration.**
  Previously every non-milestone bar was floored to a 4px minimum so a short
  task stayed a visible sliver. That made the bar overstate the task's span and
  diverge from the connector geometry (arrows attached to a phantom edge). The
  floor is now opt-in: by default a bar is exactly as wide as its duration (a
  task too short to show at the current zoom is a hairline / vanishes until you
  zoom in), so the chart is honest and connectors attach to the real edge. Set
  `min_bar_px` to e.g. `4` to restore the always-visible-sliver behavior. (A
  zero-DURATION task is still a milestone diamond regardless.) Connector endpoints
  are DRAWN from the RENDERED bar edges (so a non-zero `min_bar_px` stays
  consistent with where arrows attach), but the backward/invalid ("time-travel")
  decision is JUDGED from the NATURAL temporal edges — otherwise a zero-gap FS
  dependency (B starting exactly when A finishes) would be falsely flagged
  backward by A's min-width sliver poking past B's start.
- **`:hour` zoom + continuous coordinates.** The positioning axis is now a
  continuous "fractional days from range start" used uniformly by bars, the
  today marker, connector endpoints, and columns — so a `:hour` zoom (and
  DateTime/NaiveDateTime `start`/`end`) renders intra-day detail (a 2h and a 6h
  task differ in width/position). `Date` inputs at day/week/month zoom are
  byte-identical to before. `today` accepts a `DateTime`/`NaiveDateTime` for a
  precise "now" line + current-hour column highlight; positioning uses
  wall-clock time (DST-safe). `PhoenixLiveGantt.Layout.sequential/2` gained a
  `:min_span` `{unit, n}` option and emits sub-day temporals when its
  `:start`/`:advance` do.
- **Responsive fit-to-width (pure CSS, no round-trip).** Horizontal geometry
  (bars, columns, today marker, sub-project frames, badges, popovers) now
  renders as PERCENTAGES of the content width, and the timeline uses
  `width: 100%; min-width: {content_px}px` inside `overflow-x-auto`. A short
  chart fills the container exactly (no gap); a long one scrolls at its natural
  density — instantly, on first paint, with zero measurement or server
  round-trip. The connector SHAFT SVG keeps a pixel viewBox but renders
  `width: 100%` with `preserveAspectRatio="none"` + `vector-effect="non-scaling-stroke"`,
  so the lines scale in lockstep with the bars and stay aligned at any width (the
  connector router is unchanged). Arrow**heads** are drawn in a SEPARATE,
  non-stretched overlay (positioned by `%` so the tip tracks the bar-aligned
  shaft end, but sized in fixed px so the triangle never distorts) — a stretched
  line is still a correct line, but a stretched triangle is not an arrowhead.
  `day_width_px` now sets the natural content width (scroll threshold /
  density); `default_day_width_px/1` exposes the per-zoom defaults.
- **Off-screen Today hint.** When `today` falls outside `date_range`, a
  directional pill (`← Today` / `Today →`) now pins to the edge pointing
  toward today, instead of the consumer having to widen the axis to keep the
  marker on screen. Optional `on_show_today` makes it clickable (e.g. to jump
  to today); otherwise it's informational. The vertical marker line still
  renders only when today is in range.

### Fixes

- Connectors to/from a task at the very edge of the window no longer clip off the
  chart. A task flush against the left/right edge had no room for its connector's
  exit/entry stub (it bulges ~`@elbow_px` past the bar), so the stub — and
  sometimes the arrowhead — drew past `content_width` and got clipped by the
  chart's `overflow-x-auto`. The time axis now reserves a fixed `@axis_pad_px`
  (16px) of horizontal breathing room on each side: every x coordinate shifts in
  by the pad, `content_width` grows by 2×, and transparent spacer columns hold
  the margin so bars still exactly cover their time columns. (Absolute %s move,
  but every layer shares the padded denominator, so alignment is unchanged.)
- Arrowheads into a milestone no longer detach from the shaft at a low fill
  factor. The head is nudged `@milestone_edge_px` (12px) out to the diamond's
  edge — a fixed SCREEN px — but it rides the connector's final approach segment,
  which was only `@elbow_px` (10px) of VIEWBOX. When the chart scrolls rather
  than fills (e.g. `:min5`, where that segment renders ~1:1), 12px of nudge
  overshot the 10px segment and the head floated off the trunk, disconnected. A
  milestone target now gets an approach stem a hair longer than the nudge
  (`@milestone_edge_px + 2`), so the head always lands ON the shaft at every
  zoom. (Connectors are still the normal horizontal zigzag — longer at a high
  fill is fine.)
- An open bar/label popover now sits above everything else in the chart
  (`z-[60]`). It was `z-40` — tying with milestone diamonds — and since rows are
  `position: relative` with no z-index (one shared stacking context), a popover
  that overhung the row below lost to that row's diamond by DOM paint order and
  got clipped. Clicking a milestone in a stack of same-date diamonds now shows an
  un-occluded popover. (Above bars z-10, today line z-30, diamonds z-40, and
  badges z-50.)
- Milestone diamonds are now clickable. They rendered with `cursor-pointer`
  (the default `milestone_class`) but carried no popover wiring — only the
  optional `on_event_click` — so a consumer that relied on the built-in popover
  (as bars do) got a diamond that looked clickable but did nothing. Diamonds now
  get the same `LgBarPopover` hook + popover sibling as bars, so clicking one
  opens its title/actions popover AND highlights its dependency tree (fading
  unrelated tasks) — the tool for tracing arrows through a cluster of
  same-date milestones. (The dependency highlight walks ancestors only, by
  design; an inline comment claiming it walked "both directions" was stale and
  has been corrected.)
- Column-header "today" highlight now honors the `today` attribute instead
  of always computing against `Date.utc_today()`, so it agrees with the
  today-marker line when a consumer passes an explicit `today`.
- The built-in toolbar's **Today** button is now disabled (with an
  explanatory tooltip) when it can't actually scroll — i.e. `enable_hooks`
  is off and no `on_scroll_today` is wired — instead of rendering enabled
  and silently doing nothing. (Default scroll-to-today needs `id` +
  `enable_hooks` so the `LgAutoScroll` hook has a target + listener.)
- Popover action buttons now always expose the event id as
  `phx-value-event-id` (hyphen), even when the action sets a `phx_value`
  map — previously a map made it `phx-value-event_id` (underscore),
  disagreeing with the no-value path and the chevron. Handlers now read
  `%{"event-id" => id}` consistently.
- `LgBarPopover` re-anchors a bar popover to the bar's CURRENT geometry on
  open, instead of trusting its (frozen, `phx-update="ignore"`) server-
  rendered position. Fixes popovers opening far from their bar after the
  chart re-rendered with new geometry — e.g. switching zoom.
- Connector arrowheads no longer distort under the responsive fill. They were
  SVG `<marker>`s inside the `preserveAspectRatio="none"` shaft SVG, so at high
  fill factors they stretched into thin, disconnected-looking triangles. They
  now render in a fixed-px overlay anchored by `%` to the shaft's true terminal
  point (`PhoenixLiveGantt.PathFormat.terminal/1`), so the head stays on the shaft end
  even when `consolidate_piercing_trunks` re-routes a forward path to end at a
  different y (the old marker rode the path, the overlay must re-derive it).
  New `Inspector` arrowhead extraction + `TestHelpers.assert_arrowheads_at_path_ends/2`
  (wired into `find_geometry_issues/2`) lock the head-meets-shaft invariant.
- Sub-day tasks are no longer mis-routed as milestones. `milestone?/1` (connector
  routing) tested `Date.diff(end, start) <= 0`, so any task shorter than a full
  day — common at `:hour` zoom — started and ended on the same DATE and was
  treated as a zero-duration milestone, even though `bar_geometry/3` (which uses
  fractional days) rendered it as a thin bar. The router then applied milestone
  endpoint offsets + the 10px diamond gap and frequently mis-flagged the
  dependency as backward (dashed), so arrows routed to/from a phantom diamond and
  looked disconnected. `milestone?/1` now uses the same fractional-day duration
  test as `bar_geometry/3` (identical to the old behavior for pure-`Date` events).
- Arrow tips now land ON the target bar's edge (gap 0) instead of a 4px natural
  gap. Under the responsive fill the shaft SVG stretches with the bars, so a
  natural-px gap was magnified into a visible disconnect (4px → ~15px at a 3.8×
  fill); the fixed-px arrowhead overlay now supplies the visual separation, so
  arrows read as connected at any fill factor. (Milestone targets keep their
  diamond-clearance gap.)


### Ergonomics & docs

- `PhoenixLiveGantt.Layout.sequential/2` — optional, domain-agnostic helper that lays
  items with *durations + order + sub-projects* out into `start`/`end` dates
  (sequential waterfall, sub-project span, day-aligned `:min_span_days`) with a
  pluggable `:advance` calendar callback. Keeps `gantt/1` render-only while
  giving consumers the durations→dates layout they'd otherwise hand-roll. Does
  not do dependency-driven scheduling / critical path / resource leveling.
- `PhoenixLiveGantt.toggle_expanded/2` — convenience for `on_toggle_expand` handlers
  (accepts a MapSet/list/nil, returns a MapSet).
- README rewritten with the steps/gotchas that bite first-time consumers: the
  required **Tailwind content source** (no stylesheet ships), `end` being
  **exclusive**, the **sub-project rules** (always include descendants; give
  parents `nil` dates so they roll up), and the `on_toggle_expand` `"event-id"`
  param key.
- `expanded` / `on_toggle_expand` now carry attr docs (the `:all` shortcut,
  the hyphenated param key).

### Naming

CSS class prefix: `lg-`. JS hooks: `LgBarPopover`, `LgAutoScroll`.
Events dispatched: `lg:scroll-today`.