Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

* `setOptions(List<V>)` on `AdvancedSingleSelectFieldController` and `AdvancedMultiSelectFieldController`: the list of options can change while the form is open. A selected value that is not on the new list is cleared as a program write (no user edit, no error on an untouched field); widgets rebuild even when the value stayed the same.
* The single-select controller now copies the `options` you pass, as the multi-select always did. `options` is an unmodifiable list on both.
* The Agent Skill is a [package skill](https://dart.dev/tools/pub/package-skills): `dart run skills@ get` installs it into your agent's skills folder. The folder moved from `skills/advanced_forms/` to `skills/advanced_forms-build-forms/`, the name the CLI requires; the skill now adds the package with `flutter pub add advanced_forms` when the project lacks it.

## 0.2.1+1

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@
- **Granular rebuilds** — one builder per field, so a keystroke rebuilds one subtree and nothing else.

> [!TIP]
> **Using Claude Code (or another agent that supports Agent Skills)?** This package ships an [Agent Skill](https://advanced-forms.leancode.co/docs/agent-skill) that teaches your agent the full API, so it generates fields, validation, cross-field logic, and subforms idiomatically. Installing it is one command:
> **Using Claude Code (or another agent that supports Agent Skills)?** This package ships an [Agent Skill](https://advanced-forms.leancode.co/docs/agent-skill) that teaches your agent the full API, so it generates fields, validation, cross-field logic, and subforms idiomatically. With `advanced_forms` in your `pubspec.yaml`, one command installs it into your agent's skills folder:
>
> ```sh
> mkdir -p .claude/skills/advanced_forms && curl -fsSL https://advanced-forms.leancode.co/skill.md -o .claude/skills/advanced_forms/SKILL.md
> dart run skills@ get
> ```
>
> Or copy the file from [the docs](https://advanced-forms.leancode.co/docs/agent-skill) and paste it into `.claude/skills/advanced_forms/SKILL.md`. Then say *"build me a sign-up form"* — the agent adds the package if it is missing and writes the form.
> Not using the package yet? Copy the file from [the docs](https://advanced-forms.leancode.co/docs/agent-skill) into `.claude/skills/advanced_forms-build-forms/SKILL.md`, then say *"build me a sign-up form"* — the agent adds the package and writes the form.

## Website: interactive examples and docs

Expand Down Expand Up @@ -150,7 +150,7 @@ Own the controller wherever you like — it's a `ChangeNotifier`, so any DI pack
- [MIGRATION.md](https://github.com/leancodepl/advanced_forms/blob/main/MIGRATION.md) — coming from 0.1.x.
- `example/` — a runnable app where every pattern in these docs has a working screen. See [example/example.md](https://github.com/leancodepl/advanced_forms/blob/main/example/example.md) for the screen guide.
- [API reference](https://pub.dev/documentation/advanced_forms/latest/) — the generated dartdoc, for every member and its edge cases.
- [Agent Skill](https://advanced-forms.leancode.co/docs/agent-skill) — `skills/advanced_forms/SKILL.md`, a skill that teaches your agent the full API, so it generates fields, validation, cross-field logic, and subforms idiomatically. One `curl` or one copy-paste puts it into your project's `.claude/skills/`.
- [Agent Skill](https://advanced-forms.leancode.co/docs/agent-skill) — `skills/advanced_forms-build-forms/SKILL.md`, a skill that teaches your agent the full API, so it generates fields, validation, cross-field logic, and subforms idiomatically. `dart run skills@ get` installs it into your project's agent skills folder.

## Rendering fields

Expand Down
78 changes: 41 additions & 37 deletions docs/agent-skill.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ The package ships an [Agent Skill](https://agentskills.io): a single Markdown fi
a task looks like form work, so it generates fields, validation, cross-field logic and subforms the way these docs
describe them rather than the way a generic Flutter tutorial would.

Copy the file into your project and say *"build me a sign-up form"*. The agent adds the package to `pubspec.yaml` if
it is not there yet, writes the controller and the widgets, and wires the validation — you do not open the docs.
Copy the file below into your project, or install it with one command — then say *"build me a sign-up form"*. The
agent adds the package to `pubspec.yaml` if it is not there yet, writes the controller and the widgets, and wires the
validation — you do not open the docs.

<SkillCopy />

Expand All @@ -25,69 +26,72 @@ it is not there yet, writes the controller and the widgets, and wires the valida
hardened by having agents generate real forms from it alone, compiling those forms, and feeding every defect back.
</Callout>

## Install it
<SkillBanner>Get this skill with one command line</SkillBanner>

The skill ships inside the package, so Dart's [skills CLI](https://dart.dev/tools/pub/package-skills) can install it —
and every other skill your dependencies carry — into the folder your agent reads.

<Steps>
<Step>

### Create the file

<Tabs items={["Copy and paste", "One command"]}>
<Tab value="Copy and paste">
### Have the package in your project

Press **Copy SKILL.md** above, create this file in your project and paste:
The CLI reads `pubspec.yaml`, so `advanced_forms` has to be a dependency first:

```text
.claude/skills/advanced_forms/SKILL.md
```sh
flutter pub add advanced_forms
```

Commit it, and every contributor's agent picks it up. To have it in every project you work on instead, put the same
file under `~/.claude/skills/advanced_forms/SKILL.md`.

</Tab>
<Tab value="One command">
</Step>
<Step>

The file is served raw at [`/skill.md`](/skill.md), so the terminal can fetch it into place:
### Run the skills CLI

```sh title="Into the project"
mkdir -p .claude/skills/advanced_forms && curl -fsSL https://advanced-forms.leancode.co/skill.md -o .claude/skills/advanced_forms/SKILL.md
```sh
dart run skills@ get
```

```sh title="For every project you work on"
mkdir -p ~/.claude/skills/advanced_forms && curl -fsSL https://advanced-forms.leancode.co/skill.md -o ~/.claude/skills/advanced_forms/SKILL.md
```
`skills@` runs the CLI straight from pub.dev — nothing to install, nothing added to your pubspec. It detects the agent
from the project (`.claude/skills/` for Claude Code, `.cursor/skills/`, `.github/skills/` for Copilot, `.agents/skills/`
for the rest; `--agent claude` to force one) and asks which skills to install; `--all` skips the question.

Run it again whenever you upgrade the package: the URL always serves the skill for the current release.
</Step>
<Step>

</Tab>
</Tabs>
### Check it is loaded

`.claude/skills/` is where Claude Code looks. Other agents that support Agent Skills read the same file from their own
skills folder — check the agent's docs for the path.
`dart run skills@ list` shows what the CLI manages. In Claude Code, `/skills` lists the skills in scope. The skill's
description mentions forms, fields, validation and subforms, so it triggers on a request like *"add a shipping address
section to the checkout"* even when the word "form" never appears.

</Step>
<Step>

### Check it is loaded
### Ask for a form

In Claude Code, `/skills` lists the skills in scope. The skill's description mentions forms, fields, validation and
subforms, so it triggers on a request like *"add a shipping address section to the checkout"* even when the word
"form" never appears.
*"Build me a sign-up form with email, password and terms."* That is the whole prompt. The skill is enough on its own:
the agent does not need the docs, the README or the package source in context. It knows the two rules that prevent
most bugs — call `registerFields` once with every field, and bind text widgets to `field.textController` — and it knows
the API surface well enough not to invent parameters.

</Step>
<Step>

### Ask for a form
### Keep it current

*"Build me a sign-up form with email, password and terms."* That is the whole prompt: if `advanced_forms` is not in
`pubspec.yaml` yet, the agent runs `flutter pub add advanced_forms` first, then writes the controller, the widgets and
the validation. The skill is enough on its own: the agent does not need the docs, the README or the package source in
context. It knows the two rules that prevent most bugs — call `registerFields` once with every field, and bind text widgets to
`field.textController` — and it knows the API surface well enough not to invent parameters.
After a package upgrade, run `dart run skills@ get` again: it offers to update the skill to the one the new version
ships. `dart run skills@ prune` removes skills of dependencies you dropped.

</Step>
</Steps>

<Callout title="No CLI? No problem.">
The file is the skill. Paste what **Copy SKILL.md** gives you into `.claude/skills/advanced_forms-build-forms/SKILL.md`
(or `~/.claude/skills/…` for every project), or fetch it raw:
`curl -fsSL https://advanced-forms.leancode.co/skill.md -o .claude/skills/advanced_forms-build-forms/SKILL.md`. Other agents
read the same file from their own skills folder.
</Callout>

## What it teaches

- Setup: if `pubspec.yaml` does not list `advanced_forms`, the agent runs `flutter pub add advanced_forms` before
Expand All @@ -108,7 +112,7 @@ context. It knows the two rules that prevent most bugs — call `registerFields`
## The file

This is the skill, whole — the same bytes the copy button and `/skill.md` give you. It lives in the repository at
[`skills/advanced_forms/SKILL.md`](https://github.com/leancodepl/advanced_forms/blob/main/skills/advanced_forms/SKILL.md)
[`skills/advanced_forms-build-forms/SKILL.md`](https://github.com/leancodepl/advanced_forms/blob/main/skills/advanced_forms-build-forms/SKILL.md)
and this page is built from it, so what you read here is what ships.

<SkillSource />
Expand Down
4 changes: 2 additions & 2 deletions docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ Why the package makes the choices it makes — no widgets, no `Form`, no codegen
[Design decisions](./internals/decisions.mdx).

<Callout title="Using an agent that supports Agent Skills?">
The repository ships an [Agent Skill](./agent-skill.mdx) that teaches your agent the full API, so it generates fields,
validation, cross-field logic and subforms idiomatically.
The package ships an [Agent Skill](./agent-skill.mdx) that teaches your agent the full API, so it generates fields,
validation, cross-field logic and subforms idiomatically. `dart run skills@ get` installs it.
</Callout>

`advanced_forms` is maintained by [LeanCode](https://leancode.co/?utm_source=advanced-forms-docs&utm_medium=referral&utm_campaign=advanced-forms)
Expand Down
2 changes: 1 addition & 1 deletion docs/installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ None are required. Two are worth knowing about:
disposed for you. The example app uses it; a `StatefulWidget` works just as well. See
[Lifecycle and ownership](./lifecycle/index.mdx).
- **An [Agent Skill](./agent-skill.mdx)** — if you write forms with Claude Code or another agent that supports
skills, one copy-paste (or one `curl`) puts `SKILL.md` into your project and the agent generates idiomatic
skills, `dart run skills@ get` installs it into your agent's skills folder and the agent generates idiomatic
`advanced_forms` code.

## Coming from `leancode_forms` 0.1.x
Expand Down
3 changes: 2 additions & 1 deletion docs_app/components/mdx.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { TypeTable } from "fumadocs-ui/components/type-table"
import type { MDXComponents } from "mdx/types"
import type { ImgHTMLAttributes } from "react"
import { AdvancedFormsExample } from "./advanced-forms-example"
import { SkillCopy, SkillSource } from "./skill-install"
import { SkillBanner, SkillCopy, SkillSource } from "./skill-install"
import { cn } from "@/lib/cn"

function toPixel(value: ImgHTMLAttributes<HTMLImageElement>["width"]): number | undefined {
Expand All @@ -37,6 +37,7 @@ export function getMDXComponents(components?: MDXComponents) {
Tabs,
TypeTable,
AdvancedFormsExample,
SkillBanner,
SkillCopy,
SkillSource,
...components,
Expand Down
22 changes: 21 additions & 1 deletion docs_app/components/skill-install.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
* harness: Claude Code
*/
import { CodeBlock, Pre } from "fumadocs-ui/components/codeblock"
import { ExternalLink, FileText } from "lucide-react"
import { ExternalLink, FileText, Terminal } from "lucide-react"
import type { ReactNode } from "react"
import { CopySkillButton } from "./copy-skill-button"
import { readSkill } from "@/lib/skill"
import { skillRoute, urls } from "@/lib/shared"
Expand Down Expand Up @@ -61,3 +62,22 @@ export function SkillSource() {
</CodeBlock>
)
}

/**
* The title over the install steps. A quiet accent — a soft tint and a rule
* in the accent colour — so the one-command path reads as the main road
* without shouting over the copy card above it.
*/
export function SkillBanner({ children }: { children: ReactNode }) {
return (
<div
className="not-prose text-fd-foreground mt-12 mb-6 flex items-center gap-4 rounded-r-xl px-5 py-4"
style={{ background: "var(--af-accent-soft)", borderLeft: "4px solid var(--af-accent)" }}>
<Terminal className="size-6 shrink-0" style={{ color: "var(--af-accent-text)" }} aria-hidden />
{/* The docs' heading rules add a top margin and padding to every h2; this one sits in a box. */}
<h2 className="text-xl font-semibold tracking-tight sm:text-2xl" style={{ margin: 0, padding: 0 }}>
{children}
</h2>
</div>
)
}
4 changes: 2 additions & 2 deletions docs_app/lib/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const docsContentRoute = "/llms.mdx"
/** The Agent Skill, served raw so one `curl` (or an agent) can fetch it. */
export const skillRoute = "/skill.md"
/** Where the skill goes in a project; `~/` in front makes it global. */
export const skillInstallPath = ".claude/skills/advanced_forms/SKILL.md"
export const skillInstallPath = ".claude/skills/advanced_forms-build-forms/SKILL.md"

export const gitConfig = {
user: "leancodepl",
Expand All @@ -46,7 +46,7 @@ export const urls = {
changelog: `${repo}/blob/${gitConfig.branch}/CHANGELOG.md`,
migration: `${repo}/blob/${gitConfig.branch}/MIGRATION.md`,
license: `${repo}/blob/${gitConfig.branch}/LICENSE`,
skill: `${repo}/blob/${gitConfig.branch}/skills/advanced_forms/SKILL.md`,
skill: `${repo}/blob/${gitConfig.branch}/skills/advanced_forms-build-forms/SKILL.md`,
exampleApp: `${repo}/tree/${gitConfig.branch}/example`,
exampleGuide: `${repo}/blob/${gitConfig.branch}/example/example.md`,
exampleWidgets: `${repo}/tree/${gitConfig.branch}/example/lib/widgets`,
Expand Down
4 changes: 2 additions & 2 deletions docs_app/lib/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { readFileSync, statSync } from "node:fs"
import path from "node:path"

/**
* The Agent Skill is one Markdown file in the repo root (`skills/advanced_forms/`),
* The Agent Skill is one Markdown file in the repo root (`skills/advanced_forms-build-forms/`),
* the same one the package ships. Read at build time, like `../docs`, so the
* site can never serve a stale copy of it.
*/
const skillFile = path.join(process.cwd(), "..", "skills", "advanced_forms", "SKILL.md")
const skillFile = path.join(process.cwd(), "..", "skills", "advanced_forms-build-forms", "SKILL.md")

export interface Skill {
/** The file, verbatim. */
Expand Down
4 changes: 3 additions & 1 deletion landing/lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ class App extends StatelessComponent {
classes: 'af-landing',
children: [
Hero(version: version, demo: loadExample('hero')),
// Right under the fold: the fastest way to a working form comes
// before the explanation of how forms work.
const SkillBand(),
ModelSection(example: loadExample('signup')),
ValidationSection(example: loadExample('modes')),
AsyncSection(example: loadExample('async')),
const Features(),
const SkillBand(),
],
),
SiteFooter(version: version),
Expand Down
25 changes: 2 additions & 23 deletions landing/lib/components/hero.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:advanced_forms_landing/components/button.dart';
import 'package:advanced_forms_landing/components/example_frame.dart';
import 'package:advanced_forms_landing/components/icons.dart';
import 'package:advanced_forms_landing/components/install_command.dart';
import 'package:advanced_forms_landing/components/pill.dart';
import 'package:advanced_forms_landing/components/section.dart';
import 'package:advanced_forms_landing/examples.dart';
Expand Down Expand Up @@ -51,28 +51,7 @@ class Hero extends StatelessComponent {
.text(' your app already uses.'),
]),
div(classes: 'af-install', [
div(classes: 'af-install-command', [
const span(
classes: 'af-prompt',
attributes: {'aria-hidden': 'true'},
[.text(r'$')],
),
const code([.text(installCommand)]),
button(
classes: 'af-copy-button',
attributes: const {
'type': 'button',
'data-copy-text': installCommand,
'aria-label': 'Copy to clipboard',
'aria-live': 'polite',
},
[
span(classes: 'af-copy-idle', [Icon.copy.build(size: 15)]),
span(classes: 'af-copy-done', [Icon.check.build(size: 15)]),
const span(classes: 'af-copy-label', [.text('Copy')]),
],
),
]),
const InstallCommand(installCommand),
p(classes: 'af-install-alt', [
const .text('Coming from '),
const code([.text('leancode_forms')]),
Expand Down
40 changes: 40 additions & 0 deletions landing/lib/components/install_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'package:advanced_forms_landing/components/icons.dart';
import 'package:jaspr/dom.dart';
import 'package:jaspr/jaspr.dart';

/// A shell command in a box with a copy button: `flutter pub add …` in the
/// hero, `dart run skills@ get` in the skill band. The button is wired by
/// `[data-copy-text]` in web/landing.js.
///
/// Styled by `.af-install-command*` in web/landing.css.
class InstallCommand extends StatelessComponent {
const InstallCommand(this.command, {super.key});

final String command;

@override
Component build(BuildContext context) {
return div(classes: 'af-install-command', [
const span(
classes: 'af-prompt',
attributes: {'aria-hidden': 'true'},
[.text(r'$')],
),
code([.text(command)]),
button(
classes: 'af-copy-button',
attributes: {
'type': 'button',
'data-copy-text': command,
'aria-label': 'Copy to clipboard',
'aria-live': 'polite',
},
[
span(classes: 'af-copy-idle', [Icon.copy.build(size: 15)]),
span(classes: 'af-copy-done', [Icon.check.build(size: 15)]),
const span(classes: 'af-copy-label', [.text('Copy')]),
],
),
]);
}
}
Loading
Loading