refactor(frontend): move repeated Tailwind chains into BEM classes

This commit is contained in:
Vaka.pro
2026-04-27 20:42:21 +03:00
parent 17d59c3639
commit d46d4c4487
14 changed files with 273 additions and 101 deletions

View File

@@ -13,22 +13,20 @@ export function LanguageSwitcher({ className }: { className?: string }) {
return (
<div
className={cn(
'inline-flex items-center gap-1 rounded-md border border-border bg-surface/90 p-1 text-xs shadow-card',
'language-switcher',
className,
)}
aria-label={t('language.switch')}
>
<Languages className="ml-1 h-3.5 w-3.5 text-muted" aria-hidden />
<Languages className="language-switcher__icon" aria-hidden />
{languages.map((item) => (
<button
key={item.value}
type="button"
onClick={() => setLanguage(item.value)}
className={cn(
'rounded px-2 py-1 font-semibold transition-colors',
language === item.value
? 'bg-primary text-primary-foreground'
: 'text-muted hover:bg-surface-muted hover:text-ink',
'language-switcher__button',
language === item.value && 'language-switcher__button--active',
)}
aria-pressed={language === item.value}
title={item.value === 'ru' ? t('language.ru') : t('language.en')}

View File

@@ -4,9 +4,9 @@ import { Footer } from './Footer';
export function AppShell() {
return (
<div className="flex min-h-screen flex-col">
<div className="app-shell">
<Header />
<main className="container-page flex-1 py-6 sm:py-10">
<main className="app-shell__main">
<Outlet />
</main>
<Footer />

View File

@@ -17,15 +17,15 @@ export function Footer() {
});
return (
<footer className="container-page mt-10 py-6 text-xs text-muted">
<div className="flex flex-col items-center justify-between gap-2 sm:flex-row">
<div className="flex items-center gap-2">
<footer className="app-footer">
<div className="app-footer__inner">
<div className="app-footer__brand">
<Gift className="h-4 w-4" aria-hidden />
<span className="font-display text-sm">{t('app.name')}</span>
<span className="app-footer__brand-name">{t('app.name')}</span>
</div>
<div className="flex items-center gap-3">
<div className="app-footer__meta">
<span>{t('footer.frontend', { version: FRONTEND_VERSION })}</span>
<span className="opacity-50">·</span>
<span className="app-footer__separator">·</span>
<span>{t('footer.backend', { version: data?.backend ?? '...' })}</span>
</div>
</div>

View File

@@ -44,21 +44,21 @@ export function Header() {
if (!user) return null;
return (
<header className="container-page pt-6">
<div className="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-border bg-surface/80 px-4 py-3 backdrop-blur">
<Link to="/" className="flex items-center gap-2">
<span className="inline-flex h-9 w-9 items-center justify-center rounded-md bg-primary text-primary-foreground shadow-card">
<header className="app-header">
<div className="app-header__inner">
<Link to="/" className="app-header__brand">
<span className="app-header__brand-mark">
<Gift className="h-4 w-4" />
</span>
<div>
<div className="font-display text-lg leading-tight">{t('app.name')}</div>
<div className="text-xs text-muted">
<div className="app-header__brand-title">{t('app.name')}</div>
<div className="app-header__brand-subtitle">
{t('header.signedInAs', { name: user.displayName })}
</div>
</div>
</Link>
<nav className="flex flex-wrap items-center gap-1">
<nav className="app-header__nav">
{links.map((l) => (
<NavLink
key={l.to}
@@ -66,8 +66,8 @@ export function Header() {
end={l.end}
className={({ isActive }) =>
cn(
'inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
isActive ? 'bg-primary text-primary-foreground shadow-card' : 'text-ink hover:bg-ink/5',
'app-header__nav-link',
isActive && 'app-header__nav-link--active',
)
}
>
@@ -80,8 +80,8 @@ export function Header() {
to={`/u/${friend.data.slug}`}
className={({ isActive }) =>
cn(
'inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
isActive ? 'bg-primary text-primary-foreground shadow-card' : 'text-ink hover:bg-ink/5',
'app-header__nav-link',
isActive && 'app-header__nav-link--active',
)
}
>
@@ -91,7 +91,7 @@ export function Header() {
)}
</nav>
<div className="flex items-center gap-1">
<div className="app-header__actions">
<LanguageSwitcher />
<Button
variant="ghost"

View File

@@ -40,9 +40,9 @@ export function Modal({
if (!open) return null;
return createPortal(
<div className="fixed inset-0 z-50 flex items-end justify-center p-0 sm:items-center sm:p-6">
<div className="modal">
<div
className="absolute inset-0 bg-ink/40 backdrop-blur-sm animate-fade-in-up"
className="modal__backdrop"
onClick={onClose}
aria-hidden
/>
@@ -50,27 +50,21 @@ export function Modal({
role="dialog"
aria-modal="true"
className={cn(
'relative w-full bg-surface shadow-pop animate-fade-in-up',
'rounded-t-xl sm:rounded-xl',
size === 'md' ? 'sm:max-w-lg' : 'sm:max-w-2xl',
'max-h-[90vh] overflow-hidden flex flex-col',
'modal__panel',
size === 'md' ? 'modal__panel--md' : 'modal__panel--lg',
)}
>
<header className="flex items-start justify-between gap-4 border-b border-border px-5 py-4">
<div className="min-w-0">
<h2 className="text-lg font-semibold text-ink">{title}</h2>
{description && <p className="mt-1 text-sm text-muted">{description}</p>}
<header className="modal__header">
<div className="modal__title-wrap">
<h2 className="modal__title">{title}</h2>
{description && <p className="modal__description">{description}</p>}
</div>
<Button variant="ghost" size="icon" onClick={onClose} aria-label="Close">
<X className="h-5 w-5" />
</Button>
</header>
<div className="overflow-y-auto px-5 py-5">{children}</div>
{footer && (
<footer className="flex items-center justify-end gap-2 border-t border-border px-5 py-4">
{footer}
</footer>
)}
<div className="modal__body">{children}</div>
{footer && <footer className="modal__footer">{footer}</footer>}
</div>
</div>,
document.body,