npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tosijs-floorplan

v0.5.0

Published

Render an agent-surface map (plain records) as a floorplan SVG — the affordance grammar as a pure, dependency-free function. No DOM, no framework. Formerly tosijs-schematic.

Readme

tosijs-floorplan

Render an agent-surface map — plain records describing a UI's wired elements — as a floorplan SVG: one shape per element at its true geometry, wearing an explicit affordance grammar so "can I act here?" never needs guessing. Like a floorplan of a building, it documents a real, live structure — where the doors are, which ones open.

It is a pure function over plain data. No DOM, no framework, no dependencies. The map travels as JSON, so the renderer runs in the page, in a headless embodiment, in a test harness, or on the far side of a wire from an app nobody is viewing.

Formerly tosijs-schematic (deprecated on npm at 0.2.0; renamed before its first external consumer shipped). The old name near-collided with tosijs-schema, the JSON-schema validation library, and confused readers in practice — an honest hazard of real cross-fertilization: schemas become contracts, contracts ride the map, and the map is what this package draws. The exported API keeps its names (schematic(), SchematicRecord, …): the drawing is still a schematic in the common-noun sense, and the record format is a multi-producer contract mid-adoption.

Install

npm add tosijs-floorplan

Quick start

With tosijs, the map draws itself — describe() output is already the record format:

import { enableAgentInterface } from 'tosijs'
import { schematicSVG } from 'tosijs-floorplan'

const agent = enableAgentInterface()
const svg = schematicSVG(agent.describe({ styles: true }))

Without tosijs, emit records yourself — anything that produces them gets the renderer and the grammar:

import { schematicSVG } from 'tosijs-floorplan'

const svg = schematicSVG({
  wiring: [
    {
      tag: 'input',
      label: 'quantity',
      value: '3 ⟷ app.qty',
      bounds: { x: 10, y: 10, width: 160, height: 24 },
    },
    {
      tag: 'button',
      text: 'submit',
      on: { click: 'app.submit' },
      bounds: { x: 180, y: 10, width: 80, height: 24 },
    },
  ],
})

The record format (the contract)

One flat record per wired element. Producers may add fields beyond these — bound props ride under their own keys as "value ⟷ path" strings.

| field | type | meaning | | --- | --- | --- | | tag | string | lowercase tag name (required) | | ref | string | a durable, actionable handle from the producer (survives re-renders — an agent can act on it, where an index only looks up) | | flags | {kind, label, severity?}[] | computed verdicts about the element (contrast ratios, target sizes, …) | | image | string | data-URL snapshot of inline media, drawn in place — pixels a pure renderer can't obtain | | bounds | {x, y, width, height} | page-coordinate geometry — layout is part of the semantics; zero-size or absent = not drawn | | label | string | the accessible name (aria-label, resolved labelledby, <label> association, title, alt) | | placeholder | string | the hint — deliberately distinct from label: an empty input must never read as content | | text | string | textContent, static ("foo") or bound ("foo ⟵ path") | | href | string | a link's destination — distinct from text ("the link says X" is not "the link goes to Y"). Captions fall back to it only when nothing else names the element; it always rides the legend | | value | string | a filled control's value, distinct from label/placeholder — static ("3") or bound ("3 ⟷ app.qty"); the fact that distinguishes an empty form from a filled one | | type | string | input kind when not plain text (checkbox, radio, range, email, …) | | checked | boolean | live toggle state | | focused | boolean | holds keyboard focus right now | | invalid | boolean | live ValidityState (or aria-invalid) says invalid | | required | boolean | the field is required | | disabled | boolean | disabled right now | | contentEditable | boolean | an editable region — treated as an input field | | interactive | boolean | the producer's assertion that this element can be acted on — for producers that cannot introspect handlers (React delegates at a root; vanilla addEventListener is not enumerable from page script). Asserting is truth-telling; fabricating on to unlock the styling would be a lie in the payload. A binding framework never needs it | | editable | boolean | the producer's assertion that text goes in here — the DOM-side counterpart of contentEditable / a two-way binding | | secret | boolean | the producer withheld facts about this element (a secret-marked region: a token lives in the destination, so neither label nor href is published). Fail-closed on the renderer side: a secret record's label/text/value/placeholder/href/image never reach the drawing or the legend even if a producer bug leaves them in the record, and any truthy secret scrubs (malformed errs toward withholding) — it draws <tag> [withheld] and its legend entry says redacted: true (structural records included) — "no destination" and "destination withheld" are different facts | | on | Record<string, string \| string[]> | handlers by event type — a path when nameable, ƒ (or ƒ name) when not | | list | {path, idPath?} | this element renders a collection (drawn as ground, not figure) | | structural | boolean | structure, not affordance (headings, landmarks, containers) | | viewportFixed | boolean | rides the viewport (fixed/sticky) — bounds are screen coordinates | | style | {background, borderColor, color} | computed colors, worn when present | | id, part, role, description | string | identity and explanation, passed through to consumers |

