@imfusion/web-ui 0.6.1-dev.12.ge86ac0a1 → 0.6.1-dev.14.g8fac1dfb

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +102 -170
  2. package/dist/{code-Blo48PGr.js → code-C_56u-Vk.js} +2 -2
  3. package/dist/{icons-wBmF0U2x.js → icons-Cy1HAosO.js} +1 -1
  4. package/dist/icons.js +1 -1
  5. package/dist/index.js +31 -31
  6. package/dist/integrations/code-highlight.js +2 -2
  7. package/dist/integrations/image-display-options.js +1 -1
  8. package/package.json +1 -1
  9. package/src/llms/install-templates/AGENTS.md +15 -18
  10. package/src/llms/llms.gen.txt +33 -33
  11. package/src/llms/skills/imf-web-ui/SKILL.md +29 -39
  12. package/src/llms/skills/imf-web-ui-audit/SKILL.md +50 -102
  13. package/src/llms/skills/imf-web-ui-components/SKILL.md +47 -104
  14. package/src/llms/skills/imf-web-ui-conventions/SKILL.md +39 -52
  15. package/src/llms/skills/imf-web-ui-conventions/templates/AUDIT_CHECKLIST.md +1 -0
  16. package/src/llms/skills/imf-web-ui-conventions/topics/agent-tooling.md +40 -62
  17. package/src/llms/skills/imf-web-ui-conventions/topics/assets.md +11 -12
  18. package/src/llms/skills/imf-web-ui-conventions/topics/authentication.md +31 -46
  19. package/src/llms/skills/imf-web-ui-conventions/topics/class-names.md +18 -23
  20. package/src/llms/skills/imf-web-ui-conventions/topics/components.md +20 -69
  21. package/src/llms/skills/imf-web-ui-conventions/topics/data.md +50 -146
  22. package/src/llms/skills/imf-web-ui-conventions/topics/docs-structure.md +17 -23
  23. package/src/llms/skills/imf-web-ui-conventions/topics/git.md +15 -20
  24. package/src/llms/skills/imf-web-ui-conventions/topics/library-boundary.md +28 -19
  25. package/src/llms/skills/imf-web-ui-conventions/topics/library-setup.md +20 -16
  26. package/src/llms/skills/imf-web-ui-conventions/topics/npm-project.md +28 -42
  27. package/src/llms/skills/imf-web-ui-conventions/topics/project-structure.md +28 -30
  28. package/src/llms/skills/imf-web-ui-conventions/topics/react.md +28 -74
  29. package/src/llms/skills/imf-web-ui-conventions/topics/styling.md +65 -62
  30. package/src/llms/skills/imf-web-ui-conventions/topics/testing.md +12 -14
  31. package/src/llms/skills/imf-web-ui-conventions/topics/tokens.md +9 -4
  32. package/src/llms/skills/imf-web-ui-conventions/topics/tooling.md +40 -68
  33. package/src/llms/skills/imf-web-ui-conventions/topics/typescript.md +26 -50
  34. package/src/llms/skills/imf-web-ui-conventions/topics/validation.md +19 -25
  35. package/src/llms/skills/imf-web-ui-setup/SKILL.md +43 -64
  36. package/src/llms/skills/imf-web-ui-update/SKILL.md +48 -114
  37. package/src/llms/skills/imf-web-ui-ux/SKILL.md +64 -92
  38. package/src/llms/skills/imf-web-ui-ux/references/forms.md +16 -36
  39. package/src/llms/skills/imf-web-ui-ux/references/usability-heuristics.md +14 -27
  40. package/src/llms/skills/imf-web-ui-ux/references/visual-design.md +22 -38
@@ -1,82 +1,60 @@
1
1
  # Agent tooling
2
2
 
3
- How a consumer repo carries the `@imfusion/web-ui` agent tooling; `npx web-ui-install` does the mechanics, this topic carries
4
- the judgment.
3
+ `npx web-ui-install` manages the Web UI skills and optional lifecycle hooks in a consumer project. This topic explains the
4
+ choices around that installer.
5
5
 
