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

@privaty/ui-forms

v0.7.1

Published

Readme

@privaty/ui-forms

Form components built on SvelteKit remote functions (experimental): Form wraps a form(schema, handler) instance, inputs bind to its fields, and a small FormState layer adds the display semantics Kit doesn't ship — value-based dirty tracking, touch-gated error display, submit gating.

pnpm add @privaty/ui @privaty/ui-forms

Requires @privaty/ui as a peerDependency at the same lockstep version (single instance — Symbol-keyed contexts), @sveltejs/kit with experimental.remoteFunctions + compilerOptions.experimental.async, and Tailwind v4 (@source the package, see the core README).

Everything imports from the package root — import { Form, TextInput, Submit } from "@privaty/ui-forms" — which tree-shakes; there are no deep subpaths (as of 0.6.0). The testing fakes live behind their own barrel, @privaty/ui-forms/testing.

Quickstart

<script>
  import { Form, FormError, Submit, TextInput } from "@privaty/ui-forms";
  import { createItem } from "./data.remote";
  import { createItemSchema } from "./schema";
</script>

<Form form={createItem} schema={createItemSchema}>
  <TextInput field={createItem.fields.name} label="Name" required />
  <FormError />
  <Submit />
</Form>

Inputs: TextInput (text/email/password/search/url/tel), TextareaInput, NumberInput, DateInput (date/month/week/time/datetime-local — one component, all string-valued), SelectInput, CheckboxInput. Plus Submit, Reset, FormError. Icon-style Submit/Reset: pass children; the label stays as the accessible name. HiddenInput carries a value the user never edits (a row id, a parent record's id) — context-free, no dirty tracking or marker, works outside a <Form> too.

Picker inputs

DatePickerInput, MonthPickerInput, and WeekPickerInput pair the native input with the core library's cross-browser calendar pickers — the custom month/week UIs Firefox never got, and one consistent picker everywhere else.

  • The native input stays visible as the FormData carrier: typing works, native mobile pickers stay, and Kit reads live form data as ever. Firefox renders month/week carriers as plain text inputs — exactly the gap the calendar button fills.
  • Firefox date exception: Firefox draws an unhideable calendar icon on type="date" (Bugzilla 1830890), so DatePickerInput yields there — the native icon opens Firefox's own picker and our trigger hides, keeping one icon in every browser. Month/week use our picker everywhere.
  • The overlaid calendar button opens the picker in an anchored Popover (top layer, light dismiss, zero-lag scroll tracking). A pick writes the input the way typing does (DOM value + a bubbling input event), so validation cadence, touch marking, and dirty tracking are identical to manual entry — one write path, no programmatic special case.
  • min/max flow to both the native input and the picker, and locale is on all three; DatePickerInput adds isDateDisabled, showWeekNumbers, and firstDayOfWeek (all picker-side — constraints still belong in the schema, typing can produce anything).
  • The trigger's accessible name comes from labels.calendar.open; while the form submits, the input turns readonly and the trigger disables.

The validation model

  • With schema (a Standard Schema, e.g. valibot): validation runs client-side via Kit's preflight — immediately on input, and on submit. Kit swallows invalid submits before the enhance callback runs; the Form's own submit listener still opens the error gates, so a rejected click shows its issues.
  • Without schema: every validation is a server round-trip — typing is debounced (validationDebounce, default 400 ms), submits validate server-side. Transform schemas (Output ≠ Input) are accepted.
  • Issues per field appear once the field is touched or a submit was attempted; FormError shows path-less issues and submit failures (labels.form.generalError).
  • Server issues (a rejected submission, a server validation round-trip) are persisted by Kit through every client-side validation pass — no edit can refresh them, only another round-trip. The Form handles both consequences: a schema'd resubmission is never gated on them (the submission re-judges them authoritatively), and while they linger, input revalidation escalates to full validation — client schema first, then the server round-trip that replaces the whole issue set — debounced like schema-less typing. Rules that depend on server data (a cross-field cap, a uniqueness check) therefore belong in the server schema (an async check is fine) if they should refresh live while the user edits; issues raised in the handler (invalid()) clear optimistically on the next round-trip and are re-judged at submit.

Schema recipes (the footguns)

// Checkboxes: unchecked submits NOTHING — the schema supplies the false.
inStock: v.optional(v.boolean(), false),

// REQUIRED placeholder selects: the disabled prompt is skipped by
// submission — default "" so YOUR message fires instead of a raw
// missing-key error. (Optional selects are clearable instead: the empty
// pick SUBMITS "", so a plain v.optional(v.string()) receives it.)
category: v.pipe(v.optional(v.string(), ""), v.picklist(categories, "required")),

// Optional selects, when the handler prefers undefined over "": FormData
// cannot carry an absent value from an enabled option, so normalize at
// the schema boundary — the designed seam for exactly this.
categoryId: v.optional(v.pipe(v.string(), v.transform((value) => value || undefined))),

// Month inputs submit "YYYY-MM"; empty submits "". Two v.check actions,
// not nonEmpty+regex: valibot pipes run every action even after a failure,
// so an empty value would show BOTH messages stacked.
availableFrom: v.pipe(
  v.string(),
  v.check((value) => value.length > 0, "required"),
  v.check((value) => value === "" || /^\d{4}-\d{2}$/.test(value), "invalid-month"),
),

One form object, one <form> element

Kit enforces it at attach time: "A form object can only be attached to a single <form> element. To create multiple instances, use name.for(key)." The singleton returned by form(schema, handler) can back exactly one mounted <Form> — a second mount anywhere on the page throws.

The trap is REPEATED content: a create-form modal inside a table's expanded snippet renders once per expanded row, and a closed <dialog> still MOUNTS its children — so expanding a second row crashes before any modal is opened. Key the instance per repetition instead:

{#snippet expandedContent({ row })}
  {@const bulkForm = bulkCreateMonths.for(row.id)}
  <Modal title="Bulk create">
    <Form form={bulkForm} schema={bulkCreateSchema}>
      <!-- The key is client-side instance identity only — it is NOT
           submitted. Parent linkage still rides along explicitly. -->
      <HiddenInput field={bulkForm.fields.allocationId} value={row.id} />
      ...
    </Form>
  </Modal>
{/snippet}

<Form> accepts a .for() instance as-is (the tables' per-row edit forms are exactly this), and each key gets independent draft and issue state — two open modals never share a half-typed value.

Every input's field must come off the SAME keyed instance — hold it in one {@const} and read fields from it, never from the singleton. Each instance suffixes its field names with its own id, so singleton fields inside a keyed <Form> are invisible to it: edits sync nowhere, dirty never trips, Submit/Reset stay disabled, and a forced submit throws "Form contained a field that wasn't created with form.fields.as(...)".

Rules learned the hard way

  • Never disable controls while submitting — disabled controls are excluded from FormData, and Kit reads live form data mid-submission. The inputs lock via readonly / pointer+keyboard locks instead. Keep that contract in custom inputs.
  • Queries: await getItems() bare. query().current never server-renders (hard-coded undefined on the server), and a <svelte:boundary> with a pending snippet makes the server render the snippet INSTEAD of the children. Await, no pending boundary.
  • Required/optional markers follow the majority rule (the minority gets marked) and wait for FormState.settled — they cannot be SSR'd with self-registering fields.
  • No-JS submissions are out of scope for v1 (Submit renders its gate state into SSR HTML).
  • Browser support: Firefox has no type="month"/"week" pickers (falls back to a text input) — the picker inputs above fill exactly that gap.

Testing

@privaty/ui-forms/testing ships Kit-faithful fakes: fakeRemoteForm preflight-gates its enhance callback like Kit does, and the field fakes' edit() stores raw DOM values ("5", "on") while set() stores typed ones — mirroring Kit's mid-edit behavior so dirty-tracking tests mean something.