diff --git a/.agents/skills/formisch/.claude-plugin/plugin.json b/.agents/skills/formisch/.claude-plugin/plugin.json
new file mode 100644
index 0000000..05e30c7
--- /dev/null
+++ b/.agents/skills/formisch/.claude-plugin/plugin.json
@@ -0,0 +1,13 @@
+{
+ "name": "formisch",
+ "description": "Form validation with Formisch, the schema-based, headless form library",
+ "version": "1.0.0",
+ "author": {
+ "name": "Open Circle",
+ "url": "https://github.com/open-circle"
+ },
+ "homepage": "https://formisch.dev",
+ "repository": "https://github.com/open-circle/agent-skills",
+ "license": "MIT",
+ "keywords": ["formisch", "forms", "validation", "typescript"]
+}
diff --git a/.agents/skills/formisch/SKILL.md b/.agents/skills/formisch/SKILL.md
new file mode 100644
index 0000000..a2b27aa
--- /dev/null
+++ b/.agents/skills/formisch/SKILL.md
@@ -0,0 +1,1118 @@
+---
+name: formisch
+description: Form handling with Formisch, the schema-first and type-safe form library for Angular, Preact, Qwik, React, React Native, Solid, Svelte, and Vue. Use when creating forms, handling form state, validating inputs, working with field arrays, or using @formisch/* packages.
+license: MIT
+metadata:
+ author: open-circle
+ version: "1.3"
+---
+
+# Formisch
+
+This skill helps AI agents work effectively with [Formisch](https://formisch.dev/), the schema-based, headless form library for modern frameworks.
+
+## When to Use This Skill
+
+- When the user asks about form handling with Formisch
+- When managing form state and validation
+- When working with Angular, Preact, Qwik, React, React Native, Solid, Svelte, or Vue forms
+- When integrating Valibot schemas with forms
+
+## Introduction
+
+Formisch is a schema-based, headless form library that works across multiple frameworks. Key highlights:
+
+- **Small bundle size** — Starting at ~2.5 kB
+- **Schema-based validation** — Uses Valibot for type-safe validation
+- **Headless design** — You control the UI completely
+- **Type safety** — Full TypeScript support with autocompletion
+- **Framework-native** — Native performance for each supported framework
+
+### Supported Frameworks
+
+| Framework | Package | Hook/Primitive |
+| ------------ | ------------------------ | -------------- |
+| Angular | `@formisch/angular` | `injectForm` |
+| Preact | `@formisch/preact` | `useForm` |
+| Qwik | `@formisch/qwik` | `useForm$` |
+| React | `@formisch/react` | `useForm` |
+| React Native | `@formisch/react-native` | `useForm` |
+| SolidJS | `@formisch/solid` | `createForm` |
+| Svelte | `@formisch/svelte` | `createForm` |
+| Vue | `@formisch/vue` | `useForm` |
+
+## Installation
+
+### 1. Install Valibot (peer dependency)
+
+```bash
+npm install valibot
+```
+
+### 2. Install Formisch for your framework
+
+```bash
+npm install @formisch/react # React
+npm install @formisch/angular # Angular
+npm install @formisch/vue # Vue
+npm install @formisch/solid # SolidJS
+npm install @formisch/preact # Preact
+npm install @formisch/svelte # Svelte
+npm install @formisch/qwik # Qwik
+npm install @formisch/react-native # React Native
+```
+
+## Core Concepts
+
+### Schema-First Design
+
+Every form starts with a Valibot schema. Types are automatically inferred from the schema.
+
+```ts
+import * as v from "valibot";
+
+const LoginSchema = v.object({
+ email: v.pipe(
+ v.string("Please enter your email."),
+ v.nonEmpty("Please enter your email."),
+ v.email("The email address is badly formatted."),
+ ),
+ password: v.pipe(
+ v.string("Please enter your password."),
+ v.nonEmpty("Please enter your password."),
+ v.minLength(8, "Your password must have 8 characters or more."),
+ ),
+});
+```
+
+### Form Store
+
+The form store manages all form state. Access it via the framework-specific hook/primitive.
+
+**Form Store Properties:**
+
+- `isSubmitting` — Form is currently being submitted
+- `isSubmitted` — Form submission has been attempted
+- `isValidating` — Validation is in progress
+- `isTouched` — At least one field has been touched
+- `isEdited` — At least one field has been edited
+- `isDirty` — At least one field differs from initial value
+- `isValid` — All fields pass validation
+- `errors` — Root-level validation errors
+
+### Field Store
+
+Each field has its own reactive store with:
+
+- `path` — Path array to the field
+- `input` — Current field value
+- `errors` — Field-specific errors
+- `isTouched` — Field has been focused
+- `isEdited` — Field value has been edited
+- `isDirty` — Field value differs from initial value
+- `isValid` — Field passes validation
+- `props` — Props to spread onto native elements (Angular connects controls with `[formischControl]` instead)
+- `onChange` (React and React Native) / `onInput` (Solid, Svelte, Preact, and Qwik) / `setInput` (Angular) — Sets the field input value programmatically. Use this when the field cannot be connected to a native element. In Vue, set `field.input` directly (for example with `v-model`).
+
+Store reactivity is framework-specific. React, React Native, Solid, Svelte, and Vue expose plain reactive properties. Angular properties are signals and are called like `field.errors()`, except `path`, which is a plain value. Preact and Qwik properties are signals; use `.value` in conditions and ordinary TypeScript logic. Do not copy one framework's access syntax into another.
+
+### Dirty Tracking
+
+Formisch tracks two inputs per field:
+
+- **Initial input** — Baseline for dirty tracking (server state)
+- **Current input** — What the user is editing (client state)
+
+`isDirty` becomes `true` when current input differs from initial input.
+
+## Framework Examples
+
+### Angular Example
+
+Angular uses signals, dependency injection, and directives instead of a JSX component API.
+
+```ts
+import { Component } from "@angular/core";
+import {
+ FormischControl,
+ FormischField,
+ FormischForm,
+ injectForm,
+ type SubmitHandler,
+} from "@formisch/angular";
+import * as v from "valibot";
+
+const LoginSchema = v.object({
+ email: v.pipe(v.string(), v.email()),
+ password: v.pipe(v.string(), v.minLength(8)),
+});
+
+@Component({
+ selector: "app-login",
+ imports: [FormischForm, FormischField, FormischControl],
+ template: `
+
+ `,
+})
+export class LoginComponent {
+ readonly loginForm = injectForm({ schema: LoginSchema });
+
+ readonly handleSubmit: SubmitHandler = (output) => {
+ console.log(output);
+ };
+}
+```
+
+Let `[formischControl]` synchronize the native control. Do not add competing `[value]` or `[checked]` bindings except when `value` identifies an option in a radio or checkbox group.
+
+### React Native Example
+
+React Native has no DOM `
{errorMessage()}
diff --git a/packages/web/src/styles/drfed.css b/packages/web/src/styles/drfed.css
index b306ab6..6301508 100644
--- a/packages/web/src/styles/drfed.css
+++ b/packages/web/src/styles/drfed.css
@@ -56,6 +56,8 @@ along with this program. If not, see .
--line: #e9e5e1;
--line-strong: #dad4cf;
--danger: #cf3f43;
+ --success: #247a46;
+ --warning: #9a5800;
/* Elevation */
--shadow-sm: 0 1px 2px rgb(38 33 31 / 5%), 0 2px 8px rgb(38 33 31 / 4%);
@@ -100,6 +102,8 @@ along with this program. If not, see .
--line: #332f2a;
--line-strong: #453f39;
--danger: #ef726b;
+ --success: #66cf8a;
+ --warning: #f0ad4e;
--shadow-sm: 0 1px 2px rgb(0 0 0 / 30%);
--shadow-md: 0 6px 24px rgb(0 0 0 / 40%);
diff --git a/packages/web/src/styles/form.module.css b/packages/web/src/styles/form.module.css
index 8217954..8e71adc 100644
--- a/packages/web/src/styles/form.module.css
+++ b/packages/web/src/styles/form.module.css
@@ -102,6 +102,15 @@ along with this program. If not, see .
outline: none;
}
+.input[aria-invalid="true"] {
+ border-color: var(--danger);
+}
+
+.input[aria-invalid="true"]:focus {
+ border-color: var(--danger);
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 20%, transparent);
+}
+
.input:read-only {
border-style: dashed;
color: var(--ink-soft);
@@ -155,12 +164,25 @@ along with this program. If not, see .
display: block;
}
-.error {
+.notice.error {
+ background: color-mix(in srgb, var(--danger) 12%, var(--surface));
border-left: 3px solid var(--danger);
+ color: var(--danger);
}
-.success {
- border-left: 3px solid var(--accent);
+.notice.success {
+ background: color-mix(in srgb, var(--success) 12%, var(--surface));
+ border-left: 3px solid var(--success);
+ color: var(--success);
+}
+
+.field .notice.error {
+ background: transparent;
+ border-left: 0;
+ font-size: 0.78rem;
+ line-height: 1.45;
+ margin: 0;
+ padding: 0;
}
@media (max-width: 600px) {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index adee240..6380d0b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -130,7 +130,7 @@ importers:
version: 0.6.0-dev.263(@upyo/core@0.6.0-dev.263)
drizzle-orm:
specifier: 'catalog:'
- version: 1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(postgres@3.4.9)
+ version: 1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(postgres@3.4.9)(valibot@1.4.2(typescript@7.0.2))
graphql:
specifier: 'catalog:'
version: 16.14.2
@@ -179,7 +179,7 @@ importers:
version: 4.13.0(graphql@16.14.2)
'@pothos/plugin-drizzle':
specifier: ^0.17.4
- version: 0.17.4(@pothos/core@4.13.0(graphql@16.14.2))(drizzle-orm@1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(postgres@3.4.9))(graphql@16.14.2)
+ version: 0.17.4(@pothos/core@4.13.0(graphql@16.14.2))(drizzle-orm@1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(postgres@3.4.9)(valibot@1.4.2(typescript@7.0.2)))(graphql@16.14.2)
'@pothos/plugin-errors':
specifier: ^4.9.1
version: 4.9.1(@pothos/core@4.13.0(graphql@16.14.2))(graphql@16.14.2)
@@ -197,7 +197,7 @@ importers:
version: 0.6.0-dev.263(@upyo/core@0.6.0-dev.263)
drizzle-orm:
specifier: 'catalog:'
- version: 1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(postgres@3.4.9)
+ version: 1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(postgres@3.4.9)(valibot@1.4.2(typescript@7.0.2))
graphql:
specifier: 'catalog:'
version: 16.14.2
@@ -237,7 +237,7 @@ importers:
version: 2.3.0-dev.840
drizzle-orm:
specifier: 'catalog:'
- version: 1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(postgres@3.4.9)
+ version: 1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(postgres@3.4.9)(valibot@1.4.2(typescript@7.0.2))
postgres:
specifier: 'catalog:'
version: 3.4.9
@@ -260,6 +260,9 @@ importers:
packages/web:
dependencies:
+ '@formisch/solid':
+ specifier: 1.0.0
+ version: 1.0.0(solid-js@1.9.14)(typescript@7.0.2)(valibot@1.4.2(typescript@7.0.2))
'@kobalte/core':
specifier: ^0.13.13
version: 0.13.13(solid-js@1.9.14)
@@ -284,6 +287,9 @@ importers:
solid-relay:
specifier: 1.0.0-beta.29
version: 1.0.0-beta.29(relay-runtime@21.0.1)(seroval@1.6.0)(solid-js@1.9.14)
+ valibot:
+ specifier: 1.4.2
+ version: 1.4.2(typescript@7.0.2)
vite:
specifier: ^8.2.0
version: 8.2.0(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)
@@ -859,6 +865,16 @@ packages:
'@floating-ui/utils@0.2.12':
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
+ '@formisch/solid@1.0.0':
+ resolution: {integrity: sha512-J5RF48YCM6SNYVkt+CJ0+5MHPC38wi3zgBM5hOpwnWFU5Q/gtdjb+vO63uJKWLAU7XQlnZVDo7rBzf8bVsvvcA==}
+ peerDependencies:
+ solid-js: '>=1.6 <2'
+ typescript: '>=5 <8'
+ valibot: '>=1.4.1 <2'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
'@graphql-tools/executor@1.5.3':
resolution: {integrity: sha512-mgBFC0bsrZPZLu9EnydpMnAuQ8Iiq0CEbUcsmvXsm2/iYektGHDN/+bmb7hicA6dWZtdPfklYJmr21WD0GnOfA==}
engines: {node: '>=16.0.0'}
@@ -3726,6 +3742,14 @@ packages:
resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==}
hasBin: true
+ valibot@1.4.2:
+ resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==}
+ peerDependencies:
+ typescript: '>=5'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
verkit@0.3.0:
resolution: {integrity: sha512-Njrh4U8UODGajoZ44QS2C/BsoEM9DTI/aCqY5swsizb+/ap0FamvnCMcZAxrR5+aoC0ZqkawEfpC/N2SBc+xeA==}
engines: {node: '>=18.12.0'}
@@ -4349,6 +4373,13 @@ snapshots:
'@floating-ui/utils@0.2.12': {}
+ '@formisch/solid@1.0.0(solid-js@1.9.14)(typescript@7.0.2)(valibot@1.4.2(typescript@7.0.2))':
+ dependencies:
+ solid-js: 1.9.14
+ valibot: 1.4.2(typescript@7.0.2)
+ optionalDependencies:
+ typescript: 7.0.2
+
'@graphql-tools/executor@1.5.3(graphql@16.14.2)':
dependencies:
'@graphql-tools/utils': 11.1.0(graphql@16.14.2)
@@ -4731,10 +4762,10 @@ snapshots:
dependencies:
graphql: 16.14.2
- '@pothos/plugin-drizzle@0.17.4(@pothos/core@4.13.0(graphql@16.14.2))(drizzle-orm@1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(postgres@3.4.9))(graphql@16.14.2)':
+ '@pothos/plugin-drizzle@0.17.4(@pothos/core@4.13.0(graphql@16.14.2))(drizzle-orm@1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(postgres@3.4.9)(valibot@1.4.2(typescript@7.0.2)))(graphql@16.14.2)':
dependencies:
'@pothos/core': 4.13.0(graphql@16.14.2)
- drizzle-orm: 1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(postgres@3.4.9)
+ drizzle-orm: 1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(postgres@3.4.9)(valibot@1.4.2(typescript@7.0.2))
graphql: 16.14.2
'@pothos/plugin-errors@4.9.1(@pothos/core@4.13.0(graphql@16.14.2))(graphql@16.14.2)':
@@ -5522,13 +5553,14 @@ snapshots:
get-tsconfig: 4.14.0
jiti: 2.7.0
- drizzle-orm@1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(postgres@3.4.9):
+ drizzle-orm@1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(postgres@3.4.9)(valibot@1.4.2(typescript@7.0.2)):
optionalDependencies:
'@electric-sql/pglite': 0.5.3
'@opentelemetry/api': 1.9.1
'@types/pg': 8.20.0
pg: 8.21.0
postgres: 3.4.9
+ valibot: 1.4.2(typescript@7.0.2)
dts-resolver@3.0.0: {}
@@ -6814,6 +6846,10 @@ snapshots:
uuid@14.0.1: {}
+ valibot@1.4.2(typescript@7.0.2):
+ optionalDependencies:
+ typescript: 7.0.2
+
verkit@0.3.0: {}
vfile-message@4.0.3:
diff --git a/skills-lock.json b/skills-lock.json
index 6555002..57df429 100644
--- a/skills-lock.json
+++ b/skills-lock.json
@@ -1,6 +1,12 @@
{
"version": 1,
"skills": {
+ "formisch": {
+ "source": "open-circle/agent-skills",
+ "sourceType": "github",
+ "skillPath": "skills/formisch/SKILL.md",
+ "computedHash": "087c39dfece92b6d7e340d47c842e65492a4ad4f8c787e942df4580226061c92"
+ },
"graphql-schema-design": {
"source": "ChilliCream/agent-skills",
"sourceType": "github",
@@ -18,6 +24,12 @@
"sourceType": "github",
"skillPath": "skills/relay-performance/SKILL.md",
"computedHash": "d60fb55230c107f2f32047b5ba7f8309fb32329eb3aa3316167ce7052e31c7a5"
+ },
+ "valibot": {
+ "source": "open-circle/agent-skills",
+ "sourceType": "github",
+ "skillPath": "skills/valibot/SKILL.md",
+ "computedHash": "dc7ad7dc09548808d26aef0bc58786a4ee3559e153a1e83c9c4574f3a294ea1b"
}
}
}