[0.3.0] — 2026-06-22

Followable connector routing for dense charts, a relocated Today badge, and theme-aware sub-project frames.

Added

  • Outer-gutter routing for long dependency skips. When a forward arrow skips down a tight "staircase" of consecutive bars with no clear channel anywhere (e.g. a packed waterfall layout), the trunk now descends a clear column to the LEFT of the staircase and crosses straight to the target, instead of piercing or hugging an intervening task. It shares that descent with sibling arrows from the same source — each branches off toward its own target — so they read as one line rather than crossing strokes.

Changed

  • Connector trunks keep real clearance from unrelated bars. A forward trunk aims for a comfortable gap from any bar it crosses, tightening toward a 1px floor only when forced, and routes via a detour when no clear channel exists — instead of running flush along a bar's edge (where it reads as part of the bar) or straight through it.
  • The "Today" badge moved to the date-header row. It sits flush on the marker line (no border seam) rather than at the top of the body, where it overlapped bars and too-small-task markers.
  • Sub-project frame colors are theme-aware. The expanded-sub-project band now uses translucent daisyUI tints (color-mix + the --color-* vars) per nesting depth, so it adapts to light/dark themes and no longer washes out the label text — previously an opaque light hex that looked harsh on dark themes. Override subproject_frame_color with any CSS color to customize.

Fixed

  • No dialyzer warning on the sub-project date roll-up. The parent-span roll-up uses a map-update (%{ev | ...}) rather than a named struct-update that a generic Enum.map binding can't narrow to Task — a harmless but noisy success-typing note.

[0.2.0] — 2026-06-22

Week/month axis legibility + a solid arrowhead.

Changed

  • Week/month axis snaps to whole columns. At :week/:month granularity the date axis now rounds OUTWARD to whole-week (Mon–Sun) / whole-month boundaries, so every column is a complete, boundary-aligned week/month instead of a ragged partial stub (e.g. a 2-day "Sat–Sun" sliver). Bars keep their true dates within the widened axis. Pass a tight, task-fitted range and the chart rounds it out on its own. Finer granularities are unaffected.
  • Week columns are labeled with their date span ("Apr 27 – May 3", or "May 4 – 10" within a month) instead of the ISO ordinal ("W18") — a range reads without mapping a week number back to dates.

Fixed

  • A week straddling New Year is one column, not two. Week chunking now keys on the full ISO week (:calendar.iso_week_number/1) rather than {calendar_year, week}, so ISO week 53 (Mon 2026-12-28 → Sun 2027-01-03) no longer splits into two mislabeled stubs across the year boundary.
  • Connector arrowheads render solid. The head no longer inherits the line's alpha (the default text-base-content/50), so the shaft can't show through a half-transparent triangle. The line stays subtle; only the head is made opaque, and it works for any custom connector color (e.g. text-primary/30 → a subtle line with a solid text-primary head).

[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 NaiveDateTimes 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). Arrowheads 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.