| name | tml-frontend |
| description | Conventions and patterns for frontend development across all TML codebases. Load after the `tml-context` skill for business context, and before repo-specific frontend skills for module-level patterns. |
Stack
- Framework: Next.js (App Router)
- Language: TypeScript — strict, no
any
- Styling: CSS Modules (
.module.css per component)
- State / data fetching: React context + SWR (repo-specific)
Component structure
Every component lives in its own folder named after the component, with an index.tsx as the entry point. CSS lives in a sibling file.
ComponentName/
├── index.tsx
└── styles.module.css ← only if the component has styles
Rules:
- One component per folder. Do not put two components in the same
index.tsx.
- Keep components small and focused on a single responsibility. If a component grows beyond ~100 lines of logic, split it.
- Export components as named exports, not default exports.
- Use
'use client' at the top of any component that uses hooks, browser APIs, or event handlers.
Internal section comments
Every non-trivial component and context provider is divided into lettered sections using comments. This makes navigation predictable across the entire codebase.
export function ComponentName() {
const [value, setValue] = useState('');
const { data } = useSWR(...);
const derived = useMemo(() => ..., [data]);
const handleSubmit = async () => { ... };
const isReady = !!data && !error;
return (...);
}
Not every section is always present — use only the ones that apply. The order is always A → F. Never reorder sections.
The /* * */ divider is used between top-level logical blocks at the file level (imports, interface, context, hook, provider):
interface XxxContextState { ... }
const XxxContext = createContext(...);
export function useXxxContext() { ... }
export const XxxContextProvider = ({ children }) => {
...
};
Context pattern
Any feature with shared state across multiple child components uses a context. The pattern is always the same three exports from a single .context.tsx file:
- Interface — the shape of the context value
- Hook —
useXxxContext() for consuming the context
- Provider —
XxxContextProvider that wraps the feature tree
'use client';
interface XxxContextState {
actions: {
doSomething: () => void
}
data: {
items: Item[]
}
filters: {
query: null | string
}
flags: {
is_loading: boolean
}
}
const XxxContext = createContext<XxxContextState | undefined>(undefined);
export function useXxxContext() {
const context = useContext(XxxContext);
if (!context) {
throw new Error('useXxxContext must be used within a XxxContextProvider');
}
return context;
}
export const XxxContextProvider = ({ children }: PropsWithChildren) => {
...
return (
<XxxContext.Provider value={contextValue}>
{children}
</XxxContext.Provider>
);
};
Interface shape conventions:
actions — functions that mutate state or trigger side effects
data — raw or derived data
filters — active filter values (if the context manages filtering)
flags — booleans: loading, error, dirty, read-only, etc.
form — form instance, if the context owns a form
counters — aggregated counts derived from data
All keys inside flags use is_ / can_ / has_ prefixes (is_loading, can_save, has_error).
Folder organisation
Components are organised by feature under src/components/. Each feature gets a folder, and sub-components live inside it.
src/
├── app/ ← Next.js App Router pages and layouts
├── components/
│ ├── [feature]/ ← one folder per feature or domain area
│ │ ├── FeatureRoot/
│ │ ├── FeatureHeader/
│ │ ├── FeatureList/
│ │ └── FeatureListItem/
│ └── common/ ← components shared across features
├── contexts/ ← context files (in some repos, co-located instead)
├── hooks/ ← custom hooks
├── providers/ ← root-level providers
├── types/ ← local TypeScript types
└── utils/ ← local utility functions
Naming conventions:
- Component folders and files:
PascalCase
- Hook files:
camelCase, always prefixed with use
- Context files:
FeatureName.context.tsx
- Type files:
kebab-case.types.ts or kebab-case.ts
- Utility files:
kebab-case.ts
CSS Modules
Styles are always scoped via CSS Modules. Never use global class names inside component files.
import styles from './styles.module.css';
return <div className={styles.container}>...</div>;
For conditional classes, use string concatenation or a utility like clsx — never inline style objects for layout.
i18n
All user-facing strings are externalised for translation. String keys live in namespace JSON files under src/i18n/. Do not hardcode user-visible text directly in components.
What to avoid
- Default exports for components — use named exports
- Inline styles for layout or spacing
- Putting multiple components in a single file
- Components longer than ~150 lines — split them
- Logic inside JSX — extract to variables or handlers in the appropriate section
any in TypeScript — always type properly