|
1 | | -# Equinor Design System (EDS) |
| 1 | +# Claude Code — Equinor Design System |
2 | 2 |
|
3 | | -This file provides guidance for Claude Code working in this repository. |
| 3 | +> **The canonical conventions for this repository live in [`AGENTS.md`](../AGENTS.md).** |
| 4 | +> Read that file for component structure, code style, CSS patterns, testing, accessibility, and conventional commits. |
| 5 | +> |
| 6 | +> This file holds only Claude-Code-specific configuration that doesn't belong in `AGENTS.md`. |
4 | 7 |
|
5 | | -## Overview |
| 8 | +## Path-Scoped Rules |
6 | 9 |
|
7 | | -Equinor Design System (EDS) is a pnpm monorepo containing React component libraries and design tokens. **New components are developed in `/next`** (`packages/eds-core-react/src/components/next/`). |
| 10 | +Claude Code automatically loads rules in `.claude/rules/` when working on matching files: |
8 | 11 |
|
9 | | -### Key Packages |
| 12 | +| Rule | Scope | Purpose | |
| 13 | +| -------------------- | --------------------------------------------------------------- | ----------------------------------------------- | |
| 14 | +| `eds-component.md` | `packages/eds-core-react/src/components/next/**/*.{tsx,ts,css}` | EDS 2.0 component conventions (short reference) | |
| 15 | +| `figma-component.md` | `packages/eds-core-react/src/components/next/**/*.figma.tsx` | Figma-to-code workflow with MCP tools | |
| 16 | +| `advisor.md` | General | Read-only code review guidelines | |
10 | 17 |
|
11 | | -- `@equinor/eds-core-react` - Main React component library |
12 | | -- `@equinor/eds-core-react/next` - New EDS 2.0 components (active development) |
13 | | -- `@equinor/eds-tokens` - Design tokens, CSS variables, and theming |
14 | | -- `@equinor/eds-icons` - Icon library |
| 18 | +The path-scoped rules are intentionally short — they reference [`AGENTS.md`](../AGENTS.md) rather than duplicating it. |
15 | 19 |
|
16 | | -## Build/Lint/Test Commands |
| 20 | +## Slash Commands |
17 | 21 |
|
18 | | -Package manager: `pnpm@10.15.0` |
| 22 | +User-invokable prompts triggered with `/command-name`. |
19 | 23 |
|
20 | | -```bash |
21 | | -pnpm run build # Build all packages |
22 | | -pnpm run build:core-react # Build eds-core-react only |
23 | | -pnpm run lint:all # Lint entire codebase |
24 | | -pnpm run lint ./path/to/file.tsx # Lint specific file |
| 24 | +| Command | Usage | Description | |
| 25 | +| ----------------------- | ------------------------------------- | -------------------------------------------------------- | |
| 26 | +| `/new-component` | `/new-component Button` | Scaffold a new EDS 2.0 component with all required files | |
| 27 | +| `/create-component-doc` | `/create-component-doc <raw content>` | Restructure raw content into component documentation | |
25 | 28 |
|
26 | | -pnpm run test:core-react # Run eds-core-react tests |
27 | | -pnpm run test:watch:core-react # Watch mode |
| 29 | +## Hooks |
28 | 30 |
|
29 | | -# Run a single test file (from package directory) |
30 | | -cd packages/eds-core-react |
31 | | -pnpm test -- --testPathPattern="Icon" |
| 31 | +Configured in `.claude/settings.json`. Scripts live in `.claude/hooks/`. |
32 | 32 |
|
33 | | -pnpm run storybook # Start Storybook |
34 | | -``` |
| 33 | +| Hook | Event | Matcher | Purpose | |
| 34 | +| ---------------- | ------------- | --------------------------------------------------- | ---------------------------------------------- | |
| 35 | +| `read_hook.js` | `PreToolUse` | `Read\|Grep\|Glob\|Bash\|Edit\|Write\|NotebookEdit` | Blocks access to `.env` and other secret files | |
| 36 | +| `format_hook.js` | `PostToolUse` | `Edit\|Write` | Runs ESLint+Prettier auto-fix on edited files | |
35 | 37 |
|
36 | | -## Component File Structure (EDS 2.0) |
| 38 | +See [`.claude/README.md`](./README.md) for hook authoring details. |
37 | 39 |
|
38 | | -New components go in `packages/eds-core-react/src/components/next/`: |
| 40 | +## Settings |
39 | 41 |
|
40 | | -``` |
41 | | -ComponentName/ |
42 | | - index.ts # Named exports only |
43 | | - ComponentName.tsx # Main component with forwardRef |
44 | | - ComponentName.types.ts # TypeScript types with JSDoc |
45 | | - componentname.css # Vanilla CSS with design tokens |
46 | | - componentName.figma.tsx # Figma Code Connect file for mapping code to Figma props |
47 | | - ComponentName.test.tsx # Jest + Testing Library + jest-axe |
48 | | - ComponentName.stories.tsx |
49 | | -``` |
50 | | - |
51 | | -## Code Style |
52 | | - |
53 | | -### Formatting (Prettier) |
54 | | - |
55 | | -- 2 spaces, no semicolons, single quotes, trailing commas, LF line endings |
56 | | - |
57 | | -### Imports |
58 | | - |
59 | | -```typescript |
60 | | -// 1. React |
61 | | -import { forwardRef, useId } from 'react' |
62 | | -// 2. Types (use `import type`) |
63 | | -import type { ComponentProps } from './Component.types' |
64 | | -// 3. Styles last |
65 | | -import './component.css' |
66 | | -``` |
67 | | - |
68 | | -**No default exports** (except Storybook files). Always use named exports. |
69 | | - |
70 | | -### TypeScript |
71 | | - |
72 | | -```typescript |
73 | | -export type IconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' |
74 | | - |
75 | | -export type IconProps = { |
76 | | - /** Icon data from @equinor/eds-icons */ |
77 | | - data: IconData |
78 | | - /** Title for accessibility - makes icon semantic with role="img" */ |
79 | | - title?: string |
80 | | - /** Explicit size override */ |
81 | | - size?: IconSize |
82 | | -} & Omit<SVGProps<SVGSVGElement>, 'color'> |
83 | | -``` |
84 | | -
|
85 | | -### React Components |
86 | | -
|
87 | | -```typescript |
88 | | -export const Icon = forwardRef<SVGSVGElement, IconProps>(function Icon( |
89 | | - { data, title, color = 'currentColor', size, className, ...rest }, |
90 | | - ref, |
91 | | -) { |
92 | | - const titleId = useId() |
93 | | - |
94 | | - if (!data) { |
95 | | - console.error('Icon: data prop is required') |
96 | | - return null |
97 | | - } |
98 | | - |
99 | | - const classes = ['icon', className].filter(Boolean).join(' ') |
100 | | - |
101 | | - return ( |
102 | | - <svg ref={ref} className={classes} data-icon-size={size} {...rest}> |
103 | | - {title && <title id={titleId}>{title}</title>} |
104 | | - <path d={data.svgPathData} /> |
105 | | - </svg> |
106 | | - ) |
107 | | -}) |
108 | | -``` |
109 | | - |
110 | | -### CSS (Vanilla + Tokens + Nesting) |
111 | | - |
112 | | -One `eds-`-prefixed root class per component. Internal elements use simple class names scoped by CSS nesting. Variants and state use data attributes. |
113 | | - |
114 | | -```css |
115 | | -@layer eds-components { |
116 | | - .eds-icon { |
117 | | - font-size: var(--eds-typography-icon-size, 1.5em); |
118 | | - width: 1em; |
119 | | - height: 1em; |
120 | | - flex-shrink: 0; |
121 | | - |
122 | | - &[data-icon-size='lg'] { |
123 | | - --_explicit-size: var(--eds-sizing-icon-lg); |
124 | | - width: var(--_explicit-size); |
125 | | - height: var(--_explicit-size); |
126 | | - } |
127 | | - } |
128 | | -} |
129 | | -``` |
130 | | - |
131 | | -#### Pseudo-private custom properties |
132 | | - |
133 | | -Define component-scoped variables with a `--_` prefix at the component root. Use these variables for all properties. In variants and states, **override only the variable — never the property directly**. |
134 | | - |
135 | | -```css |
136 | | -/* CORRECT */ |
137 | | -.eds-button { |
138 | | - --_color: var(--eds-color-text-strong-on-emphasis); |
139 | | - --_bg-color: var(--eds-color-bg-fill-emphasis-default); |
140 | | - color: var(--_color); |
141 | | - background-color: var(--_bg-color); |
142 | | -} |
143 | | -.eds-button[data-variant='ghost']:disabled { |
144 | | - --_color: var(--eds-color-text-disabled); /* override the variable */ |
145 | | -} |
146 | | - |
147 | | -/* WRONG */ |
148 | | -.eds-button[data-variant='ghost']:disabled { |
149 | | - color: var(--eds-color-text-disabled); /* never override the property directly */ |
150 | | -} |
151 | | -``` |
152 | | - |
153 | | -#### CSS layers |
154 | | - |
155 | | -Wrap all component styles in `@layer eds-components { }`. Rules outside the layer (e.g. display overrides) must be placed after the layer block with a comment explaining why they are outside. |
156 | | - |
157 | | -#### Data attributes for variants and states |
158 | | - |
159 | | -Use `data-*` attributes for all variants, sizes, and boolean states — not modifier classes. |
160 | | - |
161 | | -```css |
162 | | -.eds-button[data-variant='primary'] { } |
163 | | -.eds-button[data-selectable-space='lg'] { } |
164 | | -.eds-button[data-icon-only] { } |
165 | | -.eds-button[data-round] { } |
166 | | -.eds-button[data-multiline] { } |
167 | | -``` |
168 | | - |
169 | | -#### Density via ancestor attribute |
170 | | - |
171 | | -Density variants are applied by setting `data-density` on an ancestor element. Component CSS selects against this ancestor: |
172 | | - |
173 | | -```css |
174 | | -[data-density='comfortable'] .eds-button[data-selectable-space='md'] { |
175 | | - --_min-height: 1.5rem; |
176 | | -} |
177 | | -``` |
178 | | - |
179 | | -#### Modular type scale |
180 | | - |
181 | | -Font sizes follow a mathematical scale based on a `--_base` value: |
182 | | - |
183 | | -```css |
184 | | -:root, [data-density='spacious'] { |
185 | | - --_base: 16px; |
186 | | - --font-size-md: round(calc(var(--_base) * pow(2, -1/5)), 0.5px); |
187 | | -} |
188 | | -[data-density='comfortable'] { |
189 | | - --_base: 14px; /* only the base changes; all derived values update automatically */ |
190 | | -} |
191 | | -``` |
192 | | - |
193 | | -#### Progressive enhancement with `@supports` |
194 | | - |
195 | | -Use `@supports` to layer in advanced CSS features. The base styles work everywhere; the `@supports` block adds what only supported browsers can handle: |
196 | | - |
197 | | -```css |
198 | | -/* Base — all browsers: symmetric padding keeps text vertically centred */ |
199 | | -padding-block: var(--eds-selectable-space-vertical); |
200 | | - |
201 | | -/* Enhancement — trims whitespace above/below the cap-height */ |
202 | | -@supports (text-box: trim-both ex alphabetic) { |
203 | | - padding-top: var(--padding-top-baseline); |
204 | | - padding-bottom: 0; |
205 | | - text-box: trim-both ex alphabetic; |
206 | | -} |
207 | | -``` |
208 | | - |
209 | | -CSS `@function` (Chrome/Edge 128+) is a future enhancement — define it and comment it in once Safari ships support. |
210 | | - |
211 | | -### Testing |
212 | | - |
213 | | -Jest + Testing Library. Organize tests by category with `describe` blocks: |
214 | | - |
215 | | -```typescript |
216 | | -import { render, screen } from '@testing-library/react' |
217 | | -import { axe } from 'jest-axe' |
218 | | -import { Icon } from '.' |
219 | | - |
220 | | -describe('Icon (next)', () => { |
221 | | - describe('Rendering', () => { |
222 | | - it('renders with data prop', () => { |
223 | | - render(<Icon data={save} />) |
224 | | - expect(screen.getByTestId('eds-icon')).toBeInTheDocument() |
225 | | - }) |
226 | | - }) |
227 | | - |
228 | | - describe('Accessibility', () => { |
229 | | - it('is decorative (aria-hidden) when no title', () => { |
230 | | - render(<Icon data={save} />) |
231 | | - expect(screen.getByTestId('eds-icon')).toHaveAttribute('aria-hidden', 'true') |
232 | | - }) |
233 | | - |
234 | | - it('passes axe accessibility test', async () => { |
235 | | - const { container } = render(<Icon data={save} title="Save" />) |
236 | | - expect(await axe(container)).toHaveNoViolations() |
237 | | - }) |
238 | | - }) |
239 | | -}) |
240 | | -``` |
241 | | - |
242 | | -Query priority: `getByRole` > `getByLabelText` > `getByText` > `getByTestId` |
243 | | - |
244 | | -### Naming Conventions |
245 | | - |
246 | | -- **Components/Types**: PascalCase (`Button`, `ButtonProps`) |
247 | | -- **Variables/Functions**: camelCase (`isDisabled`, `useToken`) |
248 | | -- **CSS classes**: `eds-` prefix on root class (`eds-button`, `eds-text-area`); simple nested names for internal elements (`.label-row`, `.icon`); variants via data attributes |
249 | | -- **Files**: Match export (`Icon.tsx`, `Icon.types.ts`, `icon.css`) |
250 | | - |
251 | | -## Polymorphism (`asChild` + `Slot`) |
252 | | - |
253 | | -EDS 2.0 uses the `asChild` pattern for components that need polymorphic rendering (e.g. Link, Button). This lets consumers swap the underlying element for router links, custom components, etc. |
254 | | - |
255 | | -- **`Slot`** utility in `packages/eds-core-react/src/components/next/Slot/` merges parent props onto the child element |
256 | | -- Add `asChild?: boolean` to the component's props type |
257 | | -- When `asChild` is true, render `<Slot>` instead of the default element |
258 | | -- Extract shared props into a `sharedProps` object to avoid duplication |
259 | | -- See `Slot/README.md` for merge behavior details and usage examples |
260 | | - |
261 | | -Components that should support `asChild`: **Link**, **Button**, and any component rendering an interactive element that consumers may want to swap. |
262 | | - |
263 | | -## Accessibility |
264 | | - |
265 | | -- WCAG 2.1 AA compliance required |
266 | | -- Decorative elements: `aria-hidden="true"` |
267 | | -- Semantic elements: `role="img"` with `aria-labelledby` |
268 | | -- Test with `jest-axe` in every component |
269 | | - |
270 | | -## Conventional Commits |
271 | | - |
272 | | -``` |
273 | | -type(scope): description |
274 | | -``` |
275 | | - |
276 | | -**Types**: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert` |
277 | | - |
278 | | -**Scopes (packages)**: `eds-core-react`, `eds-data-grid-react`, `eds-icons`, `eds-lab-react`, `eds-tailwind`, `eds-tokens`, `eds-tokens-build`, `eds-tokens-sync`, `eds-utils`, `design-system-docs`, `eds-color-palette-generator`, `eds-demo`, `figma-broker` |
279 | | - |
280 | | -**Scopes (infrastructure)**: `config`, `github`, `build`, `deps`, `docs`, `devcontainer` |
281 | | - |
282 | | -**Breaking**: `feat(eds-core-react)!: remove deprecated prop` |
283 | | - |
284 | | -**Scope and release-please interaction**: Release-please detects packages from file paths — you don't always need a package scope. Using a package scope with a visible type forces a bump regardless of which files changed. For non-publishable changes (config, Storybook, tests, README, docs), use hidden types: `chore`, `build`, `ci`, `docs`, or `test`. |
285 | | - |
286 | | -**PR titles** must also follow the conventional commits format — they appear in changelogs and merge history. |
287 | | - |
288 | | -See `documentation/how-to/CONVENTIONAL_COMMITS.md` for full guidelines. |
| 42 | +- `.claude/settings.json` — shared, checked into the repo (permission denies, hooks) |
| 43 | +- `.claude/settings.local.json` — personal, gitignored (per-developer overrides) |
289 | 44 |
|
290 | 45 | ## Git Workflow |
291 | 46 |
|
292 | | -⚠️ **CRITICAL: Always ask the user for permission before:** |
293 | | - |
294 | | -- Creating commits |
295 | | -- Pushing to remote |
296 | | -- Creating branches |
297 | | -- Creating PRs with `gh` |
298 | | - |
299 | | -**Never assume these actions are okay.** Even for small changes, always confirm with the user first. Example: "Ready to commit. Should I proceed?" |
300 | | - |
301 | | -NEVER attribute AI, or add to the commit message "Co-authored by Claude" or similar. |
302 | | - |
303 | | -## Additional Guidelines |
304 | | - |
305 | | -See `.github/copilot-instructions.md` and `.github/instructions/` for detailed guidelines. |
306 | | - |
307 | | -## Specialized Rules |
308 | | - |
309 | | -Path-specific rules are available in `.claude/rules/`: |
310 | | - |
311 | | -- `eds-component.md` - EDS 2.0 component building conventions |
312 | | -- `figma-component.md` - Figma-to-code workflow with MCP tools |
313 | | -- `advisor.md` - Read-only code review guidelines |
| 47 | +See the **Git Workflow** section in [`AGENTS.md`](../AGENTS.md) — the rules apply to Claude Code as well. |
0 commit comments