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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ tsconfig.tsbuildinfo

# Agents
.claude
# local-markdown issue tracker (docs/agents/issue-tracker.md) — machine-local
.scratch/
.roadmap
.playwright-mcp
# Per-machine MCP server wiring: the entries carry absolute paths to this
Expand Down Expand Up @@ -77,6 +79,9 @@ e2e/packed-app/.staging/
# made the deliverable un-commitable; children-glob + negation instead)
docs/*
!docs/standalone-extraction.md
# agent-skills repo config (issue tracker, triage labels, domain docs) SHIPS —
# referenced from AGENTS.md § Agent skills
!docs/agents/
.codex

# contract-change campaign ratchet artifacts — enforcement hooks were never
Expand Down
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,20 @@ End-of-work mutating cleanup at `scripts/hygiene/`. Never CI-invoked. See [`scri
- Escalate to OpenSpec only for a durable public contract, architectural or
dependency decision, migration, or genuinely cross-cutting behavior change.

## Agent skills

### Issue tracker

Issues live as local markdown under `.scratch/<feature-slug>/` in this repo. See `docs/agents/issue-tracker.md`.

### Triage labels

Five canonical triage roles, mapped to this repo's label strings in `docs/agents/triage-labels.md`.

### Domain docs

Multi-context: root `CONTEXT-MAP.md` + per-package `CONTEXT.md` and `docs/adr/`. See `docs/agents/domain.md`.

<!-- REPOWISE_DISTILL:START — Do not edit below this line. Auto-generated by Repowise. -->

### Output Distillation
Expand Down
152 changes: 11 additions & 141 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,6 @@ A design system builder where the TypeScript types ARE the product. Define compo

No Emotion. No styled-components. No runtime style injection. The builder chain compiles to static CSS via `@layer`, extracted by a Rust pipeline.

```tsx
import { ds } from './ds';

const Card = ds
.styles({
padding: '{space.md}',
borderRadius: 8,
backgroundColor: '{colors.surface}',
})
.variant({
elevation: {
prop: 'elevation',
variants: {
flat: { boxShadow: 'none' },
raised: { boxShadow: '{shadows.sm}' },
floating: { boxShadow: '{shadows.lg}' },
},
},
})
.states({
disabled: { opacity: 0.5, pointerEvents: 'none' },
})
.system({ surface: true, space: true })
.asElement('div');

// Fully typed — elevation, disabled, + all surface and space props
<Card elevation="raised" p={16} bg="surface.hover" disabled />;
```

## Install

```bash
Expand All @@ -58,112 +29,19 @@ Not on Vite or Next? The transform host (`@animus-ui/unplugin`) and the
resolution, the artifact set, exit codes, and a copy-pasteable rollup
quickstart.

## Setup

Two files define your design system:

**`theme.ts`** — define your theme:

```tsx
import { createTheme } from '@animus-ui/system';

export const theme = createTheme()
.addBreakpoints({ sm: 480, md: 768, lg: 1024 })
.addColors({
gray: { 50: '#fafafa', 500: '#555', 900: '#080808' },
blue: { 400: '#3d94ff', 700: '#003d99' },
})
.addColorModes('dark', {
dark: {
primary: 'blue.400',
bg: 'gray.900',
text: 'gray.50',
},
light: {
primary: 'blue.700',
bg: 'gray.50',
text: 'gray.900',
},
})
.addScale({
name: 'space',
values: { sm: '0.5rem', md: '1rem', lg: '1.5rem' },
})
.build();

// Type augmentation — token names autocomplete everywhere
type AppTheme = typeof theme;

declare module '@animus-ui/system' {
interface Theme extends AppTheme {}
}
```

**`ds.ts`** — configure your system:

```tsx
import { createSystem } from '@animus-ui/system';
import {
space,
color,
typography,
layout,
flex,
border,
shadows,
background,
} from '@animus-ui/system/groups';

// Pre-built groups compose into your own semantic groups
export const { system: ds, createGlobalStyles } = createSystem()
.addGroup('surface', { ...color, ...border, ...shadows, ...background })
.addGroup('space', space)
.addGroup('text', typography)
.addGroup('arrange', { ...flex, ...layout })
.build();
```

Consuming a published design-system kit? `.extend()` (available on both
builders, first in the chain) merges the kit's registries and tokens into
yours — its props type-check, extract, and resolve through your single merged
config, and your local definitions win on conflict:

```tsx
import { system as kitSystem, theme as kitTheme } from '@acme/kit';
## Documentation

export const theme = createTheme().extend(kitTheme).build();
export const { system: ds } = createSystem().extend(kitSystem).build();
```
Deliberately withheld. The system-definition API is still settling
(vocabulary registration: `build()` → register → `seal()`), and written
guides repeatedly drifted into teaching shapes the current pipeline
rejects. Rather than keep wrong docs, they are removed until the API
freezes. Until then, the sources of truth are:

**`vite.config.ts`**:

```tsx
import react from '@vitejs/plugin-react';
import { animusExtract } from '@animus-ui/vite-plugin';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [react(), animusExtract({ system: './src/ds.ts' })],
});
```

Using Svelte 5? See the [Svelte guide](packages/showcase/src/content/advanced/svelte.mdx)
for the native `.attrs()` and element-spread authoring pattern, and
[e2e/svelte-app](e2e/svelte-app/src/App.svelte) for the working consumer.

## The Builder Chain

Each method maps to a CSS `@layer`. The type system enforces the ordering.

```
ds.styles() → @layer base always-on styles
.variant() → @layer variants prop-driven variations
.compound() → @layer compounds variant combinations
.states() → @layer states boolean interaction states
.system() → @layer system opt into prop groups (space, color, etc.)
.props() → @layer custom component-scoped dynamic props
.asElement() → seal as typed React component
```
- the packages' TypeScript definitions — the types are the contract;
- the in-repo consumers, which are compiled, extracted, and asserted on
every verify run and therefore cannot silently drift:
`packages/test-ds/src/system.ts` (a kit), the `e2e/*/src/ds.ts` apps,
and `packages/showcase/src/ds.ts` with its live Examples pages.

## Packages

Expand All @@ -177,14 +55,6 @@ ds.styles() → @layer base always-on styles
| [`@animus-ui/extract`](packages/extract) | Rust/NAPI extraction engine + the shared extraction session every driver (plugins, host, CLI) drives |
| [`@animus-ui/properties`](packages/properties) | CSS property data (transitive dep of system) |

## Key Ideas

- **Compiler completeness**: If the types accept it, the pipeline extracts it. No silent failures for well-typed code.
- **Token refs**: `'{colors.primary}'` resolves to `var(--color-primary)` at build time. Color modes shift the value automatically.
- **Pre-built groups**: Import `space`, `color`, `typography`, etc. from `@animus-ui/system/groups` and compose them into your own semantic groups.
- **Slot composition**: `compose()` wires components into families with shared variant propagation via React context.
- **Terminals**: `.asElement('div')` for HTML elements, `.asComponent(Existing)` for wrapping React components. Both produce typed, extractable output.

## Legacy

`@animus-ui/core` and `@animus-ui/theming` are the original Emotion-based packages. They are pinned at their last published versions and no longer actively developed.
Expand Down
44 changes: 44 additions & 0 deletions docs/agents/domain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Domain Docs

How the engineering skills should consume this repo's domain documentation when exploring the codebase.

## Before exploring, read these

- **`CONTEXT-MAP.md`** at the repo root — it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
- **`docs/adr/`** — read ADRs that touch the area you're about to work in. Also check `packages/<context>/docs/adr/` for context-scoped decisions.

If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.

## File structure

This is a multi-context repo. Contexts are packages under `packages/`, not `src/` subdirectories:

```
/
├── CONTEXT-MAP.md
├── docs/adr/ ← system-wide decisions
├── packages/
│ ├── extract/
│ │ ├── CONTEXT.md
│ │ └── docs/adr/ ← context-specific decisions
│ └── system/
│ ├── CONTEXT.md
│ └── docs/adr/
└── e2e/ ← consumer fixtures; no contexts of their own
```

## Relationship to OpenSpec

`openspec/specs/` (local, gitignored) is the authoritative requirement surface for behavior contracts in this repo. ADRs record architectural rationale and trade-offs, not requirements — when a decision is already owned by an openspec spec, link to it rather than duplicating it. Flag conflicts with a spec the same way as ADR conflicts (below).

## Use the glossary's vocabulary

When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.

If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).

## Flag ADR conflicts

If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:

> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
30 changes: 30 additions & 0 deletions docs/agents/issue-tracker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Issue tracker: Local Markdown

Issues and specs for this repo live as markdown files in `.scratch/`.

## Conventions

- One feature per directory: `.scratch/<feature-slug>/`
- The spec is `.scratch/<feature-slug>/spec.md`
- Implementation issues are one file per ticket at `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01` — never a single combined tickets file
- Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings)
- Comments and conversation history append to the bottom of the file under a `## Comments` heading

