{"id":51556167,"url":"https://github.com/mdon/phoenix_live_gantt","last_synced_at":"2026-07-10T05:04:19.996Z","repository":{"id":364635017,"uuid":"1263724638","full_name":"mdon/phoenix_live_gantt","owner":"mdon","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-22T19:53:00.000Z","size":1612,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-22T21:23:46.997Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mdon.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-09T07:58:51.000Z","updated_at":"2026-06-22T19:53:03.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mdon/phoenix_live_gantt","commit_stats":null,"previous_names":["mdon/phoenix_live_gantt"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/mdon/phoenix_live_gantt","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdon%2Fphoenix_live_gantt","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdon%2Fphoenix_live_gantt/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdon%2Fphoenix_live_gantt/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdon%2Fphoenix_live_gantt/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mdon","download_url":"https://codeload.github.com/mdon/phoenix_live_gantt/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdon%2Fphoenix_live_gantt/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35321245,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-10T02:00:06.465Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-10T05:04:19.066Z","updated_at":"2026-07-10T05:04:19.982Z","avatar_url":"https://github.com/mdon.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PhoenixLiveGantt\n\nA Phoenix LiveView Gantt chart component: horizontal bars on a time axis,\ndependency arrows between them, sub-projects with roll-up bars, corner\nbadges, click-to-detail popovers, expand/collapse hierarchy, and a built-in\ngeometry audit.\n\n\u003cimg width=\"1939\" height=\"435\" alt=\"image\" src=\"https://github.com/user-attachments/assets/4a1a02e0-c3ac-4b78-88f2-432dbe2823a2\" /\u003e\n\n**Phoenix-native, not a JavaScript wrapper — this is the part that's\ndifferent.** Every bar, column, dependency-arrow route, milestone, and\nsub-project roll-up is computed **in Elixir** and rendered as plain HTML + SVG\nstraight over the LiveView socket — there's no charting JS library, no\n`\u003ccanvas\u003e`, and no `npm` dependency to wire up. The only JavaScript is two\nsmall, *optional* hooks (click-to-open popover and scroll-to-today); the chart\ndraws fine without them. So it behaves like any component you already own — it\nspeaks `~H`, `assigns`, and `phx-click`, survives LiveView diffs, and styles\nwith your app's Tailwind/daisyUI tokens. Most \"Elixir gantt\" packages wrap a\nJavaScript chart library; this one is Phoenix all the way down, so the\ndependency graph, the routing math, and the rendering are all things you can\nread, test, and override in Elixir.\n\nThe `gantt/1` component is **render-only**: you give it events with `start`/`end`\n**dates** and it draws bars, columns, connectors, and frames. It has no concept\nof durations, working hours, or scheduling. If your domain has *durations + an\norder + sub-projects* but no dates, the optional `PhoenixLiveGantt.Layout.sequential/2`\nhelper does that translation for you (sequential waterfall, sub-project span,\nday-aligned min span) with a pluggable calendar callback — see\n[Laying out from durations](#laying-out-from-durations).\n\n## Installation\n\n```elixir\ndef deps do\n  [\n    {:phoenix_live_gantt, \"~\u003e 0.1\"}\n  ]\nend\n```\n\nThere are **three** wiring steps — deps, JS, and CSS. Skipping the CSS step is\nthe most common mistake; the chart renders but library-specific styling is\nsilently missing (see below).\n\n### 1. JS hooks\n\nThe popover, fade-on-open, and auto-scroll-to-today behaviours need the JS\nhooks. In your `app.js`:\n\n```js\nimport \"../../deps/phoenix_live_gantt/priv/static/assets/phoenix_live_gantt.js\"\n\nlet liveSocket = new LiveSocket(\"/live\", Socket, {\n  hooks: { ...window.PhoenixLiveGanttHooks, ...myHooks }\n})\n```\n\nThe bars render without the hooks, but clicking a bar/label won't open its\npopover and the today-button / auto-scroll won't work.\n\n### 2. CSS / Tailwind (required)\n\n**PhoenixLiveGantt ships no stylesheet.** Its visuals are Tailwind utility classes\nthat live inside the component's template (`.ex` source in `deps/phoenix_live_gantt`).\nTailwind only emits CSS for classes it can *see*, so you must add the package\nto your content sources, or library-specific classes (the sub-project\npattern-fill, `text-[0.6rem]` connector labels, non-working-day shading,\nbadge sizing, …) get purged and the chart looks subtly broken.\n\n**Tailwind v4** — add an `@source` to your `app.css`:\n\n```css\n@import \"tailwindcss\";\n@source \"../../deps/phoenix_live_gantt/lib\";\n```\n\n**Tailwind v3** — add a glob to `content` in `tailwind.config.js`:\n\n```js\nmodule.exports = {\n  content: [\n    \"./js/**/*.js\",\n    \"../lib/my_app_web/**/*.*ex\",\n    \"../deps/phoenix_live_gantt/lib/**/*.*ex\"\n  ]\n}\n```\n\nPhoenixLiveGantt uses **daisyUI** semantic color tokens (`bg-primary`,\n`text-base-content`, `bg-success`, …). daisyUI isn't required — every color is\noverridable per attr (see `bar_default_color_class` and friends) — but the\ndefaults assume those tokens resolve to something.\n\n\u003e Quick check that CSS is wired: a sub-project's roll-up bar should show a\n\u003e diagonal hatch pattern and connector labels should be legible. If bars are\n\u003e flat-colored and labels invisible, your content source is missing.\n\n## Basic usage\n\n```heex\n\u003cPhoenixLiveGantt.gantt\n  id=\"project\"\n  events={@tasks}\n  date_range={@range}\n  connectors={@connectors}\n  zoom={:week}\n  today={@today}\n/\u003e\n```\n\nA task is a `PhoenixLiveGantt.Task` struct:\n\n```elixir\n%PhoenixLiveGantt.Task{\n  id: \"cut-wood\",                # unique within the chart; connectors + parent_id reference this\n  title: \"Cut planks to length\",\n  start: ~D[2026-04-01],\n  end: ~D[2026-04-04],           # EXCLUSIVE — see \"Dates → bars\" below\n  color: \"bg-primary\",\n  assignee: \"Sara\",\n  progress_pct: 60,\n  extra: %{}                     # badges, actions, parent_id, per-task overrides\n}\n```\n\n`date_range` is a `Date.Range` (`Date.range(first, last)`) for the visible\naxis. Pass `id` whenever you use the built-in toolbar (`show_header`) or\nauto-scroll, and always when more than one chart shares a page — DOM ids and\nJS dispatches are namespaced by it.\n\nSee `PhoenixLiveGantt.gantt/1` for the full attr list (there are many styling hooks,\nall with sane defaults) and `PhoenixLiveGantt.Task` for all task fields.\n\n## Live updates\n\nIt's a plain LiveView component, so it's **live for free**: re-assign `events`\n(or `connectors`) and it re-renders — LiveView diffs only what changed down to\nthe client. There's no chart-specific update API, no `pushEvent`, nothing\nimperative. The geometry is percent-based (no JS re-measurement on every render)\nand the JS hooks restore popover/fade state across diffs, so even frequent\nupdates don't flicker or drop an open popover.\n\nWhere the data comes from is yours — the component is render-only and has no\nopinion about it. The usual pattern is `Phoenix.PubSub`: subscribe on connect,\nand on a broadcast reload the events and re-assign them.\n\n```elixir\ndef mount(%{\"id\" =\u003e id}, _session, socket) do\n  if connected?(socket), do: Phoenix.PubSub.subscribe(MyApp.PubSub, \"project:#{id}\")\n\n  {:ok, assign(socket, id: id, events: load_events(id), connectors: load_connectors(id))}\nend\n\n# Anything that mutates the project broadcasts on that topic — this LiveView,\n# a controller, an Oban job, a console session; it doesn't matter.\ndef handle_info({:project_changed, _id}, socket) do\n  id = socket.assigns.id\n  {:noreply, assign(socket, events: load_events(id), connectors: load_connectors(id))}\nend\n```\n\nEvery connected viewer re-renders on the same broadcast, so one person's edit\nshows up on a wall-mounted dashboard with no refresh. Polling or a manual\n\"refresh\" button work identically — the component only cares that `events`\nchanged.\n\n## Making it interactive\n\nBy default the chart is **static** — bars render, but nothing is clickable.\nInteractivity is opt-in via `enable_hooks` (the JS bundle from step 1 must be\nregistered) plus an `id`:\n\n```heex\n\u003cPhoenixLiveGantt.gantt\n  id=\"project\" events={@tasks} date_range={@range}\n  enable_hooks={true}\n  on_event_click=\"task_clicked\"\n/\u003e\n```\n\n`enable_hooks` turns on: click a bar/label to open its detail **popover** (with\ndependency-tree highlight), keyboard activation (Enter/Space open it, Escape\ncloses and restores focus), and today auto-scroll.\n\n### Built-in toolbar\n\n`show_header={true}` renders a toolbar — zoom switcher, Today button, prev/next:\n\n```heex\n\u003cPhoenixLiveGantt.gantt\n  id=\"project\" events={@tasks} date_range={@range} today={@today}\n  show_header={true}\n  zooms={[:hour, :day, :week, :month]}   {!-- which zoom buttons appear --}\n  on_zoom_change=\"set_zoom\"\n  on_navigate=\"navigate\"\n/\u003e\n```\n\n`show_zoom_switcher` / `show_today_button` / `show_navigation` hide individual\npieces; the `:toolbar_start` / `:toolbar_end` slots drop your own controls in.\n\n### Server callbacks\n\nEach interaction is a plain `phx-click` you handle in your LiveView:\n\n| attr | fires when | param |\n| --- | --- | --- |\n| `on_event_click` | a bar/milestone is clicked | `%{\"event-id\" =\u003e id}` |\n| `on_toggle_expand` | a sub-project chevron is toggled | `%{\"event-id\" =\u003e id}` |\n| `on_zoom_change` | a zoom button is clicked | `%{\"zoom\" =\u003e \"week\"}` |\n| `on_navigate` | prev / next is clicked | `%{\"direction\" =\u003e \"next\"}` |\n| `on_show_earlier` / `on_show_later` | a \"← N earlier / N later →\" button | (no extra params) |\n| `on_show_today` | the off-screen \"← Today\" pill | (no extra params) |\n\n(The key is `\"event-id\"` with a **hyphen**.)\n\n### Popover actions \u0026 corner badges\n\nPer-task buttons and badges live on `extra`:\n\n```elixir\n%PhoenixLiveGantt.Task{\n  id: \"build\", title: \"Build\", start: ~D[2026-04-01], end: ~D[2026-04-05],\n  extra: %{\n    actions: [\n      %{icon: \"hero-pencil\", tooltip: \"Edit\",\n        phx_click: \"edit\", phx_value: %{id: \"build\"}},\n      %{icon: \"hero-arrow-top-right-on-square\", tooltip: \"Open\", href: \"/tasks/build\"}\n    ],\n    badges: [\n      %{content: \"3\", corner: :top_right, color: \"bg-error\"},   # e.g. a blocker count\n      %{content: \"!\", corner: :bottom_left, flash: true}\n    ]\n  }\n}\n```\n\nActions render as buttons inside the click-popover (so they need `enable_hooks`);\neach takes `icon` (required) plus any of `tooltip`, `phx_click`, `phx_value`,\n`phx_target`, `href`, `class`. Badges pin to a `corner`\n(`:top_left | :top_right | :bottom_left | :bottom_right`, default `:top_right`)\nwith optional `color`, `text_color`, `flash`, `class`.\n\n## Translations\n\nThere are two layers, and they're deliberately separate:\n\n1. **Chrome** — the chart's own strings (toolbar buttons, the Today label,\n   prev/next, the \"N earlier / later\" counts, popover + expand/collapse labels,\n   and short month names). Pass a `translations` map; anything you omit falls\n   back to English. **No chrome text is hard-coded.**\n\n   ```elixir\n   translations = %{\n     labels: %{\n       today: \"Aujourd'hui\", week: \"Semaine\", day: \"Jour\", task: \"Tâche\",\n       prev: \"Précédent\", next: \"Suivant\",\n       earlier_tasks: \"%{count} avant\", later_tasks: \"%{count} après\"\n       # ... see PhoenixLiveGantt.Utils.I18n for the full key list\n     },\n     month_names_short: %{1 =\u003e \"janv\", 2 =\u003e \"févr\", 3 =\u003e \"mars\", ...}\n   }\n\n   \u003cPhoenixLiveGantt.gantt events={@tasks} date_range={@range} translations={translations} /\u003e\n   ```\n\n2. **Content** — task titles, assignees, action tooltips. You pass these\n   **already localized** as plain strings in each `PhoenixLiveGantt.Task`. The\n   library never stores or resolves content, so it works with *any* backend:\n   gettext, [Cldr](https://hex.pm/packages/ex_cldr), or a **JSONB multilang\n   column** — resolve the string for the current locale however you already do,\n   and hand it over as `title`.\n\n   ```elixir\n   %PhoenixLiveGantt.Task{id: a.id, title: localized_title(a, @locale), ...}\n   ```\n\nSo localization is fully in your control: chrome via one map, content via the\nstrings you already produce.\n\n## Accessibility\n\nThe chart is built for keyboard and screen-reader use when `enable_hooks` is on:\n\n- Bars, milestones, and label rows are focusable (`tabindex=\"0\"`,\n  `role=\"button\"`, `aria-haspopup=\"dialog\"`); **Enter/Space** open the popover\n  (and fire `on_event_click`), **Escape** closes it and returns focus to the\n  trigger.\n- The popover is a `role=\"dialog\"` with an `aria-label`; sub-project chevrons\n  expose `aria-expanded`; toolbar zoom buttons use `aria-pressed`; prev/next and\n  the edge buttons carry `aria-label`s. All these strings come from the\n  `translations` map, so they localize too.\n- The connector/arrow SVGs are decorative (`aria-hidden`), so a screen reader\n  walks the bars, not the geometry.\n\nTwo things are on you: task **status is conveyed by bar color** — if that\ndistinction matters to your users, encode it in the `title`/an action/a badge\ntoo (don't rely on color alone); and the geometry is **LTR-only** (see Gotchas).\n\n## Dates → bars\n\nHow a task's `start`/`end` become a bar — worth reading once, because `end`\nbeing exclusive trips people up:\n\n- **`end` is exclusive.** A bar covers `[start, end)`. A task that occupies\n  just April 1 is `start: ~D[2026-04-01], end: ~D[2026-04-02]`. If `end` is\n  `nil`, `PhoenixLiveGantt.Task.effective_end/1` fills it in: `start + 1 day` for a\n  `Date`, `+30 min` for a `DateTime`/`NaiveDateTime`.\n- **Milestones.** When `end \u003c= start` (zero duration) the task renders as a\n  diamond instead of a bar. A `nil`-`end` task with **no children** is a\n  milestone too.\n- **Bar width is honest by default** (`min_bar_px: 0`): a bar is exactly as wide\n  as its duration, so a task too short to see at the current zoom is a hairline\n  (and gets a \"too small to see\" marker — see `tiny_bar_px`). Set `min_bar_px` to\n  e.g. `4` to floor every bar to a visible sliver.\n- **Out-of-range events are dropped, not clipped.** A task entirely outside the\n  visible window isn't rendered; instead it's counted into the\n  \"← N earlier / N later →\" edge indicators. Wire `on_show_earlier` /\n  `on_show_later` to let users widen the range.\n- **Zoom** (`:min5` / `:min15` / `:hour` / `:day` / `:week` / `:month`) only\n  changes column grouping and pixels-per-day; it never changes which events are\n  in range. The sub-day zooms render intra-day detail from `DateTime` /\n  `NaiveDateTime` starts.\n\n## Sub-projects (hierarchy + roll-up)\n\nAny event becomes a child of another by setting `extra.parent_id` to the\nparent event's `id`. The parent renders as a **roll-up bar** spanning its\ndescendants, with an expand/collapse chevron and a framed band across both\ncolumns.\n\nThree things that aren't obvious and cost me time when I built the first\nconsumer — they're the rules to internalize:\n\n1. **Always include every descendant in `events`.** The library decides an\n   event is a sub-project (and draws the chevron) by finding other events that\n   point at it via `parent_id`. It then hides the children of *collapsed*\n   parents itself. So you emit the full tree every render and let `expanded`\n   control visibility — do **not** add children only when expanded, or a\n   collapsed parent has nothing pointing at it and never gets a chevron.\n\n2. **Let the parent's dates roll up — pass `start: nil, end: nil`.** A\n   sub-project parent with nil dates is auto-sized to span its descendants'\n   min start / max end. If you instead give the parent explicit dates (e.g.\n   from a rolled-up duration), the library uses *those* and the children can\n   visually spill outside the bar — you'd have to size the parent to its\n   children yourself. Nil-and-let-it-roll-up is almost always what you want.\n\n3. **`on_toggle_expand` fires with the param key `event-id`** (hyphen, from\n   `phx-value-event-id`), and you own the `expanded` set:\n\n   ```elixir\n   # render\n   \u003cPhoenixLiveGantt.gantt\n     events={@events}\n     date_range={@range}\n     expanded={@expanded}                 # MapSet | list | :all | nil\n     on_toggle_expand=\"toggle_subproject\"\n   /\u003e\n\n   # the handler — note the hyphenated key\n   def handle_event(\"toggle_subproject\", %{\"event-id\" =\u003e id}, socket) do\n     expanded = socket.assigns.expanded\n     expanded =\n       if MapSet.member?(expanded, id),\n         do: MapSet.delete(expanded, id),\n         else: MapSet.put(expanded, id)\n\n     {:noreply, assign(socket, expanded: expanded)}\n   end\n   ```\n\n`expanded` accepts a `MapSet`, a plain list, `:all` (everything expanded), or\n`nil` (all collapsed). Connectors that point at a hidden child are\nautomatically retargeted to its nearest visible ancestor, so arrows never\ndangle.\n\n## Laying out from durations\n\nIf your data has durations rather than dates, `PhoenixLiveGantt.Layout.sequential/2`\nturns it into the dates `gantt/1` wants — so you don't hand-roll (and re-bug)\nthe waterfall + sub-project-span + day-alignment yourself:\n\n```elixir\nlayout =\n  PhoenixLiveGantt.Layout.sequential(tasks,\n    start: ~D[2026-06-01],\n    id: \u0026 \u00261.id,\n    parent_id: \u0026 \u00261.parent_id,      # nil = top-level; others nest\n    duration: \u0026 \u00261.hours,           # opaque — only your :advance interprets it\n    order: \u0026 \u00261.position,\n    advance: fn start_date, hours, task -\u003e\n      # your calendar: weekends, working hours, holidays — all live here\n      MyApp.Calendar.add(start_date, hours, task)\n    end\n  )\n# =\u003e %{id =\u003e %{start: ~D[...], end: ~D[...]}}\n\nevents =\n  Enum.map(tasks, fn t -\u003e\n    %{start: s, end: e} = layout[t.id]\n    %PhoenixLiveGantt.Task{id: t.id, title: t.title, start: s, end: e,\n                    extra: %{parent_id: t.parent_id}}\n  end)\n```\n\nIt works entirely in `Date`s, so each item gets at least a one-day slot\n(`:min_span_days`, default 1), siblings never overlap, and a sub-project's bar\nalways spans its laid-out children. The business calendar is *yours* (the\n`:advance` callback); the library stays domain-agnostic. It does **not** do\ndependency-driven scheduling, critical path, or resource leveling — supply your\nown dates for those.\n\n## Connectors\n\nDependency arrows are plain maps referencing event ids:\n\n```elixir\n%{from: \"cut-wood\", to: \"assemble\", type: :fs, critical: true, label: \"2d lag\"}\n```\n\n- `type` — `:fs` (finish-to-start, default), `:ss`, `:ff`, `:sf`.\n- A connector whose `from`/`to` isn't a visible event id is silently skipped\n  (e.g. it points outside `date_range`).\n- A **backward / impossible** schedule (the dependent is laid out earlier than\n  the constraint allows) is auto-detected and drawn in the `invalid` style\n  (dashed, error color) — a free correctness check on your date mapping.\n- `critical: true` draws it in the critical style; `label` annotates the line.\n\n## Debugging\n\n- `mix phoenix_live_gantt.dump` renders a chart from a fixture and prints parsed bar\n  geometry — handy for checking positions without a browser.\n- `PhoenixLiveGantt.Inspector` parses rendered HTML into geometry, and\n  `PhoenixLiveGantt.TestHelpers` adds property assertions (bar containment, ordering,\n  connector validity) you can use in your own tests.\n\n## Gotchas\n\nThe short list of things that bite, collected from building the first\nconsumer:\n\n- **No CSS content source** → chart renders but library-specific classes are\n  purged. (See CSS step above — the #1 issue.)\n- **`end` is exclusive** → a one-day task needs `end = start + 1`, not `end =\n  start` (which is a milestone diamond).\n- **Every event needs a unique, non-nil `id`** → a `nil` or duplicate id\n  **raises** (it would make connectors and popovers ambiguous). Validate your\n  data, don't feed raw rows in blind.\n- **An event with no `start` (or no resolvable end) is silently dropped** → it\n  doesn't render and isn't even counted in the edge indicators. Give every\n  event a `start`.\n- **`today` defaults to `Date.utc_today()`** → in a timezone-aware app, pass the\n  viewer's own `today`/now, or the today line lands on the wrong column for\n  non-UTC users.\n- **`window_start`/`window_end` are all-or-nothing** → set only one (or make\n  `window_end \u003c= window_start`) and the whole sub-day window is silently\n  ignored, falling back to `date_range`.\n- **Sub-project children must always be in `events`** → emit the full tree;\n  `expanded` controls visibility. Adding children only when expanded breaks\n  the chevron.\n- **Give sub-project parents `nil` dates** → so they roll up to span their\n  children. Explicit parent dates can let children spill outside the bar.\n- **`on_toggle_expand` param is `\"event-id\"`** (hyphen), not `\"event_id\"`.\n- **`id` is required** with `show_header` / auto-scroll, and whenever two\n  charts share a page (ids + JS dispatches are namespaced by it).\n- **The JS bundle is effectively required.** The `LgBarPopover` /\n  `LgAutoScroll` hooks ship in `priv/static/assets/phoenix_live_gantt.js` (registered as\n  `window.PhoenixLiveGanttHooks`). `enable_hooks` (default `false`) gates BOTH hooks; if\n  you turn it on without registering the bundle you'll get \"unknown hook\"\n  console errors. The popover and scroll-to-today need it.\n- **Sub-project chevrons use heroicons** (`hero-plus-mini` / `hero-minus-mini`).\n  Those classes exist only if your app has the heroicons Tailwind plugin\n  (the default in Phoenix ≥ 1.7, but not universal). No plugin → no chevron glyph.\n- **`dir=\"rtl\"` sets the attribute but the geometry is LTR-only** — bars, the\n  time axis, and connectors still run left-to-right. RTL text in labels renders\n  fine; the chart layout does not mirror.\n\n## Large charts\n\nHorizontal geometry is pure CSS (percent positions, no measurement), so wide\ntimelines are cheap. The cost is the connector router: collision avoidance is\nroughly O(tasks) per connector, and a chart re-renders/serializes its full HTML\nover the LiveView socket. A few hundred tasks with dependencies at a fine zoom\nproduces multi-MB diffs and second-scale re-renders. To keep big charts snappy:\n\n- Set `avoid_collisions: false` (component attr, or per-connector) to skip the\n  obstacle pass — connectors may cross unrelated bars but routing is much cheaper.\n- Narrow `date_range` (or pass a `window_start`/`window_end`) so only the\n  relevant slice renders; out-of-range tasks become cheap edge-indicator counts.\n- Prefer coarser zooms (`:week` / `:month`) for overview; reserve `:day` and the\n  sub-day zooms for focused windows.\n\n## Status\n\nPre-1.0; API may shift. See CHANGELOG for breaking changes.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmdon%2Fphoenix_live_gantt","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmdon%2Fphoenix_live_gantt","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmdon%2Fphoenix_live_gantt/lists"}