ui phase 0

This commit is contained in:
hamid
2026-07-17 14:00:22 +03:30
parent 9051bb3e18
commit f1cba6cf74
139 changed files with 1242 additions and 667 deletions
+53 -20
View File
@@ -319,13 +319,12 @@ client/
│ └── auth/ # AuthContext — AuthProvider (server-seeded) + reducer + useAuth
├── theme/
│ ├── ThemeProvider.tsx # MuiThemeProvider wrapper (RTL cache) + ColorSchemeCookieSync
│ ├── colors.ts # BRAND, LIGHT_PALETTE, DARK_PALETTE
│ ├── light.ts / dark.ts # LIGHT_THEME / DARK_THEME ThemeOptions (consumed by theme.ts)
│ ├── colors.ts # BRAND, LIGHT_PALETTE, DARK_PALETTE (incl. success/error/warning/info)
│ ├── 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 (ThemeProvider, getDirection, APP_THEME_*) — note: no ColorSchemeScript is exported/rendered today (doc drift below)
│ ├── theme.ts # APP_THEME_LTR / APP_THEME_RTL (static, created once) — the `components` brand pass + teal-tinted `shadows` array + responsiveFontSizes()
│ ├── tokens.css # CSS custom properties — [data-mui-color-scheme] selectors + the dark @media fallback (no-flash boot, no script — see "Theme System" below)
│ ├── typography.ts # TYPOGRAPHY_LTR (Space Grotesk) / TYPOGRAPHY_RTL (Mikhak) — shared size scale, 500/700 weight system
│ └── index.ts # Public re-exports (ThemeProvider, getDirection, APP_THEME_*)
├── constants/ # App-wide constants (routes.ts w/ actor paths, roles.ts, headers.ts)
├── hooks/ # incl. auth.ts → useIsAuthenticated / useActorRole (role-aware chrome)
├── utils/ # incl. money.ts (IRR/Toman, integer-safe) + date.ts (Shamsi display) + toEnglishDigits
@@ -458,15 +457,42 @@ Rules:
## Theme System
### How it works (end-to-end, no-flash)
### How it works (end-to-end, no-flash — CSS only, no boot script)
1. **Request arrives**`getThemeMode()` 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)
The no-flash mechanism is **pure CSS**, matching how every other color decision in this
app is made — no inline `<script>`, no `Storage.prototype` patching. Two visitor cases:
**Returning visitor (cookie present):**
1. `getThemeMode()` (`lib/cookies/server.ts`) reads the `'color-scheme'` cookie → returns
`{ colorScheme: 'light'|'dark', defaultMode: colorScheme }`.
2. Root layout sets `data-mui-color-scheme={colorScheme}` on `<html>` server-side.
3. `tokens.css`'s explicit `[data-mui-color-scheme='light'|'dark']` blocks match
immediately — correct on the very first paint, before any JS runs.
**First-ever visitor (no cookie yet):**
1. `getThemeMode()` returns `{ colorScheme: undefined, defaultMode: 'system' }`.
2. Root layout renders `<html>` **without** the `data-mui-color-scheme` attribute at all
(`data-mui-color-scheme={undefined}` — React omits it).
3. `tokens.css` has a `@media (prefers-color-scheme: dark)` block scoped to
`:root:not([data-mui-color-scheme])` — it only applies while the attribute is absent,
and paints the OS-preferred scheme immediately, with zero JS.
4. Once React hydrates, `<MuiThemeProvider defaultMode="system">` resolves the *same*
media query and stamps the attribute itself. The CSS values already match what was
painted, so there is nothing to visibly flip.
5. `ColorSchemeCookieSync` in `ThemeProvider.tsx` writes the cookie via
`useColorScheme().colorScheme` on mount, so the next visit is a "returning visitor".
**Trade-off, by design:** this covers the dominant visual surface — every `--bal-*` token
(page/paper background, text, dividers, all brand colors) — because that's what
`tokens.css`'s media-query fallback drives. MUI's own generated `--mui-palette-*`
variables (consumed by a bare `color="primary"` fill, e.g. a contained Button, or the
default `MuiTabs` indicator) do **not** get the same free fallback — MUI's
`colorSchemeSelector` supports either attribute-based *or* `'media'`-based generation,
not both at once — so on a cookie-less first visit with OS dark on, a raw MUI-primary
fill can very briefly show the light value until hydration (self-corrects same frame;
`disableTransitionOnChange` means it snaps, never animates). Prefer sourcing colors from
`var(--bal-*)` over `theme.vars.palette.*` in new `styleOverrides` — most of `theme.ts`'s
`components` block already does — to keep this gap as small as possible.
### Critical MUI v9 rules
@@ -484,7 +510,10 @@ The shorthand `'data'` in MUI v9 generates `[data-%s]` → `data-dark=""` / `dat
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.
It reads from localStorage, which diverges from our cookie (especially in 'system' mode),
and it's a script — this app's no-flash boot is CSS-only (see "How it works" above). Don't
add any pre-paint script for color scheme; extend the `tokens.css` media-query fallback
instead if a new token needs the same first-visit treatment.
**MUI v9 localStorage key defaults (different from v5/v6):**
- Mode key: `'mode'` (was `'mui-mode'`)
@@ -536,18 +565,22 @@ Fonts are loaded **per locale** — the Persian face is never shipped to English
| Locale | Font | CSS variable | Source | Loaded when |
|--------|------|--------------|--------|-------------|
| `fa` (RTL) | **Mikhak** | `--font-mikhak` | `next/font/local` — woff2 files in `src/app/fonts/` | only on `fa` routes |
| `en` (LTR) | **Space Grotesk** | `--font-space-grotesk` | (not currently wired — falls back to the system stack) | — |
| `en` (LTR) | **Space Grotesk** | `--font-space-grotesk` | `next/font/google` — self-hosted at build time, `preload: false` | only on `en` routes |
**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)
- Both share one size/line-height scale (`SIZE_SCALE` in `typography.ts`), wrapped in
`responsiveFontSizes()` (`theme.ts`) for per-breakpoint heading scaling. Weight system:
**700** for headings + buttons, **500** for in-text emphasis (subtitles, labels), **400**
body — never `600`, neither font loads that weight (see `typography.ts`'s header comment).
- There is no `TYPOGRAPHY` alias anymore — import `TYPOGRAPHY_LTR`/`TYPOGRAPHY_RTL` explicitly.
**Rules:**
- Mikhak is declared with `preload: false`, and its `.variable` class is attached to `<html>` **only when `locale === 'fa'`**. Both are required: a `next/font` loader called in the root layout would otherwise preload on every route (including `/en`), and `preload: false` ensures the woff2 only downloads when Persian text actually renders.
- Font files live in `src/app/fonts/` (not `public/`). next/font/local resolves paths relative to the calling file (`src/app/[locale]/layout.tsx`) at build time.
- Both fonts are declared with `preload: false`, and each `.variable` class is attached to `<html>` **only for its own locale** (Mikhak on `fa`, Space Grotesk on `en`) — never both, never neither. A `next/font` loader called in the root layout would otherwise preload on every route, and `preload: false` ensures the font file only downloads when its locale actually renders.
- Mikhak's woff2 files live in `src/app/fonts/` (not `public/`) next/font/local resolves paths relative to the calling file (`src/app/[locale]/layout.tsx`) at build time. Space Grotesk needs no local files — next/font/google fetches + self-hosts it at build time.
- Never load fonts inside components — all font loading lives in `src/app/[locale]/layout.tsx`.
- To add a new font, add woff2 files to `src/app/fonts/`, declare via `localFont`/`localFont`-equivalent in `src/app/[locale]/layout.tsx`, attach its `.variable` class conditionally on the matching locale, and update `BRAND_FONT_VARIABLE_*` constants in `typography.ts`.
- To add a new local font, add woff2 files to `src/app/fonts/`, declare via `localFont` in `src/app/[locale]/layout.tsx`, attach its `.variable` class conditionally on the matching locale, and update the `BRAND_FONT_VARIABLE_*` constants in `typography.ts`.
---