Skip to content

Latest commit

 

History

History
421 lines (320 loc) · 13.5 KB

File metadata and controls

421 lines (320 loc) · 13.5 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Instructions

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 styles in 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.
  • After your solution:

    1. Critique it—identify any shortcomings.
    2. Fix those issues, do more planning.
    3. Present the improved result.
  • When deeper debugging is needed:

    1. Outline clear, step-by-step debugging instructions for the operator.
    2. Remove any temporary debug code once the issue is resolved.
  • Lint errors you can't fix manually: Suggest running eslint --fix <filename>.

Telegram Web API Guide

1. API Definition

  • 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) in src/api/types.

2. Generating Code

  1. Make sure to include the method name in api.json.
  2. Run:
   npm run gramjs:tl

to 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 with get.
  • Use a destructured parameter object.
  • Call the API via:
    const result = await invokeRequest(
      new GramJs.namespace.MethodName({ /* params */ })
    );
  • If result is undefined, return undefined to 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*).

3. Using the API

  • In your actions, call:

    const result = await callApi('methodName', { /* params */ });
  • Always check for undefined before proceeding.

4. Example

// 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...
});

5. Handling Updates

  • Updates come in via mtpUpdateHandler.ts.
  • They're routed through src/global/actions/apiUpdaters to merge into global state.
  • Types are defined in src/api/types/updates.ts.

Component Style Guide

1. Basics & Imports

  • 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.

2. Props & Types

  • Split your props into two types:
    • OwnProps: data passed in by the parent
    • StateProps: data injected by withGlobal HOC
  • Merge them as OwnProps & StateProps when 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.

3. Hooks

  • 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.

4. Component Signature

Migrate any old FC syntax to the new form.

// Before
const OldComp: FC<OwnProps & StateProps> = ({}) => {  }

// After
const NewComp = (props: OwnProps & StateProps) => {  }

5. Memoization

  • 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.

6. Localization

  • Call const lang = useLang() at the top of your component.
  • Look up the localization guide for how to add new language keys.

Example

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 Overview

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).

1. Folder Structure

  • actions/: Actions that are used to update global from any point in the app
  • selectors/: Pure functions that read data (e.g. selectors/users.ts).
  • reducers/: Functions that update global state.
  • types/: All TypeScript types live in src/global/types.
  • cache.ts: Manages saving a slimmed-down copy of global to IndexedDB.

2. Actions

  1. Preffered way to update global. When inside action, use setGlobal, or simple return if sync.
  2. Sync actions return type should be ActionReturnType.
  3. Async actions return type should be Promise<void>.
  4. If you add or remove an action, update actions.ts accordingly.
  5. Actions in ui folder should be only sync.

3. Multi-Tab Support

  • Actions and selectors can accept a tabId parameter, so we don't lose tab context when working with multiple tabs.
  • tabId is required if calling an action or selector that can accept it.
  • Exception: UI components may call without tabId (they receive it automatically).

4. Selectors & Reducers

  • 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.

5. Data Constraints

  • 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.

Component Guidelines

1. Accessing Global in Components

  • Use withGlobal (a mapStateToProps helper) to pull in state.
  • Avoid the experimental useSelector API.
  • Use getGlobal only inside hooks for one-off reads (it's non-reactive).

2. Performance

  • Wrap withGlobal in memo so 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 useMemo hook.
  • Force Complete<StateProps> return type for withGlobal parameter, as it ensures that all defined properties are passed.

3. Example Component

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);
);

Localization Guide

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

  1. Search Translation Platform for similar strings to get the correct wording.
  2. Add it to fallback.strings.
  3. If it's plural, include _one and _other.
  4. Run npm run lang:ts.

4. Naming Rules

  • PascalCase (no dots).
  • Use short, clear prefixes for context (e.g. Acc for accessibility).
  • Keep names under ~30 chars, shorten consistently if needed.

5. API & Options

  • Basic: lang(key, vars?, options?) → string

  • Advanced (withNodes): returns TeactNode[] 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 name
  • lang.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.