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
13 changes: 7 additions & 6 deletions .atl/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Keep it minimal β€” detailed workflows live in skills (`.atl/skills/`).
**Stack**: React 19 Β· TypeScript strict Β· Tailwind v4 `@theme` Β· Radix UI Β· CVA Β· Storybook 8 Β· Biome Β· Lefthook Β· Vite Β· pnpm

**Available tooling**:
- `compilot-cli` β€” scaffolds the 5-file component structure
- `compilot-cli` β€” scaffolds the 6-file component structure
- `pnpm run storybook` β€” starts Storybook with hot reload
- `pnpm run test` β€” runs Vitest unit tests
- `pnpm run test:coverage` β€” runs tests with coverage report
Expand All @@ -30,15 +30,16 @@ Keep it minimal β€” detailed workflows live in skills (`.atl/skills/`).

## Structure

Components live in `src/components/{atoms|molecules|organisms}/{kebab-name}/` with exactly 5 files:
Components live in `src/components/{atoms|molecules|organisms}/{kebab-name}/` with exactly 6 files:

| File | Role |
|------|------|
| `ComponentName.tsx` | Presentational β€” JSX only, consumes the hook |
| `useComponentName.ts` | Logic β€” state, effects, handlers, CVA class calls |
| `types.ts` | Types + CVA variants |
| `ComponentName.test.tsx` | Complete test suite (hook + component tests) |
| `ComponentName.stories.tsx` | Storybook stories (documentation only, no tests) |
| `index.ts` | Public API re-exports |
| `ComponentName.stories.tsx` | Storybook stories |

---

Expand All @@ -48,10 +49,10 @@ Components live in `src/components/{atoms|molecules|organisms}/{kebab-name}/` wi
- Never `any` β€” use `unknown` or narrow properly
- Never hardcode colors, spacing or fonts β€” use tokens from `src/styles/theme.css`
- Use Tailwind token classes directly β€” NEVER `[var(--token)]` when the token exists in `@theme` (e.g. `text-brand-light`, `rounded-pill`, `bg-red-tint-subtle`)
- `var()` is FORBIDDEN in component source files (`src/components/**/*.ts`, `src/components/**/*.tsx`)
- If a token or mixed visual treatment does not exist, define it centrally in `src/styles/theme.css` or `src/styles/base.css` as a reusable token-backed utility/class, then consume that class from the component
- If a token does not exist for a value, CREATE it in `theme.css` first β€” never use raw `rgba()`, hex, or px values inline
- Exceptions where `var()` is acceptable (Tailwind cannot express these as utilities):
- `bg-gradient-*` β€” multi-stop gradient values (`--gradient-*` tokens)
- `shadow-glow-*` β€” multi-layer box-shadow values (`--glow-*` tokens)
- `var()` is only acceptable inside the style system (`src/styles/theme.css`, `src/styles/base.css`) to define reusable utilities such as `bg-gradient-*`, `shadow-glow-*`, or semantic component helper classes
- Token reference: `docs/DESIGN.md` β€” read it before building any component
- Never modify `theme.css` without explicit user confirmation
- Never add dependencies without explicit user confirmation
Expand Down
9 changes: 6 additions & 3 deletions .atl/skill-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,20 @@ Each skill has a "When Delegated by SDD Orchestrator" section that defines:
### component-contributor
**Load when**: contributor shares a GitHub issue URL or spec and asks to implement a component.

**Workflow**: Phase 0 (onboarding) β†’ Phase 1 (read spec) β†’ Phase 1.5 (spec review β€” critique before committing) β†’ Phase 2 (plan + confirm) β†’ Phase 3 (implement 5 files) β†’ Phase 4 (explain inline) β†’ Phase 5 (visual review)
**Workflow**: Phase 0 (onboarding) β†’ Phase 1 (read spec) β†’ Phase 1.5 (spec review β€” critique before committing) β†’ Phase 2 (plan + confirm) β†’ Phase 3 (implement 6 files) β†’ Phase 4 (explain inline) β†’ Phase 5 (visual review)

**When delegated from SDD**: skip Phase 0, start from Phase 1, return SDD envelope.

**File order**: `types.ts` β†’ `useComponentName.ts` β†’ `ComponentName.tsx` β†’ `ComponentName.stories.tsx` β†’ `index.ts`
**File order**: `types.ts` β†’ `useComponentName.ts` β†’ `ComponentName.tsx` β†’ `ComponentName.test.tsx` β†’ `ComponentName.stories.tsx` β†’ `index.ts`

