# 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.