npm.io
0.1.0 • Published 5h ago

plumbkit

Licence
MIT
Version
0.1.0
Deps
0
Size
832 kB
Vulns
0
Weekly
0

plumbkit

A dev-tool overlay that draws alignment guides over any running web app. Click two elements and plumbkit projects their edges across the viewport so you can see which lines up, which is off by a few pixels, and exactly how much — then hands the fix straight to your coding agent.

It's dev-only by design: everything is gated behind process.env.NODE_ENV (replaced statically by Vite, webpack, Turbopack, and esbuild), so a production build tree-shakes the entire tool away to nothing.

Install

npm i -D plumbkit          # or: pnpm add -D plumbkit / yarn add -D plumbkit

Zero runtime dependencies in the core. The React adapter has a single optional peer dependency on react (>= 16.8).

Quick start

Pick whichever fits your app — all three mount the same overlay.

React
import { Plumb } from 'plumbkit/react'

export function App() {
  return (
    <>
      <YourApp />
      <Plumb />
    </>
  )
}

<Plumb> mounts an instance on mount and tears it down on unmount; it renders nothing. It's enabled in dev by default (process.env.NODE_ENV !== 'production') and accepts every createPlumb option as a prop.

Next.js (App Router): wrap it in a client component and render it in the root layout, gated on dev:

// components/plumb-devtools.tsx
'use client'
import { Plumb } from 'plumbkit/react'
export function PlumbDevtools() {
  return <Plumb />
}

// app/layout.tsx
{process.env.NODE_ENV === 'development' && <PlumbDevtools />}
Auto (zero config, any bundler)
import 'plumbkit/auto'

One import at your entry point and the overlay mounts itself in dev. The instance is exposed as window.__PLUMB__ for console poking.

Core (imperative)
import { createPlumb } from 'plumbkit'

const plumb = createPlumb()

plumb.activate()                 // arm the tool (or click the on-screen button)
plumb.select('#card')            // add an element to the selection
plumb.on('change', ({ selection }) => console.log(selection))
plumb.destroy()                  // remove everything

// In production createPlumb returns an inert handle before touching the DOM,
// and the whole implementation is dropped from the bundle.

Using it

The bar spawns in the bottom-left (lifting itself above the Next.js dev indicator or any other widget already in that corner) with three always-visible toggles: the crosshair (alignment guides — A or Alt+G), padding (box-model overlay — P), and measure (distances between selections — M). Each toggles independently and they layer: any tool being on enables hover + click-to-select, so you can inspect spacing without guides, then add guides on top. Hovering a button shows a tooltip, and each active tool stacks a short description card top-right. The first few times you turn a tool on, three small key hints appear top-left (↑↓ walk the tree, Enter toggle, Esc clear) — each retires for good once you've actually used its key, so they teach the controls and then get out of the way. Drag the bar to park the dock anywhere — the spot persists across reloads.

Once armed:

  • Click any element to select it. Its edges — left · center · right and top · middle · bottom — draw as hairlines across the whole viewport.
  • Click more elements to compare. Click a selected element again to drop it.
  • Shift-click to click through the tool: the click reaches your app as a normal click, so you can open a dropdown or press a button without disarming, then release Shift and inspect it. While Shift is held the hover highlight hides to show you're in pass-through mode.
  • Keyboard: A / P / M toggle the guides / padding / measure tools; arrow keys walk the DOM tree ( parent, child, ←ↅ siblings), Enter / Space toggles the highlighted element, Esc clears.
Padding inspection

Flip the padding sub-toggle to overlay the box model of whatever you hover or select — the way browser devtools shade padding, but live over your app:

  • Padding fills green, with a per-side pixel label on each non-zero side.
  • Flex / grid gaps fill in a second colour with a single gap label.
  • Margins fill orange (no UI toggle — opt in with the showMargins option).

It's border-aware (bands sit inside the border), ignores drop shadows, and runs alongside the alignment guides — leave both on at once.

Measuring between elements

Flip the measure toggle (two-boxes icon), then click two elements: the pixel distance between their nearest edges draws as a magenta ticked line with a label — horizontally, vertically, or both when they sit diagonally apart. Selecting more elements chains the measurements in click order (AB, BC, …); overlapping elements have no gap, so nothing draws for that pair.

Spacing consistency

Select a container and plumbkit also checks whether its direct children are spaced consistently, adding up to two rows to the legend:

  • Spacing — whether the children share one padding pattern: a green swatch means consistent, an amber dashed one means drift. Any child that misses the dominant padding by a few px gets an amber pt +2 / pl −3 label on the offending side.
  • Rhythm — for children spaced with margins (not a flex/grid gap), the number of distinct inter-child gaps. 1 is an even rhythm; more is uneven.

