@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.
- package/README.md +15 -13
- package/docs/Guides/component-library-workflow.md +16 -16
- package/docs/Guides/migrating.md +18 -16
- package/docs/Guides/setup.md +9 -7
- package/docs/Standards/agent-skills.md +2 -2
- package/docs/Standards/agent-specs.md +3 -3
- package/docs/Standards/documentation.md +7 -7
- package/docs/Standards/eslint.md +321 -155
- package/docs/Standards/human-readable-names.md +15 -3
- package/docs/Standards/lint-errors.md +13 -12
- package/docs/Standards/readable-code.md +82 -43
- package/docs/Standards/stylelint.md +4 -4
- package/docs/Standards/styling.md +8 -8
- package/docs/Standards/tech-stack.md +1 -1
- package/docs/Structure/api-routes.md +3 -3
- package/docs/Structure/component-organization.md +19 -18
- package/docs/Structure/folder-structure.md +131 -12
- package/docs/Structure/naming-conventions.md +10 -8
- package/docs/design-system-components.md +3 -3
- package/docs/intro.md +20 -20
- package/docs/ontology.md +32 -31
- package/docs/templates.mdx +12 -12
- package/docs/version-history.md +77 -42
- package/package.json +19 -4
- package/static/templates/AGENTS.md +6 -5
- package/static/templates/docs/overview/writing-docs.md +1 -1
- package/static/templates/eslint/base/eslint.config.mjs +57 -33
- package/static/templates/eslint/nextjs/eslint.config.mjs +80 -32
- package/static/templates/eslint/react/eslint.config.mjs +76 -33
- package/static/templates/eslint/typescript/eslint.config.mjs +76 -33
|
@@ -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
|
|
12
|
-
|
|
|
13
|
-
| [No file longer than 300 lines](#no-file-longer-than-300-lines)
|
|
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)
|
|
16
|
-
| [Always `const`](#always-const)
|
|
17
|
-
| [A function does one thing](#a-function-does-one-thing)
|
|
18
|
-
| [Every function and component has a test file](#every-function-and-component-has-a-test-file)
|
|
19
|
-
| [Functions, never classes](#functions-never-classes)
|
|
20
|
-
| [Return early, nest shallow](#return-early-nest-shallow)
|
|
21
|
-
| [No magic numbers or strings](#no-magic-numbers-or-strings)
|
|
22
|
-
| [One parameter; always an object for more](#one-parameter-always-an-object-for-more)
|
|
23
|
-
| [No nested ternaries](#no-nested-ternaries)
|
|
24
|
-
| [No boolean flag parameters](#no-boolean-flag-parameters)
|
|
25
|
-
| [Comments explain why, not what](#comments-explain-why-not-what)
|
|
26
|
-
| [No `any`, no `!`, no unexplained `as`](#no-any-no--no-unexplained-as)
|
|
27
|
-
| [One component per file](#one-component-per-file)
|
|
28
|
-
| [Named exports only](#named-exports-only)
|
|
29
|
-
| [Type names say what they are](#type-names-say-what-they-are)
|
|
30
|
-
| [Error messages say what happened and what to do](#error-messages-say-what-happened-and-what-to-do)
|
|
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
|
-
|
|
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
|
|
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
|
|
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: (
|
|
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
|
|
552
|
+
// ❌ Bad — a wrapper type named after the role it plays in one function
|
|
541
553
|
interface StartAgentRunDependenciesType {
|
|
542
|
-
launchSession: (
|
|
543
|
-
reportStatus: (status: AgentRunStatusType) => void;
|
|
554
|
+
launchSession: (props: LaunchSessionProps) => Promise<void>;
|
|
544
555
|
}
|
|
545
556
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|