Hexpunk
InstallConceptsElementsProsehp-cellhp-codehp-copyhp-hexhp-latexhp-visually-hiddenhp-buttonhp-checkboxhp-formhp-labelhp-radiohp-selecthp-sliderhp-togglehp-toggle-grouphp-badgehp-taghp-progresshp-spinnerhp-backgroundhp-clusterhp-collapsiblehp-demohp-gridhp-scroll-areahp-separatorhp-sidebarhp-toolbarhp-avatarhp-iconhp-pixelhp-linkhp-menubarhp-navigation-menuhp-tabshp-alert-dialoghp-context-menuhp-dialoghp-dropdown-menuhp-hover-cardhp-popoverhp-tooltiphp-bannerhp-toasthp-tetherhp-module-handlehp-unfold-listhp-unfold-overlayhp-unfold-pagehp-bondhp-link-nodePaletteAnimationsReleases

Releases

v0.1.1-alpha — 2026-08-10

Added

  • <hp-background> page mode — fixed full-viewport backdrop behind page content (<hp-background page>). The canvas is browser-pinned and exactly viewport-sized; the pattern slides beneath it via the shared page-aligned offset, so it can never be scrolled into a blank region. Adopted by the showcase layout.
  • <hp-background> energy-field wake — pointer movement stirs a persistent low-res field (ping-pong sim at 1/8 viewport resolution: 5-tap diffusion + dt-normalized decay) instead of the old instantaneous halo. Splats are segments from the previous to current pointer position with speed-scaled strength, so fast sweeps carve bright continuous wakes and slow hovers glow gently. The render loop runs only while the field holds visible energy — sleep is computed analytically from the decay rate, and at idle zero frames are drawn. Tuning attributes decay / splat-strength / splat-radius, each overridable via --hp-bg-decay / --hp-bg-splat-strength / --hp-bg-splat-radius.
  • <hp-background> click-ignition runners — pressing a non-interactive target (composed-path check + proximity gate) spawns pixel runners that crawl outward along the actual rendered hex lattice, turning at random and branching only away from the press point. Holding the press sustains new waves from the live pointer position; release lets live runners finish. Runner heads carry a draw-time-only hot glow (--hp-bg-stroke-hot / --hp-bg-hot-opacity, defaulting to the bright colour at doubled opacity) while their trails are capped at the bright tier — the hot tier belongs exclusively to ignition. Suppressed under reduced motion.
  • <hp-background> scroll stirring — scroll velocity feeds the field so the effect exists during the most common interaction on every device. Placement via the scroll-stir attribute: edge (default — a band along the viewport edge new content arrives from), pointer, band, or off. Capped at the bright tier.
  • <hp-background> three-tier degradation — hardware GL renders the full effect; software-rendered GL (SwiftShader / llvmpipe / WARP detected via the renderer string, plus an adaptive frame-interval backstop) routes to the CSS tile; no-GL gets the same tile. The tile is applied as a CSS mask over background-color (theme-reactive with zero JS) across two pseudo-element layers, restoring the v1-style radial pointer reveal on the fallback path. A console.debug render-path log (Verbose level) states which tier each instance chose and why.
  • <hp-pixel> type attribute — built-in morphing icon sets resolved by name, no JS wiring: menu (dotted square → plus on hover → cross toggled), expandable (square → down-chevron → up-chevron), dropside (square → right-chevron → left-chevron). Faithful to the CodePen reference, including the parked corner pixels that fly outward and fade during arrow morphs (icons can now bundle a palette). Precedence: explicit states > type > art.
  • .ai/ contract layer — CLAUDE.md workflow contract (root CLAUDE.md imports it) and .ai/STYLE.md code-style guide, including a performance-consideration convention: every component gets an isolated size + init-cost measurement before merging, and heavy components state their numbers in their showcase Performance section.
  • Showcase: hp-background component page rewritten for v2 — energy wake, ignition, scroll stirring, tiers, and a measured performance readout (43 KB min / 14 KB gzip, ~20 ms init, ~0.1 ms/frame animating, zero at idle) behind a "show stats" disclosure composing hp-collapsible + a native-button trigger with hp-pixel type="expandable".
  • Showcase: hp-pixel component page rebuilt around the built-in types with persistence demos (aria-pressed toggling) and guidance on driving morphs from an enclosing control via the published --hp-pixel-* custom properties.

