Files
baya-monorepo/client/CLAUDE.md
T
2026-06-17 22:53:49 +03:30

16 KiB

Balinyaar Client — Claude Code Guidelines

Project Structure

client/
├── messages/              # Translation files (add keys to BOTH files)
│   ├── en.json
│   └── fa.json
├── middleware.ts          # next-intl routing middleware (locale detection + redirect)
├── next.config.mjs        # createNextIntlPlugin wires i18n into Next.js
└── src/
    ├── app/
    │   ├── layout.tsx                          # Root RSC: reads locale + cookie → sets HTML attrs
    │   ├── globals.css
    │   ├── fonts/                              # Local font files (woff2) — Mikhak for fa
    │   └── [locale]/
    │       ├── layout.tsx                      # RSC: setRequestLocale + NextIntlClientProvider + ThemeProvider + AppStoreProvider
    │       ├── (private-routes)/
    │       │   ├── layout.tsx                  # 'use client' — wraps PrivateLayout
    │       │   └── page.tsx
    │       └── (public-routes)/
    │           └── layout.tsx                  # 'use client' — wraps PublicLayout
    ├── components/        # Shared UI components (each with .test.tsx if imported >1 place)
    ├── i18n/
    │   ├── routing.ts     # defineRouting — locales: ['en', 'fa'], defaultLocale: 'fa'
    │   └── request.ts     # getRequestConfig — loads messages/${locale}.json
    ├── layout/
    │   ├── PrivateLayout.tsx   # 'use client' — authenticated shell; uses useTranslations('nav')
    │   ├── PublicLayout.tsx    # unauthenticated shell
    │   ├── TopBarAndSideBarLayout.tsx  # 'use client' — TopBar + SideBar composition
    │   ├── config.ts
    │   ├── index.ts
    │   └── components/
    │       ├── TopBar.tsx
    │       ├── SideBar.tsx
    │       ├── SideBarNavList.tsx
    │       ├── SideBarNavItem.tsx
    │       ├── DarkModeButton.tsx  # 'use client' — only subscriber to useColorScheme()
    │       └── index.tsx
    ├── lib/
    │   └── cookies/           # Cookie manager — strict server/client separation
    │       ├── constants.ts   # COOKIE_NAMES, CookieOptions, COLOR_SCHEME_COOKIE_OPTIONS
    │       ├── server.ts      # getServerCookie, getThemeMode, setServerCookie
    │       ├── client.ts      # getClientCookie, setClientCookie, deleteClientCookie
    │       └── index.ts       # Re-exports constants ONLY (never server/client)
    ├── store/             # AppStore (Redux-like client state)
    ├── theme/
    │   ├── ColorSchemeScript.tsx  # Inline <script> in <head> — sets attr + patches Storage
    │   ├── ThemeProvider.tsx      # MuiThemeProvider wrapper + ColorSchemeCookieSync
    │   ├── colors.ts              # LIGHT_PALETTE, DARK_PALETTE, BRAND
    │   ├── direction.ts           # getDirection(locale) → 'ltr' | 'rtl'
    │   ├── theme.ts               # APP_THEME_LTR / APP_THEME_RTL (static, created once)
    │   ├── tokens.css             # CSS custom properties — [data-mui-color-scheme] selectors
    │   ├── typography.ts          # TYPOGRAPHY_LTR (Space Grotesk) / TYPOGRAPHY_RTL (Mikhak)
    │   └── index.ts               # Public re-exports
    ├── constants/         # App-wide constants (routes, events, etc.)
    ├── hooks/
    ├── utils/
    └── config.ts

Server / Client Component Boundaries

Root layout (src/app/layout.tsx) is a lean Server Component (RSC). It:

  • Reads locale from the x-next-intl-locale request header set by the middleware (with fallback to defaultLocale for build-time pre-rendering where no request context exists).
  • Calls getThemeMode() from @/lib/cookies/server (only colorScheme is used).
  • Renders <html> with data-mui-color-scheme, lang, and dir from the locale/cookie.
  • Does NOT wrap the tree with NextIntlClientProvider, ThemeProvider, or AppStoreProvider — those live in [locale]/layout.tsx where the locale is reliably sourced from URL params.

