Files
ArchiTools/docs/guides/CODING-STANDARDS.md
Marius Tarau 4c46e8bcdd Initial commit: ArchiTools modular dashboard platform
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>
2026-02-17 12:50:25 +02:00

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 interface for object shapes (props, configs, data models).
  • Use type for 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 from main.
  • 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.