# easeful
Enter and exit animations for components that already have open and closed state.
One attribute, and no JavaScript ships to the browser.
```html
<Dialog.Content data-motion="scale-fade" />
```
That is the whole API. The library matches on the state attributes the host
component already writes, so there is nothing to wire up and no wrapper to add.
Four presets: `fade`, `scale-fade`, `slide-up`, `collapse`. Two attributes:
`data-motion` and `data-motion-state`. Three host families, 32 components: Radix UI
(15), Base UI (14), and the native `<dialog>`, `[popover]` and `[hidden]` elements
(3).
## Layout
A pnpm workspace with two members. The library has no dependencies at all, and the
split is what makes that visible: before it, the published dependency list and the
demo site's were the same file.
```
packages/easeful/ the library, published as `easeful`
motion.json the manifest, source of truth
motion.schema.json schema for the manifest
styles/ the layer, entry point at index.css
types/easeful.d.ts GENERATED, the data-motion augmentation
eslint/index.js the lint rule, exported as `easeful/eslint`
cli/ `easeful init`, `doctor` and `skill`, the bin
skill/SKILL.md GENERATED, the AGENTS.md body plus frontmatter
tests/ both suites, run by `pnpm test`, not published
AGENTS.md GENERATED, ships with the package
apps/docs/ the documentation site, Next.js 16
app/ routes. / redirects to /docs/overview, and the
generated ones are /llms.txt, /sitemap.xml,
/robots.txt, /search-index.json and /md/[...path]
components/ site chrome, plus one file per demo under demos/
components/registry.ts what each host writes and which rule matches it
content/ page prose, as markdown the site renders and serves
lib/ no React: manifest reader, markdown, nav, source reader
brand.json the mark's geometry, source of app/icon.svg
scripts/generate.mjs manifest and brand to their generated files
notes/ handoff context and roadmap. named notes, not docs,
because apps/docs is the site
AGENTS.md GENERATED, the same file the package ships
```
## Setup
Needs Node 20.9 or newer and pnpm.
```bash
pnpm install
pnpm dev
```
The docs site runs at http://localhost:3000.
Optional. For a "Source" link on every snippet, add `apps/docs/.env.local`:
```
NEXT_PUBLIC_REPO_URL=https://github.com/SanyamPunia/easeful
NEXT_PUBLIC_REPO_REF=main
```
Leave it out and the links are not rendered.
## Scripts
Run from the repository root.
| Command | Does |
| --- | --- |
| `pnpm dev` | Docs site in development |
| `pnpm build` | Runs `generate`, then builds the site |
| `pnpm generate` | Rewrites the generated files from `motion.json` and `brand.json` |
| `pnpm lint` | ESLint over the docs app, including the library's own rule |
| `pnpm test` | The lint rule and CLI tests, through `node --test` |
| `pnpm typecheck` | `tsc --noEmit` over the docs app |
| `pnpm check` | generate, lint, typecheck, test, build. This is the gate before pushing |
| `pnpm start` | Serves the built site |
## How it works
`packages/easeful/motion.json` is the single source of truth. `pnpm generate` reads
it and writes `styles/presets.css`, `types/easeful.d.ts` and `AGENTS.md`, and reads
`apps/docs/brand.json` to write `app/icon.svg`. Re-running it produces byte-identical
output, so a dirty tree afterwards means the manifest and its outputs have drifted.
Edit the manifest, never the generated files.
The manifest also declares the attribute surface, and the TypeScript augmentation is
generated from that list. An attribute cannot typecheck without a rule behind it.
`packages/easeful/cli` is the setup command. `easeful init` writes the layer order,
the import, the declaration file and the lint-rule registration, `easeful doctor`
checks all four and exits non-zero, and `easeful skill` copies the generated skill
into `.claude/skills/`. The skill is a separate command rather than part of init,
because writing into someone's Claude configuration is a different kind of change
from setting up a library. Both share one code path, so they cannot disagree
about what correct looks like. The flat-config edit is verified by loading the result
in a child process and keeping it only when the rule is really there.
`packages/easeful/eslint` reads the same manifest at lint time rather than being
generated from it, and it is what enforces the vocabulary. The types cannot:
TypeScript leaves a hyphenated JSX attribute unchecked, so `data-motion="nonsense"`
compiles. `pnpm test` covers the rule, and `pnpm lint` runs it over the docs app.
Two mechanisms sit behind the one attribute. Radix waits for `animationend`, so
keyframes drive it. Base UI, the native elements and plain `[hidden]` wait for
`transitionend`, so a transition drives them.
Everything sits in `@layer easeful` at deliberately low priority, so unlayered CSS in
a consuming app wins without needing `!important`.
## The docs site is the first consumer
`apps/docs` depends on `easeful` as a workspace package, imports the layer with
`import "easeful/styles.css"`, activates the types with `"easeful/types"`, and runs
`easeful/eslint` in its own flat config. Those are the same four lines the
installation page tells a consumer to write, so a broken exports map fails this
build rather than someone else's.
Every page is also served as markdown at its own path plus `.md`, indexed at
`/llms.txt`, generated from the same manifest, registry and page sources the HTML is.
Each page carries a `rel="alternate" type="text/markdown"` link to its own copy and a
"Copy page" control that hands a reader the same file.
`robots.txt`, `sitemap.xml`, the Open Graph cards, the iOS icon, the JSON-LD and the
search index are all generated too. The sitemap comes from the nav array, the cards
and the icons from `brand.json`, and the search index from the same markdown the
`.md` routes serve, so searching finds terms that only appear in a page body.
None of them are hand-maintained.
`notes/handoff.md` carries the full project context. `notes/roadmap.md` lists what is
not done yet.