@ai.all/ds7 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,88 +1,102 @@
1
- # 02-Design-System · Tokens Specification
1
+ # 02 · Design tokens
2
2
 
3
- > Дизайн-токены определяют визуальную ДНК проекта. Все значения объявляются через CSS Custom Properties в файле `theme.css`.
3
+ Tokens are the foundations: the named values every component uses. They live as CSS custom properties in `app/assets/css/theme.css`. Components read tokens only; they never contain raw values.
4
+
5
+ Format for tools that need it: the [W3C Design Tokens Format Module (2025.10)](https://www.w3.org/community/reports/design-tokens/CG-FINAL-format-20251028/), with `$value` and `$type`.
4
6
 
5
7
  ---
6
8
 
7
- ## 1. Палитра цветов (Semantic Color System)
9
+ ## 1. Colour
8
10
 
9
11
  ```css
10
12
  :root {
11
- /* Базовые цвета поверхностей */
12
- --color-paper: #f7f5ef; /* Теплый фон страниц */
13
- --color-white: #ffffff; /* Белые плашки и карточки */
14
- --color-ink: #1f1b16; /* Основной контрастный текст */
15
- --color-ink-500: #6b6459; /* Второстепенный поясняющий текст */
16
- --color-ink-100: rgba(31, 27, 22, 0.08); /* Границы и разделители */
17
- --color-ink-950: #12100d; /* Глубокий темный фон для инверсий */
18
-
19
- /* Фирменный солнечный акцент */
20
- --color-sun: #ffd84d; /* Теплый солнечный акцент */
21
- --color-sun-hover: #ffcf2e; /* Состояние ховера */
22
-
23
- /* Семантические цвета */
24
- --color-success: #2e7d32; /* Подтверждение, экономия */
25
- --color-warning: #ed6c02; /* Предупреждение */
26
- --color-error: #d32f2f; /* Ошибки */
13
+ /* surfaces and text */
14
+ --color-paper: #f7f5ef; /* page background */
15
+ --color-white: #ffffff; /* cards, panels */
16
+ --color-ink: #1f1b16; /* main text */
17
+ --color-ink-500: #6b6459; /* secondary text */
18
+ --color-ink-100: rgba(31, 27, 22, 0.08); /* borders, dividers */
19
+ --color-ink-950: #12100d; /* dark inversions */
20
+
21
+ /* brand accent */
22
+ --color-sun: #ffd84d;
23
+ --color-sun-hover: #ffcf2e;
24
+
25
+ /* semantic */
26
+ --color-success: #2e7d32;
27
+ --color-warning: #ed6c02;
28
+ --color-error: #d32f2f;
27
29
  }
28
30
  ```
29
31
 
30
- > **Правило контрастности:** Желтый цвет `--color-sun` запрещено использовать для текста. Он применяется только как фоновая заливка кнопок (`.btn.-sun`), бейджей или подложка-маркер (`box-shadow: inset 0 -0.3em 0 var(--color-sun)`).
32
+ **Contrast rule.** `--color-sun` is never used for text. It is a fill (buttons), a badge background, or a marker (`box-shadow: inset 0 -0.3em 0 var(--color-sun)`).
33
+
34
+ **Measure, do not estimate.** When a palette changes, check the contrast of every text and border pair with a tool, and record the result in the brandbook.
31
35
 
32
36
  ---
33
37
 
34
- ## 2. Ритм вертикальных и горизонтальных отступов (Spacing Scale)
38
+ ## 2. Spacing
35
39
 
36
40
  ```css
37
41
  :root {
38
- --space-1: 0.25rem; /* 4px */
39
- --space-2: 0.5rem; /* 8px */
40
- --space-3: 0.75rem; /* 12px */
41
- --space-4: 1rem; /* 16px */
42
- --space-6: 1.5rem; /* 24px */
43
- --space-8: 2rem; /* 32px */
44
- --space-12: 3rem; /* 48px */
45
- --space-16: 4rem; /* 64px */
46
-
47
- /* Семантический интервал секций (Responsive) */
48
- --space-single: clamp(1.2rem, 3vw, 2.2rem); /* Внутренний отступ блоков */
49
- --space-double: clamp(2.4rem, 6vw, 4.5rem); /* Стандартный интервал секций */
50
- --space-triple: clamp(3.6rem, 9vw, 6.5rem); /* Крупные разделы на десктопе */
51
-
52
- /* Базовая сетка страницы */
53
- --content-width: 78rem; /* 1248px */
42
+ --space-1: 0.25rem; /* 4 px */
43
+ --space-2: 0.5rem; /* 8 px */
44
+ --space-3: 0.75rem; /* 12 px */
45
+ --space-4: 1rem; /* 16 px */
46
+ --space-6: 1.5rem; /* 24 px */
47
+ --space-8: 2rem; /* 32 px */
48
+ --space-12: 3rem; /* 48 px */
49
+ --space-16: 4rem; /* 64 px */
50
+
51
+ /* responsive section rhythm */
52
+ --space-single: clamp(1.2rem, 3vw, 2.2rem);
53
+ --space-double: clamp(2.4rem, 6vw, 4.5rem);
54
+ --space-triple: clamp(3.6rem, 9vw, 6.5rem);
55
+
56
+ /* page grid */
57
+ --content-width: 78rem; /* 1248 px */
54
58
  --page-gutter: clamp(1rem, 3.5vw, 2rem);
55
59
  }
56
60
  ```
57
61
 
58
62
  ---
59
63
 
60
- ## 3. Тени и глубина поверхностей (Elevation)
61
-
62
- Вместо случайных значений `box-shadow` используются строгие семантические токены:
64
+ ## 3. Elevation
63
65
 
64
66
  ```css
65
67
  :root {
66
- /* Обычные карточки и панели в состоянии покоя */
67
- --shadow-lift: 0 4px 16px -2px rgba(31, 27, 22, 0.06);
68
+ --shadow-lift: 0 4px 16px -2px rgba(31, 27, 22, 0.06); /* resting cards */
69
+ --shadow-hover: 0 12px 32px -4px rgba(31, 27, 22, 0.12); /* hover, open items */
70
+ --shadow-modal: 0 24px 64px -8px rgba(18, 16, 13, 0.24); /* modal dialogs */
71
+ }
72
+ ```
68
73
 
69
- /* Приподнятые карточки при ховере или раскрытые элементы */
70
- --shadow-hover: 0 12px 32px -4px rgba(31, 27, 22, 0.12);
74
+ Use these three. Do not write ad-hoc `box-shadow` values in components.
71
75
 
72
- /* Всплывающие модальные окна */
73
- --shadow-modal: 0 24px 64px -8px rgba(18, 16, 13, 0.24);
76
+ ---
77
+
78
+ ## 4. Radius
79
+
80
+ ```css
81
+ :root {
82
+ --radius-sm: 0.375rem; /* 6 px — badges, chips */
83
+ --radius-md: 0.75rem; /* 12 px — cards, inputs */
84
+ --radius-lg: 1.25rem; /* 20 px — large section panels */
85
+ --radius-full: 9999px; /* pills, round buttons */
74
86
  }
75
87
  ```
76
88
 
77
89
  ---
78
90
 
79
- ## 4. Скругления (Border Radius)
91
+ ## 5. Breakpoints
80
92
 
81
93
  ```css
82
94
  :root {
83
- --radius-sm: 0.375rem; /* 6px — бейджи, кнопки-чипы */
84
- --radius-md: 0.75rem; /* 12px — карточки, инпуты */
85
- --radius-lg: 1.25rem; /* 20px — большие секционные плашки */
86
- --radius-full: 9999px; /* Таблетки, круглые кнопки */
95
+ --bp-sm: 480px; /* one column below this */
96
+ --bp-md: 800px; /* tablet-portrait boundary */
97
+ --bp-lg: 900px; /* two-column grids collapse here */
98
+ --bp-xl: 1200px; /* desktop layout */
87
99
  }
88
100
  ```
101
+
102
+ Media queries use these values. Components prefer container queries.
@@ -1,71 +1,62 @@
1
- # 03-Performance · Smart Assets Pipeline (Tight-Trim & Responsive Media)
1
+ # 03 · Assets pipeline: tight trim and responsive images
2
2
 
3
- > Стандарт подготовки и вставки графики. Исключает вставку «сырых» тяжелых файлов и гарантирует идеальный LCP.
3
+ Images are prepared before they reach the code. Raw exports make pages heavy and products small.
4
4
 
5
5
  ---
6
6
 
7
- ## 1. Правило обрезки избыточных полей (Tight-Trim)
7
+ ## 1. Tight trim
8
8
 
9
- Частая ошибка: дизайнер экспортирует PNG/WebP карточки товара или оборудования на холсте 1000×1000 px, где 60% площади — это прозрачные пустые поля вокруг предмета.
9
+ Typical problem: a product PNG exported on a 1000 × 1000 canvas where most pixels are empty transparent margin.
10
10
 
11
- ### Почему это плохо:
12
- * Файл весит лишние байты из-за пустых пикселей;
13
- * Картинку невозможно ровно отцентрировать по сетке;
14
- * На мобильных экранах сам товар становится крошечным из-за огромных невидимых полей.
11
+ Costs: extra bytes, off-centre objects in grids, and tiny products on phones.
15
12
 
16
- ### Стандарт DS7 (Tight-Trim):
17
- 1. **Кадрирование по контуру:** Все прозрачные поля вокруг объекта обрезаются вплотную (если вокруг нет реальной рассеянной тени).
18
- 2. **Прозрачный фон:** Изображения оборудования (панели, инверторы, зарядки) сохраняются на прозрачном фоне, чтобы легко ложиться на светлые, темные и цветные подложки.
19
- 3. **Стабильное соотношение сторон:** В коде карточки задается контейнер со стабильным `aspect-ratio` (например, `aspect-ratio: 4/3` или `1/1`), а сама картинка центрируется через `object-fit: contain`.
13
+ Rules:
14
+ 1. **Crop to the object** along its outline, keeping a real soft shadow if the design has one.
15
+ 2. **Keep transparency** for product cut-outs so they sit on light, dark and coloured backgrounds.
16
+ 3. **Fix the box.** The container has a stable `aspect-ratio` (`4/3` or `1/1`); the image is centred with `object-fit: contain`.
20
17
 
21
18
  ---
22
19
 
23
- ## 2. Адаптивная вставка через `<picture>` и `srcset`
20
+ ## 2. Responsive images
24
21
 
25
- Каждое растровое изображение подготавливается под конкретные размеры экрана:
22
+ Every raster image ships in several sizes. The reference component is `UiImg`: it takes a `src` string, builds a `srcset` from the generated variants, and writes `width`, `height` and lazy-loading attributes.
23
+
24
+ Generated sizes (default): **400, 800 and 1200 px wide**, WebP, quality 82. Images smaller than 900 px are not resized up.
26
25
 
27
26
  ```html
28
- <picture>
29
- <!-- 1. Мобильные устройства (ширина до 640px): компактный легкий файл -->
30
- <source
31
- media="(max-width: 640px)"
32
- srcset="/images/p/hero-mobile.webp 1x, /images/p/hero-mobile@2x.webp 2x"
33
- type="image/webp"
34
- >
35
- <!-- 2. Десктопные экраны: полноразмерный файл с поддержкой Retina -->
36
- <source
37
- srcset="/images/p/hero.webp 1x, /images/p/hero@2x.webp 2x"
38
- type="image/webp"
39
- >
40
- <!-- 3. Фоллбэк img с явными габаритами для нулевого CLS -->
41
- <img
42
- src="/images/p/hero.webp"
43
- alt="Instalación solar fotovoltaica en Sevilla"
44
- width="1600"
45
- height="900"
46
- fetchpriority="high"
47
- loading="eager"
48
- decoding="async"
49
- >
50
- </picture>
27
+ <img
28
+ src="/images/p/hero-1200.webp"
29
+ srcset="/images/p/hero-400.webp 400w, /images/p/hero-800.webp 800w, /images/p/hero-1200.webp 1200w"
30
+ sizes="(max-width: 800px) 100vw, 60vw"
31
+ width="1200" height="900"
32
+ alt="Solar installation on a rooftop"
33
+ fetchpriority="high" loading="eager" decoding="async"
34
+ >
51
35
  ```
52
36
 
37
+ Use `UiImg` rather than writing this by hand in every page.
38
+
53
39
  ---
54
40
 
55
- ## 3. Правило `fetchpriority` и `loading`:
41
+ ## 3. Loading priorities
56
42
 
57
- | Расположение изображения | `loading` | `fetchpriority` | `decoding` | Назначение |
58
- |---|---|---|---|---|
59
- | **Главный баннер Hero (LCP)** | `eager` | `high` | `async` | Мгновенно загружается первым |
60
- | **Карточки товаров (2-й экран)** | `lazy` | `auto` | `async` | Загружается перед появлением в поле зрения |
61
- | **Нижние секции, отзывы, футер** | `lazy` | `low` | `async` | Загружается только при доскролле |
43
+ | Position | `loading` | `fetchpriority` | `decoding` |
44
+ |---|---|---|---|
45
+ | The LCP image above the fold | `eager` | `high` | `async` |
46
+ | Cards below the fold | `lazy` | `auto` | `async` |
47
+ | Footer and late sections | `lazy` | `low` | `async` |
48
+
49
+ Only one image is the LCP image on a page. Mark it, do not mark several.
62
50
 
63
51
  ---
64
52
 
65
- ## 4. Скрипт автоматической подготовки ассетов
53
+ ## 4. Asset script
54
+
55
+ Keep a small script in `tools/` (Sharp or Python) that:
56
+ - takes new images from a source folder;
57
+ - trims transparent margins;
58
+ - writes WebP (and AVIF where useful) at 400, 800 and 1200 px;
59
+ - writes `app/data/image-variants.json`, the manifest `UiImg` reads;
60
+ - has a `--check` mode that fails when a referenced image has no manifest entry.
66
61
 
67
- В папке `tools/` рекомендуется держать легковесный скрипт (Python / Sharp), который:
68
- * Принимает новые картинки;
69
- * Обрезает прозрачные поля (`trim`);
70
- * Конвертирует в WebP и AVIF с качеством 82%;
71
- * Генерирует мобильные копии (`-400.webp`) и ретина-копии (`@2x.webp`).
62
+ Commit the manifest with the images it describes.
@@ -1,46 +1,39 @@
1
- # 03-Performance · Critical CSS & Zero Render-Blocking
1
+ # 03 · Critical CSS and render-blocking
2
2
 
3
- > Как гарантировать First Contentful Paint (FCP) менее 0.4 секунды и 100 баллов в Google Lighthouse по категории Performance.
3
+ A stylesheet in `<head>` blocks rendering until it has downloaded and parsed. On a slow mobile network a 50–150 KB stylesheet delays the first paint noticeably.
4
4
 
5
5
  ---
6
6
 
7
- ## 1. В чём проблема стандартного CSS
7
+ ## 1. Split
8
8
 
9
- Когда браузер загружает обычную веб-страницу:
10
- 1. Скачивается `index.html`;
11
- 2. Браузер находит в `<head>` тег `<link rel="stylesheet" href="/_nuxt/entry.css">`;
12
- 3. **Рендеринг страницы полностью блокируется (белый экран)**, пока файл `entry.css` (часто 50–150 КБ) скачивается по мобильной сети и парсится;
13
- 4. Пользователь видит контент только через 1.5–3.0 секунды.
9
+ - **Above the fold:** header, page grid, typography and the first screen. Needed for the first paint.
10
+ - **Below the fold:** calculators, reviews, map, footer, modals. Needed later.
14
11
 
15
12
  ---
16
13
 
17
- ## 2. Архитектура Critical CSS в Nuxt 4
18
-
19
- Critical CSS разделяет стили сайта на две части:
20
- 1. **Above-the-fold (Критический CSS):** Стили шапки, сетки контейнера, типографики и фонового блока первого экрана (Hero).
21
- 2. **Below-the-fold (Вторичный CSS):** Стили калькулятора, отзывов, карты, футера, модальных окон.
22
-
23
- ### Настройка в `nuxt.config.ts`:
14
+ ## 2. Nuxt configuration
24
15
 
25
16
  ```ts
26
17
  export default defineNuxtConfig({
27
18
  experimental: {
28
- // Автоматическое извлечение и встраивание критических стилей в HTML
29
- inlineSSRStyles: true
30
- }
19
+ inlineSSRStyles: true, // inline the CSS each page uses during generation
20
+ },
31
21
  })
32
22
  ```
33
23
 
34
- ### Как это работает:
35
- При статической генерации (`npx nuxt generate`):
36
- * Nuxt парсит HTML каждой страницы и определяет классы, нужные для первого экрана;
37
- * Эти правила вставляются прямо внутрь `<style>` в `<head>` документа `index.html`;
38
- * Полная таблица стилей подключается асинхронно: `<link rel="preload" as="style">`.
24
+ During `nuxt generate`:
25
+ - each page is rendered and the classes it uses are collected;
26
+ - those rules are inlined in `<style>` in the page `<head>`;
27
+ - the full stylesheet is loaded afterwards with `rel="preload"`.
28
+
29
+ ---
30
+
31
+ ## 3. What to measure
32
+
33
+ Record First Contentful Paint and render-blocking requests on a real phone, with the build and date, in `docs/METRICS.md`. Lab numbers from a sandbox are not evidence for this.
39
34
 
40
35
  ---
41
36
 
42
- ## 3. Результат для пользователя и Google Lighthouse
37
+ ## 4. Budget
43
38
 
44
- * Браузер получает `index.html` и **в ту же миллисекунду рисует идеальный первый экран**, не делая ни одного дополнительного сетевого запроса за CSS.
45
- * **FCP (First Contentful Paint):** снижается с 1.8s до **0.35s**.
46
- * **Speed Index:** зеленая зона (98–100 баллов).
39
+ Keep the entry stylesheet small enough to inline without delaying HTML. Set a budget in `docs/METRICS.md` and check it when the stylesheet changes.
@@ -1,43 +1,39 @@
1
- # 03-Performance · Fonts & Zero Layout Shift (CLS = 0.000)
1
+ # 03 · Fonts and zero layout shift
2
2
 
3
- > Как исключить «прыжки» текста при загрузке шрифтов и свести показатель Cumulative Layout Shift (CLS) к абсолютному нулю.
3
+ Text that jumps when a web font arrives moves the whole page and raises CLS. This document describes how DS7 avoids that.
4
4
 
5
5
  ---
6
6
 
7
- ## 1. Анатомия проблемы FOUT и прыжков текста
7
+ ## 1. The problem
8
8
 
9
- Когда веб-шрифт Montserrat подгружается через `@font-face`:
10
- 1. Браузер сначала рисует текст стандартным системным шрифтом (Arial или Helvetica);
11
- 2. У системного шрифта другая ширина букв и высота строки (x-height);
12
- 3. Через секунду подгружается `montserrat.woff2`, шрифт переключается, строки переносятся на новое место, и **вся страница дергается вниз на 20–40 пикселей**.
13
- 4. Пользователь случайно кликает не туда, а Google фиксирует плохой показатель CLS (> 0.15).
9
+ 1. The browser draws text with a system font first (Arial or Helvetica).
10
+ 2. The system font has different glyph widths and line heights from the brand font.
11
+ 3. When the brand font arrives, lines re-wrap and content moves, often 20–40 px.
14
12
 
15
13
  ---
16
14
 
17
- ## 2. Комплексное решение проблемы в DS7
15
+ ## 2. The approach
18
16
 
19
- ### Шаг 1: Прямой Preload в `<head>`
20
- Не ждем, пока CSS файл будет прочитан. Браузер начинает качать шрифт с первой миллисекунды:
17
+ ### Step 1: preload one weight
18
+ Preload the weight used on the first screen, as `woff2`, from the document `<head>`:
21
19
 
22
20
  ```ts
23
- // nuxt.config.ts -> app.head
24
- link: [
25
- {
26
- rel: 'preload',
27
- href: '/fonts/montserrat-latin-var.woff2',
28
- as: 'font',
29
- type: 'font/woff2',
30
- crossorigin: 'anonymous'
31
- }
32
- ]
21
+ // nuxt.config.ts → app.head.link
22
+ {
23
+ rel: 'preload',
24
+ href: '/fonts/brand-latin-var.woff2',
25
+ as: 'font',
26
+ type: 'font/woff2',
27
+ crossorigin: 'anonymous',
28
+ }
33
29
  ```
34
30
 
35
- ### Шаг 2: Модуль `@nuxtjs/fontaine` (Синхронизация пропорций)
36
- Fontaine анализирует геометрию шрифта Montserrat и создает локальный псевдо-шрифт:
31
+ ### Step 2: matched fallback
32
+ A fallback `@font-face` built from a system font, with metrics adjusted to the brand font:
37
33
 
38
34
  ```css
39
35
  @font-face {
40
- font-family: 'Montserrat-Fallback';
36
+ font-family: 'Brand Fallback';
41
37
  src: local('Arial');
42
38
  ascent-override: 95.8%;
43
39
  descent-override: 28.2%;
@@ -46,18 +42,18 @@ Fontaine анализирует геометрию шрифта Montserrat и с
46
42
  }
47
43
  ```
48
44
 
49
- **Магия Fontaine:** Буквы локального системного шрифта искусственно сжимаются и растягиваются так, чтобы **пиксель-в-пиксель занимать то же пространство**, что и Montserrat.
50
- Когда реальный шрифт скачивается, форма букв меняется, но **ни одна строчка на странице не сдвигается**.
45
+ The values come from the brand font's metrics (the `@nuxtjs/fontaine` module computes them). When the brand font loads, glyphs have the same advance widths and line boxes, so text does not move.
51
46
 
52
- ### Шаг 3: `font-display: swap`
53
- Шрифт отображается мгновенно, экран не ждет загрузки (нет белого текста FOIT).
47
+ ### Step 3: `font-display: swap`
48
+ Text appears at once with the fallback and swaps when the brand font is ready. No invisible text.
54
49
 
55
50
  ---
56
51
 
57
- ## 3. Чек-лист проверки CLS = 0.000
52
+ ## 3. Checklist
58
53
 
59
- 1. [x] Preload для основного начертания шрифта (`.woff2`).
60
- 2. [x] Настроен fallback через Fontaine (`size-adjust`).
61
- 3. [x] У всех логотипов и SVG-иконок жестко заданы атрибуты `width` и `height`.
62
- 4. [x] У всех карточек и медиа-блоков зафиксирован CSS `aspect-ratio`.
63
- 5. [x] Плавающие панели (`SiteIntro`, куки-баннер) позиционируются через `position: fixed` и не выталкивают контент.
54
+ - [ ] One preloaded weight for the first screen.
55
+ - [ ] Fallback with `size-adjust` and the override metrics.
56
+ - [ ] Every logo and inline SVG has `width` and `height`.
57
+ - [ ] Every card and media box has a fixed `aspect-ratio`.
58
+ - [ ] Fixed-position panels (cookie banner, intro) do not push content.
59
+ - [ ] Measured CLS recorded in `docs/METRICS.md` on a real device.
@@ -1,51 +1,68 @@
1
- # 03-Performance · Hydration Strategy & TBT Minimization
1
+ # 03 · Hydration strategy
2
2
 
3
- > Как избавиться от зависаний интерфейса (Total Blocking Time < 50ms) при первом входе на страницу.
3
+ Static HTML appears first. Then the framework attaches event handlers to each component (hydration). On a phone with many interactive components this can block the main thread for hundreds of milliseconds, and taps are ignored during that time.
4
4
 
5
5
  ---
6
6
 
7
- ## 1. В чём опасность полной гидратации
7
+ ## 1. Principle
8
8
 
9
- Nuxt генерирует чистый статический HTML. Браузер быстро его отображает, но затем запускается процесс **гидратации (Hydration)**:
10
- * Vue начинает «оживлять» каждый компонент на странице, вешать слушатели событий и инициализировать реактивные объекты;
11
- * Если на странице 15 секций (калькулятор, карта проектов, чат, модалки, слайдеры), этот процесс может занять **250–500 мс** на среднем мобильном процессоре;
12
- * В это время страница «висит» — пользователь тыкает по меню или кнопке, а клик не срабатывает.
9
+ Hydrate only what the visitor needs now.
13
10
 
14
- ---
11
+ - **Critical:** navigation, the first interactive element, forms the visitor sees.
12
+ - **Idle:** chat widgets, cookie banner, sound controls, modals that are not open.
13
+ - **On visibility:** heavy sections below the fold (maps, charts, interactive calculators).
15
14
 
16
- ## 2. Стратегия отложенной гидратации в DS7
15
+ ---
17
16
 
18
- ### 1. Отложенные второстепенные виджеты (`requestIdleCallback`)
19
- Виджеты, которые не нужны в первую секунду (чат WhatsApp, куки-баннер, модалка звонка, звуковые эффекты), гидрируются только тогда, когда процессор свободен:
17
+ ## 2. Idle widgets
20
18
 
21
19
  ```vue
22
20
  <script setup lang="ts">
23
- const shouldHydrateIdle = ref(false)
21
+ const ready = ref(false)
24
22
 
25
23
  onMounted(() => {
26
24
  if ('requestIdleCallback' in window) {
27
- window.requestIdleCallback(() => { shouldHydrateIdle.value = true })
25
+ window.requestIdleCallback(() => { ready.value = true })
28
26
  } else {
29
- setTimeout(() => { shouldHydrateIdle.value = true }, 1500)
27
+ setTimeout(() => { ready.value = true }, 1500)
30
28
  }
31
29
  })
32
30
  </script>
33
31
 
34
32
  <template>
35
33
  <ClientOnly>
36
- <SiteChat v-if="shouldHydrateIdle" />
37
- <SiteCookieBanner v-if="shouldHydrateIdle" />
34
+ <SiteChat v-if="ready" />
35
+ <SiteCookieBanner v-if="ready" />
38
36
  </ClientOnly>
37
+ }
39
38
  </template>
40
39
  ```
41
40
 
42
- ### 2. Ленивая гидратация по скроллу (`when-visible`)
43
- Тяжелые интерактивные секции ниже первого экрана (интерактивная карта проектов `HomeProjectMap`, модель дня и ночи `HomeDayNight`) гидрируются только тогда, когда пользователь приближается к ним при прокрутке.
41
+ Note for the banner: it must not hide content while it waits. Reserve its space with a fixed position.
42
+
43
+ ---
44
+
45
+ ## 3. Visibility
46
+
47
+ Heavy sections use lazy components that hydrate when they approach the viewport:
48
+
49
+ ```vue
50
+ <LazyHomeProjectMap hydrate-on-visible />
51
+ ```
52
+
53
+ ---
54
+
55
+ ## 4. Passive listeners
56
+
57
+ Scroll and touch listeners that never call `preventDefault` are registered as passive, so the browser can scroll without waiting for the script:
44
58
 
45
- ### 3. Пассивные слушатели событий (`passive: true`)
46
- В компоненте `UiCardSlider` и любых событиях скролла/тача:
47
59
  ```ts
48
60
  window.addEventListener('scroll', onScroll, { passive: true })
49
61
  track.addEventListener('touchstart', onTouchStart, { passive: true })
50
62
  ```
51
- Браузер заранее знает, что скрипт не отменит стандартную прокрутку страницы (`preventDefault`), и скроллит страницу с максимальной частотой 120 FPS без микро-фризов.
63
+
64
+ ---
65
+
66
+ ## 5. Measuring
67
+
68
+ Total Blocking Time and Interaction to Next Paint are measured on a real device, not in a sandbox. Record the results in `docs/METRICS.md`.