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

@duffcloudservices/site-forms

v0.7.8

Published

Shared <DcsForm/> runtime for DCS customer sites — renders managed form definitions from .dcs/forms/<formId>.yaml

Readme

@duffcloudservices/site-forms

Shared <DcsForm/> runtime for DCS customer sites. Renders a managed form definition (created in the portal Form Manager) from a build-time .dcs/forms/<formId>.yaml snapshot, validates user input, and posts submissions to the public site-forms API.

This is the single import surface for managed-form runtime code on customer sites — do not redefine field components per site.

Inside the dcs-again workspace the package is also reachable as @duffcloudservices/site-forms via workspace:* (the workspace name and the published name are the same). In sibling customer-site repos install the published package from public npm — see PUBLISHING.md for the consumption story and registry setup.

Install

In a workspace app (inside dcs-again)

// portal/package.json
{
  "dependencies": {
    "@duffcloudservices/site-forms": "workspace:*"
  }
}

Then pnpm install from the repo root.

In a sibling customer-site repo (e.g. ktbraunlaw, kept)

pnpm add @duffcloudservices/site-forms

See PUBLISHING.md for the registry, version, and release-workflow details.

Stylesheet

The library build extracts its CSS; dist/index.js does not inject it. Import it once in the site's entry (or theme index.ts):

import '@duffcloudservices/site-forms/style.css'

The ./style.css export resolves to dist/site-forms.css from 0.7.8 on (C-1828; 0.7.7 and earlier pointed at a dist/style.css the build never emitted).

Vite setup

Three pieces are required in the consuming site:

  1. vite-plugin-yaml so YAML modules return parsed objects:

    // vite.config.ts
    import yaml from '@modyfi/vite-plugin-yaml'
    
    export default defineConfig({
      plugins: [vue(), yaml()],
    })

    Without it, the loader falls back to parsing raw strings via js-yaml, which works but pays the parse cost at boot.

  2. A formsModules loader in your site that does the import.meta.glob from a path Vite can resolve (see below).

  3. Env vars the source reads (see the caveat below):

    | Variable | Purpose | | ----------------------- | ---------------------------------------------------- | | VITE_DCS_PUBLIC_API | Base URL of the DCS public API (no trailing slash). | | VITE_DCS_SITE_SLUG | Default site slug used when the prop is omitted. |

    Caveat (C-1564, W80): the published dist does NOT read either variable. The library build statically erases import.meta.env (the dist carries {} there), so a site that installs this package from npm cannot configure it through env. Use the api-base / site-slug props. No fleet site needs api-base in production: see Public API base.

Public API base

