ui phase 0
This commit is contained in:
+53
-20
@@ -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`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user