This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
You are an expert in TypeScript, JavaScript, HTML, SCSS and Teact with deep experience in our project's simplified React-like API. You are working on a modern web app for Telegram.
-
Be concise. Only change code directly related to the current task; leave unrelated parts untouched.
-
Reuse existing types, functions and components. Search before creating a new one.
-
No new libraries. Use existing dependencies only. If a task truly can't be done without a new library, stop and explain why.
-
Do not write tests.
-
SCSS modules:
- Name classes in camelCase.
- Import as
stylesin your component:/* Component.module.scss */ .myWrapper { /*…*/ }
/* Component.tsx */ import styles from "./Component.module.scss"; <div className={buildClassName(styles.myWrapper, "legacy-class")} />
- Use buildClassName.ts to merge multiple class names.
- Always extract styles to files - avoid inline styles unless absolutely necessary.
- If file already imports styles, check where they come from and add new styles there - don't create new style files.
- Prefer rem units for all measurements. Exceptions are possible, but usually rare.
-
Code Style:
- Early returns.
- Prefix boolean variables with primary or modal auxiliaries (e.q.
isOpen,willUpdate,shouldRender). - Functions should start with a verb (e.q.
openModal,closeDialog,handleClick). - Prefer checking required parameter before calling a function, avoid making it optional and checking at the beginning of the function.
- Only leave comments for complex logic.
- Do not use
null. There's linter rule to enforce it. - IMPORTANT: Avoid conditional spread operators - TypeScript doesn't check if spread fields match the target type.
// ❌ BAD - No type checking { ...condition && { field: value } } // ✅ GOOD - Full type checking { field: condition ? value : undefined }
- IMPORTANT: Use string templates for inline styles - Always use template literals for style prop. Teact does not support object:
// ✅ CORRECT style={`transform: translateX(${value}%)`} // ❌ WRONG style={{ transform: `translateX(${value}%)` }} style={{ '--custom-prop': value } as React.CSSProperties}
- IMPORTANT: Font weights in CSS - Always use existing CSS variables for font-weight. Never use numeric values or custom values.
// ✅ CORRECT font-weight: var(--font-weight-medium); font-weight: var(--font-weight-bold); // ❌ WRONG font-weight: 600; font-weight: bold;
-
Localization & Text Rules:
- ALWAYS use
lang()for all text content - never hardcode strings. lang()can accept parameters:lang('Key', { param: value }).- Add new translations to
src/assets/localization/fallback.strings.
- ALWAYS use
-
After your solution:
- Critique it—identify any shortcomings.
- Fix those issues, do more planning.
- Present the improved result.
-
When deeper debugging is needed:
- Outline clear, step-by-step debugging instructions for the operator.
- Remove any temporary debug code once the issue is resolved.
-
Lint errors you can't fix manually: Suggest running
eslint --fix <filename>.
- The master file is
src/lib/gramjs/tl/static/api.tl(TL syntax). - Don't edit this autogenerated file. TypeScript types live in
api.d.ts. - We use GramJS inside a web worker; UI code uses plain objects (
Api*types) insrc/api/types.
- Make sure to include the method name in
api.json. - Run:
npm run gramjs:tlto regenerate api.d.ts.
3. In src/api/gramjs/methods/, pick a file for your method, then:
- Name fetchers
fetch*if the TL method starts withget. - Use a destructured parameter object.
- Call the API via:
const result = await invokeRequest( new GramJs.namespace.MethodName({ /* params */ }) );
- If
resultisundefined, returnundefinedto signal an error. - Convert any returned GramJS classes into plain
Api*objects.
Convesion from and to Api* objects is done by apiBuilders (function name starts with buildApi*) and gramjsBuilders (function name buildInput*).
-
In your actions, call:
const result = await callApi('methodName', { /* params */ });
-
Always check for
undefinedbefore proceeding.
// src/api/gramjs/methods/users.ts
export async function fetchUsers({ users }: { users: ApiUser[] }) {
const result = await invokeRequest(new GramJs.users.GetUsers({
id: users.map(({ id, accessHash }) => buildInputUser(id, accessHash)),
}));
if (!result || !result.length) {
return undefined;
}
const apiUsers = result.map(buildApiUser).filter(Boolean);
const userStatusesById = buildApiUserStatuses(result);
return {
users: apiUsers,
userStatusesById,
};
}
// src/global/actions/api/users.ts
addActionHandler('loadUser', async (global, actions, { userId }) => {
const user = selectUser(global, userId);
if (!user) return;
const res = await callApi('fetchUsers', { users: [user] });
if (!res) return;
// update global state...
});- Updates come in via
mtpUpdateHandler.ts. - They're routed through
src/global/actions/apiUpdatersto merge into global state. - Types are defined in
src/api/types/updates.ts.
- All components use JSX and render with Teact.
- Only import from
'react'when you need React types that are not provided in Teact. - Built-in hooks live in Teact library. Import them from there.
- Split your props into two types:
- OwnProps: data passed in by the parent
- StateProps: data injected by
withGlobalHOC
- Merge them as
OwnProps & StatePropswhen defining your component. - You can skip one or both if they are not used.
- Order rule: list any function types last in your props definitions.
- useLastCallback is your go-to for callbacks, since it won't trigger re-renders and always uses the latest scope.
- Only use useCallback when you really need to memoize a render function.
- Prefer useFlag() over
useState<boolean>()for simple boolean toggles. - Check the
hooks/folders for additional utilities.
Migrate any old
FCsyntax to the new form.
// Before
const OldComp: FC<OwnProps & StateProps> = ({ … }) => { … }
// After
const NewComp = (props: OwnProps & StateProps) => { … }- Wrap most components with
memo()to avoid unnecessary updates. - Don't pass freshly created objects or arrays as props to memoized components.
- Exceptions (no memo):
ListItem,Button,MenuItem, etc.
- Call
const lang = useLang()at the top of your component. - Look up the localization guide for how to add new language keys.
import { memo, useState, useRef } from '../../lib/teact/teact';
import { withGlobal, getActions } from '../../global';
import useFlag from '../../hooks/useFlag';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import styles from './Component.module.scss';
type OwnProps = {
id: string;
className?: string;
onClick?: NoneToVoidFunction;
};
type StateProps = {
stateValue?: string;
};
// Constants first
const MAX_ITEMS = 10
const Component = ({ id, className, stateValue, onClick }: OwnProps & StateProps) => {
const { someAction } = getActions(); // Should always be first, if actions are used
const ref = useRef<HTMLDivElement>();
const [color, setColor] = useState('#FF00FF');
const [isOpen, open, close] = useFlag();
const lang = useLang(); // Somewhere near the top, after state definition
const handleClick = useLastCallback(() => {
if (!ref.current) return;
const el = ref.current;
setColor(el.dataset.value);
close();
onClick?.();
someAction(el.dataset.value);
});
return (
<div ref={ref} className={styles.root + (className ? ` ${className}` : '')}>
<button onClick={handleClick}>{lang('ButtonKey')}</button>
<p>{stateValue}</p>
</div>
);
}
export default memo(withGlobal<OwnProps>((global, { id }): Complete<StateProps> => {
const stateValue = selectValue(global, id);
return {
stateValue,
};
})(Component)
)Global State is our single, app-wide store, similar to Redux or Zustand. All its code lives under src/global/, with subfolders grouping related functionality (for example, selectors/users.ts holds all user-related selectors).
actions/: Actions that are used to update global from any point in the appselectors/: Pure functions that read data (e.g.selectors/users.ts).reducers/: Functions that update global state.types/: All TypeScript types live insrc/global/types.cache.ts: Manages saving a slimmed-down copy of global to IndexedDB.
- Preffered way to update global. When inside action, use
setGlobal, or simplereturnif sync. - Sync actions return type should be
ActionReturnType. - Async actions return type should be
Promise<void>. - If you add or remove an action, update
actions.tsaccordingly. - Actions in
uifolder should be only sync.
- Actions and selectors can accept a
tabIdparameter, so we don't lose tab context when working with multiple tabs. tabIdis required if calling an action or selector that can accept it.- Exception: UI components may call without
tabId(they receive it automatically).
- If logic takes more than one line, create a new selector or reducer in the appropriate folder and file.
- Selectors must be pure: only use their inputs and global. Don't allocate new objects or arrays, as that breaks memoization.
- Global may only store serializable primitives (strings, numbers, booleans).
- When you change a type that's cached in
cache.ts, add a migration to avoid errors from new selectors.
- Use
withGlobal(amapStateToPropshelper) to pull in state. - Avoid the experimental
useSelectorAPI. - Use
getGlobalonly inside hooks for one-off reads (it's non-reactive).
- Wrap
withGlobalinmemoso the component re-renders only on real data changes. - Don't return new arrays or objects inside
withGlobal; that defeats memoization. - If you need to filter or map a list, pass IDs as props and do the heavy work in a
useMemohook. - Force
Complete<StateProps>return type forwithGlobalparameter, as it ensures that all defined properties are passed.
type OwnProps = { id: string };
type StateProps = {
someValue?: string;
otherValue?: number;
thirdValue: boolean;
};
const Component = ({
id,
someValue,
otherValue,
thirdValue,
}: OwnProps & StateProps) => {
// component logic...
};
export default memo(
withGlobal<OwnProps>((global, { id }) => {
const { otherValue } = selectTabState(global);
const someValue = selectSomeValue(global, id);
const thirdValue = Boolean(global.rawValue);
return {
someValue,
otherValue,
thirdValue,
};
})(Component);
);1. Setup & Fallback
- Translations live on Translation Platform.
- Fallback file:
src/assets/localization/fallback.strings.
2. Getting Strings
const lang = useLang();
// Simple
lang('SimpleKey');
// Plurals
lang('PluralKey', undefined, { pluralValue: 3 });
// String replacements
lang('ReplKey', { name: 'Amy' });
// JSX nodes (e.g. links)
lang('LinkKey', { link: <Link /> }, { withNodes: true });
// Markdown
lang('MarkdownKey', undefined, { withNodes: true, withMarkdown: true });3. Adding a New Key
- Search Translation Platform for similar strings to get the correct wording.
- Add it to
fallback.strings. - If it's plural, include
_oneand_other. - Run
npm run lang:ts.
4. Naming Rules
- PascalCase (no dots).
- Use short, clear prefixes for context (e.g.
Accfor accessibility). - Keep names under ~30 chars, shorten consistently if needed.
5. API & Options
-
Basic:
lang(key, vars?, options?) → string -
Advanced (
withNodes): returnsTeactNode[]so you can inject JSX. -
Other options:
withMarkdown(for simple markdown + emojis)renderTextFilters(custom filters)specialReplacement(for replacing substrings, e.g. icons)
-
Object syntax: Simple form that returns string can be used in some actions.
actions.showNotification({ key: 'LangKey' }); lang.with({ key: 'hello', vars: { name }, options: { withNodes: true } });
6. Handy Extensions
lang.region(code)→ country namelang.conjunction(['a','b','c'])→ "a, b, and c"lang.disjunction(['x','y'])→ "x or y"lang.number(1234)→ locale-formatted number- Flags:
lang.isRtl,lang.code,lang.rawCode
7. Beyond React
Use getTranslationFn() to grab the same lang function in non-component code. Discouraged, use object syntax.