6
6
  ## The skill bundle
7
7
 
8
- - `npx web-ui-install` installs or refreshes the vendored `imf-web-ui-*` skills, refreshes the
9
- `<!-- imf-web-ui:begin/end -->` fence in an existing `AGENTS.md`, and removes skills dropped from the bundle.
10
- - Target: remembered first-run choice — `.claude/skills/`, vendor-neutral `.agents/skills/`, or both (`.agents/` real copy,
11
- `.claude/` symlink); `--reconfigure` re-opens it, `--target claude|agents` selects non-interactively.
12
- - Staleness: per-skill `.imf-web-ui-skill-version.json`; a marker older than the installed package means re-run the binary —
13
- never hand-diff or hand-edit vendored skill contents.
8
+ Run:
9
+
10
+ ```sh
11
+ npx web-ui-install
12
+ ```
13
+
14
+ It installs or refreshes the `imf-web-ui-*` skills, updates the managed `AGENTS.md` fence, and removes skills that the
15
+ package no longer ships. The installer remembers `.claude/skills/`, `.agents/skills/`, or both. Use `--target claude|agents`
16
+ to choose explicitly and `--reconfigure` to choose again.
17
+
18
+ Skill version markers (`.imf-web-ui-skill-version.json`) show which package version installed each skill. Refresh them with
19
+ the installer; do not hand-edit the vendored files.
14
20
 
15
21
  ## The hooks
16
22
 
