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.g.
isOpen,willUpdate,shouldRender). - Functions should start with a verb (e.g.
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.
- Avoid using default values for props that can be intentionally undefined/false.
- No unnecessary
ascasts. Prefersatisfieswhere possible. - 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-semibold); // ❌ 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:
- Think like on a code review and identify any shortcomings.
- Fix those issues. Repeat review-fix cycle until you are sure about code quality.
- Present the improved result.
-
When deeper debugging is needed:
- Outline clear, step-by-step debugging instructions in your output.
- Remove any temporary debug code once the issue is resolved.
-
Linter commands After finishing your changes, run
npm run check:tsif you touched TypeScript files and/ornpm run check:cssfor SCSS. If linter reports incorrect import order, try fixing it using command. If it fails, make ONE try to fix it manually and leave it as is. -
Lint errors you can't fix manually: Suggest running
npx 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. -
IMPORTANT: Do not pass
accessHashdirectly to API methods. Methods that accept separateidandaccessHashparameters are outdated. Instead, pass the fullApiPeer,ApiChat, orApiUserobject. ThebuildInput*functions ingramjsBuilderswill extract the necessary fields.
// 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.
- Do not import "react". React types are available globally in React namespace (e.g. React.MouseEvent).
- 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.
- Do not pass unmemoized objects as props into memo() components.
- 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.useStateis preferred when component just callssetState(someVariable). - Check the
hooks/folders for additional utilities. - Avoid adding new
useEffectwhere possible.
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. Consider skipping memo for simple wrapper components whose children change on almost every render. - 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.
- Use
<Icon>component for icons. Available icons are listed insrc/types/icons/index.ts
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.
- Prefer existing
withGlobal(amapStateToPropshelper) to pull in state. - There is an experimental
useSelectorAPI available. If your value can be retrieved using simple selector andwithGlobalis not present, use it. - Use
getGlobalonly inside callbacks 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, use
useShallowSelectorto retrieve reactive array and perform computation inuseMemo. - 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 });
lang('PluralKey', { count: 3 }, { pluralValue: 3 }); // if key has variables
// 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
- Make sure key does not exist already.
- 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.
--- frame start ---
1. effects
2. requested measures (DOM reads)
3. render JSX → DOM
4. layout effects
5. requested mutations (DOM writes)
6. forced reflow measure (avoid!)
7. forced reflow mutate (avoid!)
--- frame end ---
| Hook/Context | Can READ (measure) | Can WRITE (mutate) |
|---|---|---|
useLayoutEffect |
❌ NO | ✅ YES |
useLayout (deprecated) |
✅ YES | ❌ NO |
| Event handlers (default) | ✅ YES | ❌ NO (use requestMutation) |
requestMeasure callback |
✅ YES | ❌ NO |
requestMutation callback |
❌ NO | ✅ YES |
// ✅ CORRECT: Read in measure phase, write in mutation phase
requestMeasure(() => {
const width = element.offsetWidth; // READ
requestMutation(() => {
element.style.width = `${width * 2}px`; // WRITE
});
});
// ❌ WRONG: Alternating reads/writes causes layout thrashing
const width = element.offsetWidth; // READ → reflow
element.style.width = `${width * 2}px`; // WRITE → reflow
const height = element.offsetHeight; // READ → reflow again!Signals deliver updates without causing component renders. Use for frequently-updated values.
// Create signal
const [getValue, setValue] = createSignal(initialValue);
// Get value
getValue();
// Set value (notifies subscribers, NO re-render)
setValue(newValue);
// Subscribe to changes
getValue.subscribe(() => { /* react to change */ });Signal Hooks:
useSignal()– Create signal tied to componentuseDerivedSignal()– Derive new signal from other signals/variablesuseDerivedState()– Convert signal to render variable (triggers re-render)useStateRef()– Access current value without it being a dependency
When to use signals:
- Typing text, caret position
- Animation state tracking
- Values that change frequently but don't need re-render
- Cross-component communication without prop drilling
| Hook | Purpose |
|---|---|
useLastCallback |
Stable callback reference, always latest scope |
useStateRef |
Access state without triggering effects |
useLayoutEffectWithPrevDeps |
Synchronous effect with previous values |
useSyncEffect |
Effect that runs during render (not RAF) |
useResizeObserver |
Efficient element size observation |
useIntersectionObserver |
Viewport visibility tracking |
// Mark animation start (pauses non-critical updates)
const endAnimation = beginHeavyAnimation(duration);
// Run code only when fully idle (no animations + browser idle)
onFullyIdle(() => {
// Safe for heavy computations
});- Animations first – Evaluate if code negatively impacts animations
- Simplify algorithms – Move complex ones to
onFullyIdle - No loops in selectors – Avoid iterations in
withGlobalselectors - Minimize re-renders – Especially in
Message,Chat,Sticker, etc. - Understand effect timing –
useEffectvsuseLayoutEffect - Prefer signals – When you need effects only, not renders
- Use
requestForcedReflow– Only as last resort for sync measure+mutate