**Non-negotiables**:
- CVA variants ONLY in `types.ts`
- Logic ONLY in `useComponentName.ts`
- JSX ONLY in `ComponentName.tsx`
- Tokens from `theme.css` only β€” no hardcoded values, no `[var(--token)]` when token exists in `@theme`
- Complete tests in `ComponentName.test.tsx` β€” hook tests + component tests, NO play functions in stories
- Stories in `ComponentName.stories.tsx` β€” documentation only, use `@storybook/addon-actions`, NO play functions
- Tokens from the style system only (`theme.css` / `base.css`) β€” no hardcoded values, no `[var(--token)]` when token exists in `@theme`, and no direct `var()` in component source files
- Reusable/systemic prop types belong in `src/types`; `component/types.ts` is only for component-specific types and CVA contracts
- `type` always, never `interface`, never `any`
- Explain every decision after each file
- `Default` story args must NOT override `defaultVariants`
Expand Down
132 changes: 113 additions & 19 deletions .atl/skills/component-contributor/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: component-contributor
description: >
Guides an AI agent through implementing a design system component from a GitHub issue spec.
Covers the full contributor workflow: onboarding β†’ read spec β†’ plan β†’ implement (5-file pattern) β†’ explain decisions β†’ visual review.
Covers the full contributor workflow: onboarding β†’ read spec β†’ plan β†’ implement (6-file pattern) β†’ explain decisions β†’ visual review.
Trigger: When a contributor provides a GitHub issue URL or pastes a component spec and asks to implement it.
Also delegable from sdd-apply when implementing a component as part of a larger SDD change.
license: Apache-2.0
Expand Down Expand Up @@ -52,10 +52,11 @@ Return your result in the SDD return envelope format:
Before touching the spec, make sure the contributor understands the stack they are about to work with. Ask them to confirm they are familiar with:

1. **React + TypeScript strict** β€” `type` always, never `interface`, never `any`
2. **Tailwind v4 `@theme`** β€” token classes only (`text-brand-light`, `bg-surface-dark`); never raw hex, never `[var(--token)]` when the token exists in `@theme`
2. **Tailwind v4 `@theme`** β€” token classes only (`text-brand-light`, `bg-surface-dark`); never raw hex, never `[var(--token)]` when the token exists in `@theme`, and never `var()` inside component source files
3. **CVA (class-variance-authority)** β€” all variants live in `types.ts`, nowhere else
4. **The 5-file pattern** β€” each component has exactly: `types.ts`, `useComponentName.ts`, `ComponentName.tsx`, `ComponentName.stories.tsx`, `index.ts`
5. **Storybook 8** β€” stories are the living documentation AND the test suite
4. **The 6-file pattern** β€” each component has exactly: `types.ts`, `useComponentName.ts`, `ComponentName.tsx`, `ComponentName.test.tsx`, `ComponentName.stories.tsx`, `index.ts`
5. **Storybook 8** β€” stories are the living documentation (not the test suite β€” tests live in `.test.tsx`)
6. **Project story conventions** β€” autodocs behavior, action helper, and controls must match the repo's canonical stories exactly

If the contributor is unfamiliar with any of these, briefly explain the concept before moving on. Do NOT assume knowledge β€” a contributor who copies code without understanding it is a liability, not an asset.

Expand Down Expand Up @@ -163,8 +164,9 @@ Before writing any file, present a clear plan to the contributor:
1. `types.ts` β€” {X} props, {Y} CVA variants ({list variants})
2. `useComponentName.ts` β€” {describe logic: state, handlers, aria}
3. `ComponentName.tsx` β€” presentational, consumes hook
4. `ComponentName.stories.tsx` β€” {N} stories: Default, Disabled, {variants}
5. `index.ts` β€” re-exports
4. `ComponentName.test.tsx` β€” complete component test suite (hook tests + component tests)
5. `ComponentName.stories.tsx` β€” {N} stories: Default, Disabled, {variants}
6. `index.ts` β€” re-exports

### Design decisions
- Tokens used: {list tokens from theme.css}
Expand Down Expand Up @@ -195,7 +197,8 @@ Load these BEFORE writing the relevant file. They contain canonical patterns ext

| Module | Path | Load when |
|--------|------|-----------|
| `testing` | `references/testing.md` | Writing any test file β€” Vitest or Storybook play function |
| `testing` | `references/testing.md` | Writing any test file β€” Vitest tests in `.test.tsx` |
| `stories` | `references/stories.md` | Writing or reviewing any `*.stories.tsx` file; Storybook autodocs/actions/controls conventions |
| `html-extension` | `references/html-extension.md` | Component wraps a native HTML element (button, input, select, a, textarea) |
| `radix-patterns` | `references/radix-patterns.md` | Component uses any `@radix-ui/*` primitive |
| `tailwind-merge` | `references/tailwind-merge.md` | Component combines CVA output with conditional or external `className` |
Expand All @@ -206,6 +209,11 @@ Load these BEFORE writing the relevant file. They contain canonical patterns ext
- `biome-rules` β€” load before writing any `.ts` or `.tsx` file
- `html-extension` β€” load for any component that renders a native element

