From 996cbdb6106b1a9e751ab21e8c305105a4e9e8d1 Mon Sep 17 00:00:00 2001 From: Samir Date: Thu, 6 Aug 2026 18:15:41 +0530 Subject: [PATCH] docs: add llms.txt support for AI agents Adds plain-markdown docs so LLMs and coding agents can read the API without scraping the site, following the llms.txt convention. - packages/calligraph/llms.txt: short index (what it is, install, prop names, links) - packages/calligraph/llms-full.txt: full docs (every prop with defaults, all three variants, runnable examples, constraints, common mistakes) - Both files ship in the npm package, so agents can read them from node_modules - Site serves them at /llms.txt, /llms-full.txt and /index.md, read at build time - Home page gets an "AI & agents" section listing the URLs - link rel="alternate" type="text/markdown" points at /index.md - AGENTS.md rewritten: the old version described a single-file package and internals that have since moved - README: replaced the "Custom transitions" example, since the transition prop no longer exists, with the variants and animation presets that do --- .changeset/quiet-jars-shout.md | 5 + AGENTS.md | 67 ++++++--- README.md | 23 ++- apps/web/app/index.md/route.ts | 9 ++ apps/web/app/layout.tsx | 1 + apps/web/app/llms-full.txt/route.ts | 9 ++ apps/web/app/llms.txt/route.ts | 9 ++ apps/web/app/page.tsx | 59 ++++++++ apps/web/app/styles.module.css | 21 +++ apps/web/lib/docs.ts | 10 ++ packages/calligraph/llms-full.txt | 220 ++++++++++++++++++++++++++++ packages/calligraph/llms.txt | 29 ++++ packages/calligraph/package.json | 4 +- 13 files changed, 440 insertions(+), 26 deletions(-) create mode 100644 .changeset/quiet-jars-shout.md create mode 100644 apps/web/app/index.md/route.ts create mode 100644 apps/web/app/llms-full.txt/route.ts create mode 100644 apps/web/app/llms.txt/route.ts create mode 100644 apps/web/lib/docs.ts create mode 100644 packages/calligraph/llms-full.txt create mode 100644 packages/calligraph/llms.txt diff --git a/.changeset/quiet-jars-shout.md b/.changeset/quiet-jars-shout.md new file mode 100644 index 0000000..dd4b1e9 --- /dev/null +++ b/.changeset/quiet-jars-shout.md @@ -0,0 +1,5 @@ +--- +"calligraph": patch +--- + +docs: ship llms.txt and llms-full.txt inside the package for AI agents diff --git a/AGENTS.md b/AGENTS.md index 7f3ce7f..e7af4d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,38 +1,59 @@ -# Calligraph — Agent Guidelines +# AGENTS.md -Rules for AI agents working on this package. +Guidelines for AI coding agents working in this repository. -## Architecture +Using Calligraph in *your own* project instead of contributing to it? +Read [`packages/calligraph/llms-full.txt`](packages/calligraph/llms-full.txt) - it is the full API documentation, and it ships inside the npm package at `node_modules/calligraph/llms-full.txt`. -Single-file package. One export: `Calligraph`. Keep it that way unless there's a strong reason to split. +## Repository layout ``` -src/ - index.tsx # everything lives here +packages/calligraph/ # the published npm package + src/ + index.tsx # public entry: , prop defaults, AutoSizeWrapper + text.tsx # variant="text" — LCS grapheme diffing + number.tsx # variant="number" — vertical digit roll + slots.tsx # variant="slots" — slot-machine digit spin + reconcile.ts # key reconciliation (computeLCS, reconcileTextKeys, reconcileDigitKeys) + shared.ts # grapheme splitting, animation presets, small helpers + llms.txt # short index for assistants, served at /llms.txt + llms-full.txt # full docs, served at /llms-full.txt and /index.md +apps/web/ # Next.js docs site (calligraph.raphaelsalaja.com) ``` -## Key internals +Monorepo: pnpm workspaces + Turborepo. Releases go through Changesets. -- `computeLCS` — standard LCS dynamic programming over two strings, returns `[oldIndex, newIndex][]` pairs -- `Calligraph` — the component. Uses `useState` + render-phase diffing (not `useEffect`) to reconcile character keys when `children` changes -- Character identity is tracked via string keys (`c0`, `c1`, ...) managed by `nextIdRef` -- Entering characters get a drift offset based on position (left-side drifts left, right-side drifts right) via the `drift` prop +## Commands -## Constraints +```bash +pnpm install +pnpm dev # package watch build + docs site +pnpm build # turbo build (bunchee for the package, next build for the site) +pnpm lint # biome check --fix --unsafe +pnpm typecheck # tsc --noEmit +``` -- **Client component** — the `"use client"` directive is required. This component uses `useState` and `useRef`. -- **Peer dependencies** — `motion`, `react`, `react-dom`. Do not add these to `dependencies`. -- **Single export** — consumers import `{ Calligraph }` from `"calligraph"`. Don't add default exports. -- **No internal state leaks** — `computeLCS`, key refs, and prev-text tracking are implementation details. Don't export them. +Always use `pnpm`, never `npm` or `yarn`. -## Build +## Architecture rules -- Uses `bunchee` for bundling -- Output: `dist/index.js` + `dist/index.d.ts` -- ESM only (`"type": "module"`) +- **One public export.** Consumers import `{ Calligraph }` (and the `CalligraphProps` type). No default export. `computeLCS`, the reconcilers, the renderers, and the animation presets are internal - do not export them. +- **One file per variant.** New rendering behaviour goes in its own `*.tsx` renderer with the same props shape (`text`, `transition`, `stagger`, `animateInitial`, `onComplete`), wired up in `index.tsx`. Do not grow `index.tsx` past prop handling and dispatch. +- **Render-phase reconciliation.** Key reconciliation happens during render by comparing against state (`prevText`), not in `useEffect`. Keep it that way - effects drop frames and break rapid successive updates. +- **Client component.** The `"use client"` directive in `src/index.tsx` is required. +- **Peer dependencies.** `motion`, `react`, `react-dom` stay in `peerDependencies`. Never move them to `dependencies`, never add a runtime dependency. +- **Graphemes, not code units.** Split with `splitGraphemes` (`Intl.Segmenter`) so emoji and combining marks survive. Never use `String.prototype.split("")` or index into a string. +- **Animation presets, not raw transitions.** New timing goes into `animations` in `shared.ts` as a named preset. There is no `transition` prop. ## Style -- Follow existing Biome config (spaces, double quotes, recommended rules) -- No comments explaining obvious code -- Keep JSDoc on the public export for IDE tooltips +- Biome config is the source of truth: 2 spaces, double quotes, sorted imports, recommended rules. +- No comments restating what the code says. JSDoc on the public `Calligraph` export only - it is what shows in IDE tooltips. +- Keep the diff small. This package is deliberately tiny; prefer deleting to adding. + +## Before you finish + +1. `pnpm lint && pnpm typecheck && pnpm build`. +2. Changed public behaviour or props? Update **all three**: the JSDoc in `src/index.tsx`, `packages/calligraph/llms-full.txt`, and `README.md`. Both `llms.txt` files are served by the site and ship in the npm package, so they must stay accurate. +3. Changed the package? Add a changeset: `pnpm changeset`. +4. Commits follow Conventional Commits (`feat:`, `fix:`, `docs:`, `chore:`) - enforced by commitlint. Do not add yourself as co-author. diff --git a/README.md b/README.md index ba41b73..ad9c452 100644 --- a/README.md +++ b/README.md @@ -31,19 +31,38 @@ function App() { When `children` changes, characters common to both strings slide into their new positions. New characters fade in, removed characters fade out. -## Custom transitions +## Variants ```tsx - +Text // LCS character diffing +$35.99 // rolling digits +1204 // slot-machine spin +``` + +## Animation presets + +```tsx + {text} ``` +Presets: `default`, `smooth`, `snappy`, `bouncy`. Full prop reference in [llms.txt](packages/calligraph/llms-full.txt). + ## Requirements - React 18+ - Motion 11+ +## AI & agents + +The docs are plain markdown, following the [llms.txt convention](https://llmstxt.org): + +- [`/llms.txt`](https://calligraph.raphaelsalaja.com/llms.txt) — concise index for assistants. +- [`/llms-full.txt`](https://calligraph.raphaelsalaja.com/llms-full.txt) — the full documentation in one file (also at [`/index.md`](https://calligraph.raphaelsalaja.com/index.md)). +- `node_modules/calligraph/llms-full.txt` — the same full documentation ships inside the npm package, so agents can read it straight from your project. +- [`AGENTS.md`](AGENTS.md) — guides coding agents contributing to this repo. + ## Sponsors If Calligraph is useful to you or your team, consider [sponsoring the project](https://github.com/sponsors/raphaelsalaja). diff --git a/apps/web/app/index.md/route.ts b/apps/web/app/index.md/route.ts new file mode 100644 index 0000000..3d05456 --- /dev/null +++ b/apps/web/app/index.md/route.ts @@ -0,0 +1,9 @@ +import { full } from "../../lib/docs"; + +export const dynamic = "force-static"; + +export function GET() { + return new Response(full, { + headers: { "content-type": "text/markdown; charset=utf-8" }, + }); +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index c9d09aa..aef4e64 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -41,6 +41,7 @@ export const metadata: Metadata = { title: { default: title, template: `%s — ${title}` }, description, metadataBase: new URL(url), + alternates: { types: { "text/markdown": "/index.md" } }, openGraph: { title, description, diff --git a/apps/web/app/llms-full.txt/route.ts b/apps/web/app/llms-full.txt/route.ts new file mode 100644 index 0000000..4096ebe --- /dev/null +++ b/apps/web/app/llms-full.txt/route.ts @@ -0,0 +1,9 @@ +import { full } from "../../lib/docs"; + +export const dynamic = "force-static"; + +export function GET() { + return new Response(full, { + headers: { "content-type": "text/plain; charset=utf-8" }, + }); +} diff --git a/apps/web/app/llms.txt/route.ts b/apps/web/app/llms.txt/route.ts new file mode 100644 index 0000000..1124790 --- /dev/null +++ b/apps/web/app/llms.txt/route.ts @@ -0,0 +1,9 @@ +import { index } from "../../lib/docs"; + +export const dynamic = "force-static"; + +export function GET() { + return new Response(index, { + headers: { "content-type": "text/plain; charset=utf-8" }, + }); +} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index d1ceb4f..87b0c96 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -9,6 +9,29 @@ const usage = `import { Calligraph } from "calligraph"; Text `; +const docs = [ + { + href: "/llms.txt", + path: "/llms.txt", + description: "Short index for assistants.", + }, + { + href: "/llms-full.txt", + path: "/llms-full.txt", + description: "Every prop, variant and recipe in one file.", + }, + { + href: "/index.md", + path: "/index.md", + description: "The same full docs, as markdown.", + }, + { + href: "https://github.com/raphaelsalaja/calligraph/blob/main/AGENTS.md", + path: "AGENTS.md", + description: "For agents contributing to the repo.", + }, +]; + export default function Page() { return ( <> @@ -59,6 +82,42 @@ export default function Page() {
{usage}
+ +
+