17
- - `npx web-ui-install --hooks` adds three injection hooks and a turn-end gate.
18
- - Scripts in `.agents/hooks/imf-web-ui/` are installer-owned: refreshed wholesale each run, retired scripts pruned with their
19
- registrations.
20
- - Registrations merge idempotently into `.claude/settings.json` (Claude Code) and `.codex/hooks.json` (Codex), never touching
21
- entries the installer didn't write.
22
-
23
- - **SessionStart** — once per session: points the agent at the `imf-web-ui` skills router.
24
- - **SubagentStart** — the same line, byte for byte, for each spawned subagent; subagents don't reliably inherit the parent
25
- session's context.
26
- - **UserPromptSubmit** — one line per prompt naming the companion skills to consult.
27
- - **Stop** — the verify gate: when the turn edited source files, it blocks the agent from finishing once, with the
28
- instruction to run the project's verification and fix what it reports. Re-entry is detected from the payload, so the gate
29
- can never loop; a turn that edited nothing passes untouched.
30
-
31
- ```mermaid
32
- flowchart LR
33
- SS(["SessionStart<br/>once per session"]) --> ss["session-start.sh"]
34
- SA(["SubagentStart<br/>per spawned subagent"]) --> sa["subagent-start.sh"]
35
- UP(["UserPromptSubmit<br/>every prompt"]) --> up["user-prompt-submit.sh"]
36
- ST(["Stop<br/>turn ends after edits"]) --> st["stop.sh"]
37
- ss --> router["injects the router pointer:<br/>start at imf-web-ui"]
38
- sa --> router
39
- up --> hints["injects the per-task hints:<br/>components · conventions · ux"]
40
- st --> gate["blocks once:<br/>run verification first"]
41
- router --> agent["agent routes to the right<br/>companion skill"]
42
- hints --> agent
43
- gate --> agent2["agent verifies,<br/>then finishes"]
23
+ Install the optional lifecycle hooks with:
24
+
25
+ ```sh
26
+ npx web-ui-install --hooks
44
27
  ```
45
28
 
46
- - Each injection hook echoes one fixed line and exits 0; the router does the task-sorting a shell script can't — why a hint
47
- on every prompt isn't noise.
48
- - `baseline-staleness.sh` ships alongside but is not an agent hook; the repo's pre-commit calls it ([git.md](./git.md),
49
- staleness at commit time).
50
- - Never edit an installed script — the next install overwrites it; adapt in the repo's own hooks.
51
- - Registration entries are yours: the installer matches by script name and keeps edited commands across re-runs. Monorepo:
52
- the Codex commands anchor at `$(git rev-parse --show-toplevel)` — adjust their paths once after installing when the
53
- frontend isn't the git toplevel.
54
- - **Read the registration files before installing**; per event: nothing registered → install as shipped; already covered by
55
- the repo (its own session reminder, say) → don't stack a second hook — fold the missing line into the repo's script, or
56
- adapt the shipped one and register that; surface it and let the human pick.
29
+ The installer owns the scripts under `.agents/hooks/imf-web-ui/` and merges its registrations into Claude Code and Codex
30
+ settings. It refreshes scripts and removes retired registrations without replacing unrelated entries.
31
+
32
+ The shipped events are:
33
+
34
+ - `SessionStart`: point the agent at the Web UI router once per session.
35
+ - `SubagentStart`: provide the same pointer to spawned agents.
36
+ - `UserPromptSubmit`: name the companion skills relevant to the prompt.
37
+ - `Stop`: ask for project verification after a turn edits source files.
38
+
39
+ Read existing registrations first. If the project already covers an event, do not stack a second hook; adapt the existing
40
+ registration and keep the missing behavior.
41
+
42
+ `baseline-staleness.sh` is a pre-commit check, not an agent hook. It compares installed skill markers with the installed
43
+ package version.
57
44
 
58
45
  ## Codex trust
59
46
 
60
- - Codex runs a project's hooks only when two trust gates hold: the project itself is trusted, and each hook script has been
61
- trusted via the `/hooks` review, which keys trust to the script's hash.
62
- - Until then hooks are skipped, and skipped silently — from outside, a skipped hook and a hook that ran and said nothing look
63
- identical.
64
- - After installing or updating hooks, tell the user to trust the project and review `/hooks` in Codex, and again after any
65
- hook script changes.
66
- - When a hint doesn't show up, check trust before debugging the script.
67
- - Claude Code has no trust gate for project hooks.
47
+ Codex runs project hooks only after the project is trusted and each script is approved through `/hooks`. Revisit that review
48
+ after a hook script changes. Claude Code has no equivalent project-hook trust gate.
68
49
 
69
50
  ## Dependency-shipped skills
70
51
 
71
- - npm packages can ship Agent Skills of their own; TanStack does — [tooling.md](./tooling.md) covers TanStack Intent and its
72
- allowlist.
52
+ Dependencies can ship their own skills. TanStack is one example; its docs and allowlist determine how it is used.
73
53
 
74
54
  ## Hook docs
75
55
 
76
- Both hosts move fast; read the current references before adapting or adding a hook — payloads and stdout rules differ per
77
- host and per event.
56
+ Read the current host documentation before adding or adapting a hook:
78
57
 
79
- - [Claude Code: hooks reference](https://code.claude.com/docs/en/hooks) — event list, JSON input and output, exit codes,
80
- `disableAllHooks`
81
- - [Codex: hooks](https://learn.chatgpt.com/docs/hooks) — events, the `hooks.json` schema, trust, `/hooks`
82
- - [Codex: config reference](https://learn.chatgpt.com/docs/config-file/config-reference) — `[features]` and `[hooks.state]`
58
+ - [Claude Code hooks](https://code.claude.com/docs/en/hooks)
59
+ - [Codex hooks](https://learn.chatgpt.com/docs/hooks)
60
+ - [Codex configuration](https://learn.chatgpt.com/docs/config-file/config-reference)
@@ -2,26 +2,25 @@
2
2
 
3
3
  ## Importing
4
4
 
5
- - Import everything from `src/assets/` so the bundler fingerprints and bundles it. Never reference an image by public-path
6
- string.
5
+ Import files from `src/assets/` so the bundler fingerprints and includes them. Do not reference an asset through a
6
+ public-path string.
7
7
 
8
- ## Photographs — WebP
8
+ ## Photographs: WebP
9
9
 
10
- ```bash
10
+ Convert photographs to WebP before adding them:
11
+
12
+ ```sh
11
13
  magick source.png -resize 2000x -quality 80 -define webp:method=6 src/assets/name.webp
12
14
  ```