**Mandatory story convention check:**
- Before writing `types.ts` or `ComponentName.stories.tsx`, read the project `stories` reference if it exists.
- If no dedicated `stories` reference exists, inspect at least ONE mature atom story already accepted in the repo and mirror its conventions exactly.
- Do NOT invent Storybook conventions from generic knowledge.

**Matching rules by component type:**

| Component type | Colors | Spacing & Typography | Effects |
Expand All @@ -224,19 +232,47 @@ Load these BEFORE writing the relevant file. They contain canonical patterns ext

---

## Phase 3 β€” Implement (5-file pattern)
## Phase 3 β€” Implement (6-file pattern)

Implement files in this order: `types.ts` β†’ `useComponentName.ts` β†’ `ComponentName.tsx` β†’ `ComponentName.stories.tsx` β†’ `index.ts`
Implement files in this order: `types.ts` β†’ `useComponentName.ts` β†’ `ComponentName.tsx` β†’ `ComponentName.test.tsx` β†’ `ComponentName.stories.tsx` β†’ `index.ts`

After each file, explain what was done and why (Phase 4 runs inline).

### File 1: `types.ts`

**Rules:**
- ALL `type` definitions here β€” never `interface`
- Before declaring a new public/shared prop type in `component/types.ts`, check `src/types/index.ts` first; reusable design-system types must live in `src/types`
- Only keep types in `component/types.ts` when they are truly component-specific and not reusable across multiple components
- ALL CVA variants here β€” never in `.tsx` or `.ts` hook files
- JSDoc `/** @control select */` on every prop that needs a Storybook control
- Every PUBLIC prop exposed in stories MUST have `@control` and `@default` (if applicable) JSDoc annotations
- JSDoc format for Storybook controls:
```typescript
/**
* @control controlType
* @default defaultValue
*/
propName?: Type
```
- Props without defaults (e.g. callbacks, optional content) may omit `@default`
- Import CVA from `class-variance-authority`
- Never use `[var(--token)]` when an equivalent Tailwind utility exists in `@theme`
- Never use `var()` directly in component source files; if Tailwind cannot express a token-backed visual treatment, define a reusable class/token in `src/styles/theme.css` or `src/styles/base.css` first

**Available control types:**
- `@control text` β€” freeform text input
- `@control boolean` β€” toggle switch
- `@control number` β€” numeric input
- `@control range` β€” slider (specify min/max in description if needed)
- `@control select` β€” dropdown for single selection (requires `options` defined separately or inferred from type)
- `@control radio` β€” radio buttons for single selection
- `@control inline-radio` β€” inline radio buttons
- `@control check` β€” checkboxes for multiple selection
- `@control inline-check` β€” inline checkboxes
- `@control color` β€” color picker
- `@control date` β€” date picker
- `@control file` β€” file upload
- `@control object` β€” JSON editor for complex objects