<DcsForm> resolves where it posts in this order:

  1. the api-base prop;
  2. VITE_DCS_PUBLIC_API (workspace-source consumers only, per the caveat above);
  3. in the browser on localhost / 127.0.0.1 / ::1: the relative /api/v1 (the dev proxy);
  4. on any other host: the package default, package.json config.publicApiBase (https://api.duffcloudservices.com/api/v1, the production DCS public API).

Step 4 is the documented production contract, not a fallback: no fleet production build sets the env or passes the prop, and they all post to the default. In a production build it is silent. In a non-production build on a non-localhost host (a LAN or tunnel dev server, a dev-mode preview) it logs one console.warn per page load, because there a form under test is posting to the production API. The mode is process.env.NODE_ENV === 'production', which survives the library build and is replaced by the consuming bundler (Vite and VitePress define it for dependencies). Under Node (SSR / prerender) the base is '' and nothing is posted.

Why the formsModules prop is required in real sites

<DcsForm/> ships with an internal import.meta.glob('/.dcs/forms/*.yaml') fallback, but Vite resolves the leading / against the consumer's Vite project root (the directory containing vite.config.ts). On every customer-site repo today, the .dcs/forms/ directory lives at the repo root, one or more levels above the Vite root (typically site/ or docs/). The internal glob therefore matches nothing and you get:

[@duffcloudservices/site-forms] No form definition found for "contact".
Expected a YAML at /.dcs/forms/contact.yaml.

The fix is a one-file loader the rest of your site imports from.

Vue SPA (vite.config.ts in site/)

// site/src/dcs-forms.ts
const modules = import.meta.glob('../../.dcs/forms/*.yaml', {
  eager: true,
  import: 'default',
})
export const dcsFormsModules: Record<string, unknown> = modules

VitePress (vite block in docs/.vitepress/config.ts)

// docs/.vitepress/dcs-forms-loader.ts
const modules = import.meta.glob('../../.dcs/forms/*.yaml', {
  eager: true,
  import: 'default',
})
export const dcsFormsModules: Record<string, unknown> = modules

The relative depth (../../) depends on where the loader file lives relative to the repo root. Adjust as needed.

Usage

Place a YAML file at <site>/.dcs/forms/contact.yaml:

formId: contact
submission:
  kind: lead
fields:
  - id: name
    type: text
    label: Name
    required: true
  - id: email
    type: email
    label: Email
    required: true
  - id: message
    type: textarea
    label: Message
    required: true

Then in any page component:

<script setup lang="ts">
import { DcsForm } from '@duffcloudservices/site-forms'
import { dcsFormsModules } from '@/dcs-forms'
</script>

<template>
  <DcsForm
    form-id="contact"
    :forms-modules="dcsFormsModules"
    @submit-success="onSuccess"
    @submit-error="onError"
  />
</template>

Props

| Prop | Type | Default | Notes | | -------------------- | --------------------------------- | -------------------------------------- | ----------------------------------------------------------- | | formId | string (required) | — | Matches .dcs/forms/<formId>.yaml. | | siteSlug | string | import.meta.env.VITE_DCS_SITE_SLUG | Path segment in the submission URL. | | definitionOverride | PortalFormDefinition | — | Used by the portal preview iframe to show in-flight edits. | | apiBase | string | the production public API (see Public API base) | Override for tests / non-prod environments. Silent default in production builds. | | captchaToken | string | — | Attached to the submission payload when set. | | formsModules | Record<string, unknown> | internal fallback glob (rarely matches) | Required in real sites. Pass a Record<string, unknown> from your own import.meta.glob('../../.dcs/forms/*.yaml', { eager: true, import: 'default' }) — see Vite setup section. | | initialValues | FormValues (Record<string, unknown>) | — | Runtime values merged over the YAML defaultValues at init and on reset. Merge order: YAML default < initialValues < user input. Every key must be a value field the definition declares; an unknown or layout-only id throws at setup. Read once, not watched. See Page context. |

Page context (initialValues)

Use initialValues to record where a lead came from. Declare a hidden field in .dcs/forms/<formId>.yaml (and in the portal form, per the site-forms 3-way schema sync; the server stores only what the definition declares), then set it per page:

<DcsForm
  form-id="inquiry"
  :forms-modules="dcsFormsModules"
  :initial-values="{ course: 'pinehurst-no-2' }"
/>
  • A hidden field has no input, so it keeps the initial value through user edits, reset() and submission. It appears in the submitted values.
  • A visible field shows the initial value and the visitor can change it; the typed value wins.
  • A key the definition does not declare (or a section-heading / html-block id) throws an error naming the key and the declared fields. A typo fails the prerender or dev server instead of silently losing the context. When the form definition itself is missing, initialValues is ignored so the "not configured" state still renders.
  • Added in the release after 0.7.5 (C-1767).

Emits

| Event | Payload | When | | ------------------ | ---------------------- | ------------------------------------------- | | submit-success | DcsFormSubmitSuccess | API responded 2xx with a JSON (or empty) body. | | submit-error | DcsFormSubmitError | Network error, non-2xx after retries, or a 2xx whose body is not JSON (nonJsonResponse: true). | | validation-error | FormErrors | Submit attempted with invalid required/regex/etc. fields. |

A 2xx with a non-JSON body is a FAILURE (0.6.0, C-301)

Before 0.6.0 the submission helper swallowed every body-parse failure and returned null, so a POST answered by HTTP 200 text/html — a static host's SPA shell, a Front Door catch-all, a captive-portal or corporate-proxy interstitial — was indistinguishable from a quiet success. The visitor saw the success state and the success chime while nothing was stored: silent lead loss.

Now:

| Response | Behaviour | | ------------------------------ | ------------------------------------------------------ | | 2xx + JSON body | success, response = parsed JSON (unchanged) | | 2xx + empty body (incl. 204) | success, response = null (unchanged) | | 2xx + any other body | submit-error with nonJsonResponse: true, never retried (retrying could double-store a lead), plus a console.error naming the resolved base | | non-2xx | submit-error; see "The error line" below |

The error line (0.7.8, C-1828)

A 4xx used to render Submission failed (404): <whole response body> in the visible alert, which on a static host is kilobytes of HTML. Now error.message is only the package's own sentence (Submission failed (404)), a short server reason (a JSON error/message string or clipped plain text, at most 200 characters, never an HTML page) travels as DcsFormSubmitError.detail and renders behind a <details> toggle (.dcs-form__submit-error-detail), and the body, trimmed to 500 characters, goes to console.error.

Pre-hydration submit (0.7.8, C-1828)

The rendered <form> carries method="post", and the default submit button is disabled until the component has mounted. A prerendered form clicked before the app hydrates therefore never submits as a GET with the visitor's values in the URL. A site that replaces the actions slot should bind its own button's disabled to the slot's hydrated flag.

The assertion is inlined here on purpose rather than delegated to @duffcloudservices/cms-core's platformFetch: site-forms is a leaf package that sites pin independently (coron8 sits on 0.1.4), and a new cms-core dependency would make the fix unpublishable to exactly the sites that most need it.

Slots

Every slot exposes scoped data so consumers (KT Braun, Kept) can swap shadcn primitives in without forking field components.

| Slot | Scope | Default | | ---------- | ------------------------------------------------------------------ | ---------------------------------------------------- | | header | { definition } | Empty; page/section headings live outside the managed form | | progress | { current, total, step } | Step N of M — Title (multi-step only) | | actions | { isFirstStep, isLastStep, submitting, hydrated, prev, next } | Plain <button> elements (submit disabled until hydrated) | | success | { definition } | definition.successMessage | | missing | { formId } | Friendly fallback when the YAML can't be found |

Per-field components (DcsFormText etc.) expose #input slots so a shadcn site can replace the underlying primitive while keeping the wrapper, label, help, and error-message structure.

Composables

For sites that want a fully custom layout, drop <DcsForm/> and use the underlying composables directly:

import {
  useDcsForm,
  validateForm,
  submitFormValues,
  parseFormYaml,
} from '@duffcloudservices/site-forms'
  • useDcsForm({ definition }) — reactive values, errors, steps, next, prev, validateAll, collectSubmissionValues, etc.
  • validateForm(def, values, fieldIds?) — pure validator usable in any setting (server-side, tests, custom adapters).
  • submitFormValues({ apiBase, siteSlug, payload }) — one-shot POST with a single retry on 5xx and multipart/form-data when files are present.
  • fetchSiteHandlesPhi({ apiBase, siteSlug }) — the site's PHI posture (C-415), memoized per site. <DcsForm/> already calls it; a custom layout should use it rather than inventing a second HIPAA signal. PHI_GUIDANCE_COPY is the standard wording, resetSitePhiPostureCache() clears the memo (tests / the portal preview iframe).

File fields emit a single File by default. When attachmentPolicy.maxFiles is greater than 1, the file field emits File[], enables multiple selection, previews selected files, and allows removing files before submit.

Visual editor integration

The form root carries data-form-key="<formId>" and every field wrapper carries data-form-field-key="<fieldId>". The portal preview iframe bridge uses these to discover managed forms, show the preview affordance, and route preview click / context-menu actions into the same portal FormManagerSheet. Do not strip these attributes in custom layouts.

definitionOverride is a preview-only draft path for the iframe. The durable form truth still lives in the form definition saved by the portal and in the committed .dcs/forms/<formId>.yaml snapshot consumed by the site runtime.

For the cross-package first-party component contract (runtime markers, bridge discovery, portal entry points, rollout, validation), see ../FIRST-PARTY-COMPONENTS.md.

Schema validation

In dev (import.meta.env.DEV === true) the runtime validates each loaded definition against the JSON Schema bundled in src/schema/form-definition.schema.json (snapshot of contracts/dist/form-definition.schema.json) and logs failures via console.warn. Production builds skip the warning to avoid noisy end-user consoles.

When the contracts schema is regenerated (pnpm --filter @dcs/contracts generate), refresh the snapshot:

Copy-Item ../../contracts/dist/form-definition.schema.json ./src/schema/form-definition.schema.json -Force
pnpm --filter @duffcloudservices/site-forms test --run

Scripts

pnpm --filter @duffcloudservices/site-forms build       # vite library build (esm + dts)
pnpm --filter @duffcloudservices/site-forms test        # vitest --run
pnpm --filter @duffcloudservices/site-forms type-check  # vue-tsc --noEmit

Compliance requirements for form submissions

Sensitive forms (isSensitive: true)

Forms flagged as sensitive (law firm intake, HIPAA intake, any privilege-sensitive questionnaire) carry platform-enforced rules that form authors and site integrators must not override:

  1. Notification emails suppress submission content. When isSensitive: true, the portal notification email for a new submission contains only a portal link — never field values, names, or any submission body. This is required for attorney-client privilege (ABA Rule 1.6) and HIPAA confidentiality.

  2. Access audit logging. Every time a portal user opens a sensitive-form submission, the platform writes an audit log entry (PortalAuditLog, event: submission_viewed). This cannot be disabled.

  3. Set via portal, not YAML. The isSensitive flag lives in the PortalSiteForms table (managed via the portal Form Manager). It is not part of the .dcs/forms/*.yaml snapshot — the runtime itself has no concept of sensitivity.

Form version tracking

Every submission row stores formVersion (the schema version of the form at submission time). If a form's fields change after submissions are collected, old submissions remain interpretable: the portal can reconstruct what was shown by looking up the version-keyed schema. This is required for compliance audit trails.

General form requirements

All DCS-managed forms that collect personal data must:

  • Link a Privacy Policy URL in proximity to the submit action
  • Store formVersionId, submission timestamp, and submitter IP with every submission row (handled automatically by the platform)
  • Not collect unnecessary data fields (data minimization)

Intake questionnaires (attorneys, healthcare)

Use the Legal Intake — Standard form template (created via the portal Form Manager → Templates) when building intake forms for law firms. This template:

  • Pre-populates the attorney-client privilege disclaimer and consent checkbox (non-removable)
  • Automatically sets isSensitive: true
  • Includes matter-type and adverse-parties fields for conflict screening

For healthcare intake, mark each PHI-bearing field phi: true in the YAML. There is no hipaa form kind (the kinds are freeform, contact, revenue-contractor, resume-submission); PHI handling is driven by the per-field phi marker plus the site-level predicate below.

PHI guidance on HIPAA-mode tenants (C-415)

On a HIPAA-mode tenant, <DcsForm/> renders a standard guidance line immediately above the first free-text field:

Please don't include personal health details. A general description of what you need is enough — we'll go over anything specific with you directly.

It is on by default and there is deliberately no prop and no YAML flag to force it on or off. The verdict comes from the platform: on mount the component GETs /api/v1/sites/{slug}/forms/compliance (memoized per site; the slug-free /api/v1/forms/compliance when the build has no baked slug), which is a boolean projection of the server's one site-level PHI predicate, portal.Service.SiteHandlesPHI — authoritative input SiteAgentConfigs/{slug}/default.HipaaMode, the same row the C-352 redaction gate reads. A site-local switch could disagree with the tenant's real posture, which is exactly the class C-352 fixed.

If the read fails the component renders nothing; the server-side redactions remain the actual control. Style it via .dcs-form__phi-guidance; probe for it with [data-form-phi-guidance] (the copy contains no "HIPAA"/"protected health information" legalese by design — it is guidance for a visitor, not a legal notice).


Related docs

  • Authoring guide — .docs/forms/AUTHORING.md covers the YAML schema, worked examples, validation flow, HIPAA guardrails, and the hand-coded → <DcsForm/> migration recipe.
  • Publishing — PUBLISHING.md covers the registry, OIDC trusted publishing, version bump policy, and the exact dep line sibling customer-site repos should add.
  • First-party visual-editor contract — ../FIRST-PARTY-COMPONENTS.md captures the shared adaptation model used by forms and future component families.
  • Validation CLI — cli/forms/README.md documents dcs forms validate and dcs forms doctor, which lint the .dcs/forms/*.yaml files in a customer-site repo.

Ownership

Per packages/README.md: external/consumer-facing docs live here, not in repo-root docs. Cross-cutting details (e.g. the public submissions API contract) belong in contracts/README.md.