design: upgrade typography, shadows, spacing, empty states, 404 page, focus rings, noise overlay

This commit is contained in:
Kirill
2026-05-25 18:57:25 +05:00
parent 0771209c5d
commit f24308bb56
11 changed files with 177 additions and 66 deletions
+2
View File
@@ -3,6 +3,7 @@ import { AppProviders } from '@/app/providers/AppProviders'
import { AppRoutes } from '@/app/routes'
import { CartSnackbar } from '@/shared/ui/CartSnackbar'
import { ErrorBoundary } from '@/shared/ui/ErrorBoundary'
import { NoiseOverlay } from '@/shared/ui/NoiseOverlay'
export function App() {
return (
@@ -12,6 +13,7 @@ export function App() {
<AppRoutes />
</ErrorBoundary>
<CartSnackbar />
<NoiseOverlay />
</BrowserRouter>
</AppProviders>
)
+14 -17
View File
@@ -23,8 +23,10 @@ export function MainLayout({ children }: PropsWithChildren) {
<ScrollToTop />
<AppHeader />
<Box component="main" sx={{ flex: 1, py: { xs: 4, md: 6 } }}>
<Container maxWidth="lg">{children}</Container>
<Box component="main" sx={{ flex: 1, py: { xs: 3, md: 5 } }}>
<Container maxWidth="xl" sx={{ px: { xs: 2, sm: 3, md: 4 } }}>
{children}
</Container>
</Box>
<Box
@@ -33,24 +35,19 @@ export function MainLayout({ children }: PropsWithChildren) {
mt: 'auto',
borderTop: 1,
borderColor: 'divider',
bgcolor: 'background.default',
py: { xs: 4, md: 6 },
bgcolor: 'background.paper',
py: { xs: 5, md: 7 },
}}
>
<Container maxWidth="lg">
<Grid container spacing={4}>
<Container maxWidth="xl" sx={{ px: { xs: 2, sm: 3, md: 4 } }}>
<Grid container spacing={5}>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 600, letterSpacing: '-0.25px' }}>
Магазин
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 700, letterSpacing: '-0.5px' }}>
{STORE_NAME}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1, maxWidth: 260 }}>
Изделия ручной работы: вещи с характером и вниманием к деталям.
</Typography>
<Stack spacing={1.5}>
<Link component={RouterLink} to="/" color="inherit" underline="hover" variant="body2">
Каталог
</Link>
<Typography variant="body2" color="text.secondary">
Изделия ручной работы: вещи с характером и вниманием к деталям.
</Typography>
</Stack>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 600, letterSpacing: '-0.25px' }}>
@@ -111,7 +108,7 @@ export function MainLayout({ children }: PropsWithChildren) {
</Stack>
</Grid>
</Grid>
<Divider sx={{ my: 3 }} />
<Divider sx={{ my: 4 }} />
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5 }}>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', textAlign: 'center' }}>
© {year} {STORE_NAME}
+55 -12
View File
@@ -92,13 +92,16 @@ function AppThemeInner({ children }: PropsWithChildren) {
shape: { borderRadius: 12 },
typography: {
fontFamily: '"Outfit", "Segoe UI", system-ui, sans-serif',
h4: { fontWeight: 700, letterSpacing: '-0.5px' },
h5: { fontWeight: 600, letterSpacing: '-0.25px' },
h6: { fontWeight: 600 },
h1: { fontWeight: 700, letterSpacing: '-1px', lineHeight: 1.1, textWrap: 'balance' },
h2: { fontWeight: 700, letterSpacing: '-0.75px', lineHeight: 1.15, textWrap: 'balance' },
h3: { fontWeight: 700, letterSpacing: '-0.5px', lineHeight: 1.2, textWrap: 'balance' },
h4: { fontWeight: 700, letterSpacing: '-0.5px', textWrap: 'balance' },
h5: { fontWeight: 600, letterSpacing: '-0.25px', textWrap: 'balance' },
h6: { fontWeight: 600, textWrap: 'balance' },
subtitle1: { fontWeight: 600 },
subtitle2: { fontWeight: 500 },
body1: { fontSize: '0.875rem' },
body2: { fontSize: '0.75rem' },
body1: { fontSize: '0.875rem', lineHeight: 1.6 },
body2: { fontSize: '0.75rem', lineHeight: 1.5 },
button: { textTransform: 'none', fontWeight: 600 },
},
components: {
@@ -109,30 +112,34 @@ function AppThemeInner({ children }: PropsWithChildren) {
borderRadius: 12,
fontWeight: 600,
transition: 'all 0.2s ease-in-out',
'&:focus-visible': {
outline: '2px solid currentColor',
outlineOffset: 2,
},
},
contained: {
boxShadow: '0 4px 14px 0 rgba(0,0,0,0.15)',
boxShadow: '0 4px 14px 0 rgba(0,0,0,0.12)',
'&:hover': {
boxShadow: '0 6px 20px 0 rgba(0,0,0,0.25)',
boxShadow: '0 6px 20px 0 rgba(0,0,0,0.18)',
transform: 'translateY(-2px)',
},
'&:active': {
boxShadow: '0 2px 8px 0 rgba(0,0,0,0.15)',
transform: 'translateY(0)',
boxShadow: '0 2px 8px 0 rgba(0,0,0,0.12)',
transform: 'translateY(0) scale(0.98)',
},
},
outlined: {
border: '1px solid',
'&:hover': {
boxShadow: '0 2px 8px 0 rgba(0,0,0,0.1)',
boxShadow: '0 2px 8px 0 rgba(0,0,0,0.08)',
},
'&:active': {
boxShadow: 'none',
transform: 'scale(0.98)',
},
},
text: {
'&:hover': {
boxShadow: '0 1px 3px 0 rgba(0,0,0,0.1)',
backgroundColor: 'action.hover',
},
'&:active': {
@@ -147,12 +154,48 @@ function AppThemeInner({ children }: PropsWithChildren) {
transition: 'all 0.2s ease-in-out',
'&:hover': {
backgroundColor: 'action.hover',
transform: 'scale(1.1)',
transform: 'scale(1.08)',
},
'&:active': {
backgroundColor: 'action.selected',
transform: 'scale(0.95)',
},
'&:focus-visible': {
outline: '2px solid currentColor',
outlineOffset: 2,
},
},
},
},
MuiCard: {
styleOverrides: {
root: {
'&:focus-visible': {
outline: '2px solid currentColor',
outlineOffset: 2,
},
},
},
},
MuiLink: {
styleOverrides: {
root: {
'&:focus-visible': {
outline: '2px solid currentColor',
outlineOffset: 2,
borderRadius: 2,
},
},
},
},
MuiInputBase: {
styleOverrides: {
root: {
'&.Mui-focused': {
'& .MuiOutlinedInput-notchedOutline': {
borderWidth: 2,
},
},
},
},
},
+5
View File
@@ -30,6 +30,9 @@
:root {
color-scheme: light;
}
html {
scroll-behavior: smooth;
}
html,
body,
#root {
@@ -37,4 +40,6 @@ body,
}
body {
margin: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
+11 -7
View File
@@ -58,14 +58,14 @@ const ProductCardInner = ({ product, mediaHeight = 200, actions }: Props) => {
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
borderRadius: 3,
border: '1px solid',
borderColor: 'divider',
borderRadius: '16px 16px 12px 12px',
border: 'none',
bgcolor: 'background.paper',
transition: 'transform 200ms ease, box-shadow 250ms ease',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
transition: 'transform 250ms ease, box-shadow 300ms ease',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: '0 8px 30px rgba(0,0,0,0.10)',
transform: 'translateY(-6px)',
boxShadow: '0 12px 40px rgba(0,0,0,0.12)',
},
'&:hover .product-card__media': { transform: 'scale(1.06)' },
'&:hover .product-card__title': { color: 'primary.main' },
@@ -236,7 +236,11 @@ const ProductCardInner = ({ product, mediaHeight = 200, actions }: Props) => {
</Stack>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', pt: 1.5 }}>
<Typography variant="h6" color="primary" sx={{ fontWeight: 700, fontSize: '1.1rem' }}>
<Typography
variant="h6"
color="primary"
sx={{ fontWeight: 700, fontSize: '1.1rem', fontVariantNumeric: 'tabular-nums' }}
>
{formatPriceRub(product.priceCents)}
</Typography>
{actions}
+13 -1
View File
@@ -59,7 +59,19 @@ export function CartPage() {
{cartQuery.isError && <Alert severity="error">Не удалось загрузить корзину.</Alert>}
{cartQuery.isSuccess && items.length === 0 && <Alert severity="info">Корзина пуста.</Alert>}
{cartQuery.isSuccess && items.length === 0 && (
<Box sx={{ textAlign: 'center', py: 8 }}>
<Typography variant="h6" color="text.secondary" sx={{ mb: 1 }}>
Корзина пуста
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Добавьте что-нибудь из каталога, чтобы оформить заказ.
</Typography>
<Button component={RouterLink} to="/" variant="contained">
Перейти в каталог
</Button>
</Box>
)}
{items.length > 0 && (
<Stack spacing={2}>
+10 -5
View File
@@ -70,10 +70,10 @@ export function HomePage() {
<Box>
<CatalogSlider />
<Typography variant="h4" component="h1" gutterBottom>
<Typography variant="h4" component="h1" sx={{ mb: 1 }}>
{title}
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3, maxWidth: 560 }}>
Игрушки, сувениры и другие изделия ручной работы.
</Typography>
@@ -102,9 +102,14 @@ export function HomePage() {
)}
{productsQuery.isSuccess && products.length === 0 && (
<Typography color="text.secondary" sx={{ mt: 2 }}>
Пока нет опубликованных товаров.
</Typography>
<Box sx={{ textAlign: 'center', py: 8 }}>
<Typography variant="h6" color="text.secondary" sx={{ mb: 1 }}>
Пока нет опубликованных товаров
</Typography>
<Typography variant="body2" color="text.secondary">
Загляните позже мы регулярно обновляем каталог.
</Typography>
</Box>
)}
{productsQuery.isSuccess && products.length > 0 && (
+36 -18
View File
@@ -1,13 +1,15 @@
import { Box, Typography, Button, Stack, Paper } from '@mui/material'
import { Link as RouterLink } from 'react-router-dom'
import { Link as RouterLink, useNavigate } from 'react-router-dom'
export function NotFoundPage() {
const navigate = useNavigate()
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
minHeight: '100dvh',
bgcolor: 'background.default',
alignItems: 'center',
justifyContent: 'center',
@@ -15,28 +17,44 @@ export function NotFoundPage() {
px: 2,
}}
>
<Paper sx={{ p: 4, borderRadius: 3, bgcolor: 'background.paper' }}>
<Paper
elevation={0}
sx={{
p: { xs: 4, md: 6 },
borderRadius: 4,
bgcolor: 'background.paper',
border: '1px solid',
borderColor: 'divider',
maxWidth: 480,
width: '100%',
}}
>
<Stack spacing={3} sx={{ alignItems: 'center', textAlign: 'center' }}>
<Box sx={{ fontSize: 96, lineHeight: 1 }}>404</Box>
<Typography variant="h4" component="h1" gutterBottom>
<Typography
variant="h1"
sx={{
fontSize: { xs: 72, md: 96 },
fontWeight: 700,
lineHeight: 1,
color: 'primary.main',
opacity: 0.15,
letterSpacing: '-4px',
}}
>
404
</Typography>
<Typography variant="h5" component="h1" sx={{ fontWeight: 700, mt: -1 }}>
Страница не найдена
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ maxWidth: 400 }}>
<Typography variant="body2" color="text.secondary" sx={{ maxWidth: 360 }}>
Извините, но запрашиваемая страница не существует или была удалена.
</Typography>
<Stack direction="row" spacing={2} sx={{ mt: 2 }}>
<Button
variant="contained"
size="large"
onClick={() => {
window.location.href = '/'
}}
sx={{ px: 4 }}
>
Вернуться на главную
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1, width: '100%' }}>
<Button variant="contained" size="large" onClick={() => navigate('/')} sx={{ flexGrow: 1, px: 4 }}>
На главную
</Button>
<Button variant="outlined" size="large" component={RouterLink} to="/" sx={{ px: 4 }}>
Посмотреть каталог
<Button variant="outlined" size="large" component={RouterLink} to="/" sx={{ flexGrow: 1, px: 4 }}>
Каталог
</Button>
</Stack>
</Stack>
+6 -6
View File
@@ -72,14 +72,14 @@ export function ProductPage() {
return (
<Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{imageUrls.length > 0 ? (
<Box
sx={{
borderRadius: 2,
borderRadius: '20px 20px 12px 12px',
overflow: 'hidden',
border: 1,
borderColor: 'divider',
border: 'none',
boxShadow: '0 4px 20px rgba(0,0,0,0.08)',
bgcolor: 'background.paper',
}}
>
@@ -150,10 +150,10 @@ export function ProductPage() {
</Box>
)}
<Typography variant="h4" component="h1">
<Typography variant="h3" component="h1" sx={{ fontWeight: 700, letterSpacing: '-0.75px' }}>
{p.title}
</Typography>
<Typography variant="h5" color="primary">
<Typography variant="h4" color="primary" sx={{ fontWeight: 700, fontVariantNumeric: 'tabular-nums' }}>
{formatPriceRub(p.priceCents)}
</Typography>
+25
View File
@@ -0,0 +1,25 @@
import Box from '@mui/material/Box'
import type { SxProps, Theme } from '@mui/material/styles'
type Props = {
opacity?: number
sx?: SxProps<Theme>
}
export function NoiseOverlay({ opacity = 0.03, sx }: Props) {
return (
<Box
sx={{
position: 'fixed',
inset: 0,
pointerEvents: 'none',
zIndex: 9999,
opacity,
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`,
backgroundRepeat: 'repeat',
backgroundSize: '256px 256px',
...sx,
}}
/>
)
}