@asilluron/horizon
v0.3.0
Published
Embeddable React visual roadmap for Linear data — a flight plan, not a Gantt chart.
Maintainers
Readme
Horizon
A stunning, embeddable visual roadmap for Linear data — a flight plan, not a Gantt chart.
Horizon is a read-only React component that renders Linear Initiatives, Projects, and Milestones as a horizontally scrolling timeline, centered on today. It is built for non-technical audiences — clients, stakeholders, leadership — who need to answer two questions at a glance: how far along is this feature? and how close are we to the release?
All editing happens in Linear. Horizon never writes back.
Features
- Now beam — a soft vertical beam of light marks today; the timeline opens centered on current work (40% past / 60% future)
- Three-state visual grammar — solid past, filling present, diagonal-hatched future
- Free scrolling — trackpad, drag-to-pan, shift+wheel, arrow keys, with a one-click "Back to now" pill
- Three zoom levels — Month / Quarter / Year, cursor-anchored (ctrl/⌘+wheel or
+/-) - Plain-language everywhere — Done, In progress, Up next · On track, At risk, Off track · "Mid-July", never ISO strings
- Release mode — header band with scope-weighted progress, "9 of 14 features complete", a pace indicator, and a flag line on the timeline
- Open-ended mode — no release configured? Rolling stats instead ("3 features shipped this quarter · 5 in progress"); a first-class mode, not a broken state
- Drill-in side panel — status, health, lead, dates, progress ring, milestones, and an optional "What's inside" issue list
- Initiative drill-in — click an initiative name to open a full issue waterfall: start→completion bars per issue, grouped by project, with dependency arrows between blocked issues and a Now line
- Not-yet-scheduled tray — undated projects stay visible, but honest about being undated
- Detail mode — milestone diamonds and issue lists for internal team reviews, off by default
- Light & dark themes, tablet-friendly, keyboard-navigable,
prefers-reduced-motionaware - Mobile fallback — below 768px the timeline becomes a vertical now-centered list
Install
npm install @asilluron/horizonReact 18+ is the only peer dependency. No runtime dependencies.
Quick start
import { HorizonRoadmap } from "@asilluron/horizon";
import "@asilluron/horizon/styles.css";
<HorizonRoadmap data={data} height={560} />;data is a plain JSON document (HorizonData): initiatives, projects, an optional release, and a lastSyncedAt timestamp. See src/types.ts for the full contract.
<HorizonRoadmap
data={data}
theme="dark" // "light" | "dark" | "auto"
defaultZoom="quarter" // "month" | "quarter" | "year"
defaultDetailMode // milestone diamonds + issue lists
showLinearLinks // "View in Linear" for authenticated internal users
syncUrl // encode scroll, zoom + open drill-ins in the URL; Back closes drill-ins
loading={isLoading} // skeleton timeline with the Now beam in place
/>Getting data out of Linear
Horizon deliberately ships no Linear API client. Sync server-side so your Linear API key never reaches the browser, cache the result (Postgres, Redis, a JSON file — anything), and hand the document to the component. The mapLinearData adapter converts raw Linear shapes into HorizonData:
// server-side, e.g. a cron job or API route
import { mapLinearData } from "@asilluron/horizon";
const data = mapLinearData(rawInitiatives, rawProjects, {
release: { name: "Product Launch 2.0", initiativeId: "...", targetDate: "2026-09-01" },
lastSyncedAt: new Date().toISOString(),
});A release can be derived three ways, in order of preference: an Initiative is the release, a project label (e.g. release:2.0) selects the set, or a manual list of project ids. Omit it entirely for open-ended mode.
To power the initiative drill-in view, include each project's issues in your sync query — issues { id identifier title state { type name } startedAt completedAt relations { nodes { type relatedIssue { id } } } } — and pass them on RawLinearProject.issues. The adapter maps Linear state types to plain language and inverts blocks relations into the arrows you see on screen. In-progress issues whose state name contains "QA" get a purple bar. Omit issues and the roadmap behaves exactly as before.
Recommended sync cadence: poll every ~10 minutes as a consistency backstop, with Linear webhooks (Project, Initiative, ProjectMilestone) triggering targeted refreshes in between. Viewer traffic should never touch Linear's API directly.
Progress semantics
- A project's number comes from Linear's own
progressfield by default — one number, no arguments - 100% requires the project status to be Done; all-issues-done-but-not-called-done caps at 99%
- A started project at 0% renders a minimum sliver, so In progress never looks like Not started
- Release progress is the scope-weighted mean of member projects (issue-estimate totals → issue counts → equal), with the weighting basis disclosed on hover
Design tokens
The "night flight" palette and type system are CSS custom properties on .hz-root — override them to re-brand. Load Archivo, Inter, and IBM Plex Mono in your host app for the intended look; system fallbacks apply otherwise.
| Token | Default | Role |
|---|---|---|
| --hz-horizon | #3D6B8E | Now beam, links, focus |
| --hz-ember | #D98E4A | progress fill (the warmest thing in view) |
| --hz-green / --hz-amber / --hz-red | | health signals |
| --hz-mist | #8A94A0 | gridlines, secondary text |
Linear's project color is used only as a 3px edge stripe on bars — brand consistency beats the rainbow.
Accessibility
Status and health are always encoded in text + shape, never color alone. The timeline is keyboard-navigable (←/→ pan, +/− zoom, Tab through bars, Enter opens the panel, Home returns to now). Focus is visible throughout, and all motion collapses to instant under prefers-reduced-motion.
Development
npm install
npm run dev # demo app with sample data
npm test # unit tests (vitest)
npm run typecheck
npm run build # esm + cjs + d.ts via tsupLicense
MIT © Andrew Silluron