13
15
 
14
- - Quality 80 — visually lossless on photos, routinely an order of magnitude smaller.
15
- - `method=6` — densest encoding; a one-off cost at conversion time, so take the smaller file.
16
- - Long edge ≤ 2000px — nothing on the market resolves more in a content image.
17
- - No `<picture>` fallback — WebP is supported everywhere since 2020.
16
+ Keep the long edge at 2000px or less. WebP is supported by the browser floor.
18
17
 
19
18
  ## Other formats
20
19
 
21
- - Alpha, or pixels that must stay exact → PNG, optimized with `oxipng` or `pngquant`.
22
- - Icons, logos, line art → inline SVG component, so it inherits `currentColor` and follows the theme.
20
+ Use PNG when the image needs alpha or exact pixels. Use an inline SVG component for icons, logos, and line art so it inherits
21
+ `currentColor` and follows the theme.
23
22
 
24
23
  ## Scope
25
24
 
26
- - Rules apply to assets as they're added or touched. Existing assets in another format are not findings to sweep — convert
27
- opportunistically.
25
+ Apply these rules to assets you add or touch. Existing files do not need a format migration just because they use another
26
+ format.
@@ -1,65 +1,50 @@
1
1
  # Authentication
2
2
 
3
- One route-protection shape for every frontend; provider, session mechanism, endpoint paths, and login/logout transport stay
4
- project-owned — no provider is prescribed or named.
3
+ Use one route boundary for authentication. The provider, session mechanism, endpoint paths, and login/logout transport stay
4
+ owned by the application.
5
5
 
6
6
  ## Starter shape
7
7
 
8
- - Authentication sits at the route-group boundary, before child routes render.
9
- - `_public/` and `_app/`: pathless TanStack Router groups — guards and layouts without `public`/`app` in the URL.
10
- - The authenticated group keeps this shape even without an `AppShell`.
11
-
12
- ```text
13
- src/
14
- api/auth/ # getUser query options, keys.ts, types.ts, index.ts barrel
15
- lib/auth/
16
- login-url.ts # pure login navigation helper; not an API topic file
17
- login-url.test.ts # focused helper tests
18
- routes/
19
- __root.tsx # global Outlet and error/not-found boundaries; no auth guard
20
- _public/route.tsx # anonymous route group
21
- _app/route.tsx # authenticated route group
22
- ```
8
+ - Keep public routes under a pathless `_public/` group.
9
+ - Keep authenticated routes under a pathless `_app/` group.
10
+ - Guard `_app/` before its children render.
11
+ - Keep the root route neutral: it owns the outlet and global error/not-found boundaries.
12
+ - A greenfield app starts `_app/` with a minimal `AppShell` unless it has no persistent authenticated navigation.
13
+
14
+ The file layout is in [project-structure.md](project-structure.md).
23
15
 
24
16
  ## One current-user query
25
17
 
26
- - One identity source of truth: a server-backed current-user query; never copy session credentials or access tokens into
27
- React state.
28
- - The auth topic follows the default [data.md](data.md) shape: `getUser` in `api/auth/queries.ts`, key leading with the
29
- `auth` topic, schema in `types.ts`.
30
- - `lib/auth/login-url.ts`: project-owned navigation helper, not an options factory; tests colocated.
31
- - Reached as `context.api.auth.getUser()`; returns query options, never a hook or a user value — callers pick
32
- `ensureQueryData` or a query hook.
33
- - Never infer authentication from local storage, a decoded token, a route flag, or permission-gated chrome — stale or
34
- forgeable; the server response and its schema define the current user.
18
+ Use one server-backed current-user query as the identity source:
19
+
20
+ - Put it in `api/auth/queries.ts` and define its key and schema with the [data.md](data.md) pattern.
21
+ - Expose query options as `context.api.auth.getUser()`.
22
+ - Call `ensureQueryData` in a loader or a query hook in a component.
23
+ - Keep credentials and access tokens out of React state.
24
+ - Do not infer authentication from local storage, decoded tokens, route flags, or permission-gated UI.
25
+
26
+ The server response, parsed at the network boundary, is the source of truth.
35
27
 
