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
+5 -3
View File
@@ -11,9 +11,11 @@ import { APP_THEME_LTR, APP_THEME_RTL } from './theme';
/**
* Writes the resolved color scheme to our cookie whenever MUI's state changes.
* Acts as a safety net for the first-visit / system-mode case where the
* Storage.prototype intercept in ColorSchemeScript has nothing to intercept yet
* (no setItem call fires until the user explicitly toggles).
* On a cookie-less first visit the server never learns the OS-resolved scheme
* (tokens.css's `@media (prefers-color-scheme: dark)` fallback paints it, but
* that's CSS-only — nothing tells the server) — this is what turns that visit
* into a "returning visitor" with an explicit cookie next time, even before
* the user ever manually toggles.
*/
function ColorSchemeCookieSync() {
const { colorScheme } = useColorScheme();
+22
View File
@@ -26,6 +26,26 @@ export const BRAND = {
tealSurface: '#16302a',
} as const;
/*
* Semantic feedback colors — mirrors tokens.css --bal-{success,error,warning,info}.
* Feeds MUI's palette.{success,error,warning,info} so Alert/Chip/Badge/LinearProgress
* render brand-harmonized instead of MUI's stock green/red — the same colors the
* notistack toasts already use (NotistackProvider reads the CSS tokens directly).
*/
const LIGHT_FEEDBACK: PaletteOptions = {
success: { main: '#1f6b50', contrastText: '#f3efe9' },
error: { main: '#a8392a', contrastText: '#f3efe9' },
warning: { main: '#8a6418', contrastText: '#f3efe9' },
info: { main: '#1d4a40', contrastText: '#f3efe9' },
};
const DARK_FEEDBACK: PaletteOptions = {
success: { main: '#257659', contrastText: '#f3efe9' },
error: { main: '#b5402f', contrastText: '#f3efe9' },
warning: { main: '#97701f', contrastText: '#f3efe9' },
info: { main: '#2f6b5e', contrastText: '#f3efe9' },
};
export const LIGHT_PALETTE: PaletteOptions = {
primary: {
main: BRAND.teal,
@@ -48,6 +68,7 @@ export const LIGHT_PALETTE: PaletteOptions = {
secondary: '#5b655f',
},
divider: 'rgba(29, 74, 64, 0.14)',
...LIGHT_FEEDBACK,
};
export const DARK_PALETTE: PaletteOptions = {
@@ -72,4 +93,5 @@ export const DARK_PALETTE: PaletteOptions = {
secondary: '#9fb0a9',
},
divider: 'rgba(243, 239, 233, 0.14)',
...DARK_FEEDBACK,
};
-11
View File
@@ -1,11 +0,0 @@
import { ThemeOptions } from '@mui/material';
import { DARK_PALETTE } from './colors';
import { TYPOGRAPHY } from './typography';
export const DARK_THEME: ThemeOptions = {
palette: { mode: 'dark', ...DARK_PALETTE },
typography: TYPOGRAPHY,
shape: { borderRadius: 10 },
};
export default DARK_THEME;
+1 -8
View File
@@ -1,17 +1,10 @@
import AppThemeProvider from './ThemeProvider';
import { LIGHT_THEME } from './light';
import { DARK_THEME } from './dark';
import APP_THEME, { APP_THEME_LTR, APP_THEME_RTL } from './theme';
import { APP_THEME_LTR, APP_THEME_RTL } from './theme';
import { getDirection } from './direction';
export {
APP_THEME,
APP_THEME_LTR,
APP_THEME_RTL,
LIGHT_THEME,
DARK_THEME,
LIGHT_THEME as default,
AppThemeProvider as ThemeProvider,
getDirection,
};
-11
View File
@@ -1,11 +0,0 @@
import { ThemeOptions } from '@mui/material';
import { LIGHT_PALETTE } from './colors';
import { TYPOGRAPHY } from './typography';
export const LIGHT_THEME: ThemeOptions = {
palette: { mode: 'light', ...LIGHT_PALETTE },
typography: TYPOGRAPHY,
shape: { borderRadius: 10 },
};
export default LIGHT_THEME;
+162 -3
View File
@@ -1,7 +1,22 @@
import { createTheme } from '@mui/material/styles';
import { createTheme, responsiveFontSizes, Shadows } from '@mui/material/styles';
import { LIGHT_PALETTE, DARK_PALETTE } from './colors';
import { TYPOGRAPHY_LTR, TYPOGRAPHY_RTL } from './typography';
/*
* Teal-tinted elevation — replaces MUI's default grey shadow stack across
* EVERY component that reads theme.shadows[n] (Paper, Card, Dialog, Menu,
* Popover, AppBar elevation, Autocomplete…), not just the ones this file
* overrides directly. Tiered onto the three --bal-shadow-* steps (tokens.css)
* so low elevations (cards) stay subtle and high ones (dialogs, menus) read
* clearly raised without ever going stock-MUI grey.
*/
const TEAL_SHADOWS = Array.from({ length: 25 }, (_, index) => {
if (index === 0) return 'none';
if (index <= 4) return 'var(--bal-shadow-1)';
if (index <= 12) return 'var(--bal-shadow-2)';
return 'var(--bal-shadow-3)';
}) as unknown as Shadows;
/*
* Theme factory — called twice at module load (ltr + rtl) so components can
* pick the right theme without re-creating it on every render.
@@ -15,9 +30,13 @@ import { TYPOGRAPHY_LTR, TYPOGRAPHY_RTL } from './typography';
* → MUI flips inline-start/end padding for components that care.
* → ThemeProvider pairs this with a direction-aware Emotion cache that
* runs stylis-plugin-rtl so all generated CSS is mirrored correctly.
*
* Every override below references `var(--bal-*)` tokens (tokens.css) — never
* a raw hex — and only logical CSS properties, so the same override is
* correct in both APP_THEME_LTR and APP_THEME_RTL.
*/
function createAppTheme(direction: 'ltr' | 'rtl') {
return createTheme({
const theme = createTheme({
cssVariables: {
// MUI v9: 'data' produces boolean attrs (data-dark / data-light) which our
// CSS never matches. An explicit attribute name produces the keyed form:
@@ -30,9 +49,149 @@ function createAppTheme(direction: 'ltr' | 'rtl') {
},
defaultColorScheme: 'light',
typography: direction === 'rtl' ? TYPOGRAPHY_RTL : TYPOGRAPHY_LTR,
shape: { borderRadius: 10 },
shape: { borderRadius: 10 }, // house radius — mirrors --bal-radius-md
direction,
shadows: TEAL_SHADOWS,
components: {
MuiCssBaseline: {
styleOverrides: {
// Today exactly one hand-rolled :focus-visible rule exists in the app
// (NurseResultCard). This makes keyboard focus uniform everywhere.
'*:focus-visible': {
outline: '2px solid var(--bal-focus-ring)',
outlineOffset: '2px',
},
},
},
MuiButton: {
defaultProps: { disableElevation: true },
styleOverrides: {
root: {
borderRadius: 'var(--bal-radius-sm)',
paddingInline: 20,
paddingBlock: 9,
},
},
},
MuiPaper: {
styleOverrides: {
root: {
backgroundImage: 'none',
border: '1px solid var(--bal-divider)',
},
},
},
MuiAppBar: {
defaultProps: { color: 'transparent', elevation: 0 },
styleOverrides: {
root: {
backgroundColor: 'var(--bal-bg-paper)',
backgroundImage: 'none',
color: 'var(--bal-text-primary)',
borderBottom: '1px solid var(--bal-divider)',
},
},
},
MuiOutlinedInput: {
styleOverrides: {
root: {
borderRadius: 'var(--bal-radius-sm)',
'& .MuiOutlinedInput-notchedOutline': { borderColor: 'var(--bal-divider)' },
'&:hover .MuiOutlinedInput-notchedOutline': { borderColor: 'var(--bal-primary)' },
},
},
},
MuiChip: {
styleOverrides: {
root: { fontWeight: 500, borderRadius: 'var(--bal-radius-sm)' },
outlined: { borderColor: 'var(--bal-divider)' },
filled: ({ ownerState }: { ownerState: { color?: string } }) =>
!ownerState.color || ownerState.color === 'default'
? { backgroundColor: 'var(--bal-primary-soft)', color: 'var(--bal-primary)' }
: {},
},
},
MuiToggleButton: {
styleOverrides: {
root: {
border: '1px solid var(--bal-divider)',
textTransform: 'none',
'&.Mui-selected': {
backgroundColor: 'var(--bal-primary-soft)',
color: 'var(--bal-primary)',
},
'&.Mui-selected:hover': { backgroundColor: 'var(--bal-primary-soft)' },
},
},
},
MuiListItemButton: {
styleOverrides: {
root: {
borderRadius: 'var(--bal-radius-sm)',
'&.Mui-selected': {
backgroundColor: 'var(--bal-primary-soft)',
color: 'var(--bal-primary)',
},
'&.Mui-selected:hover': { backgroundColor: 'var(--bal-primary-soft)' },
},
},
},
MuiDialog: {
styleOverrides: {
paper: {
borderRadius: 'var(--bal-radius-lg)',
margin: 16,
width: 'calc(100% - 32px)',
},
},
},
MuiTabs: {
styleOverrides: {
indicator: {
height: 3,
borderRadius: '3px 3px 0 0',
},
},
},
MuiTab: {
styleOverrides: {
root: { minHeight: 48, textTransform: 'none' },
},
},
MuiAlert: {
styleOverrides: {
root: { borderRadius: 'var(--bal-radius-sm)' },
},
},
MuiStepIcon: {
styleOverrides: {
root: {
color: 'var(--bal-divider)',
'&.Mui-active': { color: 'var(--bal-primary)' },
'&.Mui-completed': { color: 'var(--bal-primary)' },
},
},
},
MuiSkeleton: {
styleOverrides: {
root: { backgroundColor: 'var(--bal-primary-soft)' },
},
},
MuiTooltip: {
styleOverrides: {
tooltip: {
backgroundColor: 'var(--bal-text-primary)',
color: 'var(--bal-bg-paper)',
borderRadius: 'var(--bal-radius-sm)',
fontSize: '0.75rem',
},
arrow: { color: 'var(--bal-text-primary)' },
},
},
},
});
return responsiveFontSizes(theme);
}
export const APP_THEME_LTR = createAppTheme('ltr');
+130 -8
View File
@@ -1,23 +1,57 @@
/*
* Balinyaar brand color tokens — source of truth for all colors.
*
* Palette extracted from the seed-deck proposal (balinyaar.html):
* Brand identity (construction + rationale): .claude/skills/frontend-designer/SKILL.md.
* #1d4a40 deep teal / forest-green → brand primary
* #d98c6a terracotta → brand accent (secondary)
* #f3efe9 soft cream → text-on-dark / glyph fill
* #faf9f5 cream → page surface
*
* MUI sets data-mui-color-scheme="dark" on <html> when dark mode is active
* (colorSchemeSelector: 'data' in createTheme). The same attribute drives
* every custom CSS rule in this file. You never need to read or write
* this attribute manually — MUI's useColorScheme() does it for you.
* (colorSchemeSelector: 'data-mui-color-scheme' in createTheme). The same
* attribute drives every custom CSS rule in this file. You never need to
* read or write this attribute manually — MUI's useColorScheme() does it for
* you at runtime.
*
* No-flash boot (no JS): when a returning visitor's `color-scheme` cookie is
* present, the server stamps the attribute directly and the explicit
* `[data-mui-color-scheme]` blocks below apply immediately — zero flash. On a
* cookie-less FIRST visit the server renders <html> WITHOUT the attribute
* (see lib/cookies/server.ts getThemeMode()), so the `@media
* (prefers-color-scheme: dark)` block below — scoped to the attribute being
* absent — paints the OS-preferred scheme immediately, with no script. Once
* MUI hydrates (ThemeProvider defaultMode="system") it resolves the same
* media query and stamps the attribute itself; the values already match, so
* there is nothing to visibly flip. The dark values therefore appear twice
* below (the media-query fallback + the explicit attribute block) — keep
* both in sync when changing a dark-scheme value.
*
* Usage in custom CSS outside MUI:
* color: var(--bal-primary); ← resolves to the active scheme value
* background: var(--bal-bg-default);
*
* Sync rule: any color added/changed here also lands in colors.ts (LIGHT_PALETTE/
* DARK_PALETTE) when it's palette-level (success/error/warning/info, primary,
* secondary…). Non-palette tokens (elevation, radius, motion, focus, rating,
* trust, money-emphasis) are define-only here — colors.ts never needs them.
*/
/* ── Light scheme (default) ────────────────────────────────────────────── */
/* ── Scheme-independent tokens (radius scale, motion) ──────────────────── */
:root {
/* Radius scale — controls / cards / dialogs. shape.borderRadius (theme.ts)
mirrors --bal-radius-md; never invent a radius per component. */
--bal-radius-sm: 4px;
--bal-radius-md: 10px;
--bal-radius-lg: 16px;
/* Motion — durations + easing, consumed by the phase-12 motion pass */
--bal-motion-fast: 120ms;
--bal-motion-base: 200ms;
--bal-motion-slow: 300ms;
--bal-easing-standard: cubic-bezier(0.4, 0, 0.2, 1);
}
/* ── Light scheme — the unconditional default ──────────────────────────── */
:root,
[data-mui-color-scheme='light'] {
/* Primary — deep teal */
@@ -56,16 +90,86 @@
--bal-warning-contrast: #f3efe9;
--bal-info: #1d4a40;
--bal-info-contrast: #f3efe9;
/* Elevation — teal-tinted shadow steps (replaces MUI's grey default stack).
Consumed by theme.ts's `shadows` array + the MuiPaper/MuiDialog overrides. */
--bal-shadow-1: 0 1px 2px rgba(29, 74, 64, 0.06), 0 1px 1px rgba(29, 74, 64, 0.04);
--bal-shadow-2: 0 4px 12px rgba(29, 74, 64, 0.10), 0 2px 4px rgba(29, 74, 64, 0.06);
--bal-shadow-3: 0 12px 32px rgba(29, 74, 64, 0.14), 0 4px 8px rgba(29, 74, 64, 0.08);
/* Focus ring — global :focus-visible treatment (MuiCssBaseline, theme.ts) */
--bal-focus-ring: #1d4a40;
/* Rating stars — RatingInput's star fill/empty (retires the muddy --bal-warning use) */
--bal-rating: #c8971e;
--bal-rating-empty: rgba(29, 74, 64, 0.20);
/* Trust — a distinct identity for verified marks, separate from primary/success */
--bal-trust: #2b6ca8;
--bal-trust-soft: rgba(43, 108, 168, 0.12);
/* Money emphasis — AA-contrast-safe money-text color (terracotta itself fails
AA at small sizes on white: #d98c6a ≈ 2:1). Never use --bal-secondary for
small money text; use this instead. */
--bal-money-emphasis: #8a4a2f;
}
/* ── Dark scheme ────────────────────────────────────────────────────────── */
/* ── Dark scheme — OS-preference fallback for the cookie-less first visit ── */
@media (prefers-color-scheme: dark) {
:root:not([data-mui-color-scheme]) {
--bal-primary: #6fc0ac;
--bal-primary-light: #8fd2c1;
--bal-primary-dark: #3f8a78;
--bal-primary-contrast: #06120f;
--bal-primary-soft: rgba(111, 192, 172, 0.16);
--bal-secondary: #e6a98a;
--bal-secondary-light: #f0bfa3;
--bal-secondary-dark: #d98c6a;
--bal-secondary-contrast: #2a1a12;
--bal-secondary-soft: rgba(230, 169, 138, 0.18);
--bal-bg-default: #0f1c19;
--bal-bg-paper: #16302a;
--bal-text-primary: #f3efe9;
--bal-text-secondary: #9fb0a9;
--bal-divider: rgba(243, 239, 233, 0.14);
--bal-success: #257659;
--bal-success-contrast: #f3efe9;
--bal-error: #b5402f;
--bal-error-contrast: #f3efe9;
--bal-warning: #97701f;
--bal-warning-contrast: #f3efe9;
--bal-info: #2f6b5e;
--bal-info-contrast: #f3efe9;
--bal-shadow-1: 0 1px 2px rgba(6, 18, 15, 0.24), 0 1px 1px rgba(6, 18, 15, 0.16);
--bal-shadow-2: 0 4px 12px rgba(6, 18, 15, 0.32), 0 2px 4px rgba(6, 18, 15, 0.20);
--bal-shadow-3: 0 12px 32px rgba(6, 18, 15, 0.40), 0 4px 8px rgba(6, 18, 15, 0.24);
--bal-focus-ring: #6fc0ac;
--bal-rating: #e0b542;
--bal-rating-empty: rgba(243, 239, 233, 0.20);
--bal-trust: #7fb8e6;
--bal-trust-soft: rgba(127, 184, 230, 0.18);
--bal-money-emphasis: #f0bfa3;
}
}
/* ── Dark scheme — explicit (cookie present, or after MUI hydrates/toggles).
Keep every value identical to the @media block above. ──────────────── */
[data-mui-color-scheme='dark'] {
/* Primary — lifted teal (readable on dark surfaces) */
--bal-primary: #6fc0ac;
--bal-primary-light: #8fd2c1;
--bal-primary-dark: #3f8a78;
--bal-primary-contrast: #06120f;
/* Soft primary tint — selected chips, subtle info panels */
--bal-primary-soft: rgba(111, 192, 172, 0.16);
/* Secondary — warm terracotta-light */
@@ -73,7 +177,6 @@
--bal-secondary-light: #f0bfa3;
--bal-secondary-dark: #d98c6a;
--bal-secondary-contrast: #2a1a12;
/* Soft terracotta tint — the "نمای پرستار" nurse-view chip + EVV/financial affordances */
--bal-secondary-soft: rgba(230, 169, 138, 0.18);
/* Surfaces — deep teal */
@@ -96,4 +199,23 @@
--bal-warning-contrast: #f3efe9;
--bal-info: #2f6b5e;
--bal-info-contrast: #f3efe9;
/* Elevation — black-teal shadow steps (deeper + darker than light scheme) */
--bal-shadow-1: 0 1px 2px rgba(6, 18, 15, 0.24), 0 1px 1px rgba(6, 18, 15, 0.16);
--bal-shadow-2: 0 4px 12px rgba(6, 18, 15, 0.32), 0 2px 4px rgba(6, 18, 15, 0.20);
--bal-shadow-3: 0 12px 32px rgba(6, 18, 15, 0.40), 0 4px 8px rgba(6, 18, 15, 0.24);
/* Focus ring */
--bal-focus-ring: #6fc0ac;
/* Rating stars */
--bal-rating: #e0b542;
--bal-rating-empty: rgba(243, 239, 233, 0.20);
/* Trust */
--bal-trust: #7fb8e6;
--bal-trust-soft: rgba(127, 184, 230, 0.18);
/* Money emphasis — lifted warm tone, readable on dark surfaces */
--bal-money-emphasis: #f0bfa3;
}
+74 -23
View File
@@ -1,7 +1,8 @@
import { TypographyVariantsOptions } from '@mui/material';
/** Space Grotesk CSS variable. Not currently wired to a font loader; the LTR
* stack falls back to the system fonts until it is added to the locale layout. */
/** Space Grotesk CSS variable, wired via next/font/google in
* src/app/[locale]/layout.tsx, attached to <html> only on `en` routes
* (mirrors the Mikhak/`fa` pattern — never loaded in a component). */
export const BRAND_FONT_VARIABLE_EN = '--font-space-grotesk';
/** CSS variable injected by next/font/local (Mikhak), attached to <html> only
@@ -24,32 +25,82 @@ const DISPLAY_FONT_LTR = `var(${BRAND_FONT_VARIABLE_EN}), "Space Grotesk", ${SYS
/** Persian display + body font with graceful fallbacks. */
const DISPLAY_FONT_RTL = `var(${BRAND_FONT_VARIABLE_FA}), "Mikhak", "Tahoma", "Arial Unicode MS", sans-serif`;
const displayHeadingLtr = { fontFamily: DISPLAY_FONT_LTR, fontWeight: 700 } as const;
const displayHeadingRtl = { fontFamily: DISPLAY_FONT_RTL, fontWeight: 700 } as const;
/*
* Weight system: both Mikhak (fa) and Space Grotesk (en) load only 400/500/700
* (see the font declarations in app/[locale]/layout.tsx) — there is no 600
* face in either family, so a theme/sx `fontWeight: 600` silently renders 700
* (CSS font-matching resolves upward to the nearest loaded weight). This theme
* therefore never requests 600: headings + buttons use 700, in-text emphasis
* (subtitles, chip labels, stat emphasis) uses 500. Follow this convention in
* new component styles instead of reintroducing 600.
*/
const WEIGHT_HEADING = 700;
const WEIGHT_EMPHASIS = 500;
const WEIGHT_BODY = 400;
/** LTR typography — Space Grotesk headings, system font body. */
/*
* Shared size/line-height scale (both directions). Replaces MUI's Latin-tuned
* defaults (e.g. a 6rem h1, tight h4 line-height of 1.235) with sizes actually
* used in the app and headroom for Persian ascenders/descenders. Multiplied
* across breakpoints by responsiveFontSizes() in theme.ts — do not hand-roll
* per-breakpoint sizes here.
*/
const SIZE_SCALE = {
h1: { fontSize: '2.5rem', lineHeight: 1.45 },
h2: { fontSize: '2.125rem', lineHeight: 1.45 },
h3: { fontSize: '1.75rem', lineHeight: 1.45 },
h4: { fontSize: '1.5rem', lineHeight: 1.45 },
h5: { fontSize: '1.25rem', lineHeight: 1.45 },
h6: { fontSize: '1.125rem', lineHeight: 1.4 },
subtitle1: { fontSize: '1rem', lineHeight: 1.6 },
subtitle2: { fontSize: '0.875rem', lineHeight: 1.6 },
body1: { fontSize: '1rem', lineHeight: 1.7 },
body2: { fontSize: '0.875rem', lineHeight: 1.7 },
button: { fontSize: '0.9375rem', lineHeight: 1.5 },
caption: { fontSize: '0.75rem', lineHeight: 1.6 },
overline: { fontSize: '0.75rem', lineHeight: 1.6 },
} as const;
/**
* @param headingFamily font for h1h6 + button (the display/brand voice)
* @param bodyFamily font for subtitle/body/caption/overline
* @param extra shared overrides (e.g. letterSpacing) applied to every variant
*/
function buildScale(headingFamily: string, bodyFamily: string, extra?: { letterSpacing: number }) {
const heading = extra ? { fontFamily: headingFamily, ...extra } : { fontFamily: headingFamily };
const body = extra ? { fontFamily: bodyFamily, ...extra } : { fontFamily: bodyFamily };
return {
h1: { ...heading, ...SIZE_SCALE.h1, fontWeight: WEIGHT_HEADING },
h2: { ...heading, ...SIZE_SCALE.h2, fontWeight: WEIGHT_HEADING },
h3: { ...heading, ...SIZE_SCALE.h3, fontWeight: WEIGHT_HEADING },
h4: { ...heading, ...SIZE_SCALE.h4, fontWeight: WEIGHT_HEADING },
h5: { ...heading, ...SIZE_SCALE.h5, fontWeight: WEIGHT_HEADING },
h6: { ...heading, ...SIZE_SCALE.h6, fontWeight: WEIGHT_HEADING },
subtitle1: { ...body, ...SIZE_SCALE.subtitle1, fontWeight: WEIGHT_EMPHASIS },
subtitle2: { ...body, ...SIZE_SCALE.subtitle2, fontWeight: WEIGHT_EMPHASIS },
body1: { ...body, ...SIZE_SCALE.body1, fontWeight: WEIGHT_BODY },
body2: { ...body, ...SIZE_SCALE.body2, fontWeight: WEIGHT_BODY },
button: { ...heading, ...SIZE_SCALE.button, fontWeight: WEIGHT_HEADING, textTransform: 'none' as const },
caption: { ...body, ...SIZE_SCALE.caption, fontWeight: WEIGHT_BODY },
overline: { ...body, ...SIZE_SCALE.overline, fontWeight: WEIGHT_EMPHASIS },
};
}
/** LTR typography — Space Grotesk headings, system font body (unchanged split;
* MUI's Latin letter-spacing defaults are fine as-is, so `extra` is omitted). */
export const TYPOGRAPHY_LTR: TypographyVariantsOptions = {
fontFamily: SYSTEM_FONT_STACK,
h1: displayHeadingLtr,
h2: displayHeadingLtr,
h3: displayHeadingLtr,
h4: displayHeadingLtr,
h5: displayHeadingLtr,
h6: { ...displayHeadingLtr, fontWeight: 600 },
button: { fontWeight: 600, textTransform: 'none' },
...buildScale(DISPLAY_FONT_LTR, SYSTEM_FONT_STACK),
};
/** RTL typography — Mikhak for all text (headings + body) to ensure full Persian glyph coverage. */
/**
* RTL typography — Mikhak for all text (headings + body) to ensure full Persian
* glyph coverage. `letterSpacing: 0` on every variant: Persian is a joined
* (cursive) script and any tracking breaks Mikhak's glyph connections — MUI's
* Latin defaults set non-zero spacing on body1/body2/button/caption/overline,
* which is wrong here.
*/
export const TYPOGRAPHY_RTL: TypographyVariantsOptions = {
fontFamily: DISPLAY_FONT_RTL,
h1: displayHeadingRtl,
h2: displayHeadingRtl,
h3: displayHeadingRtl,
h4: displayHeadingRtl,
h5: displayHeadingRtl,
h6: { ...displayHeadingRtl, fontWeight: 600 },
button: { fontWeight: 600, textTransform: 'none' },
...buildScale(DISPLAY_FONT_RTL, DISPLAY_FONT_RTL, { letterSpacing: 0 }),
};
/** @deprecated use TYPOGRAPHY_LTR or TYPOGRAPHY_RTL */
export const TYPOGRAPHY = TYPOGRAPHY_LTR;