diff --git a/client/index.html b/client/index.html index b8ccf79..25744e9 100644 --- a/client/index.html +++ b/client/index.html @@ -13,14 +13,14 @@ iistwin - - - - + их стоит подгружать по требованию отдельно. --> +
diff --git a/client/public/fonts/dm-mono-400-latin-ext.woff2 b/client/public/fonts/dm-mono-400-latin-ext.woff2 new file mode 100644 index 0000000..9785e91 Binary files /dev/null and b/client/public/fonts/dm-mono-400-latin-ext.woff2 differ diff --git a/client/public/fonts/dm-mono-400-latin.woff2 b/client/public/fonts/dm-mono-400-latin.woff2 new file mode 100644 index 0000000..03e4859 Binary files /dev/null and b/client/public/fonts/dm-mono-400-latin.woff2 differ diff --git a/client/public/fonts/dm-mono-500-latin-ext.woff2 b/client/public/fonts/dm-mono-500-latin-ext.woff2 new file mode 100644 index 0000000..b872009 Binary files /dev/null and b/client/public/fonts/dm-mono-500-latin-ext.woff2 differ diff --git a/client/public/fonts/dm-mono-500-latin.woff2 b/client/public/fonts/dm-mono-500-latin.woff2 new file mode 100644 index 0000000..67698d8 Binary files /dev/null and b/client/public/fonts/dm-mono-500-latin.woff2 differ diff --git a/client/public/fonts/inter-var-cyrillic-ext.woff2 b/client/public/fonts/inter-var-cyrillic-ext.woff2 new file mode 100644 index 0000000..cc9d563 Binary files /dev/null and b/client/public/fonts/inter-var-cyrillic-ext.woff2 differ diff --git a/client/public/fonts/inter-var-cyrillic.woff2 b/client/public/fonts/inter-var-cyrillic.woff2 new file mode 100644 index 0000000..fff3b01 Binary files /dev/null and b/client/public/fonts/inter-var-cyrillic.woff2 differ diff --git a/client/public/fonts/inter-var-latin-ext.woff2 b/client/public/fonts/inter-var-latin-ext.woff2 new file mode 100644 index 0000000..a0125fa Binary files /dev/null and b/client/public/fonts/inter-var-latin-ext.woff2 differ diff --git a/client/public/fonts/inter-var-latin.woff2 b/client/public/fonts/inter-var-latin.woff2 new file mode 100644 index 0000000..b0d0e2e Binary files /dev/null and b/client/public/fonts/inter-var-latin.woff2 differ diff --git a/client/public/sw.js b/client/public/sw.js index 735dffb..49dde1c 100644 --- a/client/public/sw.js +++ b/client/public/sw.js @@ -1,9 +1,19 @@ -const CACHE_NAME = 'iistwin-pwa-v12'; +const CACHE_NAME = 'iistwin-pwa-v13'; const STATIC_ASSETS = [ '/', '/index.html', '/manifest.json', - '/favicon.svg' + '/favicon.svg', + // Self-hosted шрифты — precache, чтобы PWA офлайн рендерился без FOUT/fallback + // (имена файлов должны совпадать с @font-face в client/src/index.css) + '/fonts/inter-var-latin.woff2', + '/fonts/inter-var-latin-ext.woff2', + '/fonts/inter-var-cyrillic.woff2', + '/fonts/inter-var-cyrillic-ext.woff2', + '/fonts/dm-mono-400-latin.woff2', + '/fonts/dm-mono-400-latin-ext.woff2', + '/fonts/dm-mono-500-latin.woff2', + '/fonts/dm-mono-500-latin-ext.woff2' ]; // Активный чат, о котором сообщает клиент через postMessage. diff --git a/client/src/components/AppShell.tsx b/client/src/components/AppShell.tsx index 33d2705..0060cfa 100644 --- a/client/src/components/AppShell.tsx +++ b/client/src/components/AppShell.tsx @@ -1,13 +1,13 @@ -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { useLocation } from 'wouter'; import { useQuery } from '@tanstack/react-query'; +import { AnimatePresence, motion } from 'framer-motion'; import { Loader2, AlertTriangle } from 'lucide-react'; import { useAuth } from '@/hooks/useAuth'; import { Sidebar } from '@/components/Sidebar'; import { AddBookmarkFAB } from '@/components/AddBookmarkFAB'; import { apiRequest } from '@/lib/queryClient'; import { isAdmin } from '@/lib/permissions'; -import { cn } from '@/lib/utils'; /** * Пути страниц с общим хромом (Sidebar/MobileBottomNav, биллинг-оверлей, FAB) — @@ -40,21 +40,19 @@ const SHELL_ROUTE_PATTERNS: RegExp[] = [ /^\/[^/]+\/(tasks|forms)\//, ]; -/** Оверлей блокировки биллинга с плавным появлением (opacity transition) */ +/** Оверлей блокировки биллинга: плавное появление и исчезновение (fade 150ms). + Exit-анимацию обеспечивает AnimatePresence в AppShell; reducedMotion="user" + (MotionConfig в App) оставляет opacity-переход, но отключает трансформы. */ function BillingBlockedOverlay() { - // Состояние mounted → запускаем fade-in после монтирования - const [visible, setVisible] = useState(false); - useEffect(() => { - const raf = requestAnimationFrame(() => setVisible(true)); - return () => cancelAnimationFrame(raf); - }, []); - return ( -
-
+ ); } @@ -135,7 +133,10 @@ export function AppShell({ children }: { children: React.ReactNode }) { return ( - {showBlockedOverlay && } + {/* AnimatePresence нужен для exit-анимации (fade 150ms) биллинг-оверлея */} + + {showBlockedOverlay && } + {children} diff --git a/client/src/components/ui/toast.tsx b/client/src/components/ui/toast.tsx index 5d3477a..19188cd 100644 --- a/client/src/components/ui/toast.tsx +++ b/client/src/components/ui/toast.tsx @@ -23,7 +23,9 @@ const ToastViewport = React.forwardRef< ToastViewport.displayName = ToastPrimitives.Viewport.displayName const toastVariants = cva( - "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-[color,background-color,border-color,box-shadow,transform,opacity] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + // duration-base привязывает animate-in/animate-out (150ms по умолчанию в + // tailwindcss-animate) и transition к var(--duration-base) из шкалы дизайн-системы + "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-[color,background-color,border-color,box-shadow,transform,opacity] duration-base ease-standard data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", { variants: { variant: { diff --git a/client/src/design-system/recipes.ts b/client/src/design-system/recipes.ts index 0b940b6..68e0aa2 100644 --- a/client/src/design-system/recipes.ts +++ b/client/src/design-system/recipes.ts @@ -22,7 +22,11 @@ export type ControlSize = 'sm' | 'md' | 'lg'; // Тема 2.0: радиус кнопок — var(--button-radius), fallback = прежний rounded-md export const buttonRecipe = cva( - 'inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-[var(--button-radius,calc(var(--radius)_-_2px))] text-sm font-medium ring-offset-background transition-[color,background-color,border-color,box-shadow,transform] duration-150 active:not-disabled:scale-[0.96] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', + // active:scale — микровзаимодействие нажатия; disabled-кнопки защищены + // disabled:pointer-events-none (событие :active на них не наступает). + // Вариант not-disabled НЕ использовать: Tailwind v3 его не поддерживает, + // класс молча не компилируется (проверено сборкой). + 'inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-[var(--button-radius,calc(var(--radius)_-_2px))] text-sm font-medium ring-offset-background transition-[color,background-color,border-color,box-shadow,transform] duration-150 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', { variants: { variant: { diff --git a/client/src/index.css b/client/src/index.css index ee6db90..b235110 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,7 +1,84 @@ -/* Шрифты подключаются в client/index.html (variable Inter + DM Mono). - Не добавлять @import шрифтов сюда — это дублирует запросы и ломает порядок каскада. */ +/* Шрифты self-hosted из client/public/fonts (variable Inter + DM Mono). + Не добавлять @import шрифтов сюда — это дублирует запросы и ломает порядок каскада. + Inter italic сознательно не подключаем (в codebase ~20 классов `italic` на мелких + вспомогательных текстах) — браузер синтезирует наклон из variable-начертания. + DM Mono кириллицы не имеет (её нет и в Google Fonts) — кириллический mono + рендерится системным fallback из --font-mono, как и раньше. */ @import './themes/styles/generated-skins.css'; +/* Inter variable (opsz 14..32, wght 100..900), normal — subset cyrillic-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/fonts/inter-var-cyrillic-ext.woff2') format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* Inter variable — subset cyrillic */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/fonts/inter-var-cyrillic.woff2') format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* Inter variable — subset latin-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/fonts/inter-var-latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* Inter variable — subset latin */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/fonts/inter-var-latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* DM Mono 400 — subset latin-ext */ +@font-face { + font-family: 'DM Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/dm-mono-400-latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* DM Mono 400 — subset latin */ +@font-face { + font-family: 'DM Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/dm-mono-400-latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* DM Mono 500 — subset latin-ext */ +@font-face { + font-family: 'DM Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('/fonts/dm-mono-500-latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* DM Mono 500 — subset latin */ +@font-face { + font-family: 'DM Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('/fonts/dm-mono-500-latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + @tailwind base; @tailwind components; @tailwind utilities; @@ -382,13 +459,20 @@ td.formula-highlight { background: var(--tw-font-extra-light); } -/* Focus visible — removed per user request */ +/* Focus visible — видимое кольцо фокуса для клавиатурной навигации. + Компонентные рецепты (recipes.ts, ui/*) переопределяют его своими ring-стилями. */ @layer base { - *:focus-visible { + :focus-visible { + outline: 2px solid var(--ring); + outline-offset: 1px; + } + + /*
получает фокус программно при смене роута (tabIndex=-1, + App.tsx useRouteTitleAndFocus) — кольцо ему не нужно. + Перестраховка: программный focus() обычно не триггерит :focus-visible, + но поведение зависит от браузера. */ + main:focus-visible { outline: none; - --tw-ring-offset-shadow: none; - --tw-ring-shadow: none; - box-shadow: none; } }