Provenance tokens (exported as BOUND_TO_DOM / BOUND_TWO_WAY): a bound value reads "<shown> <arrow> <path>" means state flows to the DOM (display), means two-way (a user-writable affordance). A plain string with no arrow is a live-but-unbound value. The structural arrow is the LAST one in the string — the surface appends it, so consumers must split at the last occurrence, and an arrow token buried inside the data confers nothing (the renderer parses defensively: it neutralizes interior arrows to <-> / <- in every drawn text run and in the legend's display fields — caption and value are always neutralized — and never scans identity/name fields (tag, id, part, role, label, placeholder, type, description, href, ref, image) for bindings at all, since the surface never appends an arrow to those). Two legend fields are verbatim, deliberately: href is an opaque destination — rewriting bytes inside a URL corrupts the one fact an agent acts on — and flags are copied as the producer computed them. Consumers must never parse provenance from either (they are in the never-scanned set; an arrow there is data), and must not forward them into a caption-style text run without neutralizing first. Producers whose record content derives from untrusted sources — any DOM extractor reading page content — MUST neutralize both tokens inside data at the source, as tosijs ≥ 1.8.0 does. This is normative because of an honest residual: a forged arrow in suffix position on a bindable field ("data ⟷ fake.path" as the entire text) is structurally indistinguishable from a real binding — renderer-side defense ends where the format's own syntax begins. And the bindable set is open by design (bound props ride under their own keys), so the never-scanned list is a denylist over an open key set: any key a producer invents is bindable, and arrows in it are trusted as structure. The defense is narrowed, not closed — producer-side neutralization remains the actual perimeter (issue #11). The capability scan (below) likewise counts an arrow in any position within a bindable field: under the format's own last-occurrence parse, "contains an arrow" and "has a structural arrow" are equivalent for a lone token, so a position check would add no security — only the #11 perimeter does.

Producers that cannot introspect handlers (React's synthetic delegation, Angular's compiler output, vanilla addEventListener — none enumerable from page script) assert the affordance instead: interactive / editable, per record. When a map draws affordance-shaped boxes but no record carries affordance evidence (no on, href, contentEditable, two-way binding, or assertion) and none carries capability evidence either — a handler, an assertion, or a provenance arrow in a bindable field anywhere in the map, display-only included, proves the producer can see wiring (#10: a read-only dashboard from a binding framework is a sighted map of an inert page, not a blind map) — the result carries a note, and the svg's <desc> repeats it, because "nothing here is actionable" and "the producer couldn't tell" are different statements, and a consumer must never mistake the second for the first. Two caveats pin the semantics: partial evidence does not establish the rest — on a map where some records carry evidence, a record without any still means unknown, not inert (the note only marks the total-blindness case; non-introspecting producers should assert per actable record, not rely on the note); and only interactive: true / editable: true are signalfalse is indistinguishable from absent and cannot veto evidence the record itself carries (on, href, a binding). "Introspected and found nothing" currently has no encoding; propose one via issue before relying on it.

The picture is not the whole payload. The renderer is allowed to omit: captions and badges below legibility thresholds move to the legend, keyed by the stamped index/ref, and facts that never draw well (href above all) live there always. A consumer of the raster is expected to hold schematic().legend alongside it — the image says where and which; the legend says what.

The grammar

Every rule is geometry or ASCII — hard-won: a single exotic glyph can tofu an entire caption run under a rasterizer, and text glyphs are mush at checkbox sizes.

| you see | it means | | --- | --- | | bold outline | wired to act — handlers, a destination (href: a link IS an affordance), or the producer's interactive assertion | | badge, bottom-right | editable here (two-way binding, or contenteditable) | | caption ending * | required | | red corner flag, top-left | invalid right now — live ValidityState, the same truth :invalid styles | | filling a box / dot in a circle | checkbox / radio state, live | | italic caption | placeholder hint — not content | | faded | disabled right now (beats bold: a disabled button is not an affordance) | | double outline | keyboard focus — where the user is | | faint dotted | structure — including list containers (their items are the affordances) | | number, top-right, on a white backdrop | the record's index (index: true) — the raster form of data-record: read it off the image, look up wiring[n] | | a ref (e.g. @42), top-right | the producer's durable, actionable handle — takes the index slot when present, survives re-renders, rides the group as data-ref | | colored bars, left edge | computed verdicts (flags): WCAG contrast failures and friends — error red, warn amber, info gray, first flag's label shown | | pixels inside a box | embedded media (image: a data URL) — the producer's snapshot of inline <svg>/<canvas>, drawn in place | | a bare box wearing only a stamped number | too small to label legibly — its caption, badges and flags live in the legend, matched by that number | | single amber bar, left edge | an interactive element below targetSize (default 24×24, WCAG 2.5.8; set 44/48 for the touch-target bar) — a usability defect in its own right; the measurement is in the legend | | footer strip: "N elements with details in legend" | the image's confession that it isn't the whole map — fetch schematic().legend (a machine-readable <desc> says the same) |

The target-size audit honours WCAG 2.5.8's inline exception as far as pure geometry can: a link is exempt when it has text and its box is wider than tall — the shape text layout produces (flagging prose links would fire on every paragraph — a check that cries wolf gets ignored, taking the real findings with it). Icon links stay flagged: an <a> wrapping an <svg> with no text, and equally a square icon link that happens to carry a label or a glyph — a 16×16 box was not sized by its text, whatever the text is (the text-only rule exempted exactly the header-row-of-icons case the check was built for; haltija's issue #2 caught it). A producer with DOM access computes the exception properly (computed display + parent text nodes) and ships the finding via flags — that is the intended path for DOM producers. A producer flag whose kind is one of the target-claim kinds (TARGET_FLAG_KINDS, case-insensitive: target, target-size, targetsize, target_size, smalltarget) supersedes the drawn audit, so the two never double-mark — an explicit set, because a substring match let target-ok stand the audit down (#8); new target-claim kinds are added to TARGET_FLAG_KINDS via an issue here, the same additive path as any other format change, and the set is read-only by contract. Supersession is a drawing concern and therefore opt-in: targetSizeFinding ignores producer flags by default (an audit wants the geometry verdict regardless of what got drawn); schematic() passes honorProducerFlags: true. Hidden is not small: zero-size records are never undersized (#9). Both rules are exported (isInteractive, targetSizeFinding) so audits share this implementation instead of keeping a drifting copy — and as of 0.5.0 they reproduce an audit's verdicts without consumer-side normalization (#7/#8/#9/#13).

Captions tell the truth in priority order: a held value wins (as label: value when both are known), an empty control falls back to its hint, then label, then text. Containers holding other drawn boxes get no text-derived caption — their children speak. Captions wrap when the box affords more than one line — a paragraph that wraps on the real page has the same vertical room here — and only truncate (with ) when the geometry genuinely runs out.

API

| export | what | | --- | --- | | schematic(description, options?) | the renderer's primary form — returns { svg, legend, note? }: the drawing, the metadata it could not legibly carry (cramped/truncated/undersized records, keyed by index/ref), and — when no record carries affordance evidence — the note saying so. Pair every raster with its legend. | | schematicSVG(description, options?) | schematic().svg — the string-only form; each <g> carries data-record="<i>" linking back to description.wiring[i] (the image as index) | | isInteractive(record) | "can I act here?" — the single implementation (handlers, href, contentEditable, a structural two-way binding, or the producer's assertion; ground never). Exported so audits consume it instead of keeping a drifting copy | | targetSizeFinding(record, targetSize?, {honorProducerFlags?}) | the WCAG 2.5.8 rule with the settled exemptions (toggles, text-sized links, zero-size; producer-flag supersession only when honoured — the renderer's setting, not an audit's) — the measured legend fact, or null | | TARGET_FLAG_KINDS | the flag kinds that claim to be a target-size finding and may supersede the drawn audit | | rasterizeSVG(svg, {scale}) | SVG → PNG Blob for vision encoders (browser canvas; under bun/node use @resvg/resvg-js — rasterize at 2× so labels OCR cleanly) | | boundsOf(element) | an element's page-coordinate bounds — the natural within argument | | BOUND_TO_DOM, BOUND_TWO_WAY | the provenance tokens |

Options: pad, minLabelHeight, maxCaption, fontSize; within (a page-coordinate rect — spatial scoping: the viewBox is the region); index: true (stamp record indexes); targetSize (the undersized-audit floor: 24 default, 44/48 for touch, 0 off); legendNote: false (suppress the footer strip); decorate (below).

Plugins (EXPERIMENTAL)

decorate is the extension seam: called once per drawn record, just before its <g> closes, with the record, its resolved geometry, and an emit function. The corner slots already spoken for: top-left invalid flag, top-right index, bottom-right badge, outline focus/emphasis. Claim empty real estate:

schematicSVG(map, {
  decorate({ record, x, y, width, emit }) {
    if (record.style && contrastRatio(record.style) < 4.5) {
      emit(`<text x="${x + width / 2}" y="${y - 2}" font-size="6"
        text-anchor="middle">contrast!</text>`)
    }
  },
})

The first real plugins will shape the successor API — if you build one, open an issue.

Provenance

Extracted from the tosijs one user interface work: one source of truth for state, UI, and AI, where the agent's map derives from what the framework already knows. The grammar here was debugged against real rasterizers and real assistive-tech semantics — see the tosijs docs for the living demos (a todo list whose map redraws itself, and a kitchen-sink truth page).

License

Apache-2.0