kmcom-nuxt-layers 1.7.8 → 1.7.9

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,94 @@
1
+ # Layout Shift Root Causes
2
+
3
+ Catalogue of layout shift (CLS) issues found and fixed in this monorepo, with root cause and resolution for each.
4
+
5
+ ---
6
+
7
+ ## 1. `Math.random()` SVG gradient ID — `Progress/Circular.vue`
8
+
9
+ **Cause:** `Math.random()` produces a different value on every call. Server and client generate different IDs for the `linearGradient`, so the `stroke="url(#...)"` reference breaks after hydration.
10
+
11
+ **Fix:** Replace with Nuxt's `useId()` which produces the same deterministic ID across server and client for a given component instance.
12
+
13
+ ---
14
+
15
+ ## 2. Locale-dependent date formatting — `Blog/Article.vue`
16
+
17
+ **Cause:** `new Date().toLocaleDateString()` with no locale argument uses the runtime's default locale. Node.js (server) and the browser may have different default locales, producing different date strings.
18
+
19
+ **Fix:** Pass an explicit locale string: `toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })`.
20
+
21
+ ---
22
+
23
+ ## 3. Year computed independently on server and client — `Mast/Footer.vue`
24
+
25
+ **Cause:** `new Date().getFullYear()` is evaluated independently on server and client. Around year boundaries (different timezones, late-night requests) the two values can differ.
26
+
27
+ **Fix:** Use `useState('footer-year', () => new Date().getFullYear())` so the year is serialised in the Nuxt payload and reused by the client without re-computing.
28
+
29
+ ---
30
+
31
+ ## 4. Feature flags all `false` on server — `DiagnosticsPage.vue`
32
+
33
+ **Cause:** `useFeatures()` detects browser capabilities which are unavailable on the server, so all flags default to `false`. The server renders `✗` for every feature; the client flips many to `true`, causing mismatches on every `v-if`.
34
+
35
+ **Fix:** Wrap the content in `<ClientOnly>` — browser capability detection is inherently client-only.
36
+
37
+ ---
38
+
39
+ ## 5. Theme composables using `useLocalStorage` — theme layer
40
+
41
+ **Cause:** `useLocalStorage` reads from localStorage which doesn't exist on the server. The server renders with the default value; the client reads the user's saved preference and produces different output.
42
+
43
+ **Fix:** Replace `useLocalStorage` with `useState` (SSR-safe, serialised to the Nuxt payload). A `.client.ts` plugin reads localStorage post-hydration and calls the setters to restore preferences — a deliberate two-phase update that avoids the mismatch.
44
+
45
+ ---
46
+
47
+ ## 6. Tailwind 4 `@config` restricting content scanning — `core.css`
48
+
49
+ **Cause:** The `tailwind.config.js` loaded via `@config` contained a `content:` array pointing only to the core layer's files. In Tailwind 4, this array restricts scanning to those paths, so utilities from `ui`, `layout`, `theme`, etc. layers were absent from the initial CSS bundle. In dev mode, Tailwind discovers them lazily via HMR, causing a visible shift.
50
+
51
+ **Fix:** Remove the `content:` array from `tailwind.config.js`. Tailwind 4's Vite plugin auto-discovers all Vite-processed files. Also added `@source` globs in `core.css` covering all layer and app directories for belt-and-suspenders eager scanning.
52
+
53
+ ---
54
+
55
+ ## 7. Vite CSS code splitting deferring stylesheets
56
+
57
+ **Cause:** Vite's default `cssCodeSplit: true` bundles CSS with the JS chunk that imports it. CSS for lazily-loaded components only arrives when that JS chunk is first requested, leaving the initial render unstyled.
58
+
59
+ **Fix:** Set `cssCodeSplit: false` in the playground app's `vite.build` config to merge all CSS into one bundle loaded at first paint.
60
+
61
+ ---
62
+
63
+ ## 8. `@layer` in a CSS file not processed by Tailwind — `ui.css`, `theme.css`
64
+
65
+ **Cause:** Using `@layer utilities { ... }` or `@layer base { ... }` inside a CSS file loaded via Nuxt's `css: []` array (not via `@import 'tailwindcss'`) creates a raw CSS cascade layer declaration outside Tailwind's pipeline. The browser processes this as a separate layer, triggering a style recalculation after initial paint.
66
+
67
+ **Fix:** Replace `@layer` blocks with plain selectors in any CSS file that isn't processed by Tailwind. Only CSS files that contain `@import 'tailwindcss'` should use `@layer`.
68
+
69
+ **Rule:** Never use `@layer` (or `@utility`) in a file loaded via `css: []` unless it also imports Tailwind.
70
+
71
+ ---
72
+
73
+ ## 9. `NuxtAnnouncer` / `NuxtRouteAnnouncer` inside a CSS grid — `app.vue`
74
+
75
+ **Cause:** `<NuxtAnnouncer>` and `<NuxtRouteAnnouncer>` were inside the `<NuxtLayout>` slot, making them direct children of the `mastmain` CSS grid. Before their `position: absolute` CSS loaded, they auto-placed as grid rows and pushed the `basesection` down by ~85px. When the CSS loaded and removed them from grid flow, the section jumped back up.
76
+
77
+ **Fix:** Remove `<NuxtAnnouncer>` and `<NuxtRouteAnnouncer>` entirely — they are boilerplate and not needed in this project.
78
+
79
+ ---
80
+
81
+ ## 10. Layout header hydration mismatch — `ui/layouts/default.vue`
82
+
83
+ **Cause:** The layout used `resolveComponent('MastHeader')` at runtime to detect whether a header component existed. In Nuxt 4, components are registered via compile-time auto-imports, not via `app.component()`, so `resolveComponent` always returns a string at runtime. Server-side Vue resolves it differently, so the header rendered on the server but not after client hydration — causing the section to shift up by the header height.
84
+
85
+ **Fix:** Remove the `resolveComponent` guard. The layout always renders `<MastHeader />` and `<MastFooter />` unconditionally; the components themselves handle empty states via `appConfig`.
86
+
87
+ ---
88
+
89
+ ## Key Rules Going Forward
90
+
91
+ - Never use `@layer` / `@utility` in CSS files loaded via `css: []` — use plain selectors instead.
92
+ - Never use runtime `resolveComponent` to conditionally render components in Nuxt 4 — components are compile-time, not runtime-registered.
93
+ - Use `useState` (not `useLocalStorage`) for any value rendered during SSR that might differ between server and client.
94
+ - Keep accessibility-only elements (`aria-live` regions, announcers) outside CSS grid containers to prevent them participating in grid flow.
@@ -1,18 +1,8 @@
1
- <script setup lang="ts">
2
- import { resolveComponent } from 'vue'
3
-
4
- const mastHeader = resolveComponent('MastHeader')
5
- const mastFooter = resolveComponent('MastFooter')
6
-
7
- const hasMastHeader = typeof mastHeader !== 'string'
8
- const hasMastFooter = typeof mastFooter !== 'string'
9
- </script>
10
-
11
1
  <template>
12
- <component :is="mastHeader" v-if="hasMastHeader" />
2
+ <MastHeader />
13
3
  <MastMain>
14
4
  <slot />
15
5
  </MastMain>
16
- <component :is="mastFooter" v-if="hasMastFooter" />
6
+ <MastFooter />
17
7
  <UOverlayProvider />
18
8
  </template>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kmcom-nuxt-layers",
3
3
  "private": false,
4
- "version": "1.7.8",
4
+ "version": "1.7.9",
5
5
  "description": "Composable Nuxt 4 layers for building scalable Vue applications",
6
6
  "exports": {
7
7
  "./layers/core": "./layers/core/nuxt.config.ts",