```typescript
import { cva, type VariantProps } from 'class-variance-authority'
Expand All @@ -263,11 +299,19 @@ export const componentVariants = cva(
)

export type ComponentProps = VariantProps<typeof componentVariants> & {
/** @control text */
/**
* @control text
* @default Example
*/
label?: string
/** @control boolean */
/**
* @control boolean
* @default false
*/
disabled?: boolean
/** Accessibility label for screen readers */
/**
* @control text
*/
ariaLabel?: string
}
```
Expand Down Expand Up @@ -335,14 +379,64 @@ const Component: FC<ComponentProps> = (props) => {
export default Component
```

### File 4: `ComponentName.stories.tsx`
### File 4: `ComponentName.test.tsx`

**Rules:**
- ALL tests here β€” both hook tests and component behavior tests
- Must test: hook logic with `renderHook`, component rendering with `render/screen`, ARIA attributes, user interactions with `userEvent`, disabled states
- All mocks declared BEFORE component imports
- Never test internal CSS class strings
- Load `references/testing.md` before writing this file

```typescript
import { renderHook } from '@testing-library/react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import Component from './Component'
import useComponent from './useComponent'

// Mocks first
vi.mock('lucide-react/dynamic', () => ({
DynamicIcon: () => null
}))

describe('useComponent β€” hook logic', () => {
it('returns default variant', () => {
const { result } = renderHook(() => useComponent({}))
expect(result.current.variant).toBe('default')
})
})

describe('Component β€” behavior', () => {
it('renders correctly', () => {
render(<Component label="Test" />)
expect(screen.getByRole('button')).toBeInTheDocument()
})

it('calls onClick when clicked', async () => {
const handleClick = vi.fn()
render(<Component label="Click" onClick={handleClick} />)
await userEvent.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
})
```

### File 5: `ComponentName.stories.tsx`

**Rules:**
- English only β€” titles, descriptions, arg labels
- Mandatory `parameters.docs.description.component`
- Mandatory `args` on `Default` story β€” must NOT hardcode props that override `defaultVariants`
- Always include: `Default`, `Disabled`, one story per key variant
- Each story demonstrates ONE axis only β€” no mixed props across variants in the same story
- If `autodocs` is enabled for the project, do NOT define manual `argTypes` in `meta` or individual stories unless the project reference explicitly allows an exception
- Event handlers in stories must use `@storybook/addon-actions` (`action(...)`) β€” never invent a different helper
- Never use inline no-op handlers such as `() => undefined` in story args
- Token styling in stories must use Tailwind utility classes or reusable semantic classes from the style system; do not bypass them with `[var(--token)]` or inline `var()` in story/component source
- Story conventions must match the canonical repo pattern for autodocs, controls, and actions
- NO `play` functions β€” interaction testing belongs in `.test.tsx`

```typescript
import type { Meta, StoryObj } from '@storybook/react'
Expand Down Expand Up @@ -377,12 +471,11 @@ export const Disabled: Story = {
}
```

### File 5: `index.ts`
### File 6: `index.ts`

```typescript
import ComponentName from './ComponentName'
export { ComponentName } from './ComponentName'
export * from './types'
export default ComponentName
```

---
Expand Down Expand Up @@ -457,10 +550,11 @@ Fix all CRITICAL and MAJOR before marking the component complete.
## Checklist before finishing

**Structure**
- [ ] `types.ts` β€” all props typed, all CVA variants defined, JSDoc controls present
- [ ] `types.ts` β€” all props typed, all CVA variants defined, full JSDoc present, JSDoc controls present where needed
- [ ] `useComponentName.ts` β€” all logic, no JSX, returns typed object
- [ ] `ComponentName.tsx` β€” only JSX, consumes hook, no logic
- [ ] `ComponentName.stories.tsx` β€” Default + Disabled + variant stories, English, description present, no overriding defaultVariants in Default args
- [ ] `ComponentName.test.tsx` β€” complete test suite (hook tests with renderHook + component tests with render/screen/userEvent)
- [ ] `ComponentName.stories.tsx` β€” Default + Disabled + variant stories, English, description present, no overriding defaultVariants in Default args, canonical autodocs/actions conventions followed, NO play functions
- [ ] `index.ts` β€” re-exports correct

**Tokens & theming**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,10 @@ A PR will be rejected WITHOUT detailed feedback if:
A PR is ready to merge when:

- [ ] All CI checks pass (tests, build, a11y)
- [ ] 5-file architecture followed exactly
- [ ] 6-file architecture followed exactly
- [ ] No `any`, no `interface`
- [ ] Tests cover hook logic + component behavior
- [ ] Story has args, controls, and description
- [ ] Tests in `.test.tsx` cover hook logic + component behavior
- [ ] Story has args, controls, and description (NO play functions)
- [ ] No hardcoded values β€” only design tokens
- [ ] ARIA attributes present and correct
- [ ] PR template fully filled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const Dropdown: FC<DropdownProps> = ({ ...props }) => {
role='menu'
aria-labelledby={accessibleLabelId}
className={cn(
'min-w-[8rem] rounded-md border p-1',
'min-w-32 rounded-md border p-1',
'bg-surface-light dark:bg-surface-dark',
// Radix animation data attributes β€” use these, not custom state
'data-[state=closed]:animate-out data-[state=open]:animate-in',
Expand Down Expand Up @@ -98,7 +98,7 @@ Key decisions:
'relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm',
'transition-[background,color] duration-150 ease-[ease]',
'hover:bg-white-tint-mid',
'focus-visible:outline-none focus-visible:shadow-[var(--glow-focus-dark)]'
'focus-visible:outline-none focus-visible:shadow-(--glow-focus-dark)'
)}
onClick={element.onClick}
>
Expand Down Expand Up @@ -224,6 +224,6 @@ import * as SelectPrimitive from '@radix-ui/react-select';
- [ ] Radix imported as namespace alias (`* as XxxPrimitive`)
- [ ] Floating content wrapped in `Portal`
- [ ] Trigger uses `asChild={true}`
- [ ] Focus ring uses `focus-visible:shadow-[var(--glow-focus-dark)]` β€” no outline
- [ ] Focus ring uses reusable glow classes such as `focus-visible:shadow-glow-focus-light dark:focus-visible:shadow-glow-focus-dark` β€” no outline
- [ ] Animations use `data-[state=open/closed]` attributes
- [ ] All open/close logic lives in the hook
Loading
Loading