Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/quiet-jars-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"calligraph": patch
---

docs: ship llms.txt and llms-full.txt inside the package for AI agents
67 changes: 44 additions & 23 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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: <Calligraph />, 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.
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<Calligraph transition={{ type: "spring", stiffness: 200, damping: 20 }}>
<Calligraph>Text</Calligraph> // LCS character diffing
<Calligraph variant="number">$35.99</Calligraph> // rolling digits
<Calligraph variant="slots">1204</Calligraph> // slot-machine spin
```

## Animation presets

```tsx
<Calligraph animation="bouncy" trend={1} drift={{ x: 20, y: 8 }}>
{text}
</Calligraph>
```

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).
Expand Down
9 changes: 9 additions & 0 deletions apps/web/app/index.md/route.ts
Original file line number Diff line number Diff line change
@@ -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" },
});
}
1 change: 1 addition & 0 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions apps/web/app/llms-full.txt/route.ts
Original file line number Diff line number Diff line change
@@ -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" },
});
}
9 changes: 9 additions & 0 deletions apps/web/app/llms.txt/route.ts
Original file line number Diff line number Diff line change
@@ -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" },
});
}
59 changes: 59 additions & 0 deletions apps/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ const usage = `import { Calligraph } from "calligraph";
<Calligraph>Text</Calligraph>
`;

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 (
<>
Expand Down Expand Up @@ -59,6 +82,42 @@ export default function Page() {
<Section title="Usage">
<CodeBlock>{usage}</CodeBlock>
</Section>

<Section title="AI & agents">
<p className={styles.description}>
These docs are plain markdown, following the{" "}
<a
className={styles.link}
href="https://llmstxt.org"
target="_blank"
rel="noopener noreferrer"
>
llms.txt
</a>{" "}
convention.
</p>
<table className={styles.table}>
<tbody>
{docs.map(({ href, path, description }) => (
<tr key={path}>
<td>
<a className={styles.link} href={href}>
<code>{path}</code>
</a>
</td>
<td>{description}</td>
</tr>
))}
</tbody>
</table>
<p className={styles.description}>
The full docs also ship inside the package, at{" "}
<code className={styles.code}>
node_modules/calligraph/llms-full.txt
</code>
.
</p>
</Section>
</>
);
}
Expand Down
21 changes: 21 additions & 0 deletions apps/web/app/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
10 changes: 10 additions & 0 deletions apps/web/lib/docs.ts
Original file line number Diff line number Diff line change
@@ -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");
Loading