@activatus/code-standards 3.0.2 → 3.0.5

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.
@@ -5,7 +5,7 @@ sidebar_label: "Human-Readable Names"
5
5
 
6
6
  # Human-Readable Names
7
7
 
8
- **Every name we write is made of whole words a person can read out loud.** Variables, function parameters, props, types, constants, form fields, files, folders, routes, database columns, scripts and environment variables — everything. No shortened words, no single letters, no numbered names — not even for a loop counter or an event handler.
8
+ **Every name we write is made of whole words a person can read out loud.** Variables, function parameters, props, types, constants, form fields, files, folders, routes, database columns, scripts and environment variables — everything. No shortened words, no single letters, no numbered names — not even for a loop counter or an event handler. The one exception is [`r` and `s`](#r-and-s).
9
9
 
10
10
  Anyone reading the code should never have to guess what a name stands for.
11
11
 
@@ -59,7 +59,7 @@ Everything that has a name:
59
59
  | **Doc files** | `architecture/background-jobs.md` | `arch/bg.md`, `notes2.md` |
60
60
  | **Error messages** | `"Enter an email address"` | `"Invalid input"`, `"Error 42"` |
61
61
 
62
- The words come from the [Ontology](/ontology) wherever it has a term: reusable functions are in `utils/`, never `helpers/` or `utilities/`. A project's own ontology adds its product words: in Launchpad, a Draft Ticket is `draftTicket`, never `pendingTicket`. The casing comes from [Naming Conventions](/Structure/naming-conventions).
62
+ The words come from the [Ontology](/docs/ontology) wherever it has a term: reusable functions are in `utils/`, never `helpers/` or `utilities/`. A project's own ontology adds its product words: in Launchpad, a Draft Ticket is `draftTicket`, never `pendingTicket`. The casing comes from [Naming Conventions](/docs/Structure/naming-conventions).
63
63
 
64
64
  ---
65
65
 
@@ -151,10 +151,20 @@ When a short name feels like a dictionary word, don't just use it — add it her
151
151
 
152
152
  1. **Ask:** is this the word people actually say, so the long form would only be harder to read? `docs`, `repo`: yes. `env`, `btn`, `msg`: no.
153
153
  2. **Add a row** to the table above, before or alongside the code that uses it.
154
- 3. **If ESLint flags the word**, add it to `replacements` in the `unicorn/name-replacements` rule of the [ESLint config](/Standards/eslint), so the linter agrees: `replacements: { docs: false, lib: false, props: false, ref: false, repo: false, utils: false }`.
154
+ 3. **If ESLint flags the word**, add it to `replacements` in the `unicorn/name-replacements` rule of the [ESLint config](/docs/Standards/eslint), so the linter agrees: `replacements: { docs: false, lib: false, props: false, ref: false, repo: false, utils: false }`.
155
155
 
156
156
  The list is the record. If a word is not on it, it is not a dictionary word yet.
157
157
 
158
+ ## `r` and `s`: the only single letters {#r-and-s}
159
+
160
+ **Two names are allowed to be a single letter:** `r`, the object of every route, and `s`, the object of every piece of static text. Both live in `lib/constants/`. See [Constants](/docs/Structure/folder-structure#constants).
161
+
162
+ They are used so often that the short form is easier to read: `href={r.contact}` and `{s.contact}` read cleanly in a component, where `routes.contact` and `strings.contact` crowd it.
163
+
164
+ - **Only these two.** No other single letter is allowed anywhere: not `e`, `i`, `x` or `T`, and not another constants object.
165
+ - **Only as those two objects.** `r` is never a request or a result, and `s` is never a string or a second.
166
+ - **Their keys are full words:** `r.linkedIn`, `s.pageNotFound`.
167
+
158
168
  ---
159
169
 
160
170
  ## Third-party names
@@ -272,3 +282,5 @@ users.map((u) => u.name);
272
282
  | **Everything else** — folders, props, form fields, union values, database columns, routes, environment variables, test names | **Code review and AI agents.** The linter cannot read these, so every agent and reviewer checks them |
273
283
 
274
284
  ESLint allows the [dictionary](#dictionary) words it would otherwise flag (`docs`, `lib`, `props`, `ref`, `repo`, `utils`), and exactly two third-party names as they are: `params` and `searchParams`. It still flags any of our own names built from an third-party name, such as `inputRef`.
285
+
286
+ ESLint does not flag `r` or `s`. Reviewers check that they are only ever the two constants objects.
@@ -29,12 +29,12 @@ This applies to everyone, **including AI agents**. An agent's work is not finish
29
29
 
30
30
  ## Imports
31
31
 
32
- | Rule | What you will see | What it means | How to fix it |
33
- | -------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
34
- | `import-x/no-cycle` | `Dependency cycle detected` | Two files import each other, directly or through other files. One will load before the other is ready | Move the part both files need into a **third file** that both import. If the cycle goes through an `index.ts`, import the file directly instead. See [Import safety](/Structure/import-organization#import-safety) |
35
- | `import-x/no-unresolved` | `Unable to resolve path to module '…'` | The file being imported does not exist at that path | Check the spelling and the path. If the file was renamed or moved, update the import |
36
- | `import-x/no-default-export` | `Prefer named exports` | The file uses `export default` | Change to a named export: `export const formatCurrency = …` |
37
- | `@typescript-eslint/consistent-type-imports` | `All imports in the declaration are only used as types. Use import type` | A type is imported like a value | Change `import { UserType }` to `import type { UserType }` |
32
+ | Rule | What you will see | What it means | How to fix it |
33
+ | -------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34
+ | `import-x/no-cycle` | `Dependency cycle detected` | Two files import each other, directly or through other files. One will load before the other is ready | Move the part both files need into a **third file** that both import. If the cycle goes through an `index.ts`, import the file directly instead. See [Import safety](/docs/Structure/import-organization#import-safety) |
35
+ | `import-x/no-unresolved` | `Unable to resolve path to module '…'` | The file being imported does not exist at that path | Check the spelling and the path. If the file was renamed or moved, update the import |
36
+ | `import-x/no-default-export` | `Prefer named exports` | The file uses `export default` | Change to a named export: `export const formatCurrency = …` |
37
+ | `@typescript-eslint/consistent-type-imports` | `All imports in the declaration are only used as types. Use import type` | A type is imported like a value | Change `import { UserType }` to `import type { UserType }` |
38
38
 
39
39
  ## Order and variables
40
40
 
@@ -57,6 +57,7 @@ This applies to everyone, **including AI agents**. An agent's work is not finish
57
57
  | `no-restricted-syntax` | `Use an arrow function` | `const name = function () {}` was used | Rewrite as `const name = () => {}` |
58
58
  | `no-restricted-syntax` | `Use a single object parameter when a function needs more than one value` | A function has more than one parameter | Take one object instead: `({ name, emailAddress }: CreateUserProps) =>` |
59
59
  | `no-restricted-syntax` | `Use functions, not classes` | A `class` was used | Rewrite it as functions |
60
+ | `no-restricted-syntax` | `Type a value where it is declared, not with satisfies afterwards` | `satisfies` was used outside a story file | Put the type on the declaration instead: `const config: ConfigType = { … }` |
60
61
  | `max-depth` | `Blocks are nested too deeply (4). Maximum allowed is 3` | `if`s and loops are nested more than three deep | Return early for the error cases, or move the inner part into its own function |
61
62
  | `no-nested-ternary` | `Do not nest ternary expressions` | A `? :` sits inside another `? :` | Use an `if`, or move it into its own function |
62
63
 
@@ -120,11 +121,11 @@ Nothing else is switched off. If another rule seems impossible to satisfy, the c
120
121
 
121
122
  ## Errors that stop ESLint running
122
123
 
123
- | What you will see | What it means | How to fix it |
124
- | ------------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------- |
125
- | `typescript-eslint does not support TS 7.0` | The linter cannot run against TypeScript 7 yet | See the [TypeScript 7 note](/Standards/eslint#installation) on the ESLint page |
126
- | `contextOrFilename.getFilename is not a function` | The React plugin cannot detect the React version | Set `settings.react.version` to the project's React version instead of `"detect"` |
127
- | `Cannot find module '…'` | A lint package is not installed | Run the install commands on the [ESLint page](/Standards/eslint#installation) |
124
+ | What you will see | What it means | How to fix it |
125
+ | ------------------------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------- |
126
+ | `typescript-eslint does not support TS 7.0` | The linter cannot run against TypeScript 7 yet | See the [TypeScript 7 note](/docs/Standards/eslint#installation) on the ESLint page |
127
+ | `contextOrFilename.getFilename is not a function` | The React plugin cannot detect the React version | Set `settings.react.version` to the project's React version instead of `"detect"` |
128
+ | `Cannot find module '…'` | A lint package is not installed | Run the install commands on the [ESLint page](/docs/Standards/eslint#installation) |
128
129
 
129
130
  ---
130
131
 
@@ -132,4 +133,4 @@ Nothing else is switched off. If another rule seems impossible to satisfy, the c
132
133
 
133
134
  **Work is not finished until `pnpm lint` passes.** That goes for people and AI agents alike: an agent fixes every lint error before it says a task is done.
134
135
 
135
- Most projects check it automatically on every change, with a CI job that runs `pnpm lint` (often with `pnpm test` and the build) or with a git hook. Which one, if any, is the project's own choice. See [Project workflow](/Standards/agent-specs#project-workflow).
136
+ Most projects check it automatically on every change, with a CI job that runs `pnpm lint` (often with `pnpm test` and the build) or with a git hook. Which one, if any, is the project's own choice. See [Project workflow](/docs/Standards/agent-specs#project-workflow).
@@ -8,28 +8,28 @@ Every file must be readable by a person who has never seen it before. Code is re
8
8
 
9
9
  ## The rules
10
10
 
11
- | Rule | Enforced by |
12
- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
13
- | [No file longer than 300 lines](#no-file-longer-than-300-lines) | ESLint `max-lines` (test files exempt) |
14
- | [Names are full English words](/Standards/human-readable-names) — everywhere, including form fields | ESLint for code names; code review and AI agents for the rest |
15
- | [Arrow functions everywhere](#arrow-functions-everywhere) | ESLint `func-style`, `prefer-arrow-callback` |
16
- | [Always `const`](#always-const) | ESLint `no-restricted-syntax`, `prefer-const`, `no-var` |
17
- | [A function does one thing](#a-function-does-one-thing) | Code review |
18
- | [Every function and component has a test file](#every-function-and-component-has-a-test-file) | Developers and AI agents — not linted |
19
- | [Functions, never classes](#functions-never-classes) | ESLint `no-restricted-syntax` |
20
- | [Return early, nest shallow](#return-early-nest-shallow) | ESLint `max-depth` |
21
- | [No magic numbers or strings](#no-magic-numbers-or-strings) | ESLint `no-magic-numbers` (numbers), code review (strings) |
22
- | [One parameter; always an object for more](#one-parameter-always-an-object-for-more) | ESLint `no-restricted-syntax` |
23
- | [No nested ternaries](#no-nested-ternaries) | ESLint `no-nested-ternary` |
24
- | [No boolean flag parameters](#no-boolean-flag-parameters) | Code review |
25
- | [Comments explain why, not what](#comments-explain-why-not-what) | Code review |
26
- | [No `any`, no `!`, no unexplained `as`](#no-any-no--no-unexplained-as) | ESLint `@typescript-eslint` rules |
27
- | [One component per file](#one-component-per-file) | ESLint `react/no-multi-comp` |
28
- | [Named exports only](#named-exports-only) | ESLint `import-x/no-default-export` |
29
- | [Type names say what they are](#type-names-say-what-they-are) | ESLint `@typescript-eslint/naming-convention` |
30
- | [Error messages say what happened and what to do](#error-messages-say-what-happened-and-what-to-do) | Code review |
31
-
32
- See the [ESLint configuration](/Standards/eslint) for the rule setup, and [Fixing Lint Errors](/Standards/lint-errors) for what each error means and how to fix it.
11
+ | Rule | Enforced by |
12
+ | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
13
+ | [No file longer than 300 lines](#no-file-longer-than-300-lines) | ESLint `max-lines` (test files exempt) |
14
+ | [Names are full English words](/docs/Standards/human-readable-names) — everywhere, including form fields | ESLint for code names; code review and AI agents for the rest |
15
+ | [Arrow functions everywhere](#arrow-functions-everywhere) | ESLint `func-style`, `prefer-arrow-callback` |
16
+ | [Always `const`](#always-const) | ESLint `no-restricted-syntax`, `prefer-const`, `no-var` |
17
+ | [A function does one thing](#a-function-does-one-thing) | Code review |
18
+ | [Every function and component has a test file](#every-function-and-component-has-a-test-file) | Developers and AI agents — not linted |
19
+ | [Functions, never classes](#functions-never-classes) | ESLint `no-restricted-syntax` |
20
+ | [Return early, nest shallow](#return-early-nest-shallow) | ESLint `max-depth` |
21
+ | [No magic numbers or strings](#no-magic-numbers-or-strings) | ESLint `no-magic-numbers` (numbers), code review (strings) |
22
+ | [One parameter; always an object for more](#one-parameter-always-an-object-for-more) | ESLint `no-restricted-syntax` |
23
+ | [No nested ternaries](#no-nested-ternaries) | ESLint `no-nested-ternary` |
24
+ | [No boolean flag parameters](#no-boolean-flag-parameters) | Code review |
25
+ | [Comments explain why, not what](#comments-explain-why-not-what) | Code review |
26
+ | [No `any`, no `!`, no unexplained `as`](#no-any-no--no-unexplained-as) | ESLint `@typescript-eslint` rules |
27
+ | [One component per file](#one-component-per-file) | ESLint `react/no-multi-comp` |
28
+ | [Named exports only](#named-exports-only) | ESLint `import-x/no-default-export` |
29
+ | [Type names say what they are](#type-names-say-what-they-are) | ESLint `@typescript-eslint/naming-convention` |
30
+ | [Error messages say what happened and what to do](#error-messages-say-what-happened-and-what-to-do) | Code review |
31
+
32
+ See the [ESLint configuration](/docs/Standards/eslint) for the rule setup, and [Fixing Lint Errors](/docs/Standards/lint-errors) for what each error means and how to fix it.
33
33
 
34
34
  ---
35
35
 
@@ -37,8 +37,8 @@ See the [ESLint configuration](/Standards/eslint) for the rule setup, and [Fixin
37
37
 
38
38
  A file over 300 lines is doing more than one job. Split it before it gets there:
39
39
 
40
- - Move reusable functions into a [utils folder](/Structure/folder-structure#-utils)
41
- - Move sub-components into [their own folders](/Structure/component-organization#nested-components)
40
+ - Move reusable functions into a [utils folder](/docs/Structure/folder-structure#-utils)
41
+ - Move sub-components into [their own folders](/docs/Structure/component-organization#nested-components)
42
42
  - Move types into `types.ts`
43
43
 
44
44
  Blank lines and comments do not count towards the limit. **Test files are exempt** — a thorough test file can run long.
@@ -49,7 +49,7 @@ Blank lines and comments do not count towards the limit. **Test files are exempt
49
49
 
50
50
  **Every name in the project is made of whole, human-readable words** — not just variables, but folders, files, props, form fields, types, database columns, environment variables, everything.
51
51
 
52
- This rule has its own page, because it applies everywhere: **[Human-Readable Names](/Standards/human-readable-names)**. Read it in full.
52
+ This rule has its own page, because it applies everywhere: **[Human-Readable Names](/docs/Standards/human-readable-names)**. Read it in full.
53
53
 
54
54
  ---
55
55
 
@@ -87,7 +87,7 @@ export const formatCurrency = function (amount: number) {
87
87
 
88
88
  Three things to know:
89
89
 
90
- - **Define before use.** An arrow function is written above the code that uses it. This is [enforced](/Structure/import-organization#3-define-before-use), and it means a mistake fails straight away instead of being hidden.
90
+ - **Define before use.** An arrow function is written above the code that uses it. This is [enforced](/docs/Structure/import-organization#3-define-before-use), and it means a mistake fails straight away instead of being hidden.
91
91
  - **Generic components need a trailing comma** in `.tsx` files, so the type parameter is not read as JSX: `const List = <ItemType,>({ items }: ListProps<ItemType>) => { … }`.
92
92
  - **Next.js files that need a default export** take two lines:
93
93
 
@@ -194,7 +194,7 @@ utils/
194
194
  └── index.ts
195
195
  ```
196
196
 
197
- See [Utils](/Structure/folder-structure#-utils) for the folder layout and the `index.ts` files.
197
+ See [Utils](/docs/Structure/folder-structure#-utils) for the folder layout and the `index.ts` files.
198
198
 
199
199
  ```ts title="utils/formatCurrency/formatCurrency.test.ts"
200
200
  import { describe, expect, it } from "vitest";
@@ -212,7 +212,7 @@ describe("formatCurrency", () => {
212
212
  });
213
213
  ```
214
214
 
215
- Components keep their test beside the component file (`button/Button.tsx` and `button/Button.test.tsx`) — see [Component Organization](/Structure/component-organization).
215
+ Components keep their test beside the component file (`button/Button.tsx` and `button/Button.test.tsx`) — see [Component Organization](/docs/Structure/component-organization).
216
216
 
217
217
  ---
218
218
 
@@ -309,7 +309,9 @@ if (order.status === "shipped") {
309
309
  }
310
310
  ```
311
311
 
312
- `-1`, `0` and `1` are allowed. See [Naming Conventions](/Structure/naming-conventions#constants) for how to name constants.
312
+ **`-1`, `0` and `1` are allowed**, because they are part of how code works rather than a choice anyone makes: `0` is the start or nothing (`items[0]`, `length === 0`), `1` is one more or one less (`index + 1`), and `-1` is "not found" (`indexOf`). Any other number was chosen by someone, so its name says what it controls. Even `2` gets a name.
313
+
314
+ A number used in one file is named at the top of that file. One shared across the app goes in `lib/constants/`, in a file named after its topic. See [Numbers](/docs/Structure/folder-structure#numbers-named-and-kept-by-topic) for where they go, and [Naming Conventions](/docs/Structure/naming-conventions#constants) for how to name them.
313
315
 
314
316
  ---
315
317
 
@@ -451,13 +453,13 @@ const getUserName = (user: any) => {
451
453
  };
452
454
  ```
453
455
 
454
- `as const` is fine. See the [ESLint configuration](/Standards/eslint#typescript-eslintconsistent-type-assertions-never) for how to record the reason for a cast.
456
+ `as const` is fine. See the [ESLint configuration](/docs/Standards/eslint#typescript-eslintconsistent-type-assertions-never) for how to record the reason for a cast.
455
457
 
456
458
  ---
457
459
 
458
460
  ## One component per file
459
461
 
460
- A file holds one component. A second component — however small — gets [its own folder](/Structure/component-organization#nested-components).
462
+ A file holds one component. A second component — however small — gets [its own folder](/docs/Structure/component-organization#nested-components).
461
463
 
462
464
  ---
463
465
 
@@ -523,33 +525,70 @@ Use a named type instead of a bare `string` wherever the value has a fixed set o
523
525
 
524
526
  ### Name a type after what it is, not the job it does
525
527
 
526
- A type name says what the thing is. **If a type only describes one field of a function's props, don't give it its own name**: write it inside the `Props`, so the `Props` is the only type.
528
+ A type name says what the thing is. **If a type only describes one field of a function's props, don't give it its own name**: write that field inside the `Props`.
529
+
530
+ **Every function still gets its own `Props` type** for its single object parameter, named after the function: `launchSession` takes `LaunchSessionProps`. Use it wherever that function is written, so its parameters always have a type.
527
531
 
528
532
  ```ts
529
- // ✅ Good — the dependencies are written inside the one Props type
533
+ // ✅ Good — each function has its own Props; the dependencies are written inside StartAgentRunProps
534
+ interface LaunchSessionProps {
535
+ worktreePath: string;
536
+ }
537
+
530
538
  interface StartAgentRunProps {
531
539
  ticketId: string;
532
540
  dependencies: {
533
- launchSession: (ticketId: string) => Promise<void>;
534
- reportStatus: (status: AgentRunStatusType) => void;
541
+ launchSession: (props: LaunchSessionProps) => Promise<void>;
535
542
  };
536
543
  }
544
+
545
+ const dependencies = {
546
+ launchSession: async ({ worktreePath }: LaunchSessionProps) =>
547
+ openTerminal(worktreePath),
548
+ };
537
549
  ```
538
550
 
539
551
  ```ts
540
- // ❌ Bad — a second type named after the role it plays in one function
552
+ // ❌ Bad — a wrapper type named after the role it plays in one function
541
553
  interface StartAgentRunDependenciesType {
542
- launchSession: (ticketId: string) => Promise<void>;
543
- reportStatus: (status: AgentRunStatusType) => void;
554
+ launchSession: (props: LaunchSessionProps) => Promise<void>;
544
555
  }
545
556
 
546
- interface StartAgentRunProps {
547
- ticketId: string;
548
- dependencies: StartAgentRunDependenciesType;
549
- }
557
+ // ❌ Bad — no type: worktreePath is an implicit any, and the type check fails
558
+ const dependencies = {
559
+ launchSession: async ({ worktreePath }) => openTerminal(worktreePath),
560
+ };
561
+
562
+ // ❌ Bad — the type is written out inline instead of using LaunchSessionProps
563
+ const dependencies = {
564
+ launchSession: async ({ worktreePath }: { worktreePath: string }) =>
565
+ openTerminal(worktreePath),
566
+ };
550
567
  ```
551
568
 
552
- Where that object is built somewhere else, don't annotate it with a separate type either. TypeScript checks it against `StartAgentRunProps` when it is passed in.
569
+ ### Type first, never `satisfies`
570
+
571
+ **A value gets its type where it is declared**, and the value is written to match. Never write the value first and attach the type afterwards with `satisfies`.
572
+
573
+ ```ts
574
+ // ✅ Good — the type comes first
575
+ const config: ConfigType = {
576
+ name: "Activatus",
577
+ retries: MAXIMUM_RETRY_ATTEMPTS,
578
+ };
579
+ ```
580
+
581
+ ```ts
582
+ // ❌ Bad — the type is attached after the fact
583
+ const config = {
584
+ name: "Activatus",
585
+ retries: MAXIMUM_RETRY_ATTEMPTS,
586
+ } satisfies ConfigType;
587
+ ```
588
+
589
+ ESLint reports `satisfies` as an error.
590
+
591
+ **Story files are the one exception.** Storybook's own docs type stories with `satisfies Meta<typeof Component>`, because it lets Storybook see which props are already set for every story, so each story only sets the props that change. Story files (`*.stories.tsx`) follow Storybook's docs as they are, and ESLint allows `satisfies` there. See [Storybook stories](/docs/Structure/component-organization#-componentstoriestsx).
553
592
 
554
593
  ---
555
594
 
@@ -5,7 +5,7 @@ sidebar_label: "Stylelint"
5
5
 
6
6
  # Stylelint Configuration
7
7
 
8
- Components are styled with [Tailwind CSS](/Standards/styling), and Stylelint lints the CSS that is still written by hand — `globals.css` and any CSS Modules. Start from the config below that matches the project:
8
+ Components are styled with [Tailwind CSS](/docs/Standards/styling), and Stylelint lints the CSS that is still written by hand — `globals.css` and any CSS Modules. Start from the config below that matches the project:
9
9
 
10
10
  | Config | Use when |
11
11
  | --------------------------------------- | ----------------------------------------- |
@@ -151,7 +151,7 @@ Enforces `rgb(255 0 0 / 50%)` instead of `rgba(255, 0, 0, 0.5)`. The modern synt
151
151
 
152
152
  ### `ignoreFiles`: third-party code is exempt
153
153
 
154
- CSS in a `vendor/` folder is not ours, so it is not linted. See [Third-party code is exempt](/Standards/eslint#third-party-code-is-exempt).
154
+ CSS in a `vendor/` folder is not ours, so it is not linted. See [Third-party code is exempt](/docs/Standards/eslint#third-party-code-is-exempt).
155
155
 
156
156
  ### Tailwind: `import-notation: "string"` and `at-rule-no-unknown`
157
157
 
@@ -159,11 +159,11 @@ CSS in a `vendor/` folder is not ours, so it is not linted. See [Third-party cod
159
159
 
160
160
  ### Custom properties: kebab-case
161
161
 
162
- `stylelint-config-standard` requires CSS variables to be kebab-case (`--color-brand-primary`). See [CSS Styling](/Standards/styling#css-variables).
162
+ `stylelint-config-standard` requires CSS variables to be kebab-case (`--color-brand-primary`). See [CSS Styling](/docs/Standards/styling#css-variables).
163
163
 
164
164
  ### CSS Modules: `color-named: never` and `color-no-hex`
165
165
 
166
- Colours come from the [global colour variables](/Standards/styling#-colours-always-come-from-global-variables). A CSS Module may not contain `white` or `#3b82f6` — it references `var(--color-white)` or `var(--color-brand-primary)`. Colour values are only written in `globals.css`.
166
+ Colours come from the [global colour variables](/docs/Standards/styling#-colours-always-come-from-global-variables). A CSS Module may not contain `white` or `#3b82f6` — it references `var(--color-white)` or `var(--color-brand-primary)`. Colour values are only written in `globals.css`.
167
167
 
168
168
  ### CSS Modules: `selector-class-pattern: camelCase`
169
169
 
@@ -10,7 +10,7 @@ Standards for styling React components and applications.
10
10
 
11
11
  ## 🎨 Tailwind CSS
12
12
 
13
- Use Tailwind CSS utility classes for all component styling. Tailwind is compatible with React Server Components and is the foundation [shadcn/ui](/Guides/component-library-workflow) is built on.
13
+ Use Tailwind CSS utility classes for all component styling. Tailwind is compatible with React Server Components and is the foundation [shadcn/ui](/docs/Guides/component-library-workflow) is built on.
14
14
 
15
15
  ```tsx
16
16
  // Button.tsx
@@ -94,7 +94,7 @@ export const PageHeader = () => {
94
94
 
95
95
  - **Flex first.** `flex`, `flex-col`, `gap-*`, `items-*` and `justify-*` cover almost every layout.
96
96
  - **The parent positions, the child styles itself.** A parent sets direction, spacing and alignment. Each child component owns its own padding, colour and typography.
97
- - **Split into child components** rather than growing one long `className` or a deep tree of `div`s. Each child gets [its own folder](/Structure/component-organization#nested-components).
97
+ - **Split into child components** rather than growing one long `className` or a deep tree of `div`s. Each child gets [its own folder](/docs/Structure/component-organization#nested-components).
98
98
  - **Space with `gap`**, not margins on the children.
99
99
  - **Grid only when asked.** Do not choose CSS grid on your own — use it only when the task specifically asks for a grid.
100
100
 
@@ -149,7 +149,7 @@ Each project defines its colours once, in its global stylesheet:
149
149
 
150
150
  Tailwind turns every `--color-*` variable in `@theme` into utility classes: `--color-brand-primary` becomes `bg-brand-primary`, `text-brand-primary`, `border-brand-primary` and so on.
151
151
 
152
- The project's design decisions — what each colour is for, the fonts, the spacing — are recorded in its `docs/design/design-system.md` [Decision Doc](/Standards/documentation).
152
+ The project's design decisions — what each colour is for, the fonts, the spacing — are recorded in its `docs/design/design-system.md` [Decision Doc](/docs/Standards/documentation).
153
153
 
154
154
  ### ✅ Use the colours
155
155
 
@@ -192,7 +192,7 @@ Tailwind's built-in colours are variables too — `white` is `var(--color-white)
192
192
  - **Colours**: Global palette only — Tailwind colours and project colours from `@theme` — never arbitrary values
193
193
  - **Long class lists**: Extract a component, not a CSS class
194
194
  - **Conditional classes**: Use `clsx`
195
- - **Class order**: Sorted automatically by [Prettier](/Standards/prettier#tailwind-class-sorting) — do not sort by hand
195
+ - **Class order**: Sorted automatically by [Prettier](/docs/Standards/prettier#tailwind-class-sorting) — do not sort by hand
196
196
 
197
197
  ### Global CSS
198
198
 
@@ -284,7 +284,7 @@ Name colours by **role**, not by value: `--color-brand-primary`, not `--color-bl
284
284
 
285
285
  ## 📚 Related Documentation
286
286
 
287
- - [Stylelint](/Standards/stylelint) - Linting for hand-written CSS
288
- - [Component Organization](/Structure/component-organization) - How to organize component files
289
- - [Folder Structure](/Structure/folder-structure) - Overall project structure
290
- - [Naming Conventions](/Structure/naming-conventions) - File and folder naming rules
287
+ - [Stylelint](/docs/Standards/stylelint) - Linting for hand-written CSS
288
+ - [Component Organization](/docs/Structure/component-organization) - How to organize component files
289
+ - [Folder Structure](/docs/Structure/folder-structure) - Overall project structure
290
+ - [Naming Conventions](/docs/Structure/naming-conventions) - File and folder naming rules
@@ -70,7 +70,7 @@ already contributes `_uid`, `component` and `_editable`, and a hand-written copy
70
70
  the moment the library changes.
71
71
 
72
72
  Read every library generic this way. An unparameterised one with a permissive default is
73
- usually a silent `any`, not a sensible fallback. The [`/typescript` skill](/Standards/agent-skills)
73
+ usually a silent `any`, not a sensible fallback. The [`/typescript` skill](/docs/Standards/agent-skills)
74
74
  applies this as a rule.
75
75
 
76
76
  ---
@@ -113,6 +113,6 @@ app/api/
113
113
 
114
114
  ## 📚 Related Documentation
115
115
 
116
- - [Folder Structure](/Structure/folder-structure) - Overall project structure
117
- - [Naming Conventions](/Structure/naming-conventions) - File and folder naming
118
- - [TypeScript](/Standards/tech-stack) - Type safety standards
116
+ - [Folder Structure](/docs/Structure/folder-structure) - Overall project structure
117
+ - [Naming Conventions](/docs/Structure/naming-conventions) - File and folder naming
118
+ - [TypeScript](/docs/Standards/tech-stack) - Type safety standards
@@ -103,7 +103,7 @@ describe('Button', () => {
103
103
 
104
104
  ### 📖 `Component.stories.tsx`
105
105
 
106
- Storybook stories for documentation and visual testing.
106
+ Storybook stories for documentation and visual testing. Written the way [Storybook's docs](https://storybook.js.org/docs/writing-stories) write them, with `satisfies Meta<typeof Component>`: it is the one place `satisfies` is allowed. See [Type first, never `satisfies`](/docs/Standards/readable-code#type-first-never-satisfies).
107
107
 
108
108
  ```typescript
109
109
  // Button.stories.tsx
@@ -113,10 +113,11 @@ import { Button } from "./Button";
113
113
  const meta = {
114
114
  title: "Components/Button",
115
115
  component: Button,
116
+ args: { variant: "primary" },
116
117
  } satisfies Meta<typeof Button>;
117
118
 
118
119
  export default meta;
119
- type StoryType = StoryObj<typeof Button>;
120
+ type StoryType = StoryObj<typeof meta>;
120
121
 
121
122
  export const Primary: StoryType = {
122
123
  args: {
@@ -156,16 +157,16 @@ export type { ButtonProps } from "./types";
156
157
 
157
158
  Every component file is laid out in the same order, so a reader always knows where to look.
158
159
 
159
- | # | Section | Rule |
160
- | --- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
161
- | 1 | Imports | Ordered per [Import Organization](/Structure/import-organization) |
162
- | 2 | Component | The component is the first thing in the file after the imports. Always an [arrow function](/Standards/readable-code#arrow-functions-everywhere) — [never a class](/Standards/readable-code#functions-never-classes) |
163
- | 3 | Variable declarations | Plain values worked out from props |
164
- | 4 | State declarations | `useState`, `useReducer`, `useRef` |
165
- | 5 | Calculated values | Wrapped in `useMemo` |
166
- | 6 | Nested functions | Only functions that need the component's state or props, wrapped in `useCallback` |
167
- | 7 | Effects | `useEffect` |
168
- | 8 | Render logic | The `return`. No inline functions |
160
+ | # | Section | Rule |
161
+ | --- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
162
+ | 1 | Imports | Ordered per [Import Organization](/docs/Structure/import-organization) |
163
+ | 2 | Component | The component is the first thing in the file after the imports. Always an [arrow function](/docs/Standards/readable-code#arrow-functions-everywhere) — [never a class](/docs/Standards/readable-code#functions-never-classes) |
164
+ | 3 | Variable declarations | Plain values worked out from props |
165
+ | 4 | State declarations | `useState`, `useReducer`, `useRef` |
166
+ | 5 | Calculated values | Wrapped in `useMemo` |
167
+ | 6 | Nested functions | Only functions that need the component's state or props, wrapped in `useCallback` |
168
+ | 7 | Effects | `useEffect` |
169
+ | 8 | Render logic | The `return`. No inline functions |
169
170
 
170
171
  ### Example
171
172
 
@@ -230,7 +231,7 @@ export const UserListClient = ({
230
231
 
231
232
  ### Keep the UI file clean
232
233
 
233
- **If a function can be written in another file, move it out of the UI file.** A function that does not need the component's state or props is not a nested function — it belongs in a [utils folder](/Structure/folder-structure#-utils).
234
+ **If a function can be written in another file, move it out of the UI file.** A function that does not need the component's state or props is not a nested function — it belongs in a [utils folder](/docs/Structure/folder-structure#-utils).
234
235
 
235
236
  `filterUsersByName` above only needs its arguments, so it lives in the component's `utils/` folder, in its own folder beside its test file:
236
237
 
@@ -326,7 +327,7 @@ export const UserListItem = ({
326
327
  - **`useMemo`** for any value calculated from props or state (filtering, sorting, totals)
327
328
  - **`useCallback`** for every function passed to a child component or a JSX prop
328
329
 
329
- ESLint enforces the function half of this with `react/jsx-no-bind` — see the [ESLint configuration](/Standards/eslint#reactjsx-no-bind-error).
330
+ ESLint enforces the function half of this with `react/jsx-no-bind` — see the [ESLint configuration](/docs/Standards/eslint#reactjsx-no-bind-error).
330
331
 
331
332
  ---
332
333
 
@@ -381,13 +382,13 @@ export const UserListClient = ({ users }: UserListClientProps) => {
381
382
 
382
383
  ## Reusable by design
383
384
 
384
- Every component in `components/ui/` is built so it can be copied into another project unchanged: everything comes in through props, and it imports nothing from the project's features, services, stores or `app/` folder. Before building one, [look before you build](/Guides/component-library-workflow#1-look-before-you-build), and build it to the props in the [Design System Components](/design-system-components). See [Future-proofing](/Guides/component-library-workflow#future-proofing).
385
+ Every component in `components/ui/` is built so it can be copied into another project unchanged: everything comes in through props, and it imports nothing from the project's features, services, stores or `app/` folder. Before building one, [look before you build](/docs/Guides/component-library-workflow#1-look-before-you-build), and build it to the props in the [Design System Components](/docs/design-system-components). See [Future-proofing](/docs/Guides/component-library-workflow#future-proofing).
385
386
 
386
387
  ---
387
388
 
388
389
  ## Components from shadcn
389
390
 
390
- A component added with the shadcn CLI is **our code** and follows everything on this page. Convert it when it is added: its own camelCase folder, one component per file, `types.ts`, `index.ts` and a test file. See [Adding a shadcn component](/Guides/component-library-workflow#adding-a-shadcn-component).
391
+ A component added with the shadcn CLI is **our code** and follows everything on this page. Convert it when it is added: its own camelCase folder, one component per file, `types.ts`, `index.ts` and a test file. See [Adding a shadcn component](/docs/Guides/component-library-workflow#adding-a-shadcn-component).
391
392
 
392
393
  ---
393
394
 
@@ -458,7 +459,7 @@ export type { CardProps } from "./types";
458
459
 
459
460
  ### Within the Same Folder
460
461
 
461
- Inside a component's folder, import files directly. **Never import through the folder's own `index.ts`**, and never import the parent component from a sub-component — both create a [circular import](/Structure/import-organization#import-safety).
462
+ Inside a component's folder, import files directly. **Never import through the folder's own `index.ts`**, and never import the parent component from a sub-component — both create a [circular import](/docs/Structure/import-organization#import-safety).
462
463
 
463
464
  ```typescript
464
465
  // card/Card.tsx — the parent imports its sub-components
@@ -514,7 +515,7 @@ app/
514
515
  └── page.tsx
515
516
  ```
516
517
 
517
- See [Where components live](/Structure/folder-structure#-where-components-live) for every location.
518
+ See [Where components live](/docs/Structure/folder-structure#-where-components-live) for every location.
518
519
 
519
520
  ---
520
521