WHY providers are NOT in root layout: getThemeMode() and headers() are both called inside try/catch blocks that swallow DYNAMIC_SERVER_USAGE. Next.js therefore never observes a dynamic API error propagating from the root layout and treats it as a statically renderable component. At build time the locale falls back to defaultLocale ('fa'), the static HTML is cached, and all subsequent requests — including /en — are served the pre-rendered 'fa' messages. Moving providers to [locale]/layout.tsx sidesteps this entirely: that layout always has the correct locale from URL params.

Locale layout (src/app/[locale]/layout.tsx) is an RSC that owns all i18n and theme context. It:

  • Calls setRequestLocale(locale) so server components deeper in the tree can call getLocale() / getTranslations() reliably.
  • Calls getMessages({ locale }) with the locale passed explicitly so getRequestConfig receives it via Promise.resolve(locale) (not through the React.cache read), avoiding any cache-ordering race.
  • Calls getThemeMode() for defaultMode needed by ThemeProvider.
  • Wraps children with NextIntlClientProvider, AppStoreProvider, and ThemeProvider.
  • Exports generateStaticParams so Next.js can enumerate locale routes at build time.

Route-group layouts ((private-routes)/layout.tsx, (public-routes)/layout.tsx) are 'use client' — they only wrap a layout component and need no server capabilities.

Never import from next/headers, next-intl/server, or @/lib/cookies/server in a client component. The build will fail.


i18n (next-intl v4)

Adding translations:

  1. Add the key to messages/en.json AND messages/fa.json. Both files must always be in sync.
  2. Top-level keys are namespaces: "nav", "common", etc.

Using translations in client components:

import { useTranslations } from 'next-intl';

function MyComponent() {
  const t = useTranslations('nav'); // namespace
  return <span>{t('home')}</span>;  // key
}

Using translations in Server Components:

import { getTranslations } from 'next-intl/server';

async function MyServerComponent() {
  const t = await getTranslations('nav');
  return <span>{t('home')}</span>;
}

Established namespaces and where they're used:

  • 'nav'PrivateLayout.tsx (sidebar nav items)
  • 'common'DarkModeButton.tsx (dark/light mode labels)

Never hard-code UI strings in English. Any user-visible text must have a translation key in both locale files.


The cookie manager in src/lib/cookies/ is split into three files to prevent cross-environment bundling:

File Use from Purpose
constants.ts anywhere COOKIE_NAMES, CookieOptions, COLOR_SCHEME_COOKIE_OPTIONS
server.ts Server Components, Server Actions, Route Handlers only getServerCookie, getThemeMode, setServerCookie
client.ts client components / useEffect only getClientCookie, setClientCookie, deleteClientCookie
index.ts anywhere Re-exports constants.ts only — safe barrel

Rules:

  • Import constants via the barrel: import { COOKIE_NAMES } from '@/lib/cookies'
  • Import server utils directly: import { getThemeMode } from '@/lib/cookies/server'
  • Import client utils directly: import { setClientCookie } from '@/lib/cookies/client'
  • Never import server.ts in a client component; never import client.ts in an RSC.
  • COOKIE_NAMES.COLOR_SCHEME = 'color-scheme' — the single source of truth for the theme cookie name. Do not redeclare it anywhere.

Constants

Rule: every magic string or configurable value must be a named constant — never inline.

A value is "magic" if its meaning isn't obvious from the literal alone: cookie names, event names, localStorage keys, route paths, query-param names, numeric timeouts, API endpoint slugs.

Where to define:

  • Cookie names / options: src/lib/cookies/constants.ts
  • Feature-scope constants: co-locate in a constants.ts next to that feature's files
  • App-wide constants (used across multiple features): src/constants/ — one file per concern (routes.ts, events.ts, etc.)