36
28
  ## The two route groups
37
29
 
38
- Both resolve the current-user query in `beforeLoad` when they need the answer; only the meaning of a 401 differs:
30
+ Both groups may resolve the current-user query in `beforeLoad`. They handle a 401 differently:
39
31
 
40
- | Group | 401 means | Result |
41
- | ---------- | ----------------- | ------------------------------------------- |
42
- | `_app/` | not logged in | navigate to the server-owned login endpoint |
43
- | `_public/` | anonymous visitor | continue with `user: null` |
32
+ | Group | 401 result |
33
+ | ---------- | -------------------------------------------- |
34
+ | `_app/` | Navigate to the server-owned login endpoint. |
35
+ | `_public/` | Continue as an anonymous user. |
44
36
 
45
- - The `_app/route.tsx` guard awaits the query before children render, converts only an authentication 401 into login
46
- navigation, rethrows router redirects, and lets 5xx, connection failures, and schema mismatches reach the error boundary.
47
- - A public landing route may redirect an authenticated user into `_app/`.
48
- - The root route stays neutral: global `Outlet` and error/not-found boundaries, no public/authenticated decision.
37
+ The `_app/` guard should navigate only for the authentication 401. Rethrow redirects, server failures, connection failures,
38
+ and schema errors so the normal error boundary can handle them. A public route may redirect an already-authenticated user
39
+ into `_app/`.
49
40
 
50
41
  ## Login and logout
51
42
 
52
- - Follow the project's documented transport. Server-owned flow: login is a browser navigation, not a Query fetch; logout is
53
- the server's documented state-changing action, not an ad-hoc client request.
54
- - Preserve a validated same-origin return path when the server supports returning to the interrupted route.
55
- - Never hard-code an endpoint shape or provider into shared frontend conventions.
56
- - No intermediate login route when the server owns the flow.
57
- - A failed API request is an auth failure only when its typed error is specifically a 401.
43
+ Follow the application's documented server flow. Login is a browser navigation when the server owns the session; it is not a
44
+ Query fetch. Logout uses the server's documented state-changing action. Preserve a validated same-origin return path when the
45
+ server supports one. Do not invent an endpoint shape in this shared baseline.
58
46
 
59
47
  ## App shell
60
48
 
61
- - `AppShell` is authenticated chrome, not the authentication mechanism.
62
- - Greenfield default: propose a minimal shell — ImFusion logo, route navigation, stable session action area around the
63
- `_app/` outlet.
64
- - An explicit no-persistent-navigation decision may omit the shell; the `_app/` guard stays.
65
- - An established project keeps its working choice; public pages may use a small branded header.
49
+ `AppShell` is authenticated chrome, not the authentication mechanism. It can hold the logo, navigation, and session action
50
+ around the `_app/` outlet. An established project keeps its working shell or header choice.
@@ -1,27 +1,22 @@
1
1
  # Class names in components
2
2
 
3
- How a component turns props into a `className` string. What the build does with the result: [tooling.md](tooling.md); what
4
- goes in the stylesheet: [styling.md](styling.md).
3
+ Use CVA to turn design props into CSS Module classes, then merge the caller's `className`.
5
4
 
6
5
  ## CVA is the only tool
7
6
 
