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="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="apple-touch-icon" href="/icons/icon-192x192.svg" />
|
<link rel="apple-touch-icon" href="/icons/icon-192x192.svg" />
|
||||||
<title>iistwin</title>
|
<title>iistwin</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<!-- Шрифты self-hosted (client/public/fonts), @font-face — в src/index.css:
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<!-- Загружаем только шрифты, реально используемые встроенными скинами:
|
|
||||||
Inter (variable) — sans во всех скинах (iistwin, twenty, omegaprom);
|
Inter (variable) — sans во всех скинах (iistwin, twenty, omegaprom);
|
||||||
DM Mono 400/500 — mono в скине twenty (остальные скины используют системные mono).
|
DM Mono 400/500 — mono в скине twenty (остальные скины используют системные mono).
|
||||||
|
Preload только latin-подмножества Inter — оно нужно при любом первом рендере;
|
||||||
|
остальные subset'ы подгружаются браузером по unicode-range.
|
||||||
TODO: если кастомные скины организаций будут задавать другие font-family,
|
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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<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 = [
|
const STATIC_ASSETS = [
|
||||||
'/',
|
'/',
|
||||||
'/index.html',
|
'/index.html',
|
||||||
'/manifest.json',
|
'/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.
|
// Активный чат, о котором сообщает клиент через postMessage.
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useLocation } from 'wouter';
|
import { useLocation } from 'wouter';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { Loader2, AlertTriangle } from 'lucide-react';
|
import { Loader2, AlertTriangle } from 'lucide-react';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { Sidebar } from '@/components/Sidebar';
|
import { Sidebar } from '@/components/Sidebar';
|
||||||
import { AddBookmarkFAB } from '@/components/AddBookmarkFAB';
|
import { AddBookmarkFAB } from '@/components/AddBookmarkFAB';
|
||||||
import { apiRequest } from '@/lib/queryClient';
|
import { apiRequest } from '@/lib/queryClient';
|
||||||
import { isAdmin } from '@/lib/permissions';
|
import { isAdmin } from '@/lib/permissions';
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Пути страниц с общим хромом (Sidebar/MobileBottomNav, биллинг-оверлей, FAB) —
|
* Пути страниц с общим хромом (Sidebar/MobileBottomNav, биллинг-оверлей, FAB) —
|
||||||
@@ -40,21 +40,19 @@ const SHELL_ROUTE_PATTERNS: RegExp[] = [
|
|||||||
/^\/[^/]+\/(tasks|forms)\//,
|
/^\/[^/]+\/(tasks|forms)\//,
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Оверлей блокировки биллинга с плавным появлением (opacity transition) */
|
/** Оверлей блокировки биллинга: плавное появление и исчезновение (fade 150ms).
|
||||||
|
Exit-анимацию обеспечивает AnimatePresence в AppShell; reducedMotion="user"
|
||||||
|
(MotionConfig в App) оставляет opacity-переход, но отключает трансформы. */
|
||||||
function BillingBlockedOverlay() {
|
function BillingBlockedOverlay() {
|
||||||
// Состояние mounted → запускаем fade-in после монтирования
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
const raf = requestAnimationFrame(() => setVisible(true));
|
|
||||||
return () => cancelAnimationFrame(raf);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<motion.div
|
||||||
className={cn(
|
initial={{ opacity: 0 }}
|
||||||
"fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm transition-opacity duration-base",
|
animate={{ opacity: 1 }}
|
||||||
visible ? "opacity-100" : "opacity-0"
|
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
|
<div
|
||||||
role="dialog"
|
role="dialog"
|
||||||
@@ -79,7 +77,7 @@ function BillingBlockedOverlay() {
|
|||||||
Перейти к биллингу
|
Перейти к биллингу
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +133,10 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
{showBlockedOverlay && <BillingBlockedOverlay />}
|
{/* AnimatePresence нужен для exit-анимации (fade 150ms) биллинг-оверлея */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showBlockedOverlay && <BillingBlockedOverlay />}
|
||||||
|
</AnimatePresence>
|
||||||
{children}
|
{children}
|
||||||
<AddBookmarkFAB />
|
<AddBookmarkFAB />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ const ToastViewport = React.forwardRef<
|
|||||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||||
|
|
||||||
const toastVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ export type ControlSize = 'sm' | 'md' | 'lg';
|
|||||||
|
|
||||||
// Тема 2.0: радиус кнопок — var(--button-radius), fallback = прежний rounded-md
|
// Тема 2.0: радиус кнопок — var(--button-radius), fallback = прежний rounded-md
|
||||||
export const buttonRecipe = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -1,7 +1,84 @@
|
|||||||
/* Шрифты подключаются в client/index.html (variable Inter + DM Mono).
|
/* Шрифты self-hosted из client/public/fonts (variable Inter + DM Mono).
|
||||||
Не добавлять @import шрифтов сюда — это дублирует запросы и ломает порядок каскада. */
|
Не добавлять @import шрифтов сюда — это дублирует запросы и ломает порядок каскада.
|
||||||
|
Inter italic сознательно не подключаем (в codebase ~20 классов `italic` на мелких
|
||||||
|
вспомогательных текстах) — браузер синтезирует наклон из variable-начертания.
|
||||||
|
DM Mono кириллицы не имеет (её нет и в Google Fonts) — кириллический mono
|
||||||
|
рендерится системным fallback из --font-mono, как и раньше. */
|
||||||
@import './themes/styles/generated-skins.css';
|
@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 base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
@@ -382,13 +459,20 @@ td.formula-highlight {
|
|||||||
background: var(--tw-font-extra-light);
|
background: var(--tw-font-extra-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Focus visible — removed per user request */
|
/* Focus visible — видимое кольцо фокуса для клавиатурной навигации.
|
||||||
|
Компонентные рецепты (recipes.ts, ui/*) переопределяют его своими ring-стилями. */
|
||||||
@layer base {
|
@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;
|
outline: none;
|
||||||
--tw-ring-offset-shadow: none;
|
|
||||||
--tw-ring-shadow: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user