+ These docs are plain markdown, following the{" "} + + llms.txt + {" "} + convention. +

+ + + {docs.map(({ href, path, description }) => ( + + + + + ))} + +
+ + {path} + + {description}
+

+ The full docs also ship inside the package, at{" "} + + node_modules/calligraph/llms-full.txt + + . +

+
); } diff --git a/apps/web/app/styles.module.css b/apps/web/app/styles.module.css index 09219b4..21fd85a 100644 --- a/apps/web/app/styles.module.css +++ b/apps/web/app/styles.module.css @@ -100,3 +100,24 @@ .content { min-height: 0; } + +.link { + color: var(--gray-12); + text-decoration: underline; + text-decoration-color: var(--gray-6); + text-underline-offset: 3px; + transition: text-decoration-color 0.15s; +} + +.link:hover { + text-decoration-color: var(--gray-9); +} + +.code { + padding: 1px 5px; + font-family: "JetBrains Mono", var(--font-mono), monospace; + font-size: 12px; + color: var(--gray-12); + background: var(--gray-2); + border-radius: 4px; +} diff --git a/apps/web/lib/docs.ts b/apps/web/lib/docs.ts new file mode 100644 index 0000000..0f00579 --- /dev/null +++ b/apps/web/lib/docs.ts @@ -0,0 +1,10 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +// Single source of truth: the docs files that also ship inside the npm package. +// Read at build time — every route using them is force-static. +const read = (name: string) => + readFileSync(join(process.cwd(), "../../packages/calligraph", name), "utf8"); + +export const index = read("llms.txt"); +export const full = read("llms-full.txt"); diff --git a/packages/calligraph/llms-full.txt b/packages/calligraph/llms-full.txt new file mode 100644 index 0000000..0e85a8c --- /dev/null +++ b/packages/calligraph/llms-full.txt @@ -0,0 +1,220 @@ +# Calligraph + +> Fluid text and number transitions for React, powered by [Motion](https://motion.dev). Shared characters slide to their new positions, entering characters fade in, exiting ones fade out. Numbers roll vertically or spin like a slot machine. + +- Package: `calligraph` (npm, MIT, ESM only, ~20 kB unminified) +- Docs: https://calligraph.raphaelsalaja.com +- Repo: https://github.com/raphaelsalaja/calligraph +- Exports: the `Calligraph` component and the `CalligraphProps` type. Nothing else is public. + +## Install + +```bash +npm install calligraph +# pnpm add calligraph +# yarn add calligraph +``` + +`motion`, `react` and `react-dom` are peer dependencies (`motion >=11`, `react >=18`, `react-dom >=18`). Calligraph bundles nothing but itself and ships no CSS - there is no stylesheet to import. + +## Quick start + +```tsx +"use client"; + +import { Calligraph } from "calligraph"; +import { useState } from "react"; + +const words = ["Hello", "World", "Calligraph"]; + +export function Example() { + const [index, setIndex] = useState(0); + + return ( + + ); +} +``` + +Every time `children` changes, Calligraph diffs the old string against the new one and animates the difference. You never call an imperative API - render a new string and the animation follows. + +## Framework notes + +- **Next.js App Router**: `Calligraph` is a client component (`"use client"` is baked into the package). Render it from a client component, or from a server component - importing it from a server component works, but the state that changes the text has to live in a client component. +- **Vite / CRA / Remix / React Router**: nothing special, just import and render. +- **Server rendering**: the initial string renders as markup, so there is no layout shift. Animation begins on the first change after hydration (set `initial` to animate the first paint too). +- **React Server Components**: pass the text as a prop from the server; only the component itself needs to be client-side. + +## Props + +`CalligraphProps` extends every `` prop (minus `children`), so `className`, `style`, `id`, `onClick`, `aria-*` and friends all pass through to the wrapper element. + +| Prop | Type | Default | Notes | +| --- | --- | --- | --- | +| `children` | `string \| number` | `""` | The text to render. Coerced with `String()`. Not a React node - plain text only. | +| `variant` | `"text" \| "number" \| "slots"` | `"text"` | Rendering strategy, see below. | +| `animation` | `"default" \| "smooth" \| "snappy" \| "bouncy"` | `"default"` (`"snappy"` when `variant="number"`) | Motion transition preset. | +| `as` | `React.ElementType` | `"span"` | Wrapper element, e.g. `"h1"`, `"div"`, or your own component. | +| `drift` | `{ x?: number; y?: number }` | `{ x: 15, y: 0 }` | Max spread in px for entering/exiting characters, scaled by how much of the string changed. `variant="text"` only. | +| `trend` | `1 \| -1 \| 0` | `0` | Vertical direction: `1` enters from below, `-1` from above, `0` none. `variant="text"` only. | +| `stagger` | `number` | `0.02` | Seconds of delay spread across characters. Applies to `variant="number"` and `variant="slots"`. | +| `initial` | `boolean` | `false` | Animate characters in on first mount. | +| `onComplete` | `() => void` | - | Fires when the last character finishes animating. | +| `autoSize` | `boolean` | `true` | Animate the wrapper width to match content (uses `ResizeObserver`). | + +There is no `transition` prop - pass an `animation` preset instead: + +| Preset | Value | +| --- | --- | +| `default` | `{ duration: 0.38, ease: [0.19, 1, 0.22, 1] }` | +| `smooth` | `{ type: "spring", duration: 0.4, bounce: 0 }` | +| `snappy` | `{ type: "spring", duration: 0.35, bounce: 0.15 }` | +| `bouncy` | `{ type: "spring", duration: 0.5, bounce: 0.3 }` | + +## Variants + +### `variant="text"` (default) + +Diffs old and new text with an LCS (longest common subsequence) over graphemes. Matched graphemes keep their identity and slide to their new position with Motion layout animation. New graphemes fade, blur and scale in; removed ones fade, blur and scale out. Drift and trend offsets scale with the fraction of the string that changed, so a one-letter edit barely moves while a full replacement sweeps. + +```tsx +"use client"; + +import { Calligraph } from "calligraph"; +import { useState } from "react"; + +const states = ["Idle", "Uploading", "Processing", "Done"]; + +export function Status() { + const [step, setStep] = useState(0); + + return ( + setStep((s) => (s + 1) % states.length)} + > + {states[step]} + + ); +} +``` + +### `variant="number"` + +Digits roll vertically. Columns are aligned from the right, so `99` to `100` keeps the shared digits in place. Roll direction comes from the numeric comparison: up when the value increases, down when it decreases. Any non-digit prefix (currency symbol, sign) renders without per-character animation. + +```tsx +"use client"; + +import { Calligraph } from "calligraph"; +import { useState } from "react"; + +const format = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", +}).format; + +export function Price() { + const [price, setPrice] = useState(35.99); + + return ( + <> + {format(price)} + + + ); +} +``` + +Pass an already-formatted string. Calligraph never formats numbers itself - use `Intl.NumberFormat` for currency, percentages, grouping and locales. + +### `variant="slots"` + +Slot-machine spin: each digit position renders a column of `0-9`, animates to the target digit, and is masked with a fade at the top and bottom edge. Good for counters and scores where the movement itself is the point. + +```tsx + + {String(score)} + +``` + +## Recipes + +Headings and custom elements - extra props land on the wrapper: + +```tsx + + {heading} + +``` + +Accessibility - the animated glyphs are `aria-hidden`, so name the wrapper whenever the text carries meaning: + +```tsx +{text} +``` + +Steady digits - tabular figures stop the width from jittering: + +```tsx + + {value} + +``` + +Fixed-width layout - turn off the width animation when the parent already reserves space: + +```tsx +{text} +``` + +Chaining on completion: + +```tsx + setStep((s) => s + 1)}>{message} +``` + +Typing the props: + +```tsx +import type { CalligraphProps } from "calligraph"; + +function Label(props: CalligraphProps) { + return ; +} +``` + +Live clock (any string source works): + +```tsx +const [now, setNow] = useState(() => new Date().toLocaleTimeString()); +useEffect(() => { + const id = setInterval(() => setNow(new Date().toLocaleTimeString()), 1000); + return () => clearInterval(id); +}, []); + +return {now}; +``` + +## Notes and constraints + +- Grapheme-aware: text is split with `Intl.Segmenter`, so emoji, flags and combining marks stay intact. +- Reconciliation happens during render (no `useEffect`), so keys survive rapid successive updates. +- `stagger` currently affects the `number` and `slots` variants; `text` animates all characters together. +- The wrapper is `display: inline-flex` and `position: relative` by default; your `style` merges on top of that. +- No `forwardRef` is declared. If you need a DOM ref, wrap Calligraph in your own element. +- ESM only. `main` is `dist/index.js`, `types` is `dist/index.d.ts`. +- Internals (`computeLCS`, key reconciliation, the renderer modules, the preset table) are not exported and are not a stable API. + +## Common mistakes + +- Passing a `transition` object - use `animation` instead. Unknown props are spread onto the DOM node and React will warn. +- Passing JSX, an array, or `null` as `children` - only `string | number` is supported. +- Forgetting that state driving the text has to live in a client component in the App Router. +- Expecting number formatting - format with `Intl.NumberFormat` first. +- Animating a value that changes every frame - throttle to something a human can read. diff --git a/packages/calligraph/llms.txt b/packages/calligraph/llms.txt new file mode 100644 index 0000000..a396ecf --- /dev/null +++ b/packages/calligraph/llms.txt @@ -0,0 +1,29 @@ +# Calligraph + +> Fluid text and number transitions for React, powered by [Motion](https://motion.dev). Shared characters slide to their new positions, entering characters fade in, exiting ones fade out. Numbers roll vertically or spin like a slot machine. + +One export, `Calligraph`, plus the `CalligraphProps` type. ESM only. Peer dependencies: `motion >=11`, `react >=18`, `react-dom >=18`. + +```tsx +"use client"; +import { Calligraph } from "calligraph"; + +{text} +{"$35.99"} +{"1204"} +``` + +Props: `variant` (`"text" | "number" | "slots"`), `animation` (`"default" | "smooth" | "snappy" | "bouncy"`), `as`, `drift`, `trend`, `stagger`, `initial`, `onComplete`, `autoSize`. There is no `transition` prop. All other `` props pass through to the wrapper. + +## Docs + +- [Full documentation](https://calligraph.raphaelsalaja.com/llms-full.txt): every prop with defaults, all three variants, recipes, constraints, common mistakes +- `node_modules/calligraph/llms-full.txt`: the same file, shipped inside the npm package +- [Markdown mirror](https://calligraph.raphaelsalaja.com/index.md): full documentation with a `.md` extension +- [AGENTS.md](https://github.com/raphaelsalaja/calligraph/blob/main/AGENTS.md): rules for agents contributing to the repo + +## Links + +- [npm: calligraph](https://www.npmjs.com/package/calligraph): `npm install calligraph` +- [Source](https://github.com/raphaelsalaja/calligraph/tree/main/packages/calligraph/src): one renderer file per variant +- [Live demos](https://calligraph.raphaelsalaja.com), [lab](https://calligraph.raphaelsalaja.com/lab), [compare](https://calligraph.raphaelsalaja.com/compare) diff --git a/packages/calligraph/package.json b/packages/calligraph/package.json index 7449578..760e220 100644 --- a/packages/calligraph/package.json +++ b/packages/calligraph/package.json @@ -30,7 +30,9 @@ "./package.json": "./package.json" }, "files": [ - "dist" + "dist", + "llms.txt", + "llms-full.txt" ], "sideEffects": false, "publishConfig": {