Both reuse the same tolerance clustering as the alignment engine, and absolutely-positioned children are excluded.

When two or more elements are selected, the guide lines are colour- and pattern-coded:

Line Meaning
Green, solid edges that align exactly
Amber, dashed edges that nearly align, with a signed ±px delta label
Cobalt, faint lone edges with no relationship (dimmed so the signal stands out)

Two panels appear top-right: a legend + tally (how many aligned / near / lone), and "Slightly off", which lists every near-miss with a Copy fix for agent button — it puts a ready-to-paste prompt on your clipboard describing exactly which edges are off and by how much. A separate Copy names button copies the selected components' names and location (React component + DOM path + nearby text) so you can drop them into an agent and say "align these."

API

createPlumb(options?) returns a PlumbHandle:

Method Description
toggle() Arm or disarm the tool.
activate() / deactivate() Arm / disarm explicitly.
select(target) Add an Element (or CSS selector) to the selection; arms if needed.
clear() Clear the current selection.
on(event, fn) Subscribe; returns an unsubscribe function.
destroy() Tear down the overlay and all listeners.

Events: toggle{ active }, select{ target }, change{ selection }.

Options

createPlumb(options) / <Plumb {...options} />:

Option Type Default Description
enabled boolean dev only Master switch; false → inert, no DOM work.
axes AxisKey[] all six Which edges/centers to project: left,centerX,right,top,centerY,bottom.
threshold number 2 Gap (px) under which two same-kind edges count as aligned.
nearTolerance number 16 Max gap (px) between same-kind edges of elements that share a row/column to flag a near-miss.
spacing boolean false Start with the spacing box-model overlay on (toggle it live from the sub-button).
measure boolean false Start with the measure overlay on — distances between selected elements (toggle it live from the sub-button).
showMargins boolean false Show margin bands in the spacing overlay (only relevant with spacing; API-only, no UI toggle).
theme PlumbTheme Overrides for the --plumb-* colour/size tokens.
color string cobalt Shorthand for theme.accent.
hotkey string 'alt+g' Shortcut that arms/disarms the tool.
root HTMLElement document.body Where the overlay host mounts.
Theming

Every colour and size is a --plumb-* custom property scoped to the overlay's shadow host (defined in OKLCH). Override any subset via theme:

createPlumb({
  theme: {
    accent: 'oklch(0.7 0.16 320)', // selected / lone edges, armed button
    match: 'oklch(0.8 0.16 145)',  // aligned edges
    near: 'oklch(0.8 0.15 70)',    // near-miss edges
    bg: 'oklch(0.2 0.02 300)',
    radius: '6px',
  },
})

Keys include accent, match, near, lone, warn, bg, bgAlt, fg, muted, border, ink, shadow, fab, gap, radius, lineWidth, the spacing fills paddingFill / gapFill / marginFill, the measure line colour, and the *Opacity / labelSize / fontSans guide tokens.

How alignment detection works

For the current selection, plumbkit:

  1. Measures each selected element's rect (skipping its own UI and anything display:none or zero-size).
  2. Derives up to six axis coordinates per rect: left / centerX / right (→ vertical guides) and top / centerY / bottom (→ horizontal guides).
  3. Clusters per axis in 1-D — sorted, single pass, starting a new cluster when the gap to the previous value exceeds threshold; a cluster's coordinate is the median of its members. Clustering within each axis means a right edge sitting next to a left edge is read as spacing, not alignment — it's never mis-flagged.
  4. Classifies each cluster:
    • aligned — two or more same-kind edges share the line (green).
    • near — a lone edge that misses another same-axis edge whose element shares its row/column (amber, dashed, with a signed ±px delta). Two side-by-side panels compare their tops/bottoms (same row) but not their lefts (different columns), so intentional layout gaps aren't flagged.
    • lone — nothing nearby (cobalt, faint).

Guides span the full viewport, so elements far apart on the page can still be compared. Everything is drawn in raw getBoundingClientRect viewport coordinates (the host is position:fixed) and re-tracked on scroll, resize, and a throttled rAF heartbeat — no scroll math.

Accessibility & fit

  • Keyboard-operable end to end (DOM-tree element picker, focusable controls with visible focus rings).
  • Not colour-only — near-miss lines are dashed as well as amber, and status changes are announced via an aria-live region.
  • Respects prefers-reduced-motion.
  • Stays out of the way — the chrome is corner-docked and flips sides to avoid fixed app widgets; it never dismisses your app's own modals/popovers.
  • Isolated in a shadow DOM at max z-index, with its font bundled as a data: URI so it renders even under a strict Content-Security-Policy.

License

MIT

Keywords