Changed

  • <hp-background> rewritten as a by-concern module folder (src/elements/layout/hp-background/ — geometry, gl, bake, render, field, runners, input, fallback, styles), replacing the 1,100-line monolith and the two-layer-SVG render path with a single WebGL2 canvas (baked hex-tile texture + runtime sample-and-blend). Public API preserved; pointer-radius now governs the fallback path's reveal radius (the GL wake is governed by the splat/decay knobs).
  • <hp-cell> xs / xxs label typography — the inline-control tiers now use the label-sm scale with tight padding; label-md overflowed a 20–32 px hex. Longer text belongs beside these sizes, not inside.
  • CSS colour parsing moved to src/lib/css-color.ts (shared with future consumers; parser context declares willReadFrequently).

Fixed

  • <hp-background> full-height rendering with hardware acceleration off — the canvas is never larger than the viewport, never JS-repositioned on scroll, and self-corrects its backing store on every draw, eliminating the software-compositor cutoff bug class. Where the compositor still can't display a WebGL canvas reliably (software-rendered GL), the element routes to the CSS tile instead of fighting it.
  • <hp-background> fallback tile theming — the tile was an SVG background-image stroked with currentColor, which a data-URL SVG always resolves to black; as a mask over background-color it now follows light/dark tokens live.
  • <hp-background> loop hardening — reconnect after a DOM move re-initializes GL (was: permanently frozen canvas); a zero-sized host stops its loop instead of spinning rAF; the sleep window counts accumulated sim time rather than wall clock (was: a tab-hide mid-wake could strand a bright streak on screen); splat coordinates resync on geometry/DPR changes (was: phantom full-strength streak); pointer activity outside an instance's reach no longer wakes its loop; the adaptive demotion backstop samples one contiguous wake against a 60 ms median with outlier discard (was: 30 ms — falsely demoted healthy 30 Hz displays).
  • Showcase: removed the redundant light-DOM z-index rule that silently defeated --hp-bg-z.

v0.1.0-beta — 2026-05-24