8
- - `class-variance-authority` maps variant props to CSS Module classes. No `clsx`, no `tailwind-merge`, no local `cn()`
9
- helper.
10
- - `cx` is CVA's own concatenator, re-exported by `@imfusion/web-ui` — import it from the library alongside the components.
11
- - Base class is CVA's first argument; variant values are CSS Module references, never string literals. A boolean axis uses
12
- `null` for its off-state.
13
- - Defaults live in the props destructuring, not CVA's `defaultVariants`: react-docgen-typescript reads the destructuring, so
14
- defaults declared in CVA don't reach the generated docs.
7
+ - Use `class-variance-authority` for variant axes. Do not add `clsx`, `tailwind-merge`, or a local `cn` helper.
8
+ - Import `cx` from `@imfusion/web-ui` when a component only needs concatenation.
9
+ - Pass CSS Module references to CVA, not string class names.
10
+ - Represent a boolean axis with `false: null` and `true: classes.axisOn`.
11
+ - Put defaults in function destructuring. CVA `defaultVariants` does not reach the generated docs.
15
12
 
16
13
  ```tsx
17
- interface Props extends HTMLAttributes<HTMLSpanElement> {
18
- appearance?: "outline" | "solid";
19
- inline?: boolean;
20
- }
21
-
22
14
  const chip = cva(classes.root, {
23
15
  variants: {
24
- appearance: { outline: classes.appearanceOutline, solid: classes.appearanceSolid },
16
+ appearance: {
17
+ outline: classes.appearanceOutline,
18
+ solid: classes.appearanceSolid
19
+ },
25
20
  inline: { false: null, true: classes.inline }
26
21
  }
27
22
  });
@@ -33,18 +28,18 @@ export function Chip({ appearance = "outline", inline = false, className, ...pro
33
28
 
34
29
  ## Merging `className`
35
30
 
36
- - Every component that accepts `className` merges it; a deliberately closed surface omits the prop entirely rather than
37
- accepting and ignoring it.
38
- - The incoming `className` goes into CVA's `className` slot, which appends it last so a caller's class always wins. With no
39
- variants to map, `cx(classes.inline, className)` does the same job.
40
- - Resolve a function-form `className` before merging — components built on a library that passes render state
41
- (`className={state => …}`) receive either shape:
31
+ A component that accepts `className` must merge it. Put the incoming value in CVA's `className` slot so it is appended after
32
+ the component classes. If there is no CVA configuration, use `cx`.
33
+
34
+ A library render prop may make `className` a function. Resolve it with the render state before merging:
42
35
 
43
36
  ```tsx
44
37
  className={state => cx(classes.root, typeof className === "function" ? className(state) : className)}
45
38
  ```
46
39
 
40
+ A closed component should omit `className`, not accept and ignore it.
41
+
47
42
  ## Shared CVA modules
48
43
 
49
- - Two components sharing one visual share one CVA module, a `{name}.cva.ts` next to them, so their variant axes can't drift
50
- apart.
44
+ When two components share the same visual variant axes, put their CVA call in a `{name}.cva.ts` module next to them. This
45
+ keeps the visual contract in one place.
@@ -1,26 +1,25 @@
1
1
  # Components
2
2
 
3
- How a component lives on disk and how dumb it stays. Roles, state, effects: [react.md](react.md); styling:
3
+ Keep consumer components small and easy to replace. React roles are described in [react.md](react.md); CSS ownership is in
4
4
  [styling.md](styling.md).
5
5
 
6
6
  ## As dumb as possible
7
7
 
8
- - Props-fed, never self-fetching: no query hook, no store access, no route awareness — render what's given, report events
9
- upward.
10
- - Presentational logic (formatting a label, deriving a display state) is fine; owning data is not.
11
- - Local UI state (`isOpen`, a draft value) is allowed; anything whose truth lives elsewhere is not ([react.md](react.md)).
8
+ - Feed components data and callbacks through props.
9
+ - Keep fetching, routing, and business logic in the page or feature layer.
10
+ - Local UI state such as an open state or draft value is fine.
11
+ - Formatting and display-only decisions are fine; owning server state is not.
12
12
 
13
13
  ```tsx
