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

@the-portland-company/vrm-builder

v0.8.2

Published

Shared form/survey builder surfaces (canvas, field library, public renderer, results charts, skeletons) for Politogy VRM apps. Framework-agnostic: styling via brand CSS vars, data access via injected adapters.

Readme

@the-portland-company/vrm-builder

Shared builder surfaces for Politogy VRM apps — the form/survey canvas, field library, public renderer, aggregate results charts, and skeletons — extracted from the Forms and Surveys apps so every product renders the same builder from one source (Surveys spec §4, Reference/UI-Platform).

The package is framework-agnostic: it never imports next/*, @supabase/*, or app code. All styling is driven by the shared brand CSS variables (the same --color-* tokens the shell and specs.politogyvrm.com/api/tokens define), and all data access happens through injected adapters.

Install

npm i @the-portland-company/vrm-builder

Peer deps (both apps already have them): react/react-dom ^19, @dnd-kit/core/sortable/utilities.

1. Load the primitive CSS

// app/layout.tsx (or globals)
import "@the-portland-company/vrm-builder/css";

This ships the .card / .input / .textarea / .select / .btn / .btn-primary / .btn-accent / .pill / .skeleton / .label / .muted primitives on brand vars. It expects the app to also load the runtime tokens <link> (as it already does) so overrides stay live.

2. Add the package to Tailwind's content sources

The components also use Tailwind utility classes, which your app's Tailwind build must compile. In Tailwind v4, add the @source directive to your globals.css:

@import "tailwindcss";
@source "../node_modules/@the-portland-company/vrm-builder/dist";

Without this, package components lose their utility styling.

Exports

| Subpath | Contents | Boundary | | --- | --- | --- | | . | field-type catalog, validateFieldValue + validation helpers, all types + adapter interfaces | server-safe — pure logic only, no "use client", no @dnd-kit. Import these in server actions / submit routes. | | ./builder | Canvas, CanvasRow, LibraryPanel, CustomFieldDialog, Frame, Notice, applyReorder | ships "use client" — pulls in @dnd-kit. Import from a client component. Never import these from . | | ./renderer | PublicFormRenderer, FieldInput, RendererConfig, RendererSubmitAdapter | ships "use client" — safe to render directly from a Server Component page | | ./charts | computeResults, computeQuestionResult, resultsAreNoisy, QuestionPanel, DEFAULT_PALETTE | server-renderable (pure + SVG) | | ./skeletons | Skeleton, SkeletonBar | server-renderable | | ./css | primitive stylesheet | side-effect import |

Adapter contracts

The package renders UI and delegates every mutation/side-effect to adapters you implement with your app's server actions / API routes. Every adapter method returns an ActionResult<T> = { ok, error?, code?, data? }; the code lets the builder react to typed failures (e.g. Forms' "TCPA_REQUIRED") without string-matching.

interface CanvasAdapter {
  addField(fieldId: string, extra?: Record<string, unknown>): Promise<ActionResult>;
  removeField(canvasFieldId: string): Promise<ActionResult>;
  reorder(orderedCanvasFieldIds: string[]): Promise<ActionResult>;
  updateField(canvasFieldId: string, patch: { required?: boolean; caption?: string }): Promise<ActionResult>;
}

interface FieldLibraryAdapter {
  checkDuplicate?(label, internalName): Promise<ActionResult<{ duplicate?; reason? } | null>>; // omit → no dup step
  createCustomField(spec: CustomFieldSpec): Promise<ActionResult<{ id: string } | void>>;      // spec.extra passthrough (TCPA)
  addExisting?(fieldId, extra?): Promise<ActionResult>;                                         // "use existing field"
}

interface RendererSubmitAdapter {
  submit(payload: { values; honeypot; consent; meta? }): Promise<ActionResult<{ confirmation?; redirect?; fieldErrors?; message? }>>;
  mapErrorCode?(code: string): string;
}

Product differences are expressed through BuilderConfig as predicates and slots, not flags:

type BuilderConfig = {
  fieldRestriction?: (f: BuilderLibraryField) => boolean; // Surveys Anonymous blocks identity fields
  blockedReason?: string;
  onAddIntercept?: (code, retry, ctx) => boolean;         // Forms TCPA modal; return true + call retry(extra)
  fieldBadges?: (f) => ReactNode;                         // e.g. "identity"
  instrumentLocked?: boolean;                             // Surveys: live with responses
};

App-specific frames (Forms' Publish, Surveys' Deployment Plan) stay app-owned — compose them around the shared Canvas / LibraryPanel, or pass them as FrameSlot[].

Usage

Forms / Surveys canvas (inside your "use client" Builder)

"use client";
import { Canvas, LibraryPanel } from "@the-portland-company/vrm-builder/builder";

<LibraryPanel
  library={library}                 // BuilderLibraryField[]
  onCanvas={new Set(fields.map(f => f.field_id))}
  canvasAdapter={canvasAdapter}
  fieldLibraryAdapter={fieldLibraryAdapter}
  config={config}
  anonymous={survey.identity_mode === "anonymous"}
  onAfterMutation={router.refresh}
/>

<Canvas
  fields={fields}                   // BuilderCanvasField[]
  adapter={canvasAdapter}
  config={config}
  onAfterMutation={router.refresh}
  readOnly={readOnly}
/>

Public renderer (from a Server Component page)

import { PublicFormRenderer } from "@the-portland-company/vrm-builder/renderer";

<PublicFormRenderer
  fields={rendererFields}
  config={{ accent, consentLine, privacyPolicyUrl, honeypotEnabled, meta: { token, channel } }}
  adapter={{ submit: async (p) => postToApi(p), mapErrorCode }}
/>

meta is merged into the submit payload (Surveys token/channel ride here); session_id and source_url are added automatically.

Results charts (server-rendered)

import { computeResults, QuestionPanel } from "@the-portland-company/vrm-builder/charts";

const results = computeResults(fieldSpecs, responseRows /*, lowN */);
{results.map((q) => <QuestionPanel key={q.internal_name} q={q} />)}

Server-side validation

import { validateFieldValue } from "@the-portland-company/vrm-builder"; // server-safe
const r = validateFieldValue(field, required, rawValue);
if (!r.ok) return { ok: false, error: r.error };

Development

npm run type-check   # tsc --noEmit
npm test             # vitest run (jsdom)
npm run build        # tsup → dist (dual ESM/CJS + d.ts) + ensure "use client" on renderer

Publishing runs prepublishOnly = type-check + npm audit --audit-level=high + tests + build.

Install into consumers via npm pack tarballs, never npm link — linking double-loads React under opennextjs.