Rules:

  1. Import the constant; never copy-paste the string value.
  2. When renaming, update the constant definition — the rest of the codebase follows automatically.

Theme System

How it works (end-to-end, no-flash)

  1. Request arrivesgetThemeMode() reads 'color-scheme' cookie → returns { colorScheme, defaultMode }
  2. Root layout sets data-mui-color-scheme={colorScheme} on <html> server-side
  3. <ColorSchemeScript /> in <head> runs before any paint:
    • Reads the same cookie, sets data-mui-color-scheme (handles edge cases where server attr might differ)
    • Patches Storage.prototype — routes MUI's localStorage writes for key 'mode' to our cookie; reads return null so MUI always trusts the defaultMode prop
  4. <MuiThemeProvider defaultMode={defaultMode}> mounts — uses the server-derived mode, not localStorage
  5. ColorSchemeCookieSync in ThemeProvider writes the cookie via useColorScheme().colorScheme on mount (safety net for first-visit system mode)

Critical MUI v9 rules

colorSchemeSelector must be the explicit attribute name:

// theme.ts
cssVariables: {
  colorSchemeSelector: 'data-mui-color-scheme', // CORRECT
  // colorSchemeSelector: 'data',               // WRONG — produces boolean data-dark/data-light
},

The shorthand 'data' in MUI v9 generates [data-%s]data-dark="" / data-light="" (boolean attributes). Our tokens.css uses [data-mui-color-scheme="dark"] which never matches boolean attributes. Always use the explicit attribute name.

Never use storageWindow={null}: In MUI v9's localStorageManager, the check is if (!storageWindow && typeof window !== 'undefined')null is falsy, so it silently overrides to window. This prop is a no-op in browsers. The Storage.prototype patch in ColorSchemeScript is the correct intercept.

Never use MUI's InitColorSchemeScript: It reads from localStorage which diverges from our cookie (especially in 'system' mode). Use ColorSchemeScript from @/theme instead.

MUI v9 localStorage key defaults (different from v5/v6):

  • Mode key: 'mode' (was 'mui-mode')
  • Color scheme key: 'color-scheme' (was 'mui-color-scheme')
  • HTML attribute: 'data-color-scheme' (was 'data-mui-color-scheme')

We override all of these via colorSchemeSelector: 'data-mui-color-scheme' in the theme and the Storage.prototype patch.

Color tokens

All theme-aware colors live in src/theme/tokens.css under [data-mui-color-scheme] selectors. Do not add color values to inline sx props or component styles — add a CSS variable to tokens.css and reference it via var(--my-token).

Pre-built theme objects

APP_THEME_LTR and APP_THEME_RTL are created once at module load. Never call createTheme() inside a component or hook — pass the appropriate pre-built theme to MuiThemeProvider.

Toggle components

DarkModeToggleButton and DarkModeFormSwitch in src/layout/components/DarkModeButton.tsx are the only components that subscribe to useColorScheme(). When the user toggles:

  1. setMode('dark') is called
  2. Storage.prototype.setItem intercept fires → writes 'color-scheme'='dark' cookie synchronously
  3. MUI sets data-mui-color-scheme="dark" on <html>
  4. CSS variables resolve → browser repaints. No React re-render above the button.

Use colorScheme (not mode) for the isDark check — mode can be 'system' even when dark is active.


Direction (RTL / LTR)

Derived from locale via getDirection(locale) in src/theme/direction.ts:

  • RTL locales: fa, ar, he, ur
  • All others: ltr

ThemeProvider accepts a dir prop and selects the matching pre-built theme (APP_THEME_RTL for RTL). The RTL Emotion cache uses stylis-plugin-rtl to mirror all generated CSS.

The root layout sets dir={dir} on <html> and passes dir to ThemeProvider. Changing locale → new request → new dir without any client-side state.

