From d690ef895a4513ca6bd348d3c2ac29a171047ad3 Mon Sep 17 00:00:00 2001 From: Hirotaka Mizutani <52546+hirotaka@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:41:31 +0900 Subject: [PATCH] feat(registry): Add form components --- AGENTS.md | 2 +- app/components/demo/FormDemo.vue | 118 ++++++++++++++++++ app/components/demo/FormFieldDemo.vue | 32 +++++ app/components/ui/field/Field.vue | 25 ++++ app/components/ui/field/FieldContent.vue | 15 +++ app/components/ui/field/FieldDescription.vue | 15 +++ app/components/ui/field/FieldError.vue | 47 +++++++ app/components/ui/field/FieldGroup.vue | 15 +++ app/components/ui/field/FieldLabel.vue | 20 +++ app/components/ui/field/FieldLegend.vue | 21 ++++ app/components/ui/field/FieldSeparator.vue | 15 +++ app/components/ui/field/FieldSet.vue | 15 +++ app/components/ui/field/FieldTitle.vue | 15 +++ app/components/ui/field/index.ts | 10 ++ app/components/ui/select/Select.vue | 18 +++ app/components/ui/select/SelectContent.vue | 33 +++++ app/components/ui/select/SelectGroup.vue | 15 +++ app/components/ui/select/SelectItem.vue | 32 +++++ app/components/ui/select/SelectItemText.vue | 12 ++ app/components/ui/select/SelectLabel.vue | 18 +++ app/components/ui/select/SelectSeparator.vue | 16 +++ app/components/ui/select/SelectTrigger.vue | 28 +++++ app/components/ui/select/SelectValue.vue | 15 +++ app/components/ui/select/index.ts | 9 ++ app/components/ui/textarea/Textarea.vue | 31 +++++ app/components/ui/textarea/index.ts | 1 + app/composables/useNavigation.ts | 2 +- app/registry/blocks/form-field/FormField.vue | 124 +++++++++++++++++++ app/registry/blocks/form-field/index.ts | 1 + app/registry/blocks/form/Form.vue | 100 +++++++++++++++ app/registry/blocks/form/form-types.ts | 31 +++++ app/registry/blocks/form/form-utils.ts | 121 ++++++++++++++++++ app/registry/blocks/form/index.ts | 3 + content.config.ts | 2 +- content/docs/2.components/18.form.md | 120 ++++++++++++++++++ content/docs/2.components/19.form-field.md | 90 ++++++++++++++ scripts/registry-verify.ts | 8 ++ 37 files changed, 1192 insertions(+), 3 deletions(-) create mode 100644 app/components/demo/FormDemo.vue create mode 100644 app/components/demo/FormFieldDemo.vue create mode 100644 app/components/ui/field/Field.vue create mode 100644 app/components/ui/field/FieldContent.vue create mode 100644 app/components/ui/field/FieldDescription.vue create mode 100644 app/components/ui/field/FieldError.vue create mode 100644 app/components/ui/field/FieldGroup.vue create mode 100644 app/components/ui/field/FieldLabel.vue create mode 100644 app/components/ui/field/FieldLegend.vue create mode 100644 app/components/ui/field/FieldSeparator.vue create mode 100644 app/components/ui/field/FieldSet.vue create mode 100644 app/components/ui/field/FieldTitle.vue create mode 100644 app/components/ui/field/index.ts create mode 100644 app/components/ui/select/Select.vue create mode 100644 app/components/ui/select/SelectContent.vue create mode 100644 app/components/ui/select/SelectGroup.vue create mode 100644 app/components/ui/select/SelectItem.vue create mode 100644 app/components/ui/select/SelectItemText.vue create mode 100644 app/components/ui/select/SelectLabel.vue create mode 100644 app/components/ui/select/SelectSeparator.vue create mode 100644 app/components/ui/select/SelectTrigger.vue create mode 100644 app/components/ui/select/SelectValue.vue create mode 100644 app/components/ui/select/index.ts create mode 100644 app/components/ui/textarea/Textarea.vue create mode 100644 app/components/ui/textarea/index.ts create mode 100644 app/registry/blocks/form-field/FormField.vue create mode 100644 app/registry/blocks/form-field/index.ts create mode 100644 app/registry/blocks/form/Form.vue create mode 100644 app/registry/blocks/form/form-types.ts create mode 100644 app/registry/blocks/form/form-utils.ts create mode 100644 app/registry/blocks/form/index.ts create mode 100644 content/docs/2.components/18.form.md create mode 100644 content/docs/2.components/19.form-field.md diff --git a/AGENTS.md b/AGENTS.md index 67f6860..c196138 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,7 +52,7 @@ Config: `registry.config.ts` and `components.json` `registries` field. ## Content -- `content.config.ts` defines the docs collection with `category` field: `element | content | marketing | chat | overview` +- `content.config.ts` defines the docs collection with `category` field: `form | content | marketing | chat | overview` - `@nuxt/content` v3 with D1 database (`content.database.type: "d1"`) - Markdown highlight disabled; `nuxt-shiki` module handles syntax highlighting instead diff --git a/app/components/demo/FormDemo.vue b/app/components/demo/FormDemo.vue new file mode 100644 index 0000000..17f8303 --- /dev/null +++ b/app/components/demo/FormDemo.vue @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + Admin + + + Editor + + + Viewer + + + + + + + + Submit + + + Submitted locally. Your app owns the API call. + + + + diff --git a/app/components/demo/FormFieldDemo.vue b/app/components/demo/FormFieldDemo.vue new file mode 100644 index 0000000..5a57f64 --- /dev/null +++ b/app/components/demo/FormFieldDemo.vue @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/app/components/ui/field/Field.vue b/app/components/ui/field/Field.vue new file mode 100644 index 0000000..1f1f058 --- /dev/null +++ b/app/components/ui/field/Field.vue @@ -0,0 +1,25 @@ + + + + + + + diff --git a/app/components/ui/field/FieldContent.vue b/app/components/ui/field/FieldContent.vue new file mode 100644 index 0000000..70d5d7e --- /dev/null +++ b/app/components/ui/field/FieldContent.vue @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/components/ui/field/FieldDescription.vue b/app/components/ui/field/FieldDescription.vue new file mode 100644 index 0000000..b46304f --- /dev/null +++ b/app/components/ui/field/FieldDescription.vue @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/components/ui/field/FieldError.vue b/app/components/ui/field/FieldError.vue new file mode 100644 index 0000000..6d918cb --- /dev/null +++ b/app/components/ui/field/FieldError.vue @@ -0,0 +1,47 @@ + + + + + + + {{ messages[0] }} + + + + {{ message }} + + + + + diff --git a/app/components/ui/field/FieldGroup.vue b/app/components/ui/field/FieldGroup.vue new file mode 100644 index 0000000..1e25ab1 --- /dev/null +++ b/app/components/ui/field/FieldGroup.vue @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/components/ui/field/FieldLabel.vue b/app/components/ui/field/FieldLabel.vue new file mode 100644 index 0000000..6f37231 --- /dev/null +++ b/app/components/ui/field/FieldLabel.vue @@ -0,0 +1,20 @@ + + + + + + + diff --git a/app/components/ui/field/FieldLegend.vue b/app/components/ui/field/FieldLegend.vue new file mode 100644 index 0000000..11a1fad --- /dev/null +++ b/app/components/ui/field/FieldLegend.vue @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/components/ui/field/FieldSeparator.vue b/app/components/ui/field/FieldSeparator.vue new file mode 100644 index 0000000..5524e1f --- /dev/null +++ b/app/components/ui/field/FieldSeparator.vue @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/components/ui/field/FieldSet.vue b/app/components/ui/field/FieldSet.vue new file mode 100644 index 0000000..52bfee5 --- /dev/null +++ b/app/components/ui/field/FieldSet.vue @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/components/ui/field/FieldTitle.vue b/app/components/ui/field/FieldTitle.vue new file mode 100644 index 0000000..ce620fa --- /dev/null +++ b/app/components/ui/field/FieldTitle.vue @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/components/ui/field/index.ts b/app/components/ui/field/index.ts new file mode 100644 index 0000000..cf2450a --- /dev/null +++ b/app/components/ui/field/index.ts @@ -0,0 +1,10 @@ +export { default as Field } from "./Field.vue"; +export { default as FieldContent } from "./FieldContent.vue"; +export { default as FieldDescription } from "./FieldDescription.vue"; +export { default as FieldError, type FieldErrorItem } from "./FieldError.vue"; +export { default as FieldGroup } from "./FieldGroup.vue"; +export { default as FieldLabel } from "./FieldLabel.vue"; +export { default as FieldLegend } from "./FieldLegend.vue"; +export { default as FieldSeparator } from "./FieldSeparator.vue"; +export { default as FieldSet } from "./FieldSet.vue"; +export { default as FieldTitle } from "./FieldTitle.vue"; diff --git a/app/components/ui/select/Select.vue b/app/components/ui/select/Select.vue new file mode 100644 index 0000000..9048de9 --- /dev/null +++ b/app/components/ui/select/Select.vue @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/components/ui/select/SelectContent.vue b/app/components/ui/select/SelectContent.vue new file mode 100644 index 0000000..742267b --- /dev/null +++ b/app/components/ui/select/SelectContent.vue @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/app/components/ui/select/SelectGroup.vue b/app/components/ui/select/SelectGroup.vue new file mode 100644 index 0000000..fa57ee9 --- /dev/null +++ b/app/components/ui/select/SelectGroup.vue @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/components/ui/select/SelectItem.vue b/app/components/ui/select/SelectItem.vue new file mode 100644 index 0000000..2a453fe --- /dev/null +++ b/app/components/ui/select/SelectItem.vue @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + diff --git a/app/components/ui/select/SelectItemText.vue b/app/components/ui/select/SelectItemText.vue new file mode 100644 index 0000000..ab193b1 --- /dev/null +++ b/app/components/ui/select/SelectItemText.vue @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/components/ui/select/SelectLabel.vue b/app/components/ui/select/SelectLabel.vue new file mode 100644 index 0000000..1e55542 --- /dev/null +++ b/app/components/ui/select/SelectLabel.vue @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/components/ui/select/SelectSeparator.vue b/app/components/ui/select/SelectSeparator.vue new file mode 100644 index 0000000..5f7c269 --- /dev/null +++ b/app/components/ui/select/SelectSeparator.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/app/components/ui/select/SelectTrigger.vue b/app/components/ui/select/SelectTrigger.vue new file mode 100644 index 0000000..d840d7f --- /dev/null +++ b/app/components/ui/select/SelectTrigger.vue @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/app/components/ui/select/SelectValue.vue b/app/components/ui/select/SelectValue.vue new file mode 100644 index 0000000..98c0d67 --- /dev/null +++ b/app/components/ui/select/SelectValue.vue @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/components/ui/select/index.ts b/app/components/ui/select/index.ts new file mode 100644 index 0000000..8e6b873 --- /dev/null +++ b/app/components/ui/select/index.ts @@ -0,0 +1,9 @@ +export { default as Select } from "./Select.vue"; +export { default as SelectContent } from "./SelectContent.vue"; +export { default as SelectGroup } from "./SelectGroup.vue"; +export { default as SelectItem } from "./SelectItem.vue"; +export { default as SelectItemText } from "./SelectItemText.vue"; +export { default as SelectLabel } from "./SelectLabel.vue"; +export { default as SelectSeparator } from "./SelectSeparator.vue"; +export { default as SelectTrigger } from "./SelectTrigger.vue"; +export { default as SelectValue } from "./SelectValue.vue"; diff --git a/app/components/ui/textarea/Textarea.vue b/app/components/ui/textarea/Textarea.vue new file mode 100644 index 0000000..ec2f156 --- /dev/null +++ b/app/components/ui/textarea/Textarea.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/app/components/ui/textarea/index.ts b/app/components/ui/textarea/index.ts new file mode 100644 index 0000000..178d08c --- /dev/null +++ b/app/components/ui/textarea/index.ts @@ -0,0 +1 @@ +export { default as Textarea } from "./Textarea.vue"; diff --git a/app/composables/useNavigation.ts b/app/composables/useNavigation.ts index d030f1e..c67acff 100644 --- a/app/composables/useNavigation.ts +++ b/app/composables/useNavigation.ts @@ -8,7 +8,7 @@ export const sectionCategories: Record { id: "overview", title: "Overview" }, { id: "content", title: "Content" }, { id: "marketing", title: "Marketing" }, - { id: "element", title: "Element" }, + { id: "form", title: "Form" }, { id: "chat", title: "AI Chat" }, ], }; diff --git a/app/registry/blocks/form-field/FormField.vue b/app/registry/blocks/form-field/FormField.vue new file mode 100644 index 0000000..6034afc --- /dev/null +++ b/app/registry/blocks/form-field/FormField.vue @@ -0,0 +1,124 @@ + + + + + + + + {{ label }} + + + + + + {{ hint }} + + + + + + + {{ description }} + + + + + + + + + + {{ errorMessage }} + + + + + + {{ help }} + + + + diff --git a/app/registry/blocks/form-field/index.ts b/app/registry/blocks/form-field/index.ts new file mode 100644 index 0000000..9d2150d --- /dev/null +++ b/app/registry/blocks/form-field/index.ts @@ -0,0 +1 @@ +export { default as FormField, type FormFieldProps } from "./FormField.vue"; diff --git a/app/registry/blocks/form/Form.vue b/app/registry/blocks/form/Form.vue new file mode 100644 index 0000000..f0b02c7 --- /dev/null +++ b/app/registry/blocks/form/Form.vue @@ -0,0 +1,100 @@ + + + + + + + diff --git a/app/registry/blocks/form/form-types.ts b/app/registry/blocks/form/form-types.ts new file mode 100644 index 0000000..f9795a4 --- /dev/null +++ b/app/registry/blocks/form/form-types.ts @@ -0,0 +1,31 @@ +import type { Ref } from "vue"; + +export interface FormError { + name: string; + message: string; + id?: string; + [key: string]: unknown; +} + +export interface FormSubmitEvent { + originalEvent: SubmitEvent; + data: TState; +} + +export interface FormErrorEvent { + originalEvent?: Event; + errors: FormError[]; + data: TState; +} + +export type FormInputEvent = "input" | "change" | "blur"; + +export type FormValidate = (state: TState) => FormError[] | Promise; + +export interface FormContextValue { + errors: Ref; + disabled: Ref; + getFieldErrors: (name?: string, errorPattern?: RegExp) => FormError[]; + validate: (event?: Event) => Promise; + state: Ref; +} diff --git a/app/registry/blocks/form/form-utils.ts b/app/registry/blocks/form/form-utils.ts new file mode 100644 index 0000000..196a8d3 --- /dev/null +++ b/app/registry/blocks/form/form-utils.ts @@ -0,0 +1,121 @@ +import type { InjectionKey } from "vue"; +import type { FormContextValue, FormError } from "./form-types"; + +export const FORM_CONTEXT_KEY: InjectionKey = Symbol("stackhacker-ui-form-context"); + +interface MessageLike { + message?: unknown; + $message?: unknown; + name?: unknown; + path?: unknown; + id?: unknown; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function pathToName(path: unknown) { + if (Array.isArray(path)) return path.join("."); + return typeof path === "string" ? path : undefined; +} + +function messageFrom(value: unknown) { + if (typeof value === "string") return value; + if (!isRecord(value)) return undefined; + + const item = value as MessageLike; + if (typeof item.message === "string") return item.message; + if (typeof item.$message === "string") return item.$message; + return undefined; +} + +function nameFrom(value: unknown, fallback?: string) { + if (!isRecord(value)) return fallback; + + const item = value as MessageLike; + if (typeof item.name === "string") return item.name; + return pathToName(item.path) ?? fallback; +} + +function normalizeErrorItem(value: unknown, fallbackName?: string): FormError | null { + const message = messageFrom(value); + const name = nameFrom(value, fallbackName); + if (!message || !name) return null; + + const id = isRecord(value) && typeof value.id === "string" ? value.id : undefined; + return { name, message, ...(id ? { id } : {}) }; +} + +function collectErrorTree(value: unknown, prefix = ""): FormError[] { + if (!value) return []; + + if (Array.isArray(value)) { + return value + .map(error => normalizeErrorItem(error, prefix)) + .filter((error): error is FormError => Boolean(error)); + } + + const direct = normalizeErrorItem(value, prefix || undefined); + if (direct) return [direct]; + + if (!isRecord(value)) return []; + + return Object.entries(value).flatMap(([key, child]) => { + const path = prefix ? `${prefix}.${key}` : key; + return collectErrorTree(child, path); + }); +} + +export function normalizeFormErrors(value: unknown): FormError[] { + return collectErrorTree(value); +} + +export function findFormErrors(errors: FormError[], name?: string, errorPattern?: RegExp) { + if (!name && !errorPattern) return []; + + return errors.filter((error) => { + if (name && error.name === name) return true; + return errorPattern ? errorPattern.test(error.name) : false; + }); +} + +export async function validateWithSchema(schema: unknown, state: unknown): Promise { + if (!schema) return []; + + if (isRecord(schema) && typeof schema.$validate === "function") { + await schema.$validate(); + return normalizeFormErrors(schema.$errors); + } + + if (isRecord(schema) && isRecord(schema["~standard"])) { + const standard = schema["~standard"]; + if (typeof standard.validate === "function") { + const result = await standard.validate(state); + if (isRecord(result) && Array.isArray(result.issues)) { + return normalizeFormErrors(result.issues); + } + return []; + } + } + + if (isRecord(schema) && typeof schema.safeParse === "function") { + const result = await schema.safeParse(state); + if (isRecord(result) && result.success === false && isRecord(result.error)) { + return normalizeFormErrors(result.error.issues); + } + return []; + } + + if (isRecord(schema) && typeof schema.parse === "function") { + try { + await schema.parse(state); + return []; + } + catch (error) { + return normalizeFormErrors(isRecord(error) ? error.issues : error); + } + } + + return []; +} diff --git a/app/registry/blocks/form/index.ts b/app/registry/blocks/form/index.ts new file mode 100644 index 0000000..af1039e --- /dev/null +++ b/app/registry/blocks/form/index.ts @@ -0,0 +1,3 @@ +export { default as Form, type FormProps } from "./Form.vue"; +export type { FormError, FormErrorEvent, FormInputEvent, FormSubmitEvent, FormValidate } from "./form-types"; +export { findFormErrors, FORM_CONTEXT_KEY, normalizeFormErrors, validateWithSchema } from "./form-utils"; diff --git a/content.config.ts b/content.config.ts index 6b69213..94e8516 100644 --- a/content.config.ts +++ b/content.config.ts @@ -9,7 +9,7 @@ export default defineContentConfig({ }, schema: z.object({ category: z - .enum(["element", "content", "marketing", "chat", "overview"]) + .enum(["form", "content", "marketing", "chat", "overview"]) .optional(), }), }), diff --git a/content/docs/2.components/18.form.md b/content/docs/2.components/18.form.md new file mode 100644 index 0000000..6d6f15a --- /dev/null +++ b/content/docs/2.components/18.form.md @@ -0,0 +1,120 @@ +--- +title: Form +description: Validate app-owned form state and distribute errors to named fields. +category: form +--- + +::component-preview +--- + +name: FormDemo +--- + +:: + +## Installation + +Add the shadcn-vue controls you want to compose, then add Stackhacker UI's form layer. + +```bash +npx shadcn-vue@latest add field input textarea select +npx shadcn-vue@latest add "https://ui.stackhacker.io/r/form.json" +npx shadcn-vue@latest add "https://ui.stackhacker.io/r/form-field.json" +``` + +## Usage + +```vue + + + + + + + + + +``` + +## Responsibilities + +`Form` owns the UI protocol: form event handling, validation triggering, error collection, field-level error distribution, and `submit` / `error` events. + +Your app owns API submission, mutation, persistence, toast, redirect, server error interpretation, auth flows, and business rules. + +## Regle Integration + +Regle remains an app dependency. Stackhacker UI does not import Regle. + +```vue + + + + + + + + + +``` + +## API Reference + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `state` | `unknown` | — | App-owned reactive form state. | +| `schema` | `unknown` | — | Optional schema-like validator such as Regle or Standard Schema. | +| `validate` | `(state) => FormError[] \| Promise` | — | Custom validation function. | +| `validateOn` | `('input' \| 'change' \| 'blur')[]` | `['blur']` | Input events that trigger validation. | +| `disabled` | `boolean` | `false` | Marks the form context disabled for fields. | +| `class` | `string` | — | Additional CSS classes. | + +### Events + +| Event | Payload | Description | +|------|---------|-------------| +| `submit` | `{ originalEvent, data }` | Emitted after validation succeeds. | +| `error` | `{ originalEvent, errors, data }` | Emitted after validation fails. | + +### Types + +```ts +interface FormError { + name: string + message: string + id?: string +} +``` diff --git a/content/docs/2.components/19.form-field.md b/content/docs/2.components/19.form-field.md new file mode 100644 index 0000000..110ffe8 --- /dev/null +++ b/content/docs/2.components/19.form-field.md @@ -0,0 +1,90 @@ +--- +title: FormField +description: Wrap form controls with labels, descriptions, help text, and validation errors. +category: form +--- + +::component-preview +--- +name: FormFieldDemo +--- +:: + +## Installation + +```bash +npx shadcn-vue@latest add field input +npx shadcn-vue@latest add "https://ui.stackhacker.io/r/form-field.json" +``` + +## Usage + +```vue + + + + + + + +``` + +## With Form + +When used inside `Form`, the `name` prop matches the current form errors. + +```vue + + + + + +``` + +## Slot Props + +`FormField` passes accessibility props to the default slot so the slotted control can receive a stable `id`, invalid state, and description/error references. + +| Prop | Description | +|------|-------------| +| `id` | Generated control id. | +| `name` | Field name. | +| `disabled` | Whether the parent form is disabled. | +| `aria-invalid` | ARIA invalid state for the control. | +| `aria-describedby` | Description, help, or error ids for the control. | + +## API Reference + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `name` | `string` | — | Field name used to match form errors. | +| `errorPattern` | `RegExp` | — | Optional pattern for matching related error names. | +| `label` | `string` | — | Field label. | +| `description` | `string` | — | Supporting copy below the label. | +| `help` | `string` | — | Help text shown when no error is present. | +| `hint` | `string` | — | Text shown next to the label. | +| `error` | `string \| boolean` | — | Explicit error. Overrides form context errors. | +| `required` | `boolean` | `false` | Shows a required marker. | +| `orientation` | `'vertical' \| 'horizontal' \| 'responsive'` | `'vertical'` | Field layout orientation. | +| `class` | `string` | — | Additional CSS classes. | + +### Slots + +| Slot | Description | +|------|-------------| +| `default` | Form control. Receives accessibility slot props. | +| `label` | Custom label content. | +| `description` | Custom description content. | +| `help` | Custom help content. | +| `hint` | Custom hint content. | +| `error` | Custom error content. | diff --git a/scripts/registry-verify.ts b/scripts/registry-verify.ts index 69804de..08ffc6d 100644 --- a/scripts/registry-verify.ts +++ b/scripts/registry-verify.ts @@ -163,6 +163,14 @@ const expectedItems: Record = { dependencies: [], registryDependencies: [], }, + "form": { + dependencies: [], + registryDependencies: [], + }, + "form-field": { + dependencies: [], + registryDependencies: ["field", stackhackerDependency("form")], + }, }; if (registryIndex) {
+ Submitted locally. Your app owns the API call. +
+ +
+ {{ messages[0] }} +