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;
}
}