Default locale is fa (RTL). The middleware redirects bare / to /fa/. English is explicitly accessed at /en/.


Fonts

Two fonts are loaded on every request (both CSS variables are always defined on <html>):

Locale Font CSS variable Source
fa (RTL) Mikhak --font-mikhak next/font/local — woff2 files in src/app/fonts/
en (LTR) Space Grotesk --font-space-grotesk next/font/google

Typography exports:

  • TYPOGRAPHY_LTR — Space Grotesk headings, system font body (used by APP_THEME_LTR)
  • TYPOGRAPHY_RTL — Mikhak for all text including body (used by APP_THEME_RTL, ensures full Persian glyph coverage)
  • TYPOGRAPHY — alias for TYPOGRAPHY_LTR (deprecated, prefer the explicit exports)

Rules:

  • Both font variables are loaded unconditionally so the CSS stacks can always reference them.
  • Font files live in src/app/fonts/ (not public/). next/font/local resolves paths relative to the calling file at build time.
  • Never load fonts inside components — all font loading lives in src/app/layout.tsx.
  • To add a new font, add woff2 files to src/app/fonts/, declare via localFont in layout.tsx, and update BRAND_FONT_VARIABLE_* constants in typography.ts.

Unit Testing

Rule: every shared component must have a co-located test file.

A component is "shared" if it is imported from more than one place (page, layout, or other component).

Coverage baseline for shared components:

  1. It renders without crashing.
  2. Every documented prop produces the correct HTML attribute or CSS class.
  3. User interactions (click, change) call the expected callbacks.

Test location: src/components/ComponentName/ComponentName.test.tsx next to the component. Test wrapper: wrap with <ThemeProvider> if the component uses MUI theming. Do NOT mock MUI components — test against the rendered DOM.

Enforcement: before removing or renaming a shared component, check whether src/**/*.test.{ts,tsx} files import it. If so, update or delete those tests too.


Anti-patterns (do not do these)

  • Do not read localStorage or document.cookie in render functions — use useEffect or server-side cookies() from next/headers.
  • Do not call createTheme() inside a component or hook — use APP_THEME_LTR / APP_THEME_RTL.
  • Do not use storageWindow={null} on MuiThemeProvider — it is silently ignored in MUI v9.
  • Do not use InitColorSchemeScript from MUI — use ColorSchemeScript from @/theme.
  • Do not set colorSchemeSelector: 'data' — use 'data-mui-color-scheme'.
  • Do not check mode === 'dark' for "is dark active" — use colorScheme === 'dark'.
  • Do not hard-code UI strings — add translation keys to both messages/en.json and messages/fa.json.
  • Do not put NextIntlClientProvider, ThemeProvider, or AppStoreProvider in the root layout — both headers() and getThemeMode() swallow DYNAMIC_SERVER_USAGE, so Next.js statically caches the root layout at build time with defaultLocale ('fa'). All /en requests then receive the cached 'fa' messages. These providers belong in [locale]/layout.tsx where locale is sourced from URL params.
  • Do not call getMessages() without passing { locale } explicitly — getMessages({ locale }) passes the locale directly to getRequestConfig via Promise.resolve(locale), bypassing potential React.cache ordering issues.
  • Do not remove setRequestLocale(locale) from src/app/[locale]/layout.tsx — without it, getLocale() called by deeper server components always returns defaultLocale.
  • Do not add notFound() to src/app/[locale]/layout.tsx — unknown locale URLs are handled by middleware (redirect to defaultLocale); a hard 404 here breaks fallback behavior.
  • Do not import TYPOGRAPHY — use TYPOGRAPHY_LTR or TYPOGRAPHY_RTL explicitly.
  • Do not load fonts inside components or pages — all next/font declarations belong in src/app/layout.tsx.
  • Do not import @/lib/cookies/server in client components or @/lib/cookies/client in RSCs.
  • Do not document.title = title in the render body of any component — it causes ReferenceError: document is not defined during build-time prerendering.