Changelog
What changed in Helix — release-level highlights below, the full commit-level history further down.
Recent releases
- fixed
AppShell no longer nests two main landmarks
2026-08-19v0.8.0SidebarInset rendered a <main> and AppShell rendered another <main> (the app-shell-main slot) inside it. HTML permits one main landmark per document and forbids nesting one in another, so screen-reader landmark navigation listed both and the inner region was not reliably exposed as the document's main. SidebarInset is now a plain <div> — every class and its data-slot are unchanged, so the fix is DOM-structural and pixel-identical (the inset still computes display:flex, flex:1 1 0%, and the same 1192px width beside a 248px rail). The landmark stays on the content region rather than the inset because that is the part a "skip to content" jump should land in. That also let the explicit role="banner" on AppShellTopBar go: the workaround existed only because a <header> inside <main> gets no implicit banner role, and the top bar now sits outside main, so <header> is an implicit banner on its own — the accessibility tree shows one banner and one main as siblings. Callers using the Sidebar primitives directly should note SidebarInset no longer supplies the landmark: wrap the page content in a <main> inside it, which is what every skill code sample already showed.
- new
Badge gains sky and sand tones
2026-08-19v0.8.0defaultSky / outlineSky and defaultSand / outlineSand round out the primitive colour set. Both ramps already existed in tokens.css, so no new tokens were added. The filled pair joins the LIGHT recipe (light fill + black text, as defaultOrange does) rather than the dark one: sky-600 would have been the tightest pair in the family against the navy fill (#16538E) by more than double at ΔE76 26.8, while sitting only ΔE 19.1 from sky-400 — nearly as close to a different tone as to another step of its own ramp. sky-400 sits ΔE 43.0 from navy. defaultSky measures 7.96:1 and defaultSand 11.05:1 against black. The outline pair takes the -700 text step, and that step is forced rather than chosen: the house recipe is -50 fill / -200 border / the first text step clearing 4.5:1, and sky-600 on sky-50 measures 4.49:1 (misses by 0.01) while light-sand-600 on light-sand-50 measures 4.02:1. Badge text is text-xs (12px), so the WCAG large-text allowance does not apply — the same call already made for outlineBerry. outlineSky lands at 6.23:1 and outlineSand at 5.46:1. The variants are named Sand, not LightSand: the variant name is the public API and "sand" is the spoken name, with the tc-light-sand token binding recorded in metadata.
- new
Badge takes success, warning, and info
2026-08-19v0.8.0Badge now answers to success / warning / info and outlineSuccess / outlineWarning / outlineInfo. Zero pixels change: each is an alias over an existing look — defaultSage, defaultOrange, defaultNavy and their outline counterparts — and both keys reference one shared class string in badge.tsx so an alias cannot drift from what it aliases. The gap was real: tokens.css defines --success and --warning, but those tones reached Badge only through the tc-* primitives, and destructive was the only tone with a same-named variant, so a status badge had to be spelled as a COLOUR — which invites hand-rolling. Alert already ships success/warning/info, so this was an inconsistency between two components carrying the same semantics rather than a missing feature. The tc-* colour names stay permanently and are NOT deprecated: a defaultNavy badge used as a decorative category chip is genuinely not `info`, and collapsing the two would force a false semantic onto non-status badges. Badge's filled warning is a lighter ramp step than Alert's on purpose — Alert uses its tone as a RAIL (3:1, WCAG 1.4.11) while Badge uses it as a TEXT BACKGROUND (4.5:1, WCAG 1.4.3); tc-orange-300 + black is 14.08:1, whereas matching Alert's tc-orange-700 with white would be 4.20:1, an AA failure at Badge's 12px text.
- new
Dialog caps its height, and DialogBody pins the header and footer
2026-08-19v0.8.0DialogContent had no max-height and no overflow handling. Because the panel is centred with translate-y(-50%), a dialog taller than the viewport bled off BOTH edges at once with nothing scrollable — measured at 1252px tall in a 907px viewport, losing 173px at each end and taking the title and the action buttons with it. DialogContent now sets max-h-[calc(100dvh-4rem)] and becomes a flex column. dvh, not vh: on mobile Safari vh resolves against the largest viewport, so a vh cap still clips while the URL bar shows, and the usual dvh jitter objection does not apply because a modal locks body scroll. Scrolling is opt-in-shaped so existing dialogs are fixed without touching them: with no DialogBody present the whole panel scrolls (not-has-[[data-slot=dialog-body]]:overflow-y-auto), which is what 81 of 86 admin call sites get for free. Wrap the middle in the new DialogBody and only it scrolls, with DialogHeader and DialogFooter pinned — prefer that for any dialog whose actions must stay reachable. Dialogs that already fit are unchanged, and the five call sites that invented their own max-h (80vh, 85vh, 90vh) keep their value, since a caller's class still wins through twMerge; those can now drop it.
- new
Table can pin its header row
2026-08-19v0.8.0Table now takes stickyHeader + maxHeight, e.g. <Table stickyHeader maxHeight="24rem">. Consumers could not build this themselves: className goes to the inner <table>, not to the scroll container, and the container had no bound — overflow-x-auto already makes overflow-y compute to auto, so it was a scroll container, but with unconstrained height nothing scrolled inside it and a hand-rolled `sticky top-0` on TableHeader had no scrollport to pin against. maxHeight supplies the bound as an inline style (a caller-chosen length must not become an arbitrary utility); the header gets sticky top-0 z-10 plus a bg-card fill so rows do not show through it, and a card-variant container drops its vertical padding, because a scrollbox's padding gutters sit inside the scrollport and a clipped row was visible above the pinned header. stickyHeader without maxHeight is a no-op with a dev-only warning rather than a header that silently does not stick. This is an inner scroll panel: the page does not move, and parking the header under an app bar is a different UX that is not supported.
- fixed
Table warns when a card-variant table is nested in a Card
2026-08-19v0.8.0variant="card" stays the default — no visual change — but the wrong pairing is now loud. A card-variant table inside a Card's CardContent applies the card treatment twice: the container's p-6 stacks with CardContent's px-6, doubling the intended inset, and its border/rounded-xl/shadow-sm draws a second surface inside the Card's. Six admin surfaces shipped it, because it renders almost right and nothing in the type system or the test suite can see it. Table now detects the arrangement in the DOM and logs a dev-only console warning naming the stacked classes; use variant="flush" inside a Card. Flipping the default to flush was rejected: flush's failure mode is a standalone table with no border, radius or shadow at all, which relocates the risk instead of removing it — and a nested card is the harder of the two to spot.
- breaking
AlertAction anchors to the title row instead of centring
2026-08-19v0.8.0AlertAction spans both of Alert's grid rows, so self-center centred it across the title AND the description — a disclosure chevron sat beside the title while collapsed and then slid down by half the description's height when opened (32px in a three-line specimen, more as the description grows). A control you click must not move when you click it. The default is now self-start. Alerts with a title only are unaffected, since one row renders identically either way. Where centring is genuinely wanted, pass className="self-center" — it merges cleanly, so no prop was added.
- breaking
CardTitle now sets its own size (18px)
2026-08-19v0.8.0CardTitle was only `leading-none font-semibold`, so its size came from whatever ancestor happened to set one — 14px inside a text-sm ancestor, 16px with none. The same component rendered at different sizes on different pages, and replacing a 24px heading with a bare CardTitle silently dropped it to body size. It now defaults to text-lg (18px). Call sites that already pass an explicit size are unaffected — cn is twMerge, so the caller's class still wins. Every CardTitle without one grows.
- fixed
Outline Button matches every other bordered control
2026-08-19v0.8.00.7.0 darkened --input for contrast but left --border alone, and outline Button read a bare `border` — so it kept the old #D3D4D4 stroke while Input, Select, Textarea, Checkbox, Radio, Combobox and InputOTP all moved to #646565. One toolbar rendered two different strokes. Outline Button now sets border-input explicitly. Light mode only; dark mode was already correct, and the variant's own dark:border-input override is now redundant and removed.
- new
AlertDisclosure — Alert can collapse its body
2026-08-19v0.8.0A long notice that leads with its summary and reveals the rest behind a “Read more” chevron had to be hand-built from an Alert plus a separate expand/collapse element, so every team got a slightly different result and the ARIA wiring was theirs to get wrong. AlertDisclosure supplies both halves already wired — aria-expanded and aria-controls on the trigger, and a revealed region that is `hidden` while collapsed so it leaves the accessibility tree rather than being merely invisible. Labels default to “Read more” / “Read less”, the house verbs for expanding truncated body text in place. It is its own module rather than a `collapsible` prop on Alert: disclosure needs state and "use client" is per-module, so the prop would have moved Alert across the client boundary for every consumer including the many rendering a plain static banner — and Alert is one of the 11 server-safe components. It renders a fragment rather than a wrapper because Alert is a grid and the revealed body and the trigger belong in different cells; a wrapper would be one grid item and would drop the trigger below the text. Pass role="status" on the Alert: the default role="alert" is an assertive live region, and revealing text inside one makes a screen reader interrupt itself to read what the user just asked for — a disclosure is not an event. Reuses alertActionClasses, so it inherits this release's self-start fix, which was made for exactly this control: expanding grows the alert 87px while the trigger moves 0px.
- fixed
Deprecated variant values are no longer offered as choices
2026-08-12v0.7.0Deprecated variant values are recorded in variants[].deprecatedValues and removed from the variants[].values list the docs and the MCP server serve, so they are no longer offered as valid options. Alert's info and error, and Button's secondary, still render but log a dev-only console warning. Asking for an "info banner" now points at variant="default", which is the informational style; the supporting action next to a primary button is outline.
- new
TableSkeleton compound
2026-08-12v0.7.0Loading placeholder shaped like a table — Skeleton bars inside real table rows and cells rather than a generic block, so the placeholder occupies the same grid as the data it replaces and swapping in the real rows does not reflow the page. Live-region wiring included.
- new
TablePagination compound
2026-08-12v0.7.0Table footer with a status line and Previous/Next. Pass page, pageSize and total; the row range and page count are derived and the range is announced, so callers do not compute or wire that themselves.
- new
PasswordInput and PasswordRulesList compounds
2026-08-12v0.7.0PasswordInput is a password field with a trailing show/hide toggle. PasswordRulesList shows live positive-assertion rule feedback as the user types and doubles as the error state, so a set-password form needs no separate error message. Rules are caller-supplied, because password policy is enforced by each product's own validation and server and the list has to mirror it.
- breaking
Alert signals its variant with a leading rail, not a background tint
2026-08-12v0.7.0Every Alert variant now renders on the card surface with a foreground title and muted description; the variant is carried by a 4px rounded rail on the leading edge plus the icon color. The soft tc-*-50/-200 tinted palettes are gone. No code change is required — the variant union is unchanged, and six names now resolve to four looks (default ≡ info navy, destructive ≡ error berry, warning orange, success sage) — but expect a visual diff anywhere Alert is used. Because color is now the only variant signal, always pass an icon. AlertAction also moved to its own grid track, so a long action label shrinks the content instead of overlapping it, and adding one no longer widens the title-to-description gap.
- breaking
Table card/flush variants; default is now a padded card
2026-07-24v0.6.0A single table on a page renders as its own padded, bordered card (variant="card", the new default) so rows are never flush to the edge, with its header/controls/footer around it. When several tables share a page, wrap each in a Card and use variant="flush" inside. The prior tinted bg-input-bg default is gone — an existing <Card><Table></Card> should adopt variant="flush" to avoid a double border.
- new
Tabs spacing prop (compact / page)
2026-07-24v0.6.0Tabs gains a spacing prop for the gap between the tab strip and its content — compact (8px, default) for inline tabs, page (24px) for page-level detail tabs.
- new
text-link semantic color token
2026-07-24v0.6.0A semantic text-link color (brand blue, tracking primary) for inline links, themeable in light and dark. Use it instead of raw text-blue-* utilities.
- new
AppSidebar section-aware active routes + NavBreadcrumb
2026-07-24v0.6.0AppSidebar accepts a pathname prop and resolves the active item/section itself, so nested and sibling detail routes (e.g. /clients/:id) highlight their owning section. Collapsible groups tidy on navigation — only the current section stays open. NavBreadcrumb auto-fills the PageHeader breadcrumb from the same nav model.
- new
Five-intent information architecture
2026-07-09v0.3.0Docs restructured into Guides / Foundations / Components / Patterns with a taxonomy-driven catalog, new top nav, section landing pages, and legacy-URL redirects.
- new
Compound components join the catalog
2026-07-09v0.3.0The 7 compounds (app-shell, app-sidebar, editable-section, filter-popover, list-page-template, metric-card, page-header) now have content pages and are covered by the verify:docs gate like every other component.
- fixed
Relative content links no longer 404
2026-07-09v0.3.0Links inside design-system markdown written relative to content/ (../dir/file.md, ./file.md, bare file.md) now resolve to their real routes instead of 404ing on the static export.
- new
AlertAction added
2026-06-29v0.3.0Alerts can now carry a call-to-action, positioned top-right; the container reserves space for it automatically.
- breaking
Sidebar width 256px → 248px
2026-06-26v0.3.0Default expanded sidebar width corrected from the shadcn stock 16rem to the spec's 248px; menu button height and submenu indentation were also aligned to spec.
- fixed
Tooltip uses the primary token, not foreground
2026-06-26v0.3.0Tooltip background and arrow switched from bg-foreground (rendered as a near-black tooltip) to the semantic bg-primary/text-primary-foreground pair.
- fixed
Primitive spec-conformance sweep
2026-06-26v0.3.0An audit against content/components/*.md found vendored-fork drift across button, empty, command, popover, item, switch, kbd, pagination, tabs, toggle, alert, select, and more — published classes now match the documented spec.
- new
Monorepo consolidation
2026-06-26v0.3.0Helix consolidated into one npm-workspaces monorepo: the @timelycare/helix-ui package, the docs/visualizer app, and a single canonical content/ source, with a verify:docs no-drift gate between structured metadata and prose.
Change Log
A record of changes made to Helix — both the design system specs and the workbench front-end.
| Date | Branch | Summary | Merged Into | Type | |
|---|---|---|---|---|---|
| Jun 26, 2026 | consolidate-monorepo | Monorepo consolidation: package + visualizer + canonical content | — | WorkbenchSpec | |
| May 6, 2026 | staging | Staging → Main: Table Behavior pattern, accessibility, and interactive state improvements | main | SpecWorkbench | |
| Apr 29, 2026 | staging | Staging → Main: Semantic color tokens, K12 + TimelyPulse nav icons, sidebar previews | main | SpecWorkbench | |
| Apr 29, 2026 | staging | Semantic color tokens: zinc → tc-gray migration; K12 + TimelyPulse nav icons + sidebar previews | main | SpecWorkbench | |
| Apr 22, 2026 | staging | Specs: rename magenta → berry across badge, table, and data-table docs | main | Spec | |
| Apr 22, 2026 | staging | Alert: warning, info, success, error variants; preview polish | main | SpecWorkbench | |
| Apr 15, 2026 | staging | Staging → Main: Brand color hierarchy, tc-gray tokens, berry badge variants, ScaledFrame | main | SpecWorkbench | |
| Apr 10, 2026 | staging | Staging → Main: PageTemplates, Avatar/Sidebar specs, PreviewAppShell, Sidebar height, Table overflow | main | SpecWorkbench | |
| Apr 1, 2026 | staging | Staging → Main: Interactive charts, Table striped variant, outline badge mapping | main | SpecWorkbench | |
| Mar 20, 2026 | staging | Staging → Main: Header cleanup, mobile logo, TimelyCare logos, proposal framework, color fix | main | SpecWorkbench | |
| Mar 20, 2026 | staging | Staging → Main: TimelyCare logos, design system proposal framework, tc-light-sand token fix | main | SpecWorkbench | |
| Mar 13, 2026 | staging | Staging → Main: Brand colors (tc- prefix), typography scale, badge color variants, K-12 removal | main | SpecWorkbench | |
| Mar 13, 2026 | feat/k12-removal | Remove K-12 and theme system — Helix is now single-brand | staging | SpecWorkbench | |
| Mar 13, 2026 | feat/badges-updates | Badge color variants (primitive), semibold typography, full rounded shape | staging | SpecWorkbench | |
| Mar 13, 2026 | feat/new-brand-colors | New brand colors; removed outdated primitive and semantic tokens | staging | SpecWorkbench | |
| Mar 9, 2026 | staging | Promote staging to main — full component audit, avatar & switch fixes | main | SpecWorkbench | |
| Mar 9, 2026 | REV-2026-march-09 | Comprehensive audit aligning all 57 usage previews with Helix specs | staging | SpecWorkbench | |
| Mar 3, 2026 | staging | Data table spec & preview updated to use Helix Badge, Table, and Checkbox components | main | SpecWorkbench | |
| Mar 3, 2026 | staging | Promote staging to main — button hierarchy, changelog feature, workflow fixes | main | SpecWorkbench | |
| Mar 3, 2026 | feat-button-hierarchy-2026-mar-03 | Button action hierarchy and style updates | staging | SpecWorkbench | |
| Mar 2, 2026 | REV-2026-mar-02 | Filter preview updates and button outline style refinement | staging | SpecWorkbench | |
| Feb 27, 2026 | REV-2026-02-27 | Filtering pattern, select behavior, surface colors, sidebar icons, component library reorg | staging | SpecWorkbench |