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

@doany-ai/forms

v0.1.0

Published

The business's forms on a Doany site: a form hook and a default component that renders a form's questions and submits the answers

Readme

@doany-ai/forms

The business's forms on a Doany site: contact, inquiry, subscription, application, quiz. The business defines a form (its questions, which services it asks about, what to show after); the site renders it and submits what the visitor fills in. A submission lands in the business's Inbox. Nothing here creates forms.

import { Form } from "@doany-ai/forms";

// /contact — the form by its id, as the business defined it
<Form formId="…" context={{ page_path: "/contact" }} />

List the page in doany/features.jsonc under forms.

Records

  • FormView (useForm): id, type (contact | inquiry | subscription | promotion | application | quiz | custom…), definition_hash (identifies what the visitor saw; sent back with the answers), definition (title, description, fields[], settings), availability (can_submit, reason: preview_only on a preview address, no_service_options when a required service question has nothing to offer).
  • FormField: key, type, label, help, required, control (radio | dropdown for a choice; hidden on a service question, which is then not shown and answered with the services it offers), validation (min_length, max_length, min, max), options[] (value, label), selection and resolved_options[] (service questions: the services on offer, service_mode booking | order | inquiry), consent (mode checkbox | on_submit).
  • Field types: text, textarea, email, phone, number, date, select, multiselect, checkbox, scale, agreement, consent, hidden, statement, service.
  • FormSubmitReceipt (useSubmitForm): id, submitted_at, score and outcome (a quiz), completion (the form's thank_you_message, redirect_url, cta).

Form

Renders the questions, submits the answers, shows the confirmation the form sets (or a quiz's outcome), redirects when the form says so.

| prop | | | --- | --- | | formId | required | | context | { page_path?, referrer?, utm?, language?, timezone?, source_service_id? }, kept with the submission | | defaultAnswers | answers to start with, by field key (a hidden value, a preselected service) | | submitLabel | wins over the submit label | | renderSuccess | (receipt) => node: replaces the confirmation | | showTitle | the form's title and description above the questions; default true | | variant | "single" (default): every question on one page. "steps": the questions in pages with a progress bar and Back / Next — a survey. The pages are steps when given, else one page per statement question (the statement heads its page), else one page. Each page's required questions gate Next | | steps | for variant="steps": the field keys of each page, in order; keys left out go on the last page | | frame | "plain" (default): just the questions. "card": on a raised card, the way a form embeds on a page | | onSubmitted | (receipt) => void | | labels, className | |

Answers go to the platform by field key: text, date and single choices as strings, several choices as arrays, checkbox / agreement / consent as booleans, numbers as numbers; blanks are left out. A consent with mode: "on_submit" is sent as agreed when the form is submitted.

A question that may be left blank says Optional at the end of its label row; required questions carry nothing (the merchant app's convention). A required text, textarea, email or phone question that is blank once trimmed is refused before anything is sent: answerRequired under the question, which takes focus (Next in a stepped form checks its page the same way); a phone that is not a number says phoneFormat the same way. The rest is the browser's check (required, the email format, lengths). The fields are @doany-ai/app-core/form's, shared with the booking form: an email question shows an envelope and an example address; a phone question is typed as a local number behind a country, shown as its flag and calling code (the chip opens a searchable list, the United States first and to start with; a number typed with its own + or 00 moves the flag), and is sent in E.164.

On a preview address, a notice appears before the questions. Visitors can fill in their answers, validate them and move through every page of a stepped form. The final action checks locally, keeps the answers and sends no submission; it never calls onSubmitted, renders a success receipt or follows a completion redirect. Use the published website for a real submission. A backend INVALID_STATE with details.reason="preview_only" is handled the same way if the page had older availability. A required service question with no options still blocks submission.

Failures: VERSION_CONFLICT (the questions changed meanwhile) re-reads the form and says so; RATE_LIMITED asks to wait; a VALIDATION_ERROR whose details.errors[].field names a question the visitor is asked (answers.<key>) is said under it (answerRequired or answerInvalid), on its page; anything else shows the site's submitFailed label when set, else the platform's message.

To put the form in a popup, a sidebar or a slide-in panel, wrap it in Embed from @doany-ai/app-core/ui; inside, useEmbed().markSubmitted() and close() tell the shell the visitor is done.

Hooks

| hook | returns | | --- | --- | | useForm(formId) | query of the FormView | | useSubmitForm(formId) | mutation: mutate({ definition_hash, answers, context? }) → FormSubmitReceipt |

Labels

submit, submitting, thanks, score(score), loadError, submitFailed, previewOnly, previewChecked, noServiceOptions, definitionChanged, rateLimited, tryAgain, notFound, choose, optional, emailPlaceholder, country, searchCountry, noCountry, phoneFormat, answerRequired, answerInvalid (required is kept for sites that set it, unused). The questions' own words come from the form's definition.

Customizing

  1. Theme: the site's variables style the inputs, buttons and the confirmation.
  2. Props: labels, submitLabel, renderSuccess, defaultAnswers, className.
  3. Hooks: draw the form yourself on useForm and useSubmitForm.

The package ships no CSS: the site's Tailwind build scans node_modules/@doany-ai/forms/dist.

Develop

npm install
npm run typecheck   # includes tests/sdk-contract.types.ts
npm test
npm run build