14
- // components/user-card/user-card.tsx
15
- interface Props {
14
+ interface UserCardProps {
16
15
  name: string;
17
16
  onEdit: () => void;
18
17
  }
19
18
 
20
- export function UserCard({ name, onEdit }: Props) {
19
+ export function UserCard({ name, onEdit }: UserCardProps) {
21
20
  return (
22
21
  <Card.Root>
23
- <Typo>{name}</Typo>
22
+ <Typo.P>{name}</Typo.P>
24
23
  <Button onClick={onEdit}>Edit</Button>
25
24
  </Card.Root>
26
25
  );
@@ -29,73 +28,25 @@ export function UserCard({ name, onEdit }: Props) {
29
28
 
30
29
  ## Grouping
31
30
 
32
- - `components/` groups by kind (the component roles in [react.md](react.md)) — a layout component under `layouts/`, not
33
- beside a domain widget.
34
- - Flat is fine while there are few; let grouping follow what the project has, don't impose it up front.
31
+ Group by component role, such as layout or domain UI, according to the project structure. Do not create a hierarchy before
32
+ the project needs one.
35
33
 
36
34
  ## One file or a folder
37
35
 
38
- - More than one file (sub-components, styles, tests) → a folder with an `index.ts` that only re-exports; single-file
39
- components stay single files, directly in their group.
40
- - Imports read `#/components/data-table`, so the inside can be restructured without touching call sites.
36
+ A one-file component can stay in its group. A component with parts, styles, or tests gets a folder with an `index.ts` barrel.
37
+ Call sites import the folder path:
41
38
 
42
- The full anatomy of a folder component:
43
-
44
- ```
39
+ ```text
45
40
  components/data-table/
46
- data-table.tsx # the component: props type + export, CVA variants when it has axes
47
- data-table-row.tsx # sub-component, private unless index.ts re-exports it
48
- data-table.module.css # co-located stylesheet (styling.md)
49
- data-table.test.tsx # only when the component carries decisions worth testing (testing.md)
50
- index.ts # re-exports only
41
+ data-table.tsx
42
+ data-table.module.css
43
+ data-table.test.tsx
44
+ index.ts
51
45
  ```
52
46
 
53
- Most components need only a subset — start with the `.tsx`, add files as they earn their place.
54
-
55
- ```tsx
56
- // data-table.tsx — CVA even with one variant axis, so future axes slot in (class-names.md)
57
- import { cva, type VariantProps } from "class-variance-authority";
58
- import classes from "./data-table.module.css";
59
-
60
- const dataTable = cva(classes.root, {
61
- variants: { density: { comfortable: classes.densityComfortable, compact: classes.densityCompact } }
62
- });
63
-
64
- // Intersection, not `interface extends` — VariantProps is an object type. Defaults live in the destructuring.
65
- type Props = React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof dataTable> & { rows: TableRow[] };
66
-
67
- export function DataTable({ className, density = "comfortable", rows, ...props }: Props) {
68
- return (
69
- <div {...props} className={dataTable({ density, className })}>
70
- {/* … */}
71
- </div>
72
- );
73
- }
74
- ```
75
-
76
- ```css
77
- /* data-table.module.css — native nesting + library tokens, no preprocessor, no @layer (styling.md) */
78
- .root {
79
- border-radius: var(--imf-ui-border-radius-2);
80
- font-size: var(--imf-ui-font-size-1);
81
-
82
- &:focus-visible {
83
- /* states nest under the root */
84
- }
85
- }
86
-
87
- .densityCompact {
88
- /* one class per CVA variant value */
89
- }
90
- ```
91
-
92
- ```ts
93
- // index.ts — re-exports only; callers import #/components/data-table
94
- export { DataTable } from "./data-table";
95
- ```
47
+ The barrel only re-exports the public pieces.
96
48
 
97
49
  ## Colocation
98
50
 
99
- - Stylesheet: `<component>.module.css` next to the component; only dumb components have one — a container that wants CSS is
100
- asking for a layout component instead ([react.md](react.md)).
101
- - Tests and local types sit next to their subject: a file hunted for in a parallel tree gets edited less carefully.
51
+ Keep a component's stylesheet, tests, and local types beside the component. A container that needs styling usually wants to
52
+ be a layout or dumb component instead.