Skip to content

Commit d966a28

Browse files
docs(agents): restructure agent instructions into routing tiers (#1017)
1 parent 579fc05 commit d966a28

12 files changed

Lines changed: 616 additions & 63 deletions

File tree

.claude/rules/component-css.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
paths:
3+
- "packages/components/src/**/*.module.css"
4+
---
5+
6+
# Component CSS
7+
8+
[ADR 0004](../../docs/adr/0004-design-tokens-are-the-only-source-of-visual-values.md) and
9+
[ADR 0005](../../docs/adr/0005-styling-uses-style-props-and-native-css.md) own the reasoning.
10+
11+
## Hard Rules
12+
13+
| Rule | Violation |
14+
| ---- | --------- |
15+
| Resolve every colour to a **semantic** token | A hex, `rgb()`, `hsl()`, or named colour |
16+
| Resolve a local declaration to a token, not a raw length | `--hop-MenuItem-sm-padding-block: 0.625rem` instead of a `--hop-space-*` token |
17+
| Prefer a semantic token over a core one — component CSS references no core colour today | Reaching past `--hop-neutral-text` to a core palette entry like `--hop-coastal-25` |
18+
| Read `--hop-comp-<family>-*` only from a module in that family | `list-box/src/ListBoxItem.module.css` reading `--hop-comp-select-*` |
19+
| Redefine a token-package property only inside `packages/tokens` | Overriding `--hop-comp-button-*` from another component's module |
20+
| Locate a token file by grepping its `comp-` key, not by component name | Expecting `checkbox.tokens.json`; the file is `mark.checkbox.tokens.json` |
21+
| Declare a local on the module root when the component has no token file | Adding a 22nd token file for a one-off value |
22+
23+
Stylelint owns the mechanical layer — `px` is outside `unit-allowed-list`, `selector-class-pattern`
24+
enforces `hop-ComponentName__element-name--modifier-name`, and `custom-property-pattern` enforces
25+
`hop-ComponentName-*`. Because `px` already fails at lint, the live failure mode is a `rem` literal
26+
that lint permits; converting `8px` to `0.5rem` is not tokenizing it.
27+
28+
## Sanctioned raw values
29+
30+
| Case | Why |
31+
| ---- | --- |
32+
| Hairlines as `0.0625rem` — 21 in component CSS | There is no border-width token family |
33+
| `--hop-easing-*` referenced directly | Motion has core tokens but no semantic tier |
34+
| Focus rings | There is no global `--hop-focus-ring` token. Set `outline: none` in the base rule and restore it under `[data-focus-visible]`, through a local `--hop-<Component>-focus-ring-color` pointing at `--hop-primary-border-focus` or the family's `--hop-comp-*-border-color-focus` |
35+
| Breakpoint values | Deliberately not tokens — custom properties cannot be used in media query conditions. The scale lives in `packages/styled-system/src/responsive/Breakpoints.ts` |
36+
37+
## Token families are shared
38+
39+
Only 21 token files exist per brand against 90 CSS modules, and five families have no single owning
40+
component — `field` (read by 12 modules), `mark`, `control`, `select`, `tabs`. So the read boundary is
41+
the *family*, not the file. Reading across families is the violation. The three modules in `calendar/`
42+
and `date-picker/` that override `--hop-comp-button-*` are known debt, not a pattern to copy.
43+
44+
## The three layers
45+
46+
```css
47+
.hop-Button {
48+
--hop-Button-text-font: var(--hop-comp-button-text-font);
49+
--hop-Button-column-gap: var(--hop-space-inline-xs);
50+
}
51+
```
52+
53+
| Layer | Defined in | Read from |
54+
| ----- | ---------- | --------- |
55+
| `--hop-comp-<family>-*` | `packages/tokens/src/tokens/components/<brand>/*.tokens.json` | Modules in that family |
56+
| `--hop-<category>-*` | `packages/tokens/src/tokens/core/`, `.../semantic/<brand>/<light\|dark>/` | Any module, semantic tier first |
57+
| `--hop-<PascalName>-*` | The module's own root selector | That module — unless deliberately published as a theming hook, as `--hop-RichIcon-*` is for `packages/icons` |
58+
59+
Sibling modules inherit wholesale with `composes: hop-Input from "../../inputs/src/Input.module.css"`,
60+
which pulls in the other module's class *and* its locals. Check what a `composes:` target declares
61+
before adding a local that may already exist there.

.claude/rules/component-tsx.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
paths:
3+
- "packages/components/src/**/src/*.tsx"
4+
---
5+
6+
# Component TSX
7+
8+
## Hard Rules
9+
10+
| Rule | Violation |
11+
| ---- | --------- |
12+
| Pass `className` and semantic props (`size`, `variant`, `color`, `slot`, `isHidden`) through a context object; leave raw styled-system CSS props out | `[HeadingContext, { fontWeight: "…", padding: "…" }]` |
13+
| Reach for the semantic element or the React Aria component | A `div` with `onClick` and `role="button"`, which is not equivalent |
14+
| Give an icon-only control an accessible name, and guard it at runtime as `Button`, `ToggleButton`, `Tabs` and `RichIconAvatarImage` do | An icon-only control types accept but screen readers cannot name |
15+
| Leave `tabIndex` at 0 or -1 | `tabIndex={1}` — the repo is currently fully compliant |
16+
| Express visual values through tokens, not a hand-written inline `style` object | A literal `style={{ padding: 8 }}` |
17+
| Read browser globals inside an effect, a memo, or behind `useIsSSR()` | `window.matchMedia(…)` in a component body, as `SegmentedControlItem.tsx:63` still does |
18+
| Take `useId` from `react-aria` | `import { useId } from "react"` |
19+
| Export each component in its own statement | `export { _ComboBox as ComboBox, ListBoxItem as ComboBoxItem }` |
20+
21+
Grouped exports make `react-docgen-typescript` attribute one component's props to another, corrupting
22+
the generated documentation — see `contributing/components.md`. All 101 exports are currently clean;
23+
the rule is a regression guard. Assign first, then export:
24+
25+
```tsx
26+
export const ComboBoxItem = ListBoxItem;
27+
export { _ComboBox as ComboBox };
28+
```
29+
30+
Prefer a React Aria Component over its hooks; drop to the hooks only when the component API is too
31+
rigid, and record why, as `TooltipTrigger` does in a code comment.
32+
33+
## Server rendering
34+
35+
71 of 116 components have a `tests/vitest/<Name>.ssr.test.tsx`. Add one with every new component:
36+
37+
```tsx
38+
/**
39+
* @vitest-environment node
40+
*/
41+
import { renderToString } from "react-dom/server";
42+
43+
import { Button } from "../../src/Button.tsx";
44+
45+
describe("Button", () => {
46+
it("should render on the server", () => {
47+
const renderOnServer = () => renderToString(<Button>Cutoff</Button>);
48+
49+
expect(renderOnServer).not.toThrow();
50+
});
51+
});
52+
```

.claude/rules/package-json.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
paths:
3+
- "packages/**/package.json"
4+
---
5+
6+
# Package Dependencies
7+
8+
These five packages are published and ship inside our products' bundles, so every runtime dependency
9+
is downloaded by every end user — including those on low-bandwidth connections.
10+
11+
## Hard Rules
12+
13+
| Rule | Violation |
14+
| ---- | --------- |
15+
| Reach for React, react-aria, TypeScript or CSS before adding a runtime dependency | Adding `dayjs` or `date-fns` for one format call |
16+
| Write a small local utility when the need is narrow | Depending on a utility library for one function |
17+
| Keep React Aria as the only headless primitive library | Adding Radix, Headless UI or Ark |
18+
| Keep styling in style props and native CSS | Adding `styled-components`, `emotion`, `stitches` or Tailwind |
19+
| List a package under `dependencies` only when consumers load it at runtime, or when its types are part of the published API | A build-only plugin in `dependencies` |
20+
| Keep `react-aria` and `react-aria-components` as peer dependencies; `@react-aria/*` and `@react-stately/*` are direct runtime deps | Promoting a peer to a runtime dependency |
21+
| Declare a package's own build and type tooling in that package | Putting `rslib` or `@types/react` only at the root |
22+
| Leave the repo-wide runners at the root and invoke them from there | Adding `vitest` or `stylelint` to a package |
23+
| Move the React Aria versions with `pnpm update-react-aria-deps` | Bumping one `@react-aria/*` package by hand |
24+
| Keep a shared dependency on one version across the workspace; `pnpm syncpack` is the check | Two packages pinned to different react-aria versions |
25+
26+
Two lockfile entries look like violations and are not: `@stitches/core` arrives transitively through
27+
Sandpack, and `SizingMapping` borrows Tailwind's fraction scale as a value table, not an adoption.
28+
29+
## What is already allowed
30+
31+
The react-aria ecosystem is the existing stack, not a new dependency — `@internationalized/date`,
32+
`@react-aria/*`, `@react-stately/*` and `@react-types/shared` are all in scope. Two more are
33+
grandfathered and need no justification:
34+
35+
| Package | Why it stays |
36+
| ------- | ------------ |
37+
| `clsx` | Used at 78 sites across components, icons and styled-system |
38+
| `csstype` | Types-only, but they are part of styled-system's published API surface |
39+
40+
`svg-icons` and `tokens` have zero runtime dependencies. Keep it that way.
41+
42+
## What syncpack enforces
43+
44+
Beyond a single version repo-wide, `.syncpackrc.js` also enforces range *style*: `^` for published
45+
prod and peer ranges, pinned everywhere else. Two groups are deliberately exempt — `@hopper-ui/*`
46+
prod and peer ranges, and the `react` / `react-dom` peer ranges. Leave both alone.

0 commit comments

Comments
 (0)