@activatus/code-standards 3.0.1 → 3.0.4
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 +10 -10
- package/docs/Guides/component-library-workflow.md +16 -16
- package/docs/Guides/migrating.md +15 -15
- package/docs/Guides/setup.md +12 -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 +169 -105
- package/docs/Standards/human-readable-names.md +15 -3
- package/docs/Standards/lint-errors.md +13 -12
- package/docs/Standards/readable-code.md +80 -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 +110 -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 +76 -42
- package/package.json +1 -1
- package/static/templates/AGENTS.md +5 -4
- package/static/templates/docs/overview/writing-docs.md +1 -1
- package/static/templates/eslint/nextjs/eslint.config.mjs +48 -28
- package/static/templates/eslint/react/eslint.config.mjs +48 -28
- package/static/templates/eslint/typescript/eslint.config.mjs +48 -28
|
@@ -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,7 @@ 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. See [Naming Conventions](/docs/Structure/naming-conventions#constants) for how to name constants.
|
|
313
313
|
|
|
314
314
|
---
|
|
315
315
|
|
|
@@ -451,13 +451,13 @@ const getUserName = (user: any) => {
|
|
|
451
451
|
};
|
|
452
452
|
```
|
|
453
453
|
|
|
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.
|
|
454
|
+
`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
455
|
|
|
456
456
|
---
|
|
457
457
|
|
|
458
458
|
## One component per file
|
|
459
459
|
|
|
460
|
-
A file holds one component. A second component — however small — gets [its own folder](/Structure/component-organization#nested-components).
|
|
460
|
+
A file holds one component. A second component — however small — gets [its own folder](/docs/Structure/component-organization#nested-components).
|
|
461
461
|
|
|
462
462
|
---
|
|
463
463
|
|
|
@@ -523,33 +523,70 @@ Use a named type instead of a bare `string` wherever the value has a fixed set o
|
|
|
523
523
|
|
|
524
524
|
### Name a type after what it is, not the job it does
|
|
525
525
|
|
|
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
|
|
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 that field inside the `Props`.
|
|
527
|
+
|
|
528
|
+
**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
529
|
|
|
528
530
|
```ts
|
|
529
|
-
// ✅ Good — the dependencies are written inside
|
|
531
|
+
// ✅ Good — each function has its own Props; the dependencies are written inside StartAgentRunProps
|
|
532
|
+
interface LaunchSessionProps {
|
|
533
|
+
worktreePath: string;
|
|
534
|
+
}
|
|
535
|
+
|
|
530
536
|
interface StartAgentRunProps {
|
|
531
537
|
ticketId: string;
|
|
532
538
|
dependencies: {
|
|
533
|
-
launchSession: (
|
|
534
|
-
reportStatus: (status: AgentRunStatusType) => void;
|
|
539
|
+
launchSession: (props: LaunchSessionProps) => Promise<void>;
|
|
535
540
|
};
|
|
536
541
|
}
|
|
542
|
+
|
|
543
|
+
const dependencies = {
|
|
544
|
+
launchSession: async ({ worktreePath }: LaunchSessionProps) =>
|
|
545
|
+
openTerminal(worktreePath),
|
|
546
|
+
};
|
|
537
547
|
```
|
|
538
548
|
|
|
539
549
|
```ts
|
|
540
|
-
// ❌ Bad — a
|
|
550
|
+
// ❌ Bad — a wrapper type named after the role it plays in one function
|
|
541
551
|
interface StartAgentRunDependenciesType {
|
|
542
|
-
launchSession: (
|
|
543
|
-
reportStatus: (status: AgentRunStatusType) => void;
|
|
552
|
+
launchSession: (props: LaunchSessionProps) => Promise<void>;
|
|
544
553
|
}
|
|
545
554
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
555
|
+
// ❌ Bad — no type: worktreePath is an implicit any, and the type check fails
|
|
556
|
+
const dependencies = {
|
|
557
|
+
launchSession: async ({ worktreePath }) => openTerminal(worktreePath),
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// ❌ Bad — the type is written out inline instead of using LaunchSessionProps
|
|
561
|
+
const dependencies = {
|
|
562
|
+
launchSession: async ({ worktreePath }: { worktreePath: string }) =>
|
|
563
|
+
openTerminal(worktreePath),
|
|
564
|
+
};
|
|
550
565
|
```
|
|
551
566
|
|
|
552
|
-
|
|
567
|
+
### Type first, never `satisfies`
|
|
568
|
+
|
|
569
|
+
**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`.
|
|
570
|
+
|
|
571
|
+
```ts
|
|
572
|
+
// ✅ Good — the type comes first
|
|
573
|
+
const config: ConfigType = {
|
|
574
|
+
name: "Activatus",
|
|
575
|
+
retries: MAXIMUM_RETRY_ATTEMPTS,
|
|
576
|
+
};
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
```ts
|
|
580
|
+
// ❌ Bad — the type is attached after the fact
|
|
581
|
+
const config = {
|
|
582
|
+
name: "Activatus",
|
|
583
|
+
retries: MAXIMUM_RETRY_ATTEMPTS,
|
|
584
|
+
} satisfies ConfigType;
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
ESLint reports `satisfies` as an error.
|
|
588
|
+
|
|
589
|
+
**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
590
|
|
|
554
591
|
---
|
|
555
592
|
|
|
@@ -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
|
|
|
@@ -65,7 +65,11 @@ project-root/
|
|
|
65
65
|
│ │ ├── rateLimit.ts
|
|
66
66
|
│ │ └── session.ts
|
|
67
67
|
│ ├── validation/ # Schemas that check input
|
|
68
|
-
│ ├── constants/ # Constants shared across the app
|
|
68
|
+
│ ├── constants/ # Constants shared across the app: see Constants below
|
|
69
|
+
│ │ ├── routes.ts # r: every link
|
|
70
|
+
│ │ ├── strings.ts # s: every piece of static text
|
|
71
|
+
│ │ ├── noData.ts # Stand-in data for local development
|
|
72
|
+
│ │ └── index.ts
|
|
69
73
|
│ ├── errors/ # Error types and handlers shared across the app
|
|
70
74
|
│ └── types/ # Types shared across the app; a component's own types stay in its types.ts
|
|
71
75
|
├── utils/ # Reusable functions used across the app
|
|
@@ -131,12 +135,12 @@ project-root/
|
|
|
131
135
|
|
|
132
136
|
**A component lives where it is used.** There are no feature folders: a component either belongs to one page, to one section, or is shared.
|
|
133
137
|
|
|
134
|
-
| Used by
|
|
135
|
-
|
|
|
136
|
-
| **Every project the same way** — the [Design System Components](/design-system-components) | `components/ui/` | `components/ui/button/` |
|
|
137
|
-
| **More than one page** of this project
|
|
138
|
-
| **A few pages in one section**
|
|
139
|
-
| **One page only**
|
|
138
|
+
| Used by | Lives in | Example |
|
|
139
|
+
| ----------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------- |
|
|
140
|
+
| **Every project the same way** — the [Design System Components](/docs/design-system-components) | `components/ui/` | `components/ui/button/` |
|
|
141
|
+
| **More than one page** of this project | `components/shared/` | `components/shared/siteHeader/` |
|
|
142
|
+
| **A few pages in one section** | A `components/` folder in that section's route group | `app/(billing)/components/invoiceTable/` |
|
|
143
|
+
| **One page only** | A `components/` folder in that page's route folder | `app/account-settings/components/accountForm/` |
|
|
140
144
|
|
|
141
145
|
A route group is a folder in brackets, such as `(billing)`. It groups pages without changing their URLs: `app/(billing)/invoices/page.tsx` is served at `/invoices`.
|
|
142
146
|
|
|
@@ -168,7 +172,7 @@ project-root/
|
|
|
168
172
|
### Rules
|
|
169
173
|
|
|
170
174
|
- **Start where it is used, and move it when it is shared.** A new component goes in its page's `components/` folder. When a second page in the same section needs it, move it to the section's `components/` folder. When a page in another section needs it, move it to `components/shared/`.
|
|
171
|
-
- **`components/ui/` is only for [Design System Components](/design-system-components).** They take everything through props and import nothing from the project, so they can be copied into another project unchanged. See [Reusable by design](/Structure/component-organization#reusable-by-design).
|
|
175
|
+
- **`components/ui/` is only for [Design System Components](/docs/design-system-components).** They take everything through props and import nothing from the project, so they can be copied into another project unchanged. See [Reusable by design](/docs/Structure/component-organization#reusable-by-design).
|
|
172
176
|
- **A page or shared component wraps Design System Components.** It holds this project's logic and data, and passes props down.
|
|
173
177
|
- **Never group by type** (`atoms/`, `molecules/`, `organisms/`). Group by where a component is used.
|
|
174
178
|
|
|
@@ -262,9 +266,103 @@ Add the export to `utils/index.ts` whenever you add a function.
|
|
|
262
266
|
|
|
263
267
|
- **Start local, promote when shared.** A function begins in the component's `utils/`. When another component in the same section needs it, move its folder to the section's `utils/`. When anything outside the section needs it, move it to the global `utils/`.
|
|
264
268
|
- **One function per folder**, with the folder and file named after the function (`formatCurrency/formatCurrency.ts`), plus its test file and an `index.ts`.
|
|
265
|
-
- **Every function has a test file** in the same folder. See [Readable Code](/Standards/readable-code#every-function-and-component-has-a-test-file).
|
|
269
|
+
- **Every function has a test file** in the same folder. See [Readable Code](/docs/Standards/readable-code#every-function-and-component-has-a-test-file).
|
|
266
270
|
- **Import from the `utils` folder**, never from the files inside it.
|
|
267
|
-
- **Keep UI files clean.** If a function could be written in another file, it does not belong in the component file. See [Writing a component](/Structure/component-organization#-writing-a-component).
|
|
271
|
+
- **Keep UI files clean.** If a function could be written in another file, it does not belong in the component file. See [Writing a component](/docs/Structure/component-organization#-writing-a-component).
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 🔤 Constants: `r`, `s` and noData {#constants}
|
|
276
|
+
|
|
277
|
+
Every link, every piece of text on screen and every piece of stand-in data lives in `lib/constants/`, never written straight into a component:
|
|
278
|
+
|
|
279
|
+
```text
|
|
280
|
+
lib/constants/
|
|
281
|
+
├── routes.ts # r: every link
|
|
282
|
+
├── strings.ts # s: every piece of static text
|
|
283
|
+
├── noData.ts # Stand-in data for local development
|
|
284
|
+
└── index.ts # Exports all of them
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
A file outside the folder imports everything it needs in one line:
|
|
288
|
+
|
|
289
|
+
```tsx
|
|
290
|
+
import { NO_DATA_INSIGHTS, r, s } from "@/lib/constants";
|
|
291
|
+
|
|
292
|
+
<a href={r.contact}>{s.contact}</a>;
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### `r`: routes
|
|
296
|
+
|
|
297
|
+
**`r` holds every link the app uses**: its own pages, outside sites, and `mailto:`, `tel:` and `sms:` links.
|
|
298
|
+
|
|
299
|
+
```ts title="lib/constants/routes.ts"
|
|
300
|
+
export const r = {
|
|
301
|
+
contact: "/contact",
|
|
302
|
+
email: "mailto:mail@example.com",
|
|
303
|
+
insights: "/insights",
|
|
304
|
+
linkedIn: "https://www.linkedin.com/in/example",
|
|
305
|
+
sms: "sms:+61400000000",
|
|
306
|
+
};
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
- **A dynamic route starts from its `r` route.** Build the changing part where it is used: `` `${r.insights}/${insight.slug}` ``.
|
|
310
|
+
- **Name each key after the place it goes**, in camelCase: `r.myStory`, `r.linkedIn`.
|
|
311
|
+
|
|
312
|
+
### `s`: strings
|
|
313
|
+
|
|
314
|
+
**`s` holds every piece of static text** shown on screen: headings, buttons, labels, aria-labels, page titles and descriptions, and longer Markdown copy.
|
|
315
|
+
|
|
316
|
+
```ts title="lib/constants/strings.ts"
|
|
317
|
+
export const s = {
|
|
318
|
+
contact: "CONTACT",
|
|
319
|
+
emailAriaLabel: "Email Tom",
|
|
320
|
+
pageNotFound: "PAGE NOT FOUND",
|
|
321
|
+
pageNotFoundDescription:
|
|
322
|
+
"The page you are looking for does not exist",
|
|
323
|
+
};
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
- **The keys of `s` are the site's translation keys.** When a site gets more languages, each key is translated into each language. Name each key after what the text says or does, and never use one key for two different jobs.
|
|
327
|
+
- **Only real copy goes in `s`.** Dummy text belongs in `noData.ts`.
|
|
328
|
+
|
|
329
|
+
### noData: stand-in data for local development
|
|
330
|
+
|
|
331
|
+
**`noData.ts` stands in for outside data while you work locally**, when the CMS, database or API can't be reached, such as when you have no internet. Every page still renders with something that looks real.
|
|
332
|
+
|
|
333
|
+
```ts title="lib/constants/noData.ts"
|
|
334
|
+
const DUMMY_HEADING = "A heading for local development";
|
|
335
|
+
const DUMMY_SLUG = "/insights";
|
|
336
|
+
|
|
337
|
+
export const NO_DATA_INSIGHT = {
|
|
338
|
+
heading: DUMMY_HEADING,
|
|
339
|
+
slug: DUMMY_SLUG,
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
export const NO_DATA_INSIGHTS = [NO_DATA_INSIGHT];
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
const insights =
|
|
347
|
+
query?.insights ??
|
|
348
|
+
(IS_LOCAL_DEVELOPMENT ? NO_DATA_INSIGHTS : []);
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
`IS_LOCAL_DEVELOPMENT` is a constant in `lib/constants/`: `export const IS_LOCAL_DEVELOPMENT = process.env.NODE_ENV === "development";`.
|
|
352
|
+
|
|
353
|
+
- **Local development only.** noData is never shown in production. Where the real data is missing in production, show the page's empty state.
|
|
354
|
+
- **Name each export `NO_DATA_` plus the exact thing it stands in for**: singular for one item, plural for a list. `NO_DATA_INSIGHT` and `NO_DATA_INSIGHTS`, `NO_DATA_EVENT`, `NO_DATA_TOPICS`.
|
|
355
|
+
- **Shape it exactly like the real response**, so the code using it cannot tell the difference.
|
|
356
|
+
- **Dummy values sit at the top of `noData.ts`** as their own constants, `DUMMY_HEADING` and the rest. They are never added to `s`.
|
|
357
|
+
- **noData is for the whole site.** All of it lives in this one file.
|
|
358
|
+
|
|
359
|
+
### Rules
|
|
360
|
+
|
|
361
|
+
- **Named exports only:** `export const r`, `export const s`. No default exports.
|
|
362
|
+
- **Files outside `lib/constants/` import from `@/lib/constants`**, all in one import line.
|
|
363
|
+
- **Files inside `lib/constants/` import each other directly**, `import { s } from "./strings";`, never through `index.ts`. See [Inside a folder, import files directly](/docs/Structure/import-organization#2-inside-a-folder-import-files-directly).
|
|
364
|
+
- **`r` and `s` are the only single-letter names allowed anywhere.** See [Human-Readable Names](/docs/Standards/human-readable-names#r-and-s).
|
|
365
|
+
- **Other constants** that hold a single value, such as `MAXIMUM_RETRY_ATTEMPTS`, follow [Naming Conventions](/docs/Structure/naming-conventions#constants).
|
|
268
366
|
|
|
269
367
|
---
|
|
270
368
|
|
|
@@ -310,7 +408,7 @@ project-root/
|
|
|
310
408
|
└── docsSite/ # The viewer: Docusaurus config, no content
|
|
311
409
|
```
|
|
312
410
|
|
|
313
|
-
See [Documentation](/Standards/documentation) for the full list of folders, the doc format, and how docs are displayed and kept up to date.
|
|
411
|
+
See [Documentation](/docs/Standards/documentation) for the full list of folders, the doc format, and how docs are displayed and kept up to date.
|
|
314
412
|
|
|
315
413
|
Name documentation folders and files in **kebab-case** (`background-jobs.md`), not camelCase (`backgroundJobs.md`). The name becomes the page's URL on the docs site.
|
|
316
414
|
|
|
@@ -345,7 +443,7 @@ Avoid these anti-patterns:
|
|
|
345
443
|
|
|
346
444
|
## 📚 Naming Conventions
|
|
347
445
|
|
|
348
|
-
See [Naming Conventions](/Structure/naming-conventions) for detailed file and folder naming rules.
|
|
446
|
+
See [Naming Conventions](/docs/Structure/naming-conventions) for detailed file and folder naming rules.
|
|
349
447
|
|
|
350
448
|
**Quick reference:**
|
|
351
449
|
|