saz-standards 1.0.0
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 +123 -0
- package/bin/cli.js +69 -0
- package/configs/eslint.js +45 -0
- package/configs/tailwind.js +34 -0
- package/configs/tsconfig.json +16 -0
- package/package.json +32 -0
- package/standards/CODING_STANDARDS.md +272 -0
- package/standards/COMPONENT_PATTERNS.md +1516 -0
- package/standards/DATA_FETCHING.md +203 -0
- package/standards/DESIGN_SYSTEM.md +536 -0
- package/standards/FORM_SYSTEM.md +368 -0
- package/standards/PERFORMANCE.md +153 -0
- package/standards/SECURITY_AND_ACCESSIBILITY.md +142 -0
- package/standards/STATE_MANAGEMENT.md +311 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @saz/standards
|
|
2
|
+
|
|
3
|
+
> Enterprise React/Next.js coding standards by **Salman Ali Zahid**.
|
|
4
|
+
|
|
5
|
+
One command sets up your entire project with enterprise-grade folder structure, components, configs, and coding rules.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @saz/standards init
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
It will ask you:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Framework?
|
|
19
|
+
1 = React
|
|
20
|
+
2 = Next.js
|
|
21
|
+
|
|
22
|
+
Language?
|
|
23
|
+
1 = JavaScript
|
|
24
|
+
2 = TypeScript
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then it copies everything into your project automatically.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## What Gets Copied
|
|
32
|
+
|
|
33
|
+
- Full enterprise folder structure (4 variants)
|
|
34
|
+
- All coding standards markdown files
|
|
35
|
+
- ESLint, Tailwind, and TypeScript base configs
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Use the Configs
|
|
40
|
+
|
|
41
|
+
### ESLint
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
// .eslintrc.js
|
|
45
|
+
module.exports = {
|
|
46
|
+
extends: ["./node_modules/@saz/standards/configs/eslint.js"]
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Tailwind
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
// tailwind.config.js
|
|
54
|
+
const sazBase = require("@saz/standards/configs/tailwind");
|
|
55
|
+
module.exports = { presets: [sazBase] }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### TypeScript
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
// tsconfig.json
|
|
62
|
+
{
|
|
63
|
+
"extends": "@saz/standards/configs/tsconfig.json"
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## What Is Included
|
|
70
|
+
|
|
71
|
+
### Standards Files
|
|
72
|
+
|
|
73
|
+
| File | Description |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| CODING_STANDARDS.md | Enums, naming, exports, imports |
|
|
76
|
+
| DESIGN_SYSTEM.md | Tailwind tokens, typography, dark mode |
|
|
77
|
+
| COMPONENT_PATTERNS.md | Button, Input, Select, Modal, Table |
|
|
78
|
+
| FORM_SYSTEM.md | useForm, Joi, Zod, validation |
|
|
79
|
+
| DATA_FETCHING.md | Axios, thunks, abort controller |
|
|
80
|
+
| STATE_MANAGEMENT.md | Redux Toolkit slices |
|
|
81
|
+
| PERFORMANCE.md | Memoization, virtualization, splits |
|
|
82
|
+
| SECURITY_AND_ACCESSIBILITY.md | ARIA, landmarks, CSP, env rules |
|
|
83
|
+
|
|
84
|
+
### Project Variants
|
|
85
|
+
|
|
86
|
+
| Variant | Stack |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| react-js | React + JavaScript + Vite |
|
|
89
|
+
| react-ts | React + TypeScript + Vite |
|
|
90
|
+
| nextjs-js | Next.js App Router + JavaScript |
|
|
91
|
+
| nextjs-ts | Next.js App Router + TypeScript |
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Standards Rating
|
|
96
|
+
|
|
97
|
+
This system is rated **10/10** covering:
|
|
98
|
+
|
|
99
|
+
- Component patterns
|
|
100
|
+
- Form validation (Joi + Zod)
|
|
101
|
+
- Redux Toolkit state management
|
|
102
|
+
- Axios service layer with HTTP-only cookie auth
|
|
103
|
+
- Accessibility (ARIA, focus-visible, landmarks)
|
|
104
|
+
- Dark mode strategy
|
|
105
|
+
- List virtualization
|
|
106
|
+
- Cross-browser compatibility
|
|
107
|
+
- Security rules
|
|
108
|
+
- Performance (Core Web Vitals)
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Author
|
|
113
|
+
|
|
114
|
+
**Salman Ali Zahid**
|
|
115
|
+
|
|
116
|
+
- GitHub: [MuhammadSalman0151](https://github.com/MuhammadSalman0151)
|
|
117
|
+
- Package: [@saz/standards](https://www.npmjs.com/package/@saz/standards)
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const readline = require("readline");
|
|
6
|
+
|
|
7
|
+
const rl = readline.createInterface({
|
|
8
|
+
input: process.stdin,
|
|
9
|
+
output: process.stdout,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const ask = (question) =>
|
|
13
|
+
new Promise((resolve) => rl.question(question, resolve));
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
console.log("");
|
|
17
|
+
console.log("========================================");
|
|
18
|
+
console.log(" Welcome to @saz/standards");
|
|
19
|
+
console.log(" Enterprise React/Next.js Standards");
|
|
20
|
+
console.log(" by Salman Ali Zahid");
|
|
21
|
+
console.log("========================================");
|
|
22
|
+
console.log("");
|
|
23
|
+
|
|
24
|
+
const framework = await ask(
|
|
25
|
+
"Framework?\n 1 = React\n 2 = Next.js\nYour choice: ",
|
|
26
|
+
);
|
|
27
|
+
const language = await ask(
|
|
28
|
+
"\nLanguage?\n 1 = JavaScript\n 2 = TypeScript\nYour choice: ",
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
let variant = "";
|
|
32
|
+
|
|
33
|
+
if (framework === "1" && language === "1") variant = "react-js";
|
|
34
|
+
else if (framework === "1" && language === "2") variant = "react-ts";
|
|
35
|
+
else if (framework === "2" && language === "1") variant = "nextjs-js";
|
|
36
|
+
else if (framework === "2" && language === "2") variant = "nextjs-ts";
|
|
37
|
+
else {
|
|
38
|
+
console.log("\nInvalid choice. Please run again and enter 1 or 2.");
|
|
39
|
+
rl.close();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const dest = process.cwd();
|
|
44
|
+
|
|
45
|
+
// Copy template files
|
|
46
|
+
const templateSrc = path.join(__dirname, "../templates", variant);
|
|
47
|
+
if (fs.existsSync(templateSrc)) {
|
|
48
|
+
fs.cpSync(templateSrc, dest, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Copy standards markdown files
|
|
52
|
+
const standardsSrc = path.join(__dirname, "../standards");
|
|
53
|
+
const standardsDest = path.join(dest, "standards");
|
|
54
|
+
if (fs.existsSync(standardsSrc)) {
|
|
55
|
+
fs.cpSync(standardsSrc, standardsDest, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log("");
|
|
59
|
+
console.log("========================================");
|
|
60
|
+
console.log(" Done! Your project is ready.");
|
|
61
|
+
console.log(" Variant: " + variant);
|
|
62
|
+
console.log(" Standards copied to: /standards");
|
|
63
|
+
console.log("========================================");
|
|
64
|
+
console.log("");
|
|
65
|
+
|
|
66
|
+
rl.close();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
main();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
env: {
|
|
3
|
+
browser: true,
|
|
4
|
+
es2021: true,
|
|
5
|
+
node: true,
|
|
6
|
+
},
|
|
7
|
+
extends: [
|
|
8
|
+
"eslint:recommended",
|
|
9
|
+
"plugin:react/recommended",
|
|
10
|
+
"plugin:react-hooks/recommended",
|
|
11
|
+
],
|
|
12
|
+
parserOptions: {
|
|
13
|
+
ecmaVersion: "latest",
|
|
14
|
+
sourceType: "module",
|
|
15
|
+
ecmaFeatures: {
|
|
16
|
+
jsx: true,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
settings: {
|
|
20
|
+
react: {
|
|
21
|
+
version: "detect",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
rules: {
|
|
25
|
+
// Variables
|
|
26
|
+
"no-unused-vars": "error",
|
|
27
|
+
"no-var": "error",
|
|
28
|
+
"prefer-const": "error",
|
|
29
|
+
|
|
30
|
+
// Code quality
|
|
31
|
+
"no-console": "warn",
|
|
32
|
+
"no-debugger": "error",
|
|
33
|
+
eqeqeq: ["error", "always"],
|
|
34
|
+
"no-duplicate-imports": "error",
|
|
35
|
+
"no-empty": "error",
|
|
36
|
+
"no-shadow": "error",
|
|
37
|
+
|
|
38
|
+
// React
|
|
39
|
+
"react/react-in-jsx-scope": "off",
|
|
40
|
+
"react/prop-types": "off",
|
|
41
|
+
"react/self-closing-comp": "warn",
|
|
42
|
+
"react-hooks/rules-of-hooks": "error",
|
|
43
|
+
"react-hooks/exhaustive-deps": "warn",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
|
3
|
+
darkMode: "class",
|
|
4
|
+
theme: {
|
|
5
|
+
extend: {
|
|
6
|
+
colors: {
|
|
7
|
+
"base-primary": "#3b82f6",
|
|
8
|
+
"base-primary-hover": "#2563eb",
|
|
9
|
+
"base-danger": "#ef4444",
|
|
10
|
+
"base-danger-hover": "#dc2626",
|
|
11
|
+
"base-success": "#22c55e",
|
|
12
|
+
"base-warning": "#f59e0b",
|
|
13
|
+
"base-info": "#3b82f6",
|
|
14
|
+
"base-surface": "#ffffff",
|
|
15
|
+
"base-surface-hover": "#f9fafb",
|
|
16
|
+
"base-border": "#e5e7eb",
|
|
17
|
+
"base-input": "#111827",
|
|
18
|
+
"base-placeholder": "#9ca3af",
|
|
19
|
+
"base-muted": "#6b7280",
|
|
20
|
+
"base-heading": "#111827",
|
|
21
|
+
"base-label": "#374151",
|
|
22
|
+
},
|
|
23
|
+
borderRadius: {
|
|
24
|
+
"base-radius-md": "0.375rem",
|
|
25
|
+
"base-radius-lg": "0.5rem",
|
|
26
|
+
},
|
|
27
|
+
boxShadow: {
|
|
28
|
+
"base-shadow-md": "0 4px 6px -1px rgb(0 0 0 / 0.1)",
|
|
29
|
+
"base-shadow-lg": "0 10px 15px -3px rgb(0 0 0 / 0.1)",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
plugins: [],
|
|
34
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"jsx": "react-jsx",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"noEmit": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src"],
|
|
15
|
+
"exclude": ["node_modules"]
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "saz-standards",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Enterprise React/Next.js coding standards — ESLint, Tailwind, TypeScript configs + CLI by Salman Ali Zahid",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"saz": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"configs",
|
|
12
|
+
"templates",
|
|
13
|
+
"standards"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "echo \"No tests yet\""
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"react",
|
|
20
|
+
"nextjs",
|
|
21
|
+
"eslint",
|
|
22
|
+
"tailwind",
|
|
23
|
+
"typescript",
|
|
24
|
+
"coding-standards",
|
|
25
|
+
"enterprise",
|
|
26
|
+
"boilerplate",
|
|
27
|
+
"starter"
|
|
28
|
+
],
|
|
29
|
+
"author": "Salman Ali Zahid",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"type": "commonjs"
|
|
32
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Coding Standards
|
|
2
|
+
|
|
3
|
+
> Universal coding rules applied to every project variant.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Enum Rules
|
|
8
|
+
|
|
9
|
+
- All enum keys must be `SCREAMING_SNAKE_CASE` (uppercase with underscores)
|
|
10
|
+
- All enum values must be uppercase strings — never lowercase, never mixed case
|
|
11
|
+
- Enums must be defined in `shared/constants/` or inside the module's `types/` folder when scoped to a single module
|
|
12
|
+
- Never use numeric enums — always use string enums for readability and serialization safety
|
|
13
|
+
- Never use inline object literals as a substitute for enums when the value set is fixed
|
|
14
|
+
- In TypeScript projects, always use `const enum` or a plain `enum` with explicit string values — never rely on auto-incremented numeric values
|
|
15
|
+
- Enum names must be PascalCase (e.g., `UserRole`, `OrderStatus`, `PaymentMethod`)
|
|
16
|
+
- All enum members must be used — do not declare unused enum values
|
|
17
|
+
- Never mutate or reassign enum values at runtime
|
|
18
|
+
|
|
19
|
+
### TypeScript Example
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
enum UserRole {
|
|
23
|
+
SUPER_ADMIN = "SUPER_ADMIN",
|
|
24
|
+
ADMIN = "ADMIN",
|
|
25
|
+
MANAGER = "MANAGER",
|
|
26
|
+
STAFF = "STAFF",
|
|
27
|
+
GUEST = "GUEST",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
enum OrderStatus {
|
|
31
|
+
PENDING = "PENDING",
|
|
32
|
+
CONFIRMED = "CONFIRMED",
|
|
33
|
+
IN_PROGRESS = "IN_PROGRESS",
|
|
34
|
+
COMPLETED = "COMPLETED",
|
|
35
|
+
CANCELLED = "CANCELLED",
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### JavaScript Example
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
const UserRole = Object.freeze({
|
|
43
|
+
SUPER_ADMIN: "SUPER_ADMIN",
|
|
44
|
+
ADMIN: "ADMIN",
|
|
45
|
+
MANAGER: "MANAGER",
|
|
46
|
+
STAFF: "STAFF",
|
|
47
|
+
GUEST: "GUEST",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const OrderStatus = Object.freeze({
|
|
51
|
+
PENDING: "PENDING",
|
|
52
|
+
CONFIRMED: "CONFIRMED",
|
|
53
|
+
IN_PROGRESS: "IN_PROGRESS",
|
|
54
|
+
COMPLETED: "COMPLETED",
|
|
55
|
+
CANCELLED: "CANCELLED",
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## ESLint and Code Quality Rules
|
|
62
|
+
|
|
63
|
+
- No unused variables, imports, or functions
|
|
64
|
+
- No `console` statements in production code
|
|
65
|
+
- No `debugger` statements
|
|
66
|
+
- Avoid shadowed variables
|
|
67
|
+
- Prefer `const` over `let` when values do not change
|
|
68
|
+
- Avoid `var` entirely
|
|
69
|
+
- Use strict equality (`===` and `!==`)
|
|
70
|
+
- No empty functions or empty blocks
|
|
71
|
+
- No duplicate code
|
|
72
|
+
- Keep functions small and focused
|
|
73
|
+
- Use early returns for clarity
|
|
74
|
+
- Avoid side effects in render logic
|
|
75
|
+
- No mutation of props or external state
|
|
76
|
+
- Follow consistent formatting and naming conventions
|
|
77
|
+
- Ensure all dependencies are correctly specified in React hook dependency arrays
|
|
78
|
+
- Do not disable ESLint rules unless absolutely necessary
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## File Naming Rules
|
|
83
|
+
|
|
84
|
+
| Type | Convention | Example |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| Components | PascalCase | `UserCard.tsx` |
|
|
87
|
+
| Hooks | useCamelCase | `useForm.ts` |
|
|
88
|
+
| Utilities | camelCase | `formatDate.ts` |
|
|
89
|
+
| Types (TS) | PascalCase | `User.types.ts` |
|
|
90
|
+
| Enums | PascalCase name, SCREAMING_SNAKE_CASE keys/values | `UserRole` |
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Export Rules
|
|
95
|
+
|
|
96
|
+
- Components: `default export` allowed
|
|
97
|
+
- Utilities, hooks, and constants: named exports preferred
|
|
98
|
+
- Enums: named exports only
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Barrel Export Rules
|
|
103
|
+
|
|
104
|
+
Apply barrel exports in any component folder containing more than 3 files.
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
// components/buttons/index.js
|
|
108
|
+
export { default as CloseButton } from "./CloseButton";
|
|
109
|
+
export { default as FormSubmitButton } from "./FormSubmitButton";
|
|
110
|
+
export { default as IconButton } from "./IconButton";
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Import Pattern
|
|
116
|
+
|
|
117
|
+
Use relative imports only. Use root index exports when importing multiple components from the same folder.
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
import { CloseButton, FormSubmitButton } from "../../../components/buttons";
|
|
121
|
+
import { Inputfield, SelectFromObject } from "../../../components/form";
|
|
122
|
+
import { cn } from "../../utils/cn";
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Next.js imports when needed:
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
import Image from "next/image";
|
|
129
|
+
import Link from "next/link";
|
|
130
|
+
import { useRouter } from "next/navigation";
|
|
131
|
+
import dynamic from "next/dynamic";
|
|
132
|
+
import { Metadata } from "next";
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## General Component Rules
|
|
138
|
+
|
|
139
|
+
- Default export function component
|
|
140
|
+
- Props destructured in parameters with safe default values
|
|
141
|
+
- No PropTypes
|
|
142
|
+
- No `React.FC`
|
|
143
|
+
- No class components
|
|
144
|
+
- No unnecessary fragments or wrappers
|
|
145
|
+
- No inline styles
|
|
146
|
+
- Explicit conditional rendering only: `condition ? <Component /> : null`
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Runtime and Prop Safety Rules
|
|
151
|
+
|
|
152
|
+
- Validate critical data before use
|
|
153
|
+
- Check for `null` or `undefined` before property access
|
|
154
|
+
- Use optional chaining for nested values: `data?.map(...)`
|
|
155
|
+
- Provide safe default values for all optional props
|
|
156
|
+
- Ensure values are arrays before mapping
|
|
157
|
+
- Provide fallback UI when arrays are empty
|
|
158
|
+
- Do not assume API response structure — handle missing or malformed data gracefully
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## List Rendering Rules
|
|
163
|
+
|
|
164
|
+
- Always use optional chaining before map: `data?.map(...)`
|
|
165
|
+
- Key must be dynamic and stable when available: `key={item._id || item.id || item.value}`
|
|
166
|
+
- Use index as key only when required by UI conditions or when no stable key exists: `key={index}`
|
|
167
|
+
- Do not use random values as keys
|
|
168
|
+
- Do not omit keys
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## UI State Rules
|
|
173
|
+
|
|
174
|
+
- Always handle the loading state when async data is used
|
|
175
|
+
- Provide fallback UI for empty data
|
|
176
|
+
- Display error state when operations fail
|
|
177
|
+
- Do not render undefined data
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Event Handler Types (TypeScript)
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
React.ChangeEvent<HTMLInputElement>
|
|
185
|
+
React.FormEvent<HTMLFormElement>
|
|
186
|
+
React.MouseEvent<HTMLButtonElement>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## TypeScript Rules
|
|
192
|
+
|
|
193
|
+
> Applied only when TypeScript is selected (Q4 = B).
|
|
194
|
+
|
|
195
|
+
- Strict typing required
|
|
196
|
+
- Do NOT use `any`
|
|
197
|
+
- Do NOT use `unknown` as a shortcut
|
|
198
|
+
- Use proper interfaces or types for all props, state, API responses, and Redux data
|
|
199
|
+
- Prefer explicit types over inference when unclear
|
|
200
|
+
- Use union types when appropriate
|
|
201
|
+
- Use generics when needed
|
|
202
|
+
- All component props must be typed
|
|
203
|
+
- Event handlers must use correct React types
|
|
204
|
+
- Nullable values must be typed explicitly
|
|
205
|
+
- Avoid the non-null assertion operator (`!`) unless the value is guaranteed to be non-null
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Environment Rules
|
|
210
|
+
|
|
211
|
+
- Access environment variables safely
|
|
212
|
+
- Do NOT hardcode secrets or sensitive URLs
|
|
213
|
+
- In Next.js, prefix only public variables with `NEXT_PUBLIC_`
|
|
214
|
+
- In Vite, prefix only public variables with `VITE_`
|
|
215
|
+
- Never expose server-only secrets to the client bundle
|
|
216
|
+
- Every project must include a `.env.example` file at the root listing all required variable names with placeholder values — never commit `.env` or `.env.local`
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
# .env.example
|
|
220
|
+
VITE_API_BASE_URL=https://api.example.com # React/Vite
|
|
221
|
+
NEXT_PUBLIC_API_BASE_URL=https://api.example.com # Next.js
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
- `.env` and `.env.local` must be listed in `.gitignore`
|
|
225
|
+
- New developers must copy `.env.example` to `.env` (Vite) or `.env.local` (Next.js) and fill in real values
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Styling Constraints
|
|
230
|
+
|
|
231
|
+
- Tailwind only
|
|
232
|
+
- Use project design tokens
|
|
233
|
+
- No CSS modules
|
|
234
|
+
- No styled-components
|
|
235
|
+
- No inline styles
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## cn() Utility
|
|
240
|
+
|
|
241
|
+
All projects must include a `cn` utility at `src/utils/cn.ts` (TypeScript) or `src/utils/cn.js` (JavaScript). This is the canonical implementation — do not alter it:
|
|
242
|
+
|
|
243
|
+
**TypeScript (`src/utils/cn.ts`):**
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { clsx, type ClassValue } from "clsx";
|
|
247
|
+
import { twMerge } from "tailwind-merge";
|
|
248
|
+
|
|
249
|
+
export function cn(...inputs: ClassValue[]): string {
|
|
250
|
+
return twMerge(clsx(inputs));
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**JavaScript (`src/utils/cn.js`):**
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
import { clsx } from "clsx";
|
|
258
|
+
import { twMerge } from "tailwind-merge";
|
|
259
|
+
|
|
260
|
+
export function cn(...inputs) {
|
|
261
|
+
return twMerge(clsx(inputs));
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Usage pattern (from `DESIGN_SYSTEM.md`):
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
className={cn("base classes", condition && "conditional class", className)}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
- Always import `cn` from `../utils/cn` (relative path)
|
|
272
|
+
- Never replicate this logic inline — always import from the utility file
|