feat(ui): фаза 7 — self-host шрифты Inter/DM Mono (woff2, precache в SW v13), возвращён focus-visible (кольцо --ring), active:scale для кнопок (фикс мёртвого not-disabled), плавные появление/исчезновение биллинг-оверлея, длительности тостов по шкале токенов
This commit is contained in:
@@ -13,14 +13,14 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/icons/icon-192x192.svg" />
|
||||
<title>iistwin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<!-- Загружаем только шрифты, реально используемые встроенными скинами:
|
||||
<!-- Шрифты self-hosted (client/public/fonts), @font-face — в src/index.css:
|
||||
Inter (variable) — sans во всех скинах (iistwin, twenty, omegaprom);
|
||||
DM Mono 400/500 — mono в скине twenty (остальные скины используют системные mono).
|
||||
Preload только latin-подмножества Inter — оно нужно при любом первом рендере;
|
||||
остальные subset'ы подгружаются браузером по unicode-range.
|
||||
TODO: если кастомные скины организаций будут задавать другие font-family,
|
||||
их стоит подгружать по требованию отдельно, а не одним общим линком. -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap" rel="stylesheet">
|
||||
их стоит подгружать по требованию отдельно. -->
|
||||
<link rel="preload" href="/fonts/inter-var-latin.woff2" as="font" type="font/woff2" crossorigin>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
BIN
client/public/fonts/dm-mono-400-latin-ext.woff2
Normal file
BIN
client/public/fonts/dm-mono-400-latin-ext.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/dm-mono-400-latin.woff2
Normal file
BIN
client/public/fonts/dm-mono-400-latin.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/dm-mono-500-latin-ext.woff2
Normal file
BIN
client/public/fonts/dm-mono-500-latin-ext.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/dm-mono-500-latin.woff2
Normal file
BIN
client/public/fonts/dm-mono-500-latin.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/inter-var-cyrillic-ext.woff2
Normal file
BIN
client/public/fonts/inter-var-cyrillic-ext.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/inter-var-cyrillic.woff2
Normal file
BIN
client/public/fonts/inter-var-cyrillic.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/inter-var-latin-ext.woff2
Normal file
BIN
client/public/fonts/inter-var-latin-ext.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/inter-var-latin.woff2
Normal file
BIN
client/public/fonts/inter-var-latin.woff2
Normal file
Binary file not shown.
@@ -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.
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm transition-opacity duration-base",
|
||||
visible ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
// Длительность и easing — по шкале дизайн-системы:
|
||||
// 0.15s = var(--duration-base), [0.2, 0, 0, 1] = var(--ease-standard)
|
||||
transition={{ duration: 0.15, ease: [0.2, 0, 0, 1] }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
@@ -79,7 +77,7 @@ function BillingBlockedOverlay() {
|
||||
Перейти к биллингу
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -135,7 +133,10 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
|
||||
return (
|
||||
<Sidebar>
|
||||
{showBlockedOverlay && <BillingBlockedOverlay />}
|
||||
{/* AnimatePresence нужен для exit-анимации (fade 150ms) биллинг-оверлея */}
|
||||
<AnimatePresence>
|
||||
{showBlockedOverlay && <BillingBlockedOverlay />}
|
||||
</AnimatePresence>
|
||||
{children}
|
||||
<AddBookmarkFAB />
|
||||
</Sidebar>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/* <main> получает фокус программно при смене роута (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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user