diff --git a/client/src/App.tsx b/client/src/App.tsx index 6eb6ec6..0be805b 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,6 +1,7 @@ import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { AuthProvider } from '@/hooks/useAuth'; +import { ThemeProvider } from '@/hooks/useTheme'; import { ToastProvider } from '@/components/Toast'; import ProtectedRoute from '@/components/ProtectedRoute'; import Navbar from '@/components/Navbar'; @@ -102,6 +103,7 @@ export default function App() { return ( + @@ -110,6 +112,7 @@ export default function App() { + ); diff --git a/client/src/components/CatCard.tsx b/client/src/components/CatCard.tsx index a34dcdc..a6446ac 100644 --- a/client/src/components/CatCard.tsx +++ b/client/src/components/CatCard.tsx @@ -43,7 +43,7 @@ export default function CatCard({ cat, onOpen, likedIds }: CatCardProps) { return (
-
onOpen(cat.id)}> +
onOpen(cat.id)}>
e.stopPropagation()}> @@ -56,11 +56,11 @@ export default function CatCard({ cat, onOpen, likedIds }: CatCardProps) {
-
+
{cat.caption @@ -79,20 +79,20 @@ export default function CatCard({ cat, onOpen, likedIds }: CatCardProps) {
@@ -105,4 +105,4 @@ export default function CatCard({ cat, onOpen, likedIds }: CatCardProps) {
); -} \ No newline at end of file +} diff --git a/client/src/components/CatModal.tsx b/client/src/components/CatModal.tsx index 36b2216..ff1d57a 100644 --- a/client/src/components/CatModal.tsx +++ b/client/src/components/CatModal.tsx @@ -86,7 +86,7 @@ export default function CatModal({ catId, onClose }: CatModalProps) { if (isLoading || !cat) { return createPortal(
-
+
Загрузка...
@@ -102,7 +102,7 @@ export default function CatModal({ catId, onClose }: CatModalProps) { return createPortal(
e.stopPropagation()} >
@@ -115,7 +115,7 @@ export default function CatModal({ catId, onClose }: CatModalProps) {
{isOwner && ( - )} @@ -210,7 +210,7 @@ export default function CatModal({ catId, onClose }: CatModalProps) { onChange={e => setCommentText(e.target.value)} placeholder="Комментарий..." maxLength={500} - className="flex-1 h-9 px-3 text-sm rounded-xl border border-[var(--border)] bg-white placeholder:text-[var(--fg-tertiary)] focus-ring" + className="flex-1 h-9 px-3 text-sm rounded-xl border border-[var(--border)] bg-[var(--surface)] placeholder:text-[var(--fg-tertiary)] focus-ring" />
void; +} + +const ThemeContext = createContext({ + theme: 'system', + resolvedTheme: 'light', + setTheme: () => {}, +}); + +export function ThemeProvider({ children }: { children: ReactNode }) { + const [theme, setThemeState] = useState(() => { + return (localStorage.getItem('theme') as Theme) ?? 'system'; + }); + + const [systemDark, setSystemDark] = useState(() => + window.matchMedia('(prefers-color-scheme: dark)').matches + ); + + useEffect(() => { + const mq = window.matchMedia('(prefers-color-scheme: dark)'); + const handler = (e: MediaQueryListEvent) => setSystemDark(e.matches); + mq.addEventListener('change', handler); + return () => mq.removeEventListener('change', handler); + }, []); + + const resolvedTheme: 'light' | 'dark' = + theme === 'system' ? (systemDark ? 'dark' : 'light') : theme; + + useEffect(() => { + const root = document.documentElement; + root.setAttribute('data-theme', resolvedTheme); + }, [resolvedTheme]); + + const setTheme = (t: Theme) => { + localStorage.setItem('theme', t); + setThemeState(t); + }; + + return ( + + {children} + + ); +} + +export function useTheme() { + return useContext(ThemeContext); +} diff --git a/client/src/index.css b/client/src/index.css index 8b001b5..baa7cc8 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,36 +1,68 @@ @import "tailwindcss"; @plugin "tailwindcss-animate"; -:root { - --bg: #F5F3EE; +/* ── Светлая тема — тёплый пастель à la Instagram ── */ +:root, +[data-theme="light"] { + --bg: #FAFAFA; --surface: #FFFFFF; - --surface-hover: #F9F7F3; - --fg: #1C1917; - --fg-secondary: #78716C; - --fg-tertiary: #A8A29E; - --accent: #C2410C; - --accent-hover: #9A3412; - --accent-soft: rgba(194, 65, 12, 0.07); - --accent-light: #FFF7ED; - --secondary: #F5F3EE; - --border: #E7E5E4; - --border-light: #F0EDE8; - --danger: #DC2626; - --danger-hover: #B91C1C; - --danger-light: #FEF2F2; - --success: #15803D; - --success-light: #F0FDF4; - --radius: 12px; - --radius-lg: 16px; - --radius-xl: 24px; - --shadow-sm: 0 1px 2px rgba(28,25,23,0.04); - --shadow-md: 0 2px 8px rgba(28,25,23,0.06); - --shadow-lg: 0 8px 24px rgba(28,25,23,0.08); - --shadow-xl: 0 16px 48px rgba(28,25,23,0.12); + --surface-hover: #F7F7F7; + --fg: #1A1A1A; + --fg-secondary: #737373; + --fg-tertiary: #B0B0B0; + --accent: #C9445D; + --accent-hover: #B03650; + --accent-soft: rgba(201, 68, 93, 0.09); + --accent-light: #FFF0F3; + --secondary: #FAFAFA; + --border: #DBDBDB; + --border-light: #EFEFEF; + --danger: #C9445D; + --danger-hover: #B03650; + --danger-light: #FFF0F3; + --success: #2E9E6B; + --success-light: #EEF8F3; + --radius: 10px; + --radius-lg: 14px; + --radius-xl: 20px; + --shadow-sm: 0 1px 3px rgba(0,0,0,0.06); + --shadow-md: 0 4px 12px rgba(0,0,0,0.08); + --shadow-lg: 0 10px 32px rgba(0,0,0,0.10); + --shadow-xl: 0 20px 56px rgba(0,0,0,0.14); +} + +/* ── Тёмная тема — Instagram dark ── */ +[data-theme="dark"] { + --bg: #000000; + --surface: #121212; + --surface-hover: #1C1C1C; + --fg: #F5F5F5; + --fg-secondary: #A8A8A8; + --fg-tertiary: #555555; + --accent: #E8687E; + --accent-hover: #F07A8E; + --accent-soft: rgba(232, 104, 126, 0.12); + --accent-light: #2A1018; + --secondary: #000000; + --border: #262626; + --border-light: #1C1C1C; + --danger: #E8687E; + --danger-hover: #F07A8E; + --danger-light: #2A1018; + --success: #48B884; + --success-light: #0D2419; + --shadow-sm: 0 1px 3px rgba(0,0,0,0.40); + --shadow-md: 0 4px 14px rgba(0,0,0,0.50); + --shadow-lg: 0 10px 36px rgba(0,0,0,0.60); + --shadow-xl: 0 20px 60px rgba(0,0,0,0.75); } * { border-color: var(--border); } +html { + transition: background-color 0.3s ease, color 0.3s ease; +} + body { background: var(--bg); color: var(--fg); @@ -39,6 +71,7 @@ body { -moz-osx-font-smoothing: grayscale; line-height: 1.6; letter-spacing: -0.01em; + transition: background-color 0.3s ease, color 0.3s ease; } ::selection { background: var(--accent); color: white; } @@ -94,16 +127,51 @@ body { background: var(--surface); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); - transition: box-shadow 0.2s ease, transform 0.2s ease; + transition: box-shadow 0.25s ease, transform 0.25s ease, background-color 0.3s ease; } .card-hover:hover { box-shadow: var(--shadow-md); - transform: translateY(-1px); + transform: translateY(-2px); } .card-interactive:hover { box-shadow: var(--shadow-md); } +/* Product-ready card — без layout shift, плавное поднятие + тень */ +.cat-card { + background: var(--surface); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + overflow: hidden; + cursor: pointer; + transition: + box-shadow 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94), + transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); + will-change: transform, box-shadow; +} +.cat-card:hover { + box-shadow: var(--shadow-lg); + transform: translateY(-3px); +} +.cat-card:active { + transform: translateY(-1px); + box-shadow: var(--shadow-md); + transition-duration: 0.1s; +} + +/* Изображение в карточке — зум при ховере */ +.cat-card-image { + background: var(--bg); + overflow: hidden; +} +.cat-card-img { + transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94); + will-change: transform; +} +.cat-card:hover .cat-card-img { + transform: scale(1.03); +} + .avatar-colored { border-radius: 50%; display: flex; @@ -119,13 +187,35 @@ body { align-items: center; justify-content: center; border-radius: 50%; - transition: background 0.15s, transform 0.1s; + transition: background 0.18s ease, transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1); cursor: pointer; border: none; background: transparent; } -.btn-ghost:active { transform: scale(0.9); } -.btn-ghost:hover { background: var(--border-light); } +.btn-ghost:hover { background: var(--border-light); transform: scale(1.08); } +.btn-ghost:active { transform: scale(0.92); transition-duration: 0.08s; } + +/* Кнопки действий в карточке */ +.action-btn { + padding: 5px 8px; + border-radius: 8px; + border: none; + background: transparent; + cursor: pointer; + transition: + background 0.18s ease, + color 0.18s ease, + transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1); +} +.action-btn:hover { + background: var(--border-light); + color: var(--fg); + transform: scale(1.05); +} +.action-btn:active { + transform: scale(0.95); + transition-duration: 0.08s; +} .tag { display: inline-flex; @@ -151,9 +241,41 @@ body { .text-tertiary { color: var(--fg-tertiary); } .nav-glass { - background: rgba(245, 243, 238, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); + background: rgba(250, 250, 250, 0.92); + backdrop-filter: blur(20px) saturate(1.6); + -webkit-backdrop-filter: blur(20px) saturate(1.6); + transition: background 0.3s ease; +} +[data-theme="dark"] .nav-glass { + background: rgba(0, 0, 0, 0.90); +} + +.nav-editorial-bar { + height: 3px; + background: var(--accent); + width: 100%; + position: absolute; + top: 0; + left: 0; +} + +.wordmark { + font-size: 13px; + font-weight: 800; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--fg); +} + +.wordmark-dot { + display: inline-block; + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--accent); + margin-left: 1px; + margin-bottom: 1px; + vertical-align: middle; } .divider { diff --git a/client/src/pages/AdminPage.tsx b/client/src/pages/AdminPage.tsx index 3bc2455..3616690 100644 --- a/client/src/pages/AdminPage.tsx +++ b/client/src/pages/AdminPage.tsx @@ -94,7 +94,7 @@ function TabBtn({ active, onClick, children }: { active: boolean; onClick: () => return ( @@ -127,7 +127,7 @@ function UserRow({ u, onDelete, qc }: { u: any; onDelete: () => void; qc: any })
setPoints(Math.max(0, parseInt(e.target.value) || 0))} className="w-20 h-7 px-2 text-xs rounded-lg border focus-ring" autoFocus /> - +
) : ( @@ -143,7 +143,7 @@ function UserRow({ u, onDelete, qc }: { u: any; onDelete: () => void; qc: any })
@@ -174,7 +174,7 @@ function CatRow({ c, onDelete, qc }: { c: any; onDelete: () => void; qc: any }) setCaption(e.target.value)} className="flex-1 h-7 px-2 text-xs rounded-lg border focus-ring" autoFocus onKeyDown={e => { if (e.key === 'Enter') saveCaption(); if (e.key === 'Escape') { setEditing(false); setCaption(c.caption || ''); } }} /> - +
) : ( @@ -187,7 +187,7 @@ function CatRow({ c, onDelete, qc }: { c: any; onDelete: () => void; qc: any })
diff --git a/client/src/pages/CatPage.tsx b/client/src/pages/CatPage.tsx index 6dd34eb..4147e96 100644 --- a/client/src/pages/CatPage.tsx +++ b/client/src/pages/CatPage.tsx @@ -129,7 +129,7 @@ export default function CatPage() {
{isOwner && ( - )} @@ -180,7 +180,7 @@ export default function CatPage() { {(user && (c.user_id === user.id || user.is_admin)) && ( @@ -199,7 +199,7 @@ export default function CatPage() { onChange={e => setCommentText(e.target.value)} placeholder="Написать комментарий..." maxLength={500} - className="flex-1 h-9 px-3 text-sm rounded-xl border border-[var(--border)] bg-white placeholder:text-[var(--fg-tertiary)] focus-ring" + className="flex-1 h-9 px-3 text-sm rounded-xl border border-[var(--border)] bg-[var(--surface)] placeholder:text-[var(--fg-tertiary)] focus-ring" /> )} @@ -200,7 +200,7 @@ export default function ProfilePage() { {showAvatarPicker && createPortal(
setShowAvatarPicker(false)}> -
e.stopPropagation()}> +
e.stopPropagation()}>

Выбрать аватар