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

postx-framer-form

v0.4.2

Published

Reusable React 18 + TypeScript multi-step recruitment form, designed to be dropped into Framer as a Code Component.

Readme

postx-framer-form

Reusable multi-step recruitment form. React 18 + TypeScript (strict), no framework dependency, designed to be dropped into Framer as a Code Component.

  • No Next.js, no server-side Node APIs. The runtime touches only fetch, AbortController and the DOM.
  • API code is separate from UI. src/api/** and src/validation/** contain no React at all; src/components/** contains no fetch.
  • React 18 compatible. useReducer / useState / useEffect only — no use(), no useActionState, no server actions.
  • Zero runtime dependencies. React and React DOM are peer dependencies.

Flow

| Step | What happens | | --- | --- | | 1 — Email | POST https://api.postx.ai/public/recruitment/applications/start with { email, integrationId }, plus formVariantId when the integration runs more than one form variant. The returned applicationId is stored in state. | | 2 — Personal details | POST .../applications/{applicationId}/personal-details with the details plus integrationId. | | 3 — Success | Confirmation, optionally showing the application reference. |

Usage

import { RecruitmentForm } from 'postx-framer-form';

<RecruitmentForm
  integrationId="int_abc123"
  onCompleted={(applicationId) => console.log('done', applicationId)}
/>;

Props

| Prop | Type | Default | Notes | | --- | --- | --- | --- | | integrationId | string | — | Required. Rendering without it shows a configuration error instead of firing a request. | | formVariantId | string | — | Optional. Only needed if this customer runs more than one recruitment form (e.g. van driver vs car driver) — identifies which one this is. Backend validation/matching on this field is tracked separately. | | apiBaseUrl | string | https://api.postx.ai | Point at staging or a proxy. | | requestTimeoutMs | number | 20000 | Per-request client-side timeout. | | apiHeaders | Record<string, string> | {} | Merged into every request. | | theme | Partial<FormTheme> | see DEFAULT_THEME | Colours, radius, font family. | | copy | Partial<RecruitmentFormCopy> | see DEFAULT_COPY | All user-facing strings. | | showApplicationReference | boolean | true | Show the applicationId on success. | | allowBackNavigation | boolean | true | "Back" button on step 2. | | allowRestart | boolean | false | "Start another application" on success. | | className, style | — | — | Applied to the outer wrapper. | | onApplicationStarted | (applicationId, email) => void | — | Fired after step 1 succeeds. | | onCompleted | (applicationId) => void | — | Fired after step 2 succeeds. | | onError | (error: unknown) => void | — | Receives ApiError / NetworkError / TimeoutError. | | onStepChange | (step: FormStep) => void | — | 'email' \| 'personalDetails' \| 'success'. |

Using the pieces separately

The API client and the state hook are exported on their own, so a bespoke UI can reuse the transport and validation:

import { RecruitmentApi, useRecruitmentForm } from 'postx-framer-form';

const api = new RecruitmentApi({ baseUrl: 'https://staging-api.postx.ai' });
const { applicationId } = await api.startApplication({ email, integrationId });

Using it in Framer

The PostX Recruitment Framer plugin bundles this package at build time and installs the wrapper Code File into a customer's project automatically — that's the supported path for customers, and none of this section applies to them.

For a manual integration instead:

  1. npm run build.
  2. Framer Code Files only resolve bare imports for react, react-dom and framer — not arbitrary npm packages or third-party CDN URLs — so bundle framer/PostXRecruitmentForm.tsx together with this package (e.g. with esbuild, external: ['react', 'framer'], JSX left unpreserved is fine since Framer transpiles the file itself) into one self-contained file.
  3. In Framer: Assets → Code → New Code File, then paste the bundled output. The wrapper adds addPropertyControls and sizing hints and nothing else, so integrationId, the colours and every string become editable on the Framer canvas.

framer/ is excluded from the library tsconfig — it only type-checks inside Framer, where the framer module exists.

Layout and responsiveness

The form has no CSS file to import (a Framer Code Component is a single module), so the stylesheet is injected once into document.head, ref-counted across instances, and namespaced under .pxf-root. Sizing is driven by container queries (cqw, @container) rather than viewport media queries, so it adapts to the frame it is placed in rather than to the browser window. Inputs are 16px to stop iOS Safari zooming on focus, and tap targets are 48px tall.

Error handling

Every failure surfaces as one of ApiError, NetworkError, TimeoutError or AbortedError (the last is swallowed by the UI — it means the user navigated away).

  • 400 responses in NestJS/class-validator shape ({ message: ["email must be an email"] }) are mapped onto inline field errors.
  • 5xx and 429 render a step-level alert with a Try again action.
  • A 2xx from step 1 that omits applicationId is treated as a failure rather than letting step 2 POST to a broken URL.
  • Requests are aborted on unmount and when the user navigates back.

Development

npm install
npm run dev        # playground at http://localhost:5173
npm run typecheck
npm run build      # dist/ ESM + UMD + .d.ts
npm test           # build + API smoke test + jsdom multi-step flow test

VITE_INTEGRATION_ID and VITE_API_BASE_URL in .env.local point the playground at a real integration.

Assumed API contract

Only POST /public/recruitment/applications/start was specified. The rest is assumed and lives in one place — DEFAULT_ENDPOINTS in src/api/recruitmentApi.ts — so it can be corrected without touching the UI. Every step below is scoped to the application id start returns, and every step 2-6 response has the same shape: { applicationId, currentStep }, where currentStep is the backend's RecruitmentApplicationStep the application just advanced to (PERSONAL_DETAILS / CV / RIGHT_TO_WORK / DRIVING_LICENCE / INSURANCE_DECLARATIONS / AVAILABILITY / REVIEW / COMPLETED) — used to resume an in-progress application on the right step.

  • Step 1 response: { applicationId: string } ({ id } and { data: { applicationId } } are also accepted)
  • Step 2 — personal details: PATCH /public/recruitment/applications/{applicationId}/personal-details, sent as multipart/form-data (not JSON — it carries the CV file). Fields: firstName, lastName, email, phone (E.164), dateOfBirth (YYYY-MM-DD), gender? (MALE / FEMALE), line1, line2?, town, state, postcode, cv (PDF/DOC/DOCX, max 10MB), integrationId.
  • Step 3 — right to work: PATCH /public/recruitment/applications/{applicationId}/eligibility, sent as multipart/form-data (to carry the conditional document uploads). Fields: rightToWorkStatus (NOT_ELIGIBLE / BRITISH_CITIZEN / VALID_VISA), passportPhotoPage (required for BRITISH_CITIZEN/VALID_VISA; PNG/JPEG/WebP/PDF, max 10MB), immigrationStatusProof (required only for VALID_VISA; same file types/size limit), integrationId.
  • Step 4 — driving licence & experience: PATCH /public/recruitment/applications/{applicationId}/driving-details, sent as multipart/form-data (to carry the screenshot upload). Fields: hasFullValidUkDrivingLicence (boolean), drivingExperienceYears, vanDrivingExperienceYears, drivingLicenceNumber, drivingLicenceExpiryDate (YYYY-MM-DD), drivingLicenceCheckCode, checkCodeScreenshot (PNG/JPEG/WebP, max 10MB), integrationId.
  • Step 5 — insurance declarations: PATCH /public/recruitment/applications/{applicationId}/insurance. Fields: maritalStatus (required — one of SINGLE / MARRIED / CIVIL_PARTNER / DIVORCED / CIVIL_PARTNERSHIP_DISSOLVED / WIDOWED / SURVIVING_CIVIL_PARTNER / SEPARATED / OTHER), plus optional declarations: hasChildrenUnder16?, ukResidencyStatus? (LESS_THAN_12_MONTHS / AT_LEAST_12_MONTHS / SINCE_BIRTH), drivingLicenceObtainedAt? (YYYY-MM-DD), hasNoMoreThanSixPoints?, hasNoSeriousMotoringConvictions?, hasNoDrivingBansLastFiveYears?, hasNoCancelledInsurancePolicies?, hasNoUnspentCriminalConvictions?, integrationId.
  • Step 6 — availability: PATCH /public/recruitment/applications/{applicationId}/availability. Fields: availabilityType (WEEKDAYS_AND_WEEKENDS / WEEKDAYS_ONLY / WEEKENDS_ONLY / SPECIFIC_DAYS), availableDays? (required and non-empty only when availabilityType is SPECIFIC_DAYS), availableStartTime / availableEndTime (24-hour HH:MM, end strictly after start), integrationId.
  • Step 7 — submit: POST /public/recruitment/applications/{applicationId}/submit. Body: { integrationId }. Unlike every other step, the response is a flat object, not wrapped in { data }: { applicationId, reference, status: 'SUBMITTED' }.
  • Branding endpoint: GET /public/recruitment/integrations/{integrationId}/branding, returning { data: { publicIntegrationId, customerName, imageUrl, primaryColour, active } }. Fetched once on mount by useRecruitmentBranding and rendered by RecruitmentForm — an unknown/revoked integration (404) or any network error resolves to null rather than throwing, so the form falls back to its default styling and copy. A resolvable but active: false integration is fetched but not shown or applied — the form keeps its default (or Framer-configured) look rather than branding itself with a suspended customer.

Override without forking:

new RecruitmentApi({
  endpoints: {
    personalDetails: (id) => `/public/recruitment/applications/${id}/applicant`,
  },
});

The endpoints are called with credentials: 'omit', so api.postx.ai must allow the Framer site origin via CORS.

License

Copyright © PostX. Published to the public npm registry for distribution into Framer projects only. No license is granted to use, copy, modify, or redistribute this package outside of that purpose without written permission from PostX.