skills/form-modernizer/SKILL.md
Modernize an existing form through multi-phase analysis, redesign, TypeScript typing, and visual verification. Use when: 'modernize this form', 'redesign this form', 'form audit', 'improve this form'.
npx skillsauth add ryan-mahoney/ryan-llm-skills form-modernizerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Compound, multi-step skill that fundamentally rethinks an existing React Final Form sidebar form — not surface-level polish, but structural redesign. Challenges every field's existence, mode visibility, and grouping. Produces a TypeScript contract, design-system-aligned implementation, and pixel-accurate Playwright screenshots for visual review.
The skill begins by creating an isolated worktree and opening it in a new VSCode window, so all modernization work happens on a dedicated branch without touching the main working tree.
Operating principle: Do the work autonomously. Do not pause for user approval at intermediate steps — proceed through all phases, take screenshots, review them, fix issues, and present the finished result.
formPath — path to the form component file (e.g., app/components/forms/OfferForm.js)--skip-screenshots — skip the Playwright visual verification phases (useful when Playwright is not set up)--edit-only — only produce analysis and recommendations without modifying codeformPath. If it does not exist, stop and tell the user.app/libraries/nodejs-manager/src/manager/SidebarSingleton.js to confirm it is wired into the sidebar system.OfferForm from OfferForm.js). Derive a kebab-case slug (e.g., offer-form). This slug is used for branch names, screenshot filenames, and contract file names throughout all phases.Create an isolated worktree so the modernization work happens on a dedicated branch.
Derive the branch name. Use the pattern modernize/{formNameKebab} (e.g., modernize/offer-form).
Extract the repository name from git remote get-url origin (last path segment, strip .git suffix).
Check for existing worktree/branch (enables re-entry after a crashed run):
~/.worktrees/<repo-name>/modernize-{formNameKebab} already exists, reuse it and skip to step 6.modernize/{formNameKebab} exists but no worktree, run:
git worktree add ~/.worktrees/<repo-name>/modernize-{formNameKebab} modernize/{formNameKebab}git fetch origin
mkdir -p ~/.worktrees/<repo-name>
git worktree add ~/.worktrees/<repo-name>/modernize-{formNameKebab} -b modernize/{formNameKebab} origin/main
# CRITICAL: unset upstream so `git push` doesn't push to main
git -C ~/.worktrees/<repo-name>/modernize-{formNameKebab} branch --unset-upstream
Record the absolute worktree path. All subsequent phases operate from this path. The formPath argument is relative to the repo root and remains valid in the worktree.
Copy environment files from the original repository root into the worktree:
cp .env ~/.worktrees/<repo-name>/modernize-{formNameKebab}/.env
If .env does not exist in the source repo, skip without failing.
Color-code the VSCode window. Use a consistent teal accent (#0d7377) for all form modernization worktrees. Write .vscode/settings.json in the worktree:
.vscode/settings.json exists, write:
{
"workbench.colorCustomizations": {
"titleBar.activeBackground": "#0d7377",
"titleBar.activeForeground": "#ffffff",
"statusBar.background": "#0d7377",
"statusBar.foreground": "#ffffff"
}
}
.vscode/settings.json already exists, merge via jq:
jq --arg bg "#0d7377" \
'.["workbench.colorCustomizations"] = {"titleBar.activeBackground": $bg, "titleBar.activeForeground": "#ffffff", "statusBar.background": $bg, "statusBar.foreground": "#ffffff"}' \
.vscode/settings.json > /tmp/vscode-settings-tmp.json && mv /tmp/vscode-settings-tmp.json .vscode/settings.json
jq is not available, write the file from scratch (color settings only).Write a continuation hook so the new VSCode window's Claude Code session automatically picks up from Phase 2.
Create <worktree-path>/.claude/hooks/continue.sh:
#!/bin/bash
cat <<'PROMPT'
# Continue Form Modernization — {FormName}
This worktree was created by the form-modernizer skill for `{formPath}`.
The branch is `modernize/{formNameKebab}`.
Pick up from Phase 2 of `~/.agents/skills/form-modernizer/SKILL.md`.
Before proceeding, load design rules:
- `~/.agents/rules/form-design.md`
- `~/.agents/rules/functionalist-design.md``
- `~/.agents/rules/cta-design.md`
- `docs/engineering-standards.md`
Then execute Phase 2 (Playwright setup if needed), Phase 3 (parallel analysis), Phase 4 (design decisions), Phase 5 (implementation), Phase 6 (visual verification), and Phase 7 (final verification) in order.
Arguments: {formPath} {any flags passed}
PROMPT
Make it executable: chmod +x <worktree-path>/.claude/hooks/continue.sh
Write <worktree-path>/.claude/settings.json (merge with existing if present):
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/continue.sh",
"timeout": 10
}
]
}
]
}
}
Open the worktree in a new VSCode window:
code --new-window ~/.worktrees/<repo-name>/modernize-{formNameKebab}
STOP. Report the worktree path, branch name, and form being modernized to the user. The new VSCode window's Claude Code session will receive the continuation prompt via the SessionStart hook — the user just needs to type "go".
Skip this phase if --skip-screenshots is passed or if app/test/screenshots/harness/serve.js already exists.
Do NOT screenshot the live app. The form harness renders components in isolation — no auth, no backend, pixel-accurate CSS via the project's real Tailwind build.
bun add -d @playwright/test
bunx playwright install chromium
Write playwright.screenshot.config.js at the repo root:
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./app/test/screenshots",
testMatch: "*.screenshot.js",
use: {
baseURL: "http://localhost:3333",
screenshot: "only-on-failure",
},
projects: [
{
name: "form-screenshots",
use: { browserName: "chromium", viewport: { width: 1440, height: 900 } },
},
],
outputDir: "./tmp/form-screenshots",
});
The harness has four files in app/test/screenshots/harness/:
harness.css — Tailwind entry point using the project's real config:
@import "tailwindcss";
@config "../../../../tailwind.config.js";
@plugin "@tailwindcss/forms";
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
mock-api.js — Stub API functions with realistic mock data. Export every function the form imports from app/components/api.js. Return plausible payloads so the form renders fully populated.
entry.jsx — React entry point that:
StateContext from app/store and provides mock values (pageRefresher: async () => {}, etc.)<Dialog.Root open={true}> + <Dialog.Portal> + <Dialog.Content> (required by Radix Dialog.Title in FormSidebarHeader)?mode=new or ?mode=edit from the URL to switch between add/edit mode with sample dataserve.js — Bun script that:
entry.jsx via Bun.build() with external: ["html2canvas"]bunx @tailwindcss/cli -i harness.css -o tmp/form-harness/harness.cssimportmap to stub html2canvas in the browserHARNESS_PORT)tmp/ is already in .gitignore. Verify with: grep -q "^tmp/" .gitignore.
Launch three sub-agents in parallel. Each agent receives the form file contents and design rules.
Prompt pattern:
Read
{formPath}. Analyze:
- Who uses this form? Identify the user persona (admin, hiring manager, recruiter, candidate, etc.) based on the route it appears on and the data it collects.
- What is the form's goal? Single sentence: what outcome does submitting this form produce?
- Field inventory. List every
<Field>or input, noting: field name, current component type, current label, whether it has validation, current order.- Add vs Edit behavior. Does the form handle both create and edit? Check for
initialValuesand conditional rendering based onidpresence.- Current validation rules. Extract the
validationRulesorrequiredFieldsobject.- Current initialValues logic. How are defaults set?
Also read any context file that prepares data for this form (check the controller/context that serves the page containing this sidebar).
Output a structured analysis document. Do not modify any files.
Prompt pattern:
Read
{formPath}and identify the API function it calls on submit (the second argument toonSubmitHelper).
- Find that function in
app/components/api.js— note the HTTP method and endpoint path.- Find the backend route for that endpoint in
app/routes/backend-routes.js.- Find the controller handler. Read the controller to understand what fields it expects, validates, and passes to the context.
- Find the context function. Read it to understand the database model and which fields are persisted.
- Find the model. Read the relevant model file to understand the database column names and types.
Produce:
- A list of all fields the API accepts (with types inferred from usage)
- Which fields are required server-side
- Any fields the form does NOT currently expose but the API supports
- Any fields the form exposes that the API ignores
Do not modify any files.
Prompt pattern:
Read
{formPath}and the design rules in~/.agents/rules/form-design.md,~/.agents/rules/functionalist-design.md, and~/.agents/rules/cta-design.md.Also read the design system reference at
docs/design/design-system.md. For form work, the key files are:
docs/design/system/patterns/form-drawer.html— canonical form drawer layout, button styles, footer alignmentdocs/design/system/patterns/form-controls.html— input styling, label typography, error statesdocs/design/system/patterns/accordion.html— accordion button + panel border patterndocs/design/system/color.md— color roles (brand-600 for primary actions, gray-400 for borders, etc.)docs/design/system/typography.md— label sizes (text-sm font-medium text-gray-700), heading weightsAudit the form AND the shared library components it uses:
- Accessibility:
- Every input has a programmatic label (via
<label>oraria-label)- Focus management: does the sidebar use
DelayedFocusTrap?- Tab order follows visual order
- Error messages are associated with fields via
aria-describedbyor equivalent- Form works without mouse (keyboard-operable)
- Required fields are communicated to assistive tech
- Design system alignment (check both the form AND the shared components it uses):
- Read
FormSidebarHeader,FormSidebarFooter,FieldWrapper,AccordionPanelsource code- Compare their Tailwind classes against the design system patterns (form-drawer.html, form-controls.html, accordion.html)
- Check: primary button color (should be
bg-brand-600notbg-indigo-600), footer layout (justify-end gap-3), label size (text-sm text-gray-700), input borders (border-gray-400), focus rings (ring-brand-100), accordion borders (border-gray-400with connected panel)- Button labels follow CTA guide (Verb + Noun, sentence case, 1-3 words)
- Flag misalignments in shared components — these are fixable in the modernization branch
- Inline help text opportunities:
- Fields where the label alone is ambiguous
- Fields with non-obvious format expectations
- Fields where a wrong choice has significant consequences
- Avoid help text on fields in side-by-side grids (causes vertical misalignment)
Produce a grouped checklist of findings, split into form-level and shared-component-level issues. Do not modify any files.
After all Phase 3 agents complete, synthesize their outputs into a modernization plan. Do not wait for approval — proceed to implementation.
The goal is a fundamental rethink, not surface polish. Question every field:
| Decision | Criteria |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Remove field | API ignores it, or it duplicates another field |
| Add mode: show or hide? | Add mode should collect the minimum needed to create the entity. All other fields belong in edit mode only. If only one field is required, add mode may be just that field + a save button. |
| Required | Server requires it, OR leaving it blank produces a broken record |
| Order | Group related fields; put the most important / identifying field first; put optional fields last |
| Inline help | Add a note prop to FieldWrapper where Sub-Agent C identified ambiguity. Do NOT add notes that duplicate section headings or are obvious from the label. |
AccordionPanel groups — they are hidden entirely in add modegrid grid-cols-2 gap-x-4rounded-t-lg when open, panel border border-gray-400 border-t-0 rounded-b-lg p-4padding="" on FieldWrapper; the panel's p-4 and flex flex-col gap-4 handle spacingIf Sub-Agent C identified misalignments in shared library components (FormSidebarFooter, FormSidebarHeader, FieldWrapper, AccordionPanel, inputTextClasses), include those fixes in the plan. These are app-wide improvements that happen to be caught during form modernization.
Output the modernization plan as a markdown table for reference:
| # | Field | Label | Type | Required | Group | Add/Edit/Both | Help Text | Change |
Proceed immediately after Phase 4.
Prompt pattern:
Create a TypeScript contract file for the
{FormName}form following the pattern inapp/components/apps/sales-admin/discount-codes/discount-code-form.contract.ts.Based on the approved field plan and API analysis, produce:
{FormName}FormValuesinterface — all fields the form UI works with. UI-only fields (like toggle states) use?optional. Fields from the server use their DB types.{FormName}Changesetinterface — the shape sent to the API. Required fields are non-optional. Strips any UI-only fields.normalize{FormName}InitialValuesfunction — normalizes server data for edit mode (e.g., converting arrays to display strings). Critical: do NOT add fields that aren't present in the input. Adding fields to initialValues that aren't rendered in the form causessanitizeEmptyValuesinonSubmitHelperto sendnullvalues to the API, overriding server defaults.to{FormName}Changesetfunction — transforms FormValues into Changeset, with assertion for required fields.- Validation helpers (if the form has conditional validation like the discount code form's type selection).
Write the contract file adjacent to the form component:
{formDir}/{formNameKebab}.contract.tsWrite the contract test in the mirror location:app/test/{mirrorPath}/{formNameKebab}.contract.test.jsFollow
docs/engineering-standards.md: fail fast, explicit assertions, simple over clever.
Prompt pattern:
Modernize
{formPath}according to the approved plan.Field changes: {Insert the approved field plan table here}
Implementation rules:
- Use React Final Form
<Field>components with inputs fromapp/libraries/nodejs-manager/src/final-form/- Wrap every field in
FieldWrapperwith:fieldName,labelText,required(boolean),note(for help text),stacked={true}(sidebar forms are always stacked)- Use
FormSidebarHeaderwith the entity name andFormSidebarFooterwith the save/update pattern- Import and call
normalizeInitialValuesfrom the new contract file forinitialValues- Import and call
toChangesetfrom the contract file inside the submit handler, before calling the API- For collapsible groups, wrap in
AccordionPanelfromapp/components/common/AccordionPanel.js- For add-only vs edit-only fields, conditionally render based on
!!initialValues?.id- Button labels must follow CTA guide: "Save {entity}" for create, "Update {entity}" for edit
- Validation rules must match the contract's required fields
- Use
DelayedFocusTrapwrapper for accessibilityDesign system rules:
- Single-column layout
- Tailwind design tokens only (brand, teal, isabel from tailwind.config.js)
- No decoration, shadows, or rounded corners that don't serve function
- High contrast (WCAG AA minimum)
Do NOT change the form's API function, sidebar registration, or external interface (props).
Prompt pattern:
Review the modernized form (after Sub-Agent E completes) and fix any remaining accessibility issues:
- Ensure
DelayedFocusTrapwraps the sidebar content- Ensure
FormErrorscomponent is present and renders above form fields- Ensure every
<Field>has an associated<label>viaFieldWrapper- Ensure required fields have
aria-required="true"on the input- Ensure error messages use
aria-describedbylinkage- Ensure the form has
role="form"and an accessible name- Test tab order matches visual order (top to bottom, left to right)
- Ensure
FormSidebarHeaderusesuseDialogTitle={true}for screen reader announcementMake minimal, targeted fixes. Do not restructure the form.
Note: Sub-Agent F depends on Sub-Agent E completing first. Run D and E in parallel, then F after E.
Skip this phase if --skip-screenshots is passed.
Screenshots use the form harness (Phase 2), not the live app. No auth, no backend dependency — pixel-accurate CSS from the project's real Tailwind build.
Create app/test/screenshots/{formNameKebab}.screenshot.js with tests for each mode:
import { test } from "@playwright/test";
import { join } from "path";
const SCREENSHOT_DIR = join(process.cwd(), "tmp", "form-screenshots");
const HARNESS_URL = "http://localhost:3333";
test("capture {FormName} — new mode", async ({ page }) => {
await page.goto(`${HARNESS_URL}?mode=new`);
await page.waitForSelector('[role="dialog"]', { timeout: 5000 });
await page.waitForTimeout(1000);
const dialog = page.locator('[role="dialog"]');
await dialog.screenshot({
path: join(SCREENSHOT_DIR, "{formNameKebab}-new.png"),
});
});
test("capture {FormName} — edit mode", async ({ page }) => {
await page.goto(`${HARNESS_URL}?mode=edit`);
await page.waitForSelector('[role="dialog"]', { timeout: 5000 });
await page.waitForTimeout(1000);
const dialog = page.locator('[role="dialog"]');
await dialog.screenshot({
path: join(SCREENSHOT_DIR, "{formNameKebab}-edit.png"),
});
});
test("capture {FormName} — edit mode expanded", async ({ page }) => {
await page.goto(`${HARNESS_URL}?mode=edit`);
await page.waitForSelector('[role="dialog"]', { timeout: 5000 });
await page.waitForTimeout(1000);
// Expand all accordion sections
const buttons = page.locator('[role="dialog"] button[data-headlessui-state]');
const count = await buttons.count();
for (let i = 0; i < count; i++) {
await buttons.nth(i).click();
await page.waitForTimeout(200);
}
await page.waitForTimeout(500);
// Remove fixed height to capture full content
await page.evaluate(() => {
const dialog = document.querySelector('[role="dialog"]');
dialog.style.position = "static";
dialog.style.height = "auto";
dialog.style.overflow = "visible";
});
await page.waitForTimeout(300);
const dialog = page.locator('[role="dialog"]');
await dialog.screenshot({
path: join(SCREENSHOT_DIR, "{formNameKebab}-edit-expanded.png"),
});
});
bun app/test/screenshots/harness/serve.js &bunx playwright test --config playwright.screenshot.config.jsChecklist:
note causing offset)?gap-3 between Cancel and primary button?brand-600, not indigo-600?text-sm text-gray-700?gray-400, no shadows?text-sm text-gray-500?Run the contract tests to verify the TypeScript types:
bun test app/test/{mirrorPath}/{formNameKebab}.contract.test.js
bunx eslint {formPath} {contractPath}
Present to the user:
tmp/form-screenshots/onSubmitHelper(rules, apiFunction, callback, initialValues) from app/libraries/nodejs-manager/src/final-form/utilities.js. Be aware that sanitizeEmptyValues iterates over keys in initialValues — any key present in initialValues but absent from form values becomes null in the API payload.{name}.contract.ts. Tests mirror in app/test/.app/components/apps/sales-admin/discount-codes/discount-code-form.contract.ts exactly: FormValues interface, Changeset interface, normalize* function, to*Changeset function.null by sanitizeEmptyValues on submit.tmp/form-screenshots/. Never commit screenshots.app/test/screenshots/harness/. Uses Bun.build() for JS + @tailwindcss/cli for CSS. Renders forms in a Radix Dialog wrapper with mock StateContext. No auth, no backend.tailwind.config.js (brand, teal, isabel). No arbitrary hex values.note prop on FieldWrapper, not tooltips or placeholder text. Do not add notes that duplicate section headings. Avoid notes on fields in side-by-side grids (causes vertical misalignment).AccordionPanel from app/components/common/AccordionPanel.js. Expanded panels get className="border border-gray-400 border-t-0 rounded-b-lg p-4 flex flex-col gap-4" on DisclosurePanel. Fields inside use padding="" on FieldWrapper.{id !== "new" && (...)}.~/.worktrees/<repo-name>/modernize-{formNameKebab}. Never modify the main working tree after Phase 1.modernize/{formNameKebab} (e.g., modernize/offer-form).FieldWrapper, FormSidebarHeader/Footer, AccordionPanel, DelayedFocusTrap.bun:test.testing
This skill should be used when the user asks to "run the spec", "implement the spec", or "execute the spec". Implements every step in a SpecOps implementation spec by delegating each step (or logical group of adjacent steps) to a sequential subagent, conventional-committing each one independently, and — when `roborev` is on the path — running `roborev check` on every commit and `roborev fix` (with spec context, so the fix cannot silently drift the implementation away from the spec) on any commit that fails.
development
Exhaustively audit a top-level UI implementation component against an HTML prototype and produce a grouped markdown checklist of corrections. Use when a user asks for UI parity review, visual QA, design implementation audit, pixel-level drift detection, or behavior/style mismatch analysis between prototype HTML and shipped component code.
development
Audit a SpecOps implementation spec against its source analysis spec to find requirements, policies, contracts, edge cases, error modes, invariants, defaults, side effects, or implementation steps that the implementation has dropped, weakened, contradicted, or silently changed — then patch the implementation spec to restore them. Use this skill whenever the user mentions auditing, comparing, conforming, reconciling, or checking an implementation spec against an analysis spec, finding gaps between two specs, ensuring an implementation spec preserves analysis behavior, or verifying spec derivation or traceability. Also trigger when the user describes "did the implementation spec lose anything from the analysis," "does the implementation match the analysis," "verify the implementation spec covers everything," or asks to confirm one spec is faithful to another. Run this before generating code from an implementation spec and after either spec is edited.
development
Audit a set of SpecOps analysis specs for cross-spec coherence — establish a dependency-ordered implementation sequence, then verify pairwise integration contracts at module boundaries plus three cross-cutting consistency dimensions (shared data models, side-effect ownership, terminology) — and patch the affected specs to resolve gaps. Use this skill whenever the user mentions cross-spec consistency, integration gaps between specs, conflicts between specs, duplicate work across specs, implementation order, dependency order for migration, building an implementation-order checklist, ensuring specs interoperate, terminology drift across specs, or shared data model conflicts. Also trigger when the user describes "do my specs agree with each other," "what order should I implement these in," "find inconsistencies across all my specs," or asks to audit a folder of analysis specs as a set rather than individually. Run this once after generating a full set of analysis specs, before deriving implementation specs.