Complete Next.js 16 application with 13 fully implemented modules: Email Signature, Word XML Generator, Registratura, Dashboard, Tag Manager, IT Inventory, Address Book, Password Vault, Mini Utilities, Prompt Generator, Digital Signatures, Word Templates, and AI Chat. Includes core platform systems (module registry, feature flags, storage abstraction, i18n, theming, auth stub, tagging), 16 technical documentation files, Docker deployment config, and legacy HTML tool reference. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
18 KiB
Coding Standards
Conventions and rules for all code in the ArchiTools repository.
Language
TypeScript in strict mode. The tsconfig.json must include:
{
"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.
// Correct
export function SignaturePreview({ config }: SignaturePreviewProps) {
return <div>...</div>;
}
// 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.
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.
// 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.
// Correct
<div className="rounded-xl border bg-card p-6 shadow-sm">
// Wrong: external CSS file
import styles from './signature-preview.module.css';
<div className={styles.card}>
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):
import { cn } from '@/shared/utils/cn';
<div className={cn(
'rounded-xl border p-6',
isActive && 'border-primary bg-primary/5',
className
)} />
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.
// 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:
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
// 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
// Correct: zip library passed in (or imported at module level, testable via jest.mock)
export async function createZipArchive(
files: Record<string, string>,
zipFactory: () => JSZip = () => new JSZip()
): Promise<Blob> {
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.
// 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:
// 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:
{
"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.
// 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.
// 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
interfacefor object shapes (props, configs, data models). - Use
typefor unions, intersections, mapped types, and utility types.
// 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<SignatureConfig>;
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:
// 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:
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
export function parseXmlConfig(raw: string): Result<XmlConfig> {
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):
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
// 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
// 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:
// Wrong
<Button>Descarca HTML</Button>
// Correct
import { labels } from '@/core/i18n/labels';
<Button>{labels.signature.exportHtml}</Button>
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:
<type>(<scope>): <description>
[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/<description>-- feature branches, branched frommain.fix/<description>-- bugfix branches.chore/<description>-- 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
// Wrong
const data: any = fetchSomething();
// Correct
const data: unknown = fetchSomething();
if (isValidConfig(data)) { /* narrow type */ }
2. Default exports
// 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
// Wrong
useEffect(() => {
document.getElementById('preview')!.innerHTML = html;
}, [html]);
// Correct
return <div dangerouslySetInnerHTML={{ __html: html }} />;
// Or better: render structured JSX from data
4. Inline Romanian strings
// Wrong
<Label>Nume si Prenume</Label>
// Correct
<Label>{labels.signature.fieldName}</Label>
5. God hooks
// 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
// 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
// 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
// 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
// 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.