@ai.all/ds7 0.0.0-stage → 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.
@@ -0,0 +1,68 @@
1
+ # 01 · Atomic Design and component levels
2
+
3
+ Components are organised in five levels, after [Brad Frost's Atomic Design](https://atomicdesign.bradfrost.com/chapter-2/). Each level depends only on the levels below it. The goal is that a block works anywhere: on a page, in a sidebar, in a modal, or nested inside another block.
4
+
5
+ ---
6
+
7
+ ## 1. Levels
8
+
9
+ ```
10
+ [1. Atoms] indivisible parts: button, icon, badge, field, price
11
+ │
12
+ [2. Molecules] small groups of atoms: product card, plan card, slider controls
13
+ │
14
+ [3. Organisms] self-contained widgets: card slider, feature grid, FAQ, calculator
15
+ │
16
+ [4. Templates] section layouts that compose organisms
17
+ │
18
+ [5. Pages] routes: fetch data, set SEO, pass props
19
+ ```
20
+
21
+ ---
22
+
23
+ ## 2. Rules per level
24
+
25
+ ### Atoms — `app/components/ui/`
26
+ - Examples: `UiButton`, `UiIcon`, `UiPrice`, `UiBadge`, `UiField`, `UiLink`.
27
+ - Know nothing about pages, products or application state.
28
+ - Styled with a base class and short modifiers: `.btn.-sun`, `.btn.-sm`, `.btn.-outline`.
29
+
30
+ ### Molecules — `app/components/ui/`
31
+ - Examples: `UiProductCard`, `UiSectionHeading`, `UiFeatureCard`, `UiPlanCard`, `UiSliderNav`.
32
+ - Built only from atoms and plain markup. Typed props and slots. No side effects.
33
+ - Must look right in a four-column grid and in a narrow sidebar. Use container queries.
34
+
35
+ ### Organisms — `app/components/ui/`, `sections/`, `site/`
36
+ - Examples: `UiCardSlider`, `UiFeatureGrid`, `UiFaq`, `CalcPanel`, `SiteHeader`, `SiteFooter`.
37
+ - Own their local state: scroll position, active tab, open accordion item.
38
+ - Work through slots. A slider does not know that it shows products; it shows whatever molecules it receives.
39
+
40
+ ### Templates and sections — `app/components/sections/`
41
+ - Compose organisms into a complete block of a page.
42
+ - No URL-specific logic and no hard-coded page content.
43
+
44
+ ### Pages — `app/pages/`
45
+ Pages do three things only:
46
+ 1. Call composables that read data from `app/data/`.
47
+ 2. Set page metadata (`usePageSeo`).
48
+ 3. Pass data to sections as props.
49
+
50
+ Pages do not contain inline card markup, custom sliders or bespoke grids.
51
+
52
+ ---
53
+
54
+ ## 3. Embedding rule
55
+
56
+ Every component must be placeable:
57
+ 1. in normal page flow;
58
+ 2. inside a modal;
59
+ 3. inside a tab or a calculator panel;
60
+ 4. inside a narrow sidebar or a drawer.
61
+
62
+ Size comes from the parent box (`@container`), not from the screen (`@media`).
63
+
64
+ ---
65
+
66
+ ## 4. Brand first
67
+
68
+ Levels are built top-down from the brandbook. Atoms come from tokens and the sandbox entries in `02-design-system/BRANDBOOK-SANDBOX.md`. A component that exists only in a page is not finished until it is in the sandbox.
@@ -0,0 +1,101 @@
1
+ # 01 · Component anatomy and container queries
2
+
3
+ The reference shape of a Vue 3 single-file component in DS7. The template file is in `templates/component.vue`.
4
+
5
+ ---
6
+
7
+ ## 1. Shape
8
+
9
+ ```vue
10
+ <script setup lang="ts">
11
+ /**
12
+ * UiProductCard — product card molecule.
13
+ * Container-aware. Modifiers are separate classes.
14
+ */
15
+
16
+ const props = withDefaults(defineProps<{
17
+ title: string
18
+ description?: string
19
+ price?: number | null
20
+ badge?: 'top' | 'new' | 'sale'
21
+ specs?: Array<{ label: string; value: string }>
22
+ top?: boolean
23
+ compact?: boolean
24
+ dark?: boolean
25
+ }>(), {
26
+ description: '',
27
+ price: null,
28
+ specs: () => [],
29
+ top: false,
30
+ compact: false,
31
+ dark: false,
32
+ })
33
+
34
+ const rootClasses = computed(() => [
35
+ 'p-card',
36
+ props.top && '-top',
37
+ props.compact && '-compact',
38
+ props.dark && '-dark',
39
+ props.badge && `-${props.badge}`,
40
+ ])
41
+ </script>
42
+
43
+ <template>
44
+ <article :class="rootClasses" data-ref="CARD-01">
45
+ <div class="p-card__art">
46
+ <slot name="art" />
47
+ </div>
48
+
49
+ <div class="p-card__body">
50
+ <h3 class="p-card__title">{{ title }}</h3>
51
+ <p v-if="description" class="p-card__desc">{{ description }}</p>
52
+ <dl v-if="specs.length" class="p-card__specs">
53
+ <div v-for="s in specs" :key="s.label">
54
+ <dt>{{ s.label }}</dt>
55
+ <dd>{{ s.value }}</dd>
56
+ </div>
57
+ </dl>
58
+ </div>
59
+
60
+ <div class="p-card__footer">
61
+ <UiPrice v-if="price !== null" :amount="price" />
62
+ <slot name="action" />
63
+ </div>
64
+ </article>
65
+ </template>
66
+
67
+ <style>
68
+ .p-card {
69
+ --card-bg: var(--color-white);
70
+ --card-ink: var(--color-ink);
71
+
72
+ container-type: inline-size;
73
+ display: flex;
74
+ flex-direction: column;
75
+ background: var(--card-bg);
76
+ color: var(--card-ink);
77
+ border-radius: var(--radius-md);
78
+ padding: var(--space-4);
79
+ }
80
+
81
+ @container (max-width: 320px) {
82
+ .p-card {
83
+ padding: var(--space-2);
84
+ }
85
+ .p-card__title {
86
+ font-size: 0.9rem;
87
+ }
88
+ }
89
+ </style>
90
+ ```
91
+
92
+ ---
93
+
94
+ ## 2. Rules
95
+
96
+ 1. **Typed props.** Booleans for modifiers keep templates readable; `rootClasses` turns them into classes.
97
+ 2. **`container-type: inline-size`** on any component with responsive states. It reacts to its box, not to the screen.
98
+ 3. **Component-level custom properties** (`--card-bg`, `--card-ink`). Modifiers change the variables, not ten child selectors.
99
+ 4. **Stable ID** in `data-ref` (`CARD-01`). It links the live component to its entry in the brandbook.
100
+ 5. **Slots with fallbacks** for media and actions. The component works with any content in those slots.
101
+ 6. **A one-line summary** at the top of the file, as in the example.
@@ -0,0 +1,90 @@
1
+ # 01 · Modifier system and ancestry scoping
2
+
3
+ One base component, many variants, no duplicate components.
4
+
5
+ ---
6
+
7
+ ## 1. Syntax
8
+
9
+ Use a base class plus separate modifier classes. Each modifier starts with a dash:
10
+
11
+ ```html
12
+ <article class="p-card -top -new -compact">
13
+ <div class="p-card__art">…</div>
14
+ <h3 class="p-card__title">Panel 450 W</h3>
15
+ <div class="p-card__price">…</div>
16
+ </article>
17
+ ```
18
+
19
+ Why:
20
+ - **Short class names** keep HTML and CSS small.
21
+ - **Modifiers combine** freely: `-top -compact`, `-dark -lg`.
22
+ - **Modifiers cascade:** a modifier on the root can change every child.
23
+
24
+ Avoid long compound names such as `catalog-product-card--featured-large-dark`.
25
+
26
+ ---
27
+
28
+ ## 2. Cascade from a modifier
29
+
30
+ ```css
31
+ .p-card {
32
+ --card-bg: var(--color-white);
33
+ --card-text: var(--color-ink);
34
+ --card-border: var(--color-ink-100);
35
+
36
+ background: var(--card-bg);
37
+ color: var(--card-text);
38
+ border: 1px solid var(--card-border);
39
+ border-radius: var(--radius-md);
40
+ padding: var(--space-4);
41
+ }
42
+
43
+ .p-card.-top {
44
+ --card-border: var(--color-sun);
45
+ box-shadow: var(--shadow-lift);
46
+ }
47
+ .p-card.-top .p-card__title {
48
+ font-weight: 700;
49
+ }
50
+
51
+ .p-card.-compact {
52
+ padding: var(--space-2);
53
+ }
54
+
55
+ .p-card.-dark {
56
+ --card-bg: var(--color-ink-950);
57
+ --card-text: var(--color-white);
58
+ --card-border: rgba(255, 255, 255, 0.12);
59
+ }
60
+ ```
61
+
62
+ Modifiers change custom properties where possible. That keeps the number of selectors small.
63
+
64
+ ---
65
+
66
+ ## 3. Context from an ancestor
67
+
68
+ Sometimes a shared component must look different on one page. Do not create a second component. Scope the difference by an ancestor:
69
+
70
+ ```css
71
+ /* the hook everywhere */
72
+ .hero-hook {
73
+ background: var(--color-paper);
74
+ color: var(--color-ink);
75
+ }
76
+
77
+ /* the same hook on a dark page */
78
+ body.page-business .hero-hook {
79
+ background: var(--color-ink-950);
80
+ color: var(--color-white);
81
+ }
82
+
83
+ /* the same hook inside a modal */
84
+ .site-modal-box .hero-hook {
85
+ background: transparent;
86
+ padding: 0;
87
+ }
88
+ ```
89
+
90
+ The page class on `<body>` is set by `usePageSeo` (`page-home`, `page-residential`, `page-business`, …). Wrong: `HookHome.vue` and `HookBusiness.vue`. Right: one `UiHeroHook` with context rules.
@@ -0,0 +1,52 @@
1
+ # 02 · Brandbook and sandbox (`/ui`)
2
+
3
+ The `/ui` page of a project is a live sandbox: every foundation token and every base component, shown in its real code. It is the shared vocabulary between people and agents.
4
+
5
+ ---
6
+
7
+ ## 1. Stable component IDs
8
+
9
+ Every reusable pattern has one short ID:
10
+
11
+ | ID | Name | Use |
12
+ |---|---|---|
13
+ | `BTN-01` | Primary button | main action; sun fill with an arrow |
14
+ | `BTN-02` | Outline button | secondary action |
15
+ | `CTL-01` | Segmented switch | tabs or periods |
16
+ | `COUNTER-01` | Bounded numeric counter | number input with − / + buttons; variant `-inner` |
17
+ | `PRICE-01` | Formatted price | amount with currency suffix and optional "from" label |
18
+ | `CARD-01` | Base product card | catalogue item with modifiers |
19
+ | `CARD-02` | Plan / tariff card | package with a list of options |
20
+ | `SLD-01` | Card slider | scroll-snap slider with navigation |
21
+ | `HOOK-01` | Overlap hook | panel that overlaps a cover image |
22
+
23
+ **How it is used.** Instead of describing a card in full, say:
24
+
25
+ > "Put `CARD-01 -top` in this block, with `BTN-01` as the action."
26
+
27
+ Both a person and an agent understand that without a second description.
28
+
29
+ ---
30
+
31
+ ## 2. What the sandbox shows
32
+
33
+ 1. **Modifier toggles** for each component: `-top`, `-new`, `-compact`, `-dark`.
34
+ 2. **Background switch:** light, dark, inside a modal. Contrast is visible on each.
35
+ 3. **Copy code** next to each example: the reference snippet for pasting into a project.
36
+
37
+ ---
38
+
39
+ ## 3. Update rule
40
+
41
+ Every new atom, molecule or token must appear in the sandbox before it is used on a page. A component that is missing from `/ui` is not finished. The sandbox is excluded from search indexing (`noindex, nofollow`) and from `sitemap.xml`.
42
+
43
+ ---
44
+
45
+ ## 4. Brand-first workflow
46
+
47
+ 1. Add or change foundations in `theme.css`.
48
+ 2. Add or change the entry in the sandbox.
49
+ 3. Build or change the component.
50
+ 4. Use the component on a page.
51
+
52
+ Changing a token changes the sandbox first; the sandbox is the visual reference for review.
@@ -0,0 +1,45 @@
1
+ # 02 · Naming conventions
2
+
3
+ Short, semantic class names keep HTML small and make CSS readable. The formula and the rules are the same for every project.
4
+
5
+ ---
6
+
7
+ ## 1. Examples
8
+
9
+ | Long name | DS7 name |
10
+ |---|---|
11
+ | `.catalog-product-card` | `.p-card` |
12
+ | `.catalog-product-card--featured` | `.p-card.-top` |
13
+ | `.catalog-product-card__description` | `.p-card__desc` |
14
+ | `.section-heading-eyebrow` | `.s-head__eye` |
15
+ | `.overlap-panel` | `.panel.-overlap` |
16
+ | `.hook-bill-calculator` | `.hook.-bill` |
17
+ | `.button-variant-primary-large` | `.btn.-sun.-lg` |
18
+
19
+ ---
20
+
21
+ ## 2. Formula
22
+
23
+ ```
24
+ .[component] base name, short kebab-case
25
+ .[component]__[part] element, double underscore
26
+ .-[modifier] modifier, separate class with a leading dash
27
+ .is-[state] state
28
+ ```
29
+
30
+ ---
31
+
32
+ ## 3. Rules
33
+
34
+ 1. **No word appended to a base name.** `.btn-lg`, `.card-big`, `.hero-active` are wrong. Write `.btn.-lg`, `.card.-big`, `.hero.is-active`.
35
+ 2. **Size and colour words are modifiers.** `-sm`, `-lg`, `-sun`, `-dark`. Not `-big`, `-red`, `-orange`.
36
+ 3. **Element names describe the part.** `__title`, `__price`, `__art`, not `__text2` or `__left`.
37
+ 4. **Only tokens in values.** Naming does not replace the token rule in `TOKENS-SPEC.md`.
38
+
39
+ ---
40
+
41
+ ## 4. Build-time minification
42
+
43
+ - Unused styles are removed when the site is generated. Only classes that appear in the output ship.
44
+ - The CSS minifier removes comments and shortens values (`#ffffff` → `#fff`).
45
+ - Critical CSS is inlined; the rest is a cached file with a content hash in its name.
@@ -0,0 +1,102 @@
1
+ # 02 · Design tokens
2
+
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`.
6
+
7
+ ---
8
+
9
+ ## 1. Colour
10
+
11
+ ```css
12
+ :root {
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;
29
+ }
30
+ ```
31
+
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.
35
+
36
+ ---
37
+
38
+ ## 2. Spacing
39
+
40
+ ```css
41
+ :root {
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 */
58
+ --page-gutter: clamp(1rem, 3.5vw, 2rem);
59
+ }
60
+ ```
61
+
62
+ ---
63
+
64
+ ## 3. Elevation
65
+
66
+ ```css
67
+ :root {
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
+ ```
73
+
74
+ Use these three. Do not write ad-hoc `box-shadow` values in components.
75
+
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 */
86
+ }
87
+ ```
88
+
89
+ ---
90
+
91
+ ## 5. Breakpoints
92
+
93
+ ```css
94
+ :root {
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 */
99
+ }
100
+ ```
101
+
102
+ Media queries use these values. Components prefer container queries.
@@ -0,0 +1,62 @@
1
+ # 03 · Assets pipeline: tight trim and responsive images
2
+
3
+ Images are prepared before they reach the code. Raw exports make pages heavy and products small.
4
+
5
+ ---
6
+
7
+ ## 1. Tight trim
8
+
9
+ Typical problem: a product PNG exported on a 1000 × 1000 canvas where most pixels are empty transparent margin.
10
+
11
+ Costs: extra bytes, off-centre objects in grids, and tiny products on phones.
12
+
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`.
17
+
18
+ ---
19
+
20
+ ## 2. Responsive images
21
+
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.
25
+
26
+ ```html
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
+ >
35
+ ```
36
+
37
+ Use `UiImg` rather than writing this by hand in every page.
38
+
39
+ ---
40
+
41
+ ## 3. Loading priorities
42
+
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.
50
+
51
+ ---
52
+
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.
61
+
62
+ Commit the manifest with the images it describes.
@@ -0,0 +1,39 @@
1
+ # 03 · Critical CSS and render-blocking
2
+
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
+
5
+ ---
6
+
7
+ ## 1. Split
8
+
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.
11
+
12
+ ---
13
+
14
+ ## 2. Nuxt configuration
15
+
16
+ ```ts
17
+ export default defineNuxtConfig({
18
+ experimental: {
19
+ inlineSSRStyles: true, // inline the CSS each page uses during generation
20
+ },
21
+ })
22
+ ```
23
+
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.
34
+
35
+ ---
36
+
37
+ ## 4. Budget
38
+
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.
@@ -0,0 +1,59 @@
1
+ # 03 · Fonts and zero layout shift
2
+
3
+ Text that jumps when a web font arrives moves the whole page and raises CLS. This document describes how DS7 avoids that.
4
+
5
+ ---
6
+
7
+ ## 1. The problem
8
+
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.
12
+
13
+ ---
14
+
15
+ ## 2. The approach
16
+
17
+ ### Step 1: preload one weight
18
+ Preload the weight used on the first screen, as `woff2`, from the document `<head>`:
19
+
20
+ ```ts
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
+ }
29
+ ```
30
+
31
+ ### Step 2: matched fallback
32
+ A fallback `@font-face` built from a system font, with metrics adjusted to the brand font:
33
+
34
+ ```css
35
+ @font-face {
36
+ font-family: 'Brand Fallback';
37
+ src: local('Arial');
38
+ ascent-override: 95.8%;
39
+ descent-override: 28.2%;
40
+ line-gap-override: 0%;
41
+ size-adjust: 101.4%;
42
+ }
43
+ ```
44
+
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.
46
+
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.
49
+
50
+ ---
51
+
52
+ ## 3. Checklist
53
+
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.
@@ -0,0 +1,68 @@
1
+ # 03 · Hydration strategy
2
+
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
+
5
+ ---
6
+
7
+ ## 1. Principle
8
+
9
+ Hydrate only what the visitor needs now.
10
+
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).
14
+
15
+ ---
16
+
17
+ ## 2. Idle widgets
18
+
19
+ ```vue
20
+ <script setup lang="ts">
21
+ const ready = ref(false)
22
+
23
+ onMounted(() => {
24
+ if ('requestIdleCallback' in window) {
25
+ window.requestIdleCallback(() => { ready.value = true })
26
+ } else {
27
+ setTimeout(() => { ready.value = true }, 1500)
28
+ }
29
+ })
30
+ </script>
31
+
32
+ <template>
33
+ <ClientOnly>
34
+ <SiteChat v-if="ready" />
35
+ <SiteCookieBanner v-if="ready" />
36
+ </ClientOnly>
37
+ }
38
+ </template>
39
+ ```
40
+
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:
58
+
59
+ ```ts
60
+ window.addEventListener('scroll', onScroll, { passive: true })
61
+ track.addEventListener('touchstart', onTouchStart, { passive: true })
62
+ ```
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`.