# Coding Standards
Conventions and rules for all code in the ArchiTools repository.
---
## Language
TypeScript in strict mode. The `tsconfig.json` must include:
```json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true
}
}
```
**No `any`** unless there is a documented, unavoidable reason (e.g., third-party library with missing types). When `any` is truly necessary, use `// eslint-disable-next-line @typescript-eslint/no-explicit-any` with a comment explaining why.
Prefer `unknown` over `any` when the type is genuinely unknown. Narrow with type guards.
---
## File Naming
| Entity | Convention | Example |
|---|---|---|
| Files (components) | kebab-case | `signature-preview.tsx` |
| Files (hooks) | kebab-case with `use-` prefix | `use-signature-builder.ts` |
| Files (utils/services) | kebab-case | `sanitize-xml-name.ts` |
| Files (types) | `types.ts` within module | `types.ts` |
| Files (tests) | `*.test.ts` / `*.test.tsx` | `sanitize-xml-name.test.ts` |
| Component names | PascalCase | `SignaturePreview` |
| Hook names | camelCase with `use` prefix | `useSignatureBuilder` |
| Utility functions | camelCase | `sanitizeXmlName` |
| Constants | UPPER_SNAKE_CASE | `DEFAULT_NAMESPACE` |
| Type/Interface names | PascalCase | `SignatureConfig` |
| Enum values | PascalCase | `FieldVariant.Upper` |
---
## Component Patterns
### Functional Components Only
No class components. All components are functions.
```tsx
// Correct
export function SignaturePreview({ config }: SignaturePreviewProps) {
return
...
;
}
// Wrong: arrow function export (less debuggable in stack traces)
export const SignaturePreview = ({ config }: SignaturePreviewProps) => { ... };
// Wrong: default export
export default function SignaturePreview() { ... }
```
Exception: arrow functions are acceptable for small inline components passed as props or used in `.map()`.
### Props Interface
Define the props interface directly above the component, in the same file. Suffix with `Props`.
```tsx
interface SignaturePreviewProps {
config: SignatureConfig;
className?: string;
onExport?: () => void;
}
export function SignaturePreview({ config, className, onExport }: SignaturePreviewProps) {
// ...
}
```
If the props interface is reused across multiple files, define it in the module's `types.ts` and import it.
### Named Exports
All exports are named. No default exports anywhere in the codebase.
Rationale: named exports enforce consistent import names, improve refactoring, and work better with tree-shaking.
```tsx
// Correct
export function SignaturePreview() { ... }
export function useSignatureBuilder() { ... }
export interface SignatureConfig { ... }
// Wrong
export default function SignaturePreview() { ... }
```
### Co-located Styles
All styling is done with Tailwind classes in JSX. No CSS modules, no styled-components, no separate `.css` files per component.
```tsx
// Correct
// Wrong: external CSS file
import styles from './signature-preview.module.css';
```
Global styles live only in `src/app/globals.css` (theme tokens, font imports, base resets).
### Conditional Classes
Use the `cn()` utility (from `src/shared/utils/cn.ts`, wrapping `clsx` + `tailwind-merge`):
```tsx
import { cn } from '@/shared/utils/cn';
```
---
## Hook Patterns
### Naming
All hooks start with `use`. File names match: `use-signature-builder.ts` exports `useSignatureBuilder`.
### Return Type
Return a typed object, not a tuple/array.
```tsx
// Correct
export function useSignatureBuilder(config: SignatureConfig): SignatureBuilderState {
// ...
return { html, previewData, isGenerating };
}
// Wrong: tuple return (positional args are fragile)
export function useSignatureBuilder(config: SignatureConfig) {
return [html, previewData, isGenerating];
}
```
Exception: simple two-value hooks that mirror `useState` semantics may return a tuple if the pattern is unambiguous: `const [value, setValue] = useSomeState()`.
### Single Responsibility
Each hook does one thing. If a hook grows beyond ~80 lines, consider splitting.
```
useSignatureConfig -- manages form state
useSignatureBuilder -- generates HTML from config
useSignatureExport -- handles file download
```
Not:
```
useSignature -- does everything
```
### Dependencies
Hooks that depend on external services (storage, API) receive them as parameters:
```tsx
export function useCategoryManager(storage: StorageAdapter): CategoryManagerState {
// ...
}
```
This enables testing with mock storage.
---
## Service Patterns
Services are modules that encapsulate business logic outside of React's component lifecycle.
### Pure Functions Where Possible
```tsx
// Correct: pure function, easy to test
export function sanitizeXmlName(name: string): string | null {
// ...
}
// Correct: pure function with explicit dependencies
export function generateXml(input: XmlGeneratorInput): XmlGeneratorResult {
// ...
}
```
### Accept Dependencies via Parameters
```tsx
// Correct: zip library passed in (or imported at module level, testable via jest.mock)
export async function createZipArchive(
files: Record,
zipFactory: () => JSZip = () => new JSZip()
): Promise {
const zip = zipFactory();
// ...
}
```
### No Direct DOM Access
Services must never call `document.*`, `window.localStorage`, or any browser API directly. All browser interactions are mediated through hooks or adapters that are injected.
```tsx
// Wrong: service directly accessing DOM
export function downloadFile(content: string) {
const a = document.createElement('a'); // NO
// ...
}
// Correct: service returns data, hook handles DOM interaction
export function prepareDownload(content: string, filename: string): DownloadPayload {
return { blob: new Blob([content]), filename };
}
```
---
## Import Ordering
Imports are grouped in this order, separated by blank lines:
```tsx
// 1. React
import { useState, useCallback } from 'react';
// 2. Next.js
import { useRouter } from 'next/navigation';
// 3. Third-party libraries
import JSZip from 'jszip';
import { z } from 'zod';
// 4. Core (@/core)
import { labels } from '@/core/i18n/labels';
import { useStorage } from '@/core/storage/use-storage';
// 5. Shared (@/shared)
import { Button } from '@/shared/components/ui/button';
import { cn } from '@/shared/utils/cn';
// 6. Module-level (@/modules)
import { useXmlGenerator } from '@/modules/xml-generator/hooks/use-xml-generator';
// 7. Relative imports (same module)
import { SignaturePreview } from './signature-preview';
import type { SignatureConfig } from '../types';
```
Enforce with ESLint `import/order` rule.
---
## Path Aliases
Configured in `tsconfig.json`:
```json
{
"compilerOptions": {
"paths": {
"@/core/*": ["./src/core/*"],
"@/shared/*": ["./src/shared/*"],
"@/modules/*": ["./src/modules/*"],
"@/config/*": ["./src/config/*"]
}
}
}
```
**Rules:**
- Always use path aliases for cross-boundary imports.
- Use relative imports only within the same module directory.
- Never use `../../../` chains that cross module boundaries.
```tsx
// Correct: cross-boundary via alias
import { labels } from '@/core/i18n/labels';
// Correct: within same module via relative
import { SignaturePreview } from './signature-preview';
// Wrong: relative path crossing module boundary
import { labels } from '../../../core/i18n/labels';
```
---
## No Barrel Re-exports from Module Boundaries
Do not create `index.ts` files that re-export everything from a module.
```
// WRONG: src/modules/xml-generator/index.ts
export * from './hooks/use-xml-generator';
export * from './hooks/use-category-manager';
export * from './utils/sanitize-xml-name';
export * from './types';
```
Rationale: barrel files break tree-shaking in Next.js, cause circular dependency issues, and make import costs invisible. Import directly from the specific file.
```tsx
// Correct: direct import
import { useXmlGenerator } from '@/modules/xml-generator/hooks/use-xml-generator';
// Wrong: barrel import
import { useXmlGenerator } from '@/modules/xml-generator';
```
Exception: `src/shared/components/ui/` may use barrel re-exports since shadcn/ui generates them and they are leaf components with no circular dependency risk.
---
## Type Conventions
### `interface` vs `type`
- Use `interface` for object shapes (props, configs, data models).
- Use `type` for unions, intersections, mapped types, and utility types.
```tsx
// Object shape: interface
interface SignatureConfig {
prefix: string;
name: string;
colors: ColorMap;
}
// Union: type
type FieldVariant = 'base' | 'short' | 'upper' | 'lower' | 'initials' | 'first';
// Intersection: type
type SignatureWithMeta = SignatureConfig & { id: string; createdAt: string };
// Mapped type: type
type PartialSignature = Partial;
```
### Naming Suffixes
| Suffix | Usage | Example |
|---|---|---|
| `Props` | Component props | `SignaturePreviewProps` |
| `State` | Hook return types | `SignatureBuilderState` |
| `Result` | Service return types | `XmlGeneratorResult` |
| `Config` | Configuration objects | `SignatureConfig` |
| `Input` | Service/function input parameters | `XmlGeneratorInput` |
### Enums
Prefer union types over TypeScript enums for simple value sets:
```tsx
// Preferred
type GeneratorMode = 'simple' | 'advanced';
// Acceptable for larger sets with associated logic
enum FieldVariant {
Base = 'base',
Short = 'short',
Upper = 'upper',
Lower = 'lower',
Initials = 'initials',
First = 'first',
}
```
---
## Error Handling
### Result Pattern for Services
Service functions that can fail return a discriminated union instead of throwing:
```tsx
type Result =
| { success: true; data: T }
| { success: false; error: E };
export function parseXmlConfig(raw: string): Result {
try {
const parsed = JSON.parse(raw);
// validate...
return { success: true, data: parsed };
} catch (e) {
return { success: false, error: new Error('Invalid configuration format') };
}
}
```
### try/catch at Boundaries
Use `try/catch` only at the boundary between services and UI (in hooks or event handlers):
```tsx
export function useXmlExport() {
const handleDownload = useCallback(async () => {
try {
const blob = await createZipArchive(files);
triggerDownload(blob, filename);
} catch (error) {
toast.error(labels.common.exportFailed);
}
}, [files, filename]);
return { handleDownload };
}
```
Do not scatter `try/catch` through utility functions. Let errors propagate to the boundary.
### Never Swallow Errors Silently
```tsx
// Wrong: silent failure
try { storage.set('key', data); } catch (e) {}
// Correct: at minimum, log
try {
storage.set('key', data);
} catch (error) {
console.error('Failed to persist data:', error);
}
```
---
## Comments
### When to Comment
- Non-obvious business logic (e.g., "POT = SuprafataConstruitaLaSol / SuprafataTeren per Romanian building code").
- Workarounds for known issues (link to issue/PR).
- Regex patterns (always explain what the regex matches).
- Magic numbers that cannot be replaced with named constants.
### When Not to Comment
- Self-explanatory code. If the function is `sanitizeXmlName`, do not add `/** Sanitizes an XML name */`.
- JSDoc on every function. Add JSDoc only on public API boundaries of shared utilities.
- TODO comments without an owner or issue reference.
### Comment Style
```tsx
// Single-line comments for inline explanations.
/**
* Multi-line JSDoc only for shared utility public API.
* Describe what the function does, not how.
*/
export function formatPhoneNumber(raw: string): PhoneFormatResult {
// Romanian mobile numbers: 07xx xxx xxx -> +40 7xx xxx xxx
// ...
}
```
---
## Romanian Text
All user-facing Romanian text lives exclusively in `src/core/i18n/labels.ts`.
**Components never contain inline Romanian strings:**
```tsx
// Wrong
// Correct
import { labels } from '@/core/i18n/labels';
```
**Exceptions:**
- Test files may contain Romanian strings as test fixtures.
- Data files (presets, defaults) may contain Romanian field names that are domain terms, not UI labels.
Romanian diacritics in label values: use them where correct (`Semnatura` vs `Semnatura` -- prefer with diacritics in label constants if the display context supports it). For technical identifiers (XML field names, CSS classes, file paths), never use diacritics.
---
## Git Conventions
### Commit Messages
Follow [Conventional Commits](https://www.conventionalcommits.org/):
```
():
[optional body]
[optional footer]
```
**Types:**
| Type | Usage |
|---|---|
| `feat` | New feature |
| `fix` | Bug fix |
| `refactor` | Code restructuring without behavior change |
| `docs` | Documentation only |
| `style` | Formatting, whitespace (not CSS) |
| `test` | Adding or fixing tests |
| `chore` | Build, tooling, dependencies |
| `perf` | Performance improvement |
**Scope:** module name or area (`signature`, `xml-generator`, `core`, `ui`, `config`).
**Examples:**
```
feat(signature): add multi-company support to signature builder
fix(xml-generator): handle duplicate field names in category
refactor(core): extract storage abstraction from localStorage calls
docs(guides): add HTML tool integration plan
chore(deps): upgrade shadcn/ui to 2.1.0
```
### Branching
- `main` -- production-ready code. Protected. No direct pushes.
- `feature/` -- feature branches, branched from `main`.
- `fix/` -- bugfix branches.
- `chore/` -- maintenance branches.
### Pull Requests
- One logical change per PR.
- PR title follows conventional commit format.
- PR description includes: what changed, why, how to test.
- All CI checks must pass before merge.
- Squash merge to `main` (clean linear history).
---
## Anti-Patterns
The following patterns are explicitly prohibited in this codebase. Each entry includes the reason and the correct alternative.
### 1. `any` as escape hatch
```tsx
// Wrong
const data: any = fetchSomething();
// Correct
const data: unknown = fetchSomething();
if (isValidConfig(data)) { /* narrow type */ }
```
### 2. Default exports
```tsx
// Wrong
export default function Page() { ... }
// Correct (Next.js pages are the sole exception -- App Router requires default exports for page.tsx/layout.tsx)
// For page.tsx and layout.tsx ONLY, default export is acceptable because Next.js requires it.
export default function Page() { ... }
// Everything else: named exports
export function SignatureForm() { ... }
```
### 3. Direct DOM manipulation in components
```tsx
// Wrong
useEffect(() => {
document.getElementById('preview')!.innerHTML = html;
}, [html]);
// Correct
return ;
// Or better: render structured JSX from data
```
### 4. Inline Romanian strings
```tsx
// Wrong
// Correct
```
### 5. God hooks
```tsx
// Wrong: hook that manages 15 pieces of state and 10 callbacks
export function useEverything() { ... }
// Correct: split by responsibility
export function useSignatureConfig() { ... }
export function useSignatureBuilder() { ... }
export function useSignatureExport() { ... }
```
### 6. Barrel re-exports at module boundary
```tsx
// Wrong: src/modules/xml-generator/index.ts
export * from './hooks/use-xml-generator';
// Correct: import directly from source file
import { useXmlGenerator } from '@/modules/xml-generator/hooks/use-xml-generator';
```
### 7. Business logic in components
```tsx
// Wrong: XML sanitization logic inline in JSX
function XmlForm() {
const sanitize = (name: string) => {
let n = name.replace(/\s+/g, '_');
// 20 more lines of logic...
};
}
// Correct: extract to utility, test separately
import { sanitizeXmlName } from '../utils/sanitize-xml-name';
```
### 8. Untyped hook returns
```tsx
// Wrong: caller has to guess the shape
export function useConfig() {
return [config, setConfig, isLoading, error];
}
// Correct: explicit interface
interface ConfigState {
config: Config;
setConfig: (c: Config) => void;
isLoading: boolean;
error: Error | null;
}
export function useConfig(): ConfigState { ... }
```
### 9. `useEffect` for derived state
```tsx
// Wrong: effect to compute a value from props
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${prefix} ${name}`);
}, [prefix, name]);
// Correct: derive during render
const fullName = `${prefix} ${name}`;
// Or useMemo if computation is expensive
const fullName = useMemo(() => expensiveFormat(prefix, name), [prefix, name]);
```
### 10. Committing secrets or environment files
Never commit `.env`, `.env.local`, credentials, API keys, or tokens. The `.gitignore` must include these patterns. If a secret is accidentally committed, rotate it immediately -- do not just delete the file.