@duffcloudservices/site-forms
v0.4.2
Published
Shared <DcsForm/> runtime for DCS customer sites — renders managed form definitions from .dcs/forms/<formId>.yaml
Maintainers
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-againworkspace the package is also reachable as@duffcloudservices/site-formsviaworkspace:*(the workspace name and the published name are the same). In sibling customer-site repos install the published package from public npm — seePUBLISHING.mdfor 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-formsSee PUBLISHING.md for the registry, version, and
release-workflow details.
Vite setup
Three pieces are required in the consuming site:
vite-plugin-yamlso 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.A
formsModulesloader in your site that does theimport.meta.globfrom a path Vite can resolve (see below).Env vars the runtime reads:
| 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. |
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> = modulesVitePress (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> = modulesThe 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: trueThen 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 | import.meta.env.VITE_DCS_PUBLIC_API | Override for tests / non-prod environments. |
| 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. |
Emits
| Event | Payload | When |
| ------------------ | ---------------------- | ------------------------------------------- |
| submit-success | DcsFormSubmitSuccess | API responded 2xx. |
| submit-error | DcsFormSubmitError | Network or non-2xx response after retries. |
| validation-error | FormErrors | Submit attempted with invalid required/regex/etc. fields. |
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, prev, next } | Plain <button> elements |
| 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 })— reactivevalues,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 andmultipart/form-datawhen files are present.
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 --runScripts
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 --noEmitCompliance 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:
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.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.Set via portal, not YAML. The
isSensitiveflag lives in thePortalSiteFormstable (managed via the portal Form Manager). It is not part of the.dcs/forms/*.yamlsnapshot — 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 the form as FormKind: "hipaa" so the submission handler applies PHI-aware redaction for notification emails.
Related docs
- Authoring guide —
.docs/forms/AUTHORING.mdcovers the YAML schema, worked examples, validation flow, HIPAA guardrails, and the hand-coded →<DcsForm/>migration recipe. - Publishing —
PUBLISHING.mdcovers 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.mdcaptures the shared adaptation model used by forms and future component families. - Validation CLI —
cli/forms/README.mddocumentsdcs forms validateanddcs forms doctor, which lint the.dcs/forms/*.yamlfiles 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.