## When a skill says "publish to the issue tracker"

Create a new file under `.scratch/<feature-slug>/` (creating the directory if needed).

## When a skill says "fetch the relevant ticket"

Read the file at the referenced path. The user will normally pass the path or the issue number directly.

## Wayfinding operations

Used by `/wayfinder`. The **map** is a file with one **child** file per ticket.

- **Map**: `.scratch/<effort>/map.md` — the Notes / Decisions-so-far / Fog body.
- **Child ticket**: `.scratch/<effort>/issues/NN-<slug>.md`, numbered from `01`, with the question in the body. A `Type:` line records the ticket type (`research`/`prototype`/`grilling`/`task`); a `Status:` line records `claimed`/`resolved`.
- **Blocking**: a `Blocked by: NN, NN` line near the top. A ticket is unblocked when every file it lists is `resolved`.
- **Frontier**: scan `.scratch/<effort>/issues/` for files that are open, unblocked, and unclaimed; first by number wins.
- **Claim**: set `Status: claimed` and save before any work.
- **Resolve**: append the answer under an `## Answer` heading, set `Status: resolved`, then append a context pointer (gist + link) to the map's Decisions-so-far in `map.md`.
15 changes: 15 additions & 0 deletions docs/agents/triage-labels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Triage Labels

The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.