Added

  • <hp-cluster> layout attribute — rosette (default) / honeycomb modes. rosette preserves the canonical 5-hex cross via named slots (centre / top / middle-left / middle-right / bottom). honeycomb accepts N default-slot children: the first child is the centre (axial 0, 0); remaining children fill outward clockwise from north — ring 1 holds 6 positions, ring 2 holds 12 (capped at 19 hexes total in v1). Drag handling stays in <hp-grid> via drag-handle="..."; canonical handle is :first-child for honeycomb mode, [slot='centre'] for rosette. Auto-sizes the host: 2-ring footprint for rosette, 4-ring for honeycomb. Existing rosette usages render unchanged.
  • <hp-cell variant="anchor" filled> styling. The filled boolean now applies to the anchor variant in addition to action. Idle paints fill + stroke + label in --hp-primary / --hp-on-primary; hover / focus-visible / aria-pressed swap the entire trio to --hp-secondary / --hp-on-secondary, matching the unfilled anchor's hue-swap motion. Used by <hp-cluster layout="honeycomb"> as the cluster centre / drag handle so the focal hex reads at a glance.
  • <hp-grid> pack() public method — runs the FFD bin-pack against the grid's current children. Triggered automatically on first render and on layout attribute changes; consumers can call it manually after dynamic child additions / filters / drags.
  • <hp-grid> recenter() fits content to the viewport. Walks every [q][r] child's per-cell fill mask (data-fill-cells) to compute the visible bbox, picks the zoom that frames the layout with ≤ 1 cell of padding around the outermost hex, then pans the bbox midpoint to viewport centre. Max zoom = 1 (never zooms past native scale). The recenter control button now snaps to "show everything" after any pan / zoom interaction. Replaces the previous reset-to-zoom-1 + pan-to-bbox-midpoint behaviour.
  • <hp-grid> skip-hidden plumbing. pack(), recenter(), and computePanBounds() all skip children carrying the hidden attribute. Filter-controlled visibility (e.g. the showcase components page's search) now plays cleanly with the layout pipeline — hidden clusters don't claim slots, don't drag the fit-bbox off-centre, and don't constrain pan range.
  • src/icons/github.ts — locally-sourced GitHub Octocat brand icon. Lucide 1.16.0 dropped brand icons, so tools/build-icons.ts gains a LOCAL_ICONS list that pulls SVGs from tools/icons/ alongside the Lucide ones. The generated module documents its source (tools/icons/github.svg) and notes that the icon ships as a fill-mark (<svg fill="currentColor">), not a stroke icon — the standard stroke-* props don't apply.
  • <hp-grid layout="spiral"> / layout="rows"> — FFD bin-pack with per-cell occupancy and hex-adjacency gap rule, sharing a common algorithm and differing only in scan order. On first render and on explicit .pack() calls, direct children are sorted by mask size (largest first), then each is placed at the first free position the chosen strategy returns. spiral scans outward from the origin in honeycomb rings — the largest cluster anchors (0, 0), smaller ones nest around it with ≥1-hex gaps, producing a tight roughly-square honeycomb. rows scans row-major with a width cap (10 axial cells half-width) — the layout grows as left-to-right rows that wrap downward, ideal for full-page-width surfaces. Children publish their actual filled hexes via data-fill-cells (space-separated "q,r" pairs); composite elements like <hp-cluster> publish these on slotchange. Children without the attr are treated as a single hex. The gap check uses hex-adjacency (the 6 axial-distance-1 neighbours) — not rectangular 3×3 padding — so non-symmetric cluster shapes leave their empty corners available for neighbours to tuck into. No auto-repack on resize / slotchange — only on explicit .pack() call or layout attribute toggle. Strategy modules live at src/elements/layout/hp-grid/layouts/{spiral,rows}.ts with shared primitives in layouts/index.ts; full unit-test coverage in layouts/{spiral,rows,layout}.test.ts (20+ tests covering hex-adjacency rejection, rect-diagonal acceptance, the layout's compactness bound, the components-page 12-cluster snapshot, and the row-wrap behaviour).
  • <hp-cluster> per-cell fill mask — both layout="rosette" and layout="honeycomb" now publish their actual filled hexes as data-fill-cells="q,r q,r …" host attribute (rosette: 5 fixed positions; honeycomb: first N entries of the fill-order table, derived from child count on slotchange). Bounding-box mirror attrs data-axial-q-min / q-max / r-min / r-max retained alongside for cheap consumers that only care about overall footprint dimensions. Consumed by <hp-grid layout="spiral"> / layout="rows"> for the tight bin-pack — empty bbox corners of non-symmetric cluster shapes (e.g. honeycomb with only NE ring-2 hexes filled) stay available for neighbour clusters to tuck into.
  • Showcase: /components index page switched to <hp-grid layout="rows"> — drops the hand-tuned per-category POSITIONS map; adding or removing categories in sitemap.ts now auto-reflows the cluster grid with a 1-cell gap between every cluster.
  • Showcase: /components index page — full-page-width <hp-grid draggable> filling the main column. Each category renders one <hp-cluster layout="honeycomb" drag-handle=":first-child"> with a centre anchor cell (<hp-cell variant="anchor" filled>) for the category label and one <a>-wrapped action cell per component. Web Awesome-style centred header (title + tagline + filter input), filter input live-filters cells by name substring, clusters whose entire ring filters out auto-hide, and a 300ms debounce kicks off a repack so the layout reflows around just the visible clusters. Sitemap's Components branch gains /components as the branch path so the sidebar heading is itself clickable.
  • Showcase: home page redesign — hero (<h1>Hexpunk</h1> + tagline) + Setup section (copy-paste <head> snippet in <hp-code language="html"> with an <hp-copy> button positioned in the top-right corner; the snippet documents the eventual esm.sh CDN surface: two stylesheet links, an importmap, a module script) + Get Started section (5-hex <hp-cluster layout="rosette">: Install centre + Concepts top + Components middle-left + Palette middle-right + GitHub bottom). Drops the old "Spatial primitives" + "Quick links" prose sections.
  • Showcase: navbar Docs (/getting-started/install) and Components (/components) links between the brand and the toolbar. GitHub icon link as the rightmost item in .header-tools, right of the theme toggle (aria-label="View on GitHub", opens in a new tab).
  • Showcase: Layout.astro prose?: boolean prop. When set, slotted page content wraps in a .hp-prose container so it picks up the elements.css prose chrome (vertical rhythm, list indent, 70ch max-width). Applied to install, concepts, elements, animations, releases pages. The /getting-started/prose page deliberately controls its own wrappers (it demos .hp-prose with side-by-side comparisons) so it stays unwrapped.
  • Showcase: right-rail TOC walker skips headings inside <hp-demo> blocks (demo content is sample, not page structure) and headings carrying data-no-toc for one-off opt-outs.
  • @hexpunk/core/elements.css — token-driven preflight for native HTML primitives. Bare reset (auto-applied) styles every native element (headings, paragraphs, links, lists, tables, code, blockquote, hr, figure, details/summary, form inputs) using --hp-* tokens; .hp-prose (opt-in class) adds vertical rhythm, list indent, table chrome, and a 70ch max-width for long-form content. Focus ring mirrors hp-base; reduced-motion and forced-colors fallbacks included. Custom <summary> marker rotates 90° on open.
  • Showcase: /getting-started/elements — preflight tour walking every styled primitive inside <hp-demo> blocks, with hp-banner callouts pointing to the <hp-*> component upgrade path for code, dividers, and form elements.
  • Showcase: /getting-started/prose.hp-prose walkthrough with side-by-side comparison, plain-English rhythm-rule breakdown, and a "when NOT to use it" section.
  • README: top-level Styling section documenting the three canonical CSS imports (tokens.dark.css / tokens.light.css / elements.css).
  • Package exports: ./elements.css, ./hp-base.css, ./tokens.dark.css, ./tokens.light.css. The four canonical CSS files now ship explicitly from src/ and are importable as @hexpunk/core/<name>.css.
  • <hp-latex> — render-only LaTeX math primitive. Consumer-registered renderer via HpLatex.setRenderer((latex, "inline" | "block") => string | null); @hexpunk/core has zero math-engine dependency. value attribute carries the LaTeX source (text content fallback captured once on connect). Boolean attributes: block (block/display mode, centred + full-width), background (mounts <hp-background> backdrop inside the element, block mode only), copyable (mounts <hp-copy> source button in the top-right corner, block mode only). Without a renderer, source renders mono-spaced fallback; render-error CustomEvent dispatches on renderer throw. Only light-DOM hp-\* element in the system — required for KaTeX's class-scoped CSS to cascade into the rendered output (file header explains the rationale). Not SSR-friendly via @lit-labs/ssr; consumers wanting pre-rendered math in MDX use Astro's remark-math + rehype-katex directly.
  • <hp-copy> — copy-to-clipboard button widget. Lucide copy icon + slotted label (translatable per-instance, default "Copy") + sibling "Copied" toast with aria-live="polite". value attribute for the text to copy; copied attribute for the toast text; icon-only boolean drops the visible label while keeping it in the a11y tree. Bubbling hp-copy-success / hp-copy-error events. Async Clipboard API only — no execCommand fallback. Now powers <hp-demo>'s copy-code action and <hp-latex>'s source-copy button.
  • @hexpunk/core/hp-latex.css — light-DOM chrome stylesheet for <hp-latex> (display modes, backdrop positioning, source-fallback typography). Consumer imports alongside elements.css and katex.min.css.
  • Lucide copy icon mirrored into src/icons/copy.ts via bun run icons for use inside <hp-copy>'s shadow root.
  • Showcase: /components/primitives/hp-latex — component page with Examples / API / Intent / Changelog tabs. Demos cover inline math, block mode (integral / summation / limit), matrix, quadratic formula, multi-line aligned and cases environments, backdrop + copy combination, no-backdrop variant, and error handling for broken LaTeX. Intent tab documents the KaTeX setup recipe, the light-DOM rationale, alternatives (KaTeX 0.17.0 / Temml 0.13.3 / MathJax v4 4.1.2 — maintenance verified May 2026), and the remark-math + rehype-katex SSR path.
  • Showcase: /components/primitives/hp-copy — component page documenting the copy widget. Demos cover default, custom idle text via slot, translated (French), and icon-only.
  • KaTeX 0.17.0 wired in the showcase as a devDep — registered once at module load in Layout.astro via HpLatex.setRenderer. katex.min.css imported at the layout root.

Changed

  • <hp-cluster> host pointer-events. Host is now pointer-events: none; ::slotted(*) re-enables pointer-events: auto. The cluster's rectangular bbox no longer intercepts pointerdowns through its empty corners — pan drags on the surrounding <hp-grid> reach the host correctly, and at the page bottom-right the cluster bbox no longer overlaps hp-grid's zoom / recenter controls. Only the actual hex cells catch clicks; the drag-handle child is still interactive because the ::slotted rule re-enables pointer-events on every positioned hex.
  • <hp-grid> drag-handle gating. Resolution switched from event.target.closest(selector) to target.querySelector(selector). The closest()-from-event-target approach matched :first-child ancestors transitively — e.g. inside <a><hp-cell></hp-cell></a>, the hp-cell is its <a> parent's first child, so closest() returned it and the drag-handle gate passed even when the click was outside the cluster's actual centre handle. querySelector resolves to the cluster's first matching descendant in DOM order (the actual centre), and the gate checks whether the pointerdown landed on it or one of its descendants.
  • <hp-grid> pan range swaps when content overflows the viewport. Previously, when the layout was bigger than the visible area, the pan-bounds rectangle collapsed to its midpoint — the user could centre the content but couldn't pan to see the overflow. Now, when minX > maxX (or minY > maxY), the two values swap so the legal range covers "child's left edge at viewport's left" through "child's right edge at viewport's right".
  • Showcase: sidebar drops the standalone "Home" leaf; the brand link in the navbar already navigates home. Tagline moved out of the navbar (frees space for Docs + Components nav links); it now lives on the home page hero.
  • Showcase global.css — removed duplicated typography rules (html/body font, a colour, code chip) now provided by elements.css. Removed the bespoke h2 { label-md uppercase } override that contradicted the design system's type ramp (h2 → headline-lg); h2 now renders correctly between h1 and h3 in the size hierarchy. Removed the p { --hp-on-surface-variant } override; paragraphs now render at full body colour.
  • <hp-demo> copy action — replaced the bespoke inline copy button + animated toast with <hp-copy> so the same widget powers both <hp-demo> and <hp-latex>. Slot-text source caching via slotchange on the code slot; no behaviour change for consumers.
  • Showcase code panels — JSX → plain HTML. All 116 <hp-code slot="code" language="jsx"> panels across 51 pages converted to language="html" showing element markup directly. JSX import "@hexpunk/core"; export default () => (…) wrapper dropped — copy-pasteable HTML matches what Lit-native consumers actually paste, and the convention every comparable Lit design system (Shoelace / Web Awesome / Adobe Spectrum / Carbon Web Components) uses. Edge cases (hp-copy single-arrow shape, hp-latex comment-between-import, hp-code const-string pattern) needed manual rewrites; hp-icon's Lucide-mirror inline-import documentation moved into a new Intent-tab section. Bulk transformation tool added at tools/convert-jsx-panels.ts for any future bulk format changes. Full-doctype standalone-runnable template wrap deferred to a post-publish PR (CDN URLs would 404 today since @hexpunk/core isn't on npm yet).

Fixed

  • <hp-grid> recenter no longer clips content larger than the viewport. The previous algorithm reset zoom to 1 and panned to the bbox midpoint — fine if content fit at native scale, but layouts taller than the viewport stayed clipped because zoom never adjusted. The new fit-to-content pass zooms out to frame everything inside the viewport with ≤ 1 cell of padding (capped at zoom = 1).
  • <hp-grid> drag-handle resolution false-positive inside anchor wrappers. With drag-handle=":first-child" on a cluster whose centre cell sat inside <a><hp-cell></hp-cell></a>, the previous event.target.closest(":first-child") walk matched the wrapped hp-cell (it's its parent's first child), so clicking an outer link incorrectly initiated a cluster drag instead of navigating. Resolution now uses the cluster's own querySelector and asserts that the pointer landed inside the resolved handle.

This page renders from CHANGELOG.md — the canonical release record.

esc