| Label in mattpocock/skills | Label in our tracker | Meaning |
| -------------------------- | -------------------- | ---------------------------------------- |
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
| `needs-info` | `needs-info` | Waiting on reporter for more information |
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
| `ready-for-human` | `ready-for-human` | Requires human implementation |
| `wontfix` | `wontfix` | Will not be actioned |

When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.

Edit the right-hand column to match whatever vocabulary you actually use.
18 changes: 13 additions & 5 deletions e2e/next-app/src/ds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,7 @@ declare module '@animus-ui/system' {
// (showcase remains on `includes:` pending its deferred migration —
// registry row 13; see its ds.ts). Do not migrate this lane until removal
// is specced.
export const {
system: ds,
createGlobalStyles,
createKeyframes,
} = createSystem({
const bundle = createSystem({
includes: [testDs],
})
.addGroup('space', space)
Expand All @@ -216,6 +212,8 @@ export const {
.addGroup('positioning', positioning)
.build();

export const { createGlobalStyles, createKeyframes } = bundle;

// ─── Keyframes ──────────────────────────────────────────────

export const animations = createKeyframes({
Expand Down Expand Up @@ -244,3 +242,13 @@ export const globalStyles = createGlobalStyles({
a: { color: 'primary', textDecoration: 'none' },
'code, kbd': { fontFamily: 'ui-monospace, monospace', fontSize: 14 },
});

// Sealed system (vocabulary-registration): `animations` registers under its
// export name. The `includes:` alias above deliberately CANNOT carry the
// kit's registered `kitMotion` — this lane is the legacy-verb witness: the
// sealed record carries the coded `animus.vocabulary.legacy-verb` entry the
// hosts surface as a warning.
export const ds = bundle
.registerKeyframes({ animations })
.registerGlobalStyles({ globalStyles })
.seal();
34 changes: 20 additions & 14 deletions e2e/next16-app/src/ds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,20 +163,18 @@ declare module '@animus-ui/system' {

// ─── System ─────────────────────────────────────────────────

export const {
system: ds,
createGlobalStyles,
createKeyframes,
// extend()-form lane (openspec: first-class-extension, D1): test-ds's
// registries MERGE into this system — the kit alone provides the space/
// layout/text/surface/positioning groups the components use. The only
// LOCAL registration is the additive, transform-free `shadows` prop set
// (boxShadow/shadow/textShadow — the kit does not register them, and the
// Card/Button styles resolve their `shadows`-scale values through the
// registry). Re-spreading kit groups would coalesce under D12 transform
// equality (name + captured source); this lane stays pure-extend + additive
// as the recommended consumption shape.
} = createSystem().extend(testDs).addProps(shadows).build();
// extend()-form lane (openspec: first-class-extension, D1): test-ds's
// registries MERGE into this system — the kit alone provides the space/
// layout/text/surface/positioning groups the components use. The only
// LOCAL registration is the additive, transform-free `shadows` prop set
// (boxShadow/shadow/textShadow — the kit does not register them, and the
// Card/Button styles resolve their `shadows`-scale values through the
// registry). Re-spreading kit groups would coalesce under D12 transform
// equality (name + captured source); this lane stays pure-extend + additive
// as the recommended consumption shape.
const bundle = createSystem().extend(testDs).addProps(shadows).build();

export const { createGlobalStyles, createKeyframes } = bundle;

// ─── Keyframes ──────────────────────────────────────────────

Expand Down Expand Up @@ -206,3 +204,11 @@ export const globalStyles = createGlobalStyles({
a: { color: 'primary', textDecoration: 'none' },
'code, kbd': { fontFamily: 'ui-monospace, monospace', fontSize: 14 },
});

// Sealed system (vocabulary-registration): `animations` registers under its
// export name; the kit's `kitMotion` arrives through the sealed test-ds
// record via `.extend()`.
export const ds = bundle
.registerKeyframes({ animations })
.registerGlobalStyles({ globalStyles })
.seal();
Loading