srcdev-nuxt-components 9.1.29 → 9.1.31

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,6 +1,6 @@
1
1
  ---
2
2
  name: BannerVideo
3
- description: BannerVideo full-width hero video banner — props, verticalPosition/horizontalPosition, responsive max-height, reduced-motion fallback, CSS tokens, consumer styling
3
+ description: BannerVideo full-width hero video banner — props, depth tier system, verticalPosition/horizontalPosition, reduced-motion fallback, CSS tokens, consumer styling
4
4
  type: reference
5
5
  ---
6
6
 
@@ -10,7 +10,7 @@ type: reference
10
10
 
11
11
  `BannerVideo` renders a full-width banner section that plays a muted, looping mp4 video. A poster image is shown as fallback when the user has `prefers-reduced-motion: reduce` set — handled entirely in CSS, no JS.
12
12
 
13
- The banner is sized via `aspect-ratio` so it scales naturally, with `max-height` props capping height at each breakpoint.
13
+ The banner is sized via `aspect-ratio` so it scales naturally. The `depth` prop selects a responsive `max-height` tier (`xs` → `xl`) implemented with `clamp()` — no breakpoint props needed. Each tier exposes a `--theme-banner-video-max-height-{depth}` CSS token that consuming pages can override.
14
14
 
15
15
  ### Autoplay mechanism
16
16
 
@@ -26,9 +26,7 @@ The video uses `autoplay muted loop playsinline preload="auto"` attributes on th
26
26
  | `imgWidth` | `number` | `1920` | Intrinsic width of the poster image — required for NuxtImg/IPX optimisation. |
27
27
  | `imgHeight` | `number` | `1080` | Intrinsic height of the poster image — required for NuxtImg/IPX optimisation. |
28
28
  | `tag` | `"section" \| "div" \| "header" \| "main" \| "article"` | `"section"` | HTML element rendered as the root. |
29
- | `maxHeight` | `string` | `"56rem"` | Maximum height at desktop (≥64em / 1024px). |
30
- | `maxHeightTablet` | `string` | `undefined` | Maximum height at tablet (48em–64em). Falls back to `maxHeight`. |
31
- | `maxHeightMobile` | `string` | `undefined` | Maximum height on mobile (<48em). Falls back through tablet → desktop. |
29
+ | `depth` | `"xs" \| "sm" \| "md" \| "lg" \| "xl"` | `"md"` | Responsive max-height tier. Each maps to a `clamp()` scale; override via `--theme-banner-video-max-height-{depth}`. |
32
30
  | `aspectRatio` | `string` | `"21/9"` | CSS `aspect-ratio` of the container (e.g. `"16/9"`, `"21/9"`, `"4/3"`). |
33
31
  | `objectFit` | `"cover" \| "contain" \| "fill" \| "none" \| "scale-down"` | `"cover"` | How the video and fallback image fill the banner frame. |
34
32
  | `verticalPosition` | `"start" \| "center" \| "end"` | `"center"` | Vertical crop position. Maps to `align-self` on the video element and `object-position` Y on the fallback image. |
@@ -47,29 +45,23 @@ The video uses `autoplay muted loop playsinline preload="auto"` attributes on th
47
45
 
48
46
  ## Common variants
49
47
 
50
- ### Responsive max-height per breakpoint
48
+ ### Depth tiers
51
49
 
52
50
  ```vue
53
- <BannerVideo
54
- src="/videos/hero.mp4"
55
- poster="/images/hero-poster.jpg"
56
- alt="Studio interior"
57
- max-height="56rem"
58
- max-height-tablet="40rem"
59
- max-height-mobile="24rem"
60
- />
61
- ```
51
+ <!-- xs: clamp(12rem, 15vw, 24rem) — thin strip -->
52
+ <BannerVideo src="" poster="…" depth="xs" />
62
53
 
63
- ### Tall viewport-filling banner
54
+ <!-- sm: clamp(18rem, 22vw, 36rem) -->
55
+ <BannerVideo src="…" poster="…" depth="sm" />
64
56
 
65
- ```vue
66
- <BannerVideo
67
- src="/videos/hero.mp4"
68
- poster="/images/hero-poster.jpg"
69
- alt="Studio interior"
70
- max-height="100vh"
71
- aspect-ratio="16/9"
72
- />
57
+ <!-- md (default): clamp(28rem, 38vw, 56rem) -->
58
+ <BannerVideo src="…" poster="…" />
59
+
60
+ <!-- lg: clamp(40rem, 52vw, 72rem) -->
61
+ <BannerVideo src="…" poster="…" depth="lg" />
62
+
63
+ <!-- xl: clamp(52rem, 65vw, 90rem) — near full-screen hero -->
64
+ <BannerVideo src="…" poster="…" depth="xl" aspect-ratio="16/9" />
73
65
  ```
74
66
 
75
67
  ### Custom focal point
@@ -125,32 +117,35 @@ Always match the intrinsic dimensions of the poster file. NuxtImg uses them to a
125
117
 
126
118
  ## CSS custom properties
127
119
 
128
- All set from props via inline `:style` on the root element.
120
+ Private tokens (set via `data-depth` + CSS selectors, not inline style):
121
+
122
+ | Property | Default (md tier) | Controlled by |
123
+ |---|---|---|
124
+ | `--_max-height` | `clamp(28rem, 38vw, 56rem)` | `data-depth` selector |
125
+
126
+ Inline style tokens (set from props):
129
127
 
130
128
  | Property | Default | Set by prop |
131
129
  |---|---|---|
132
- | `--_max-height` | `56rem` | `maxHeight` |
133
- | `--_max-height-tablet` | *(unset)* | `maxHeightTablet` |
134
- | `--_max-height-mobile` | *(unset)* | `maxHeightMobile` |
135
130
  | `--_aspect-ratio` | `21/9` | `aspectRatio` |
136
131
  | `--_align-self` | `center` | `verticalPosition` |
137
132
  | `--_justify-self` | `center` | `horizontalPosition` |
138
133
 
139
- **Responsive override example:**
134
+ ### Depth token defaults
140
135
 
141
- ```css
142
- .my-page {
143
- .banner-video {
144
- --_max-height: 56rem;
136
+ | depth | token | clamp value |
137
+ |---|---|---|
138
+ | `xs` | `--theme-banner-video-max-height-xs` | `clamp(12rem, 15vw, 24rem)` |
139
+ | `sm` | `--theme-banner-video-max-height-sm` | `clamp(18rem, 22vw, 36rem)` |
140
+ | `md` | `--theme-banner-video-max-height-md` | `clamp(28rem, 38vw, 56rem)` |
141
+ | `lg` | `--theme-banner-video-max-height-lg` | `clamp(40rem, 52vw, 72rem)` |
142
+ | `xl` | `--theme-banner-video-max-height-xl` | `clamp(52rem, 65vw, 90rem)` |
145
143
 
146
- @media (width < 1024px) {
147
- --_max-height: 40rem;
148
- }
144
+ **Override a tier in a consuming page:**
149
145
 
150
- @media (width < 768px) {
151
- --_max-height: 24rem;
152
- }
153
- }
146
+ ```css
147
+ .my-page {
148
+ --theme-banner-video-max-height-md: clamp(32rem, 45vw, 64rem);
154
149
  }
155
150
  ```
156
151
 
@@ -169,11 +164,8 @@ Use an unscoped style block scoped by a page or section wrapper class. No `:deep
169
164
  ```vue
170
165
  <style>
171
166
  .my-page {
172
- .banner-video {
173
- @media (width < 900px) {
174
- --_max-height: 36rem;
175
- }
176
- }
167
+ /* Override the md tier's clamp range for this page */
168
+ --theme-banner-video-max-height-md: clamp(32rem, 45vw, 64rem);
177
169
  }
178
170
  </style>
179
171
  ```
@@ -0,0 +1,133 @@
1
+ ---
2
+ name: GridStack
3
+ description: GridStack CSS Grid z-axis stacking component — slot API, z-order rules, sizing behaviour, consumer patterns (video+overlay, image+text)
4
+ type: reference
5
+ ---
6
+
7
+ # GridStack
8
+
9
+ ## Overview
10
+
11
+ `GridStack` stacks slot content in the z-axis using a single `grid-template-areas: "stack"` — no `position: absolute` needed. Every slot is wrapped in a `.grid-stack__layer` div sharing that grid area. The container sizes itself from the tallest layer; all layers stretch to fill that height.
12
+
13
+ ## Props
14
+
15
+ | Prop | Type | Default | Description |
16
+ |------|------|---------|-------------|
17
+ | `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | HTML element rendered as the root. |
18
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
19
+
20
+ ## Slot API
21
+
22
+ Any named slot is accepted — there are no declared slot names. Convention is `layer-1`, `layer-2`, `layer-3` etc., but any name works. The component iterates `$slots` and wraps each in a `.grid-stack__layer`.
23
+
24
+ **Z-order rule: DOM order = z-order. The last slot is on top.**
25
+
26
+ ```vue
27
+ <GridStack>
28
+ <template #layer-1><!-- base, behind everything --></template>
29
+ <template #layer-2><!-- middle --></template>
30
+ <template #layer-3><!-- on top --></template>
31
+ </GridStack>
32
+ ```
33
+
34
+ ## Basic usage
35
+
36
+ ```vue
37
+ <GridStack>
38
+ <template #layer-1>
39
+ <img src="/images/hero.jpg" alt="" />
40
+ </template>
41
+ <template #layer-2>
42
+ <div class="hero-overlay">
43
+ <h1>Heading over image</h1>
44
+ </div>
45
+ </template>
46
+ </GridStack>
47
+ ```
48
+
49
+ ## Common patterns
50
+
51
+ ### Video background + overlay
52
+
53
+ ```vue
54
+ <GridStack>
55
+ <template #layer-1>
56
+ <BannerVideo
57
+ src="/videos/hero.mp4"
58
+ poster="/images/hero-poster.jpg"
59
+ alt=""
60
+ depth="lg"
61
+ />
62
+ </template>
63
+ <template #layer-2>
64
+ <div class="video-overlay">
65
+ <h1>Content over video</h1>
66
+ </div>
67
+ </template>
68
+ </GridStack>
69
+ ```
70
+
71
+ ### Decorative background + content
72
+
73
+ ```vue
74
+ <GridStack tag="section">
75
+ <template #layer-1>
76
+ <div class="decorative-bg" aria-hidden="true"></div>
77
+ </template>
78
+ <template #layer-2>
79
+ <div class="section-content">
80
+ <p>Real content here</p>
81
+ </div>
82
+ </template>
83
+ </GridStack>
84
+ ```
85
+
86
+ ## Sizing
87
+
88
+ The container height is determined by the tallest child layer. All layers stretch to match. If layers have different intrinsic heights, the shorter ones will stretch — use `align-self` on the layer's inner content to control vertical position within the stretched space.
89
+
90
+ To pin the stack to a fixed height, set it on the root from the consuming page:
91
+
92
+ ```css
93
+ .my-page {
94
+ .grid-stack {
95
+ block-size: 60rem;
96
+ }
97
+ }
98
+ ```
99
+
100
+ ## CSS classes
101
+
102
+ | Class | Element |
103
+ |---|---|
104
+ | `.grid-stack` | Root element |
105
+ | `.grid-stack__layer` | Wrapper div around each slot — all share `grid-area: stack` |
106
+
107
+ ## Consumer styling
108
+
109
+ No `:deep()` needed — `@layer components` means page styles win automatically.
110
+
111
+ ```vue
112
+ <style>
113
+ .my-page {
114
+ .grid-stack {
115
+ border-radius: 1.2rem;
116
+ overflow: hidden; /* clips layers to rounded corners */
117
+ }
118
+
119
+ /* Style the overlay layer by targeting content inside it */
120
+ .my-overlay {
121
+ display: grid;
122
+ place-items: center;
123
+ pointer-events: none; /* let clicks through to the layer below */
124
+ }
125
+ }
126
+ </style>
127
+ ```
128
+
129
+ ## Notes
130
+
131
+ - `pointer-events: none` on overlay layers (and `pointer-events: auto` on interactive children within them) is the standard pattern for overlays that shouldn't block interaction with layers behind them.
132
+ - There is no built-in `z-index` — stacking is handled purely by DOM order. If a consumer applies `z-index` on a layer for other reasons, be aware it creates a new stacking context.
133
+ - The slot name is used as the Vue `:key` on the layer wrapper, so slot names must be unique.
@@ -0,0 +1,92 @@
1
+ # useCanonicalUrl Composable
2
+
3
+ ## Overview
4
+
5
+ `useCanonicalUrl` sets a `<link rel="canonical">` tag in the page `<head>` using the current
6
+ route path and the app's configured canonical host. Call it once in the default layout so every
7
+ page gets a canonical URL automatically.
8
+
9
+ **This composable ships inside the `srcdev-nuxt-components` layer** (`app/composables/useCanonicalUrl.ts`).
10
+ Consuming apps get it via Nuxt's layer auto-import — **do not create a local copy** in the consuming app.
11
+
12
+ ## Prerequisites
13
+
14
+ - `NUXT_PUBLIC_CANONICAL_HOST` env var set to the production domain (e.g. `myapp.co.uk`).
15
+
16
+ ## Setup in the consuming app
17
+
18
+ ### 1. Runtime config
19
+
20
+ `canonicalHost` must be declared in `runtimeConfig.public` in `nuxt.config.ts`. The standard
21
+ scaffold pattern reads it from an env var with a Vercel preview URL as the fallback:
22
+
23
+ ```ts
24
+ // nuxt.config.ts
25
+ const PROD_HOST = "myapp.co.uk"
26
+ const canonicalHost = process.env.NUXT_PUBLIC_CANONICAL_HOST ?? "myapp.vercel.app"
27
+
28
+ export default defineNuxtConfig({
29
+ runtimeConfig: {
30
+ public: {
31
+ canonicalHost,
32
+ },
33
+ },
34
+ })
35
+ ```
36
+
37
+ Set `NUXT_PUBLIC_CANONICAL_HOST` in `.env` locally and in your hosting provider's environment
38
+ variables for production. On Vercel preview deployments the fallback URL is used, so canonical
39
+ tags point to preview — that is intentional and correct.
40
+
41
+ ### 2. Call in the default layout
42
+
43
+ Call `useCanonicalUrl()` at the top of `<script setup>` in `app/layouts/default.vue`. This runs
44
+ on every page without needing to repeat it per-page:
45
+
46
+ ```vue
47
+ <!-- app/layouts/default.vue -->
48
+ <script setup lang="ts">
49
+ useCanonicalUrl()
50
+ </script>
51
+ ```
52
+
53
+ ### 3. Node types (required for `process.env` in nuxt.config)
54
+
55
+ The `process.env` access at the top of `nuxt.config.ts` requires Node type definitions. Ensure
56
+ `@types/node` is installed and `"node"` is in the types array:
57
+
58
+ ```bash
59
+ npm i --save-dev @types/node
60
+ ```
61
+
62
+ ```ts
63
+ // nuxt.config.ts — typescript block
64
+ typescript: {
65
+ tsConfig: { compilerOptions: { types: ["srcdev-nuxt-components", "node"] } },
66
+ },
67
+ ```
68
+
69
+ ## Composable reference
70
+
71
+ ```ts
72
+ export const useCanonicalUrl = () => {
73
+ const route = useRoute();
74
+ const config = useRuntimeConfig();
75
+
76
+ const canonicalHost = config.public.canonicalHost;
77
+ const canonicalUrl = `https://${canonicalHost}${route.path}`;
78
+
79
+ useHead({
80
+ link: [{ rel: "canonical", href: canonicalUrl }],
81
+ });
82
+
83
+ return { canonicalUrl };
84
+ };
85
+ ```
86
+
87
+ ## Notes
88
+
89
+ - The composable returns `{ canonicalUrl }` if you need to read the value elsewhere (e.g. for
90
+ structured data), but in most cases you call it purely for the side effect.
91
+ - On pages with query strings or hash fragments only the `route.path` (pathname) is used, which
92
+ is the correct canonical behaviour.
@@ -40,7 +40,9 @@ Each skill is a single markdown file named `<area>-<task>.md`.
40
40
  ├── vue-video-autoplay.md — autoplay on client-side navigation: use <source> child (not :src on <video>), :key, and explicit v.load()
41
41
  ├── icon-sets.md — icon set packages required by layer components, FOUC prevention, component→package map
42
42
  ├── robots-env-aware.md — @nuxtjs/robots: allow crawling on prod domain only, block on preview/staging via env var
43
+ ├── new-app-scaffold.md — scaffold a new Nuxt consumer app extending this layer (package.json, nuxt.config, app structure, CLAUDE.md)
43
44
  ├── release-notes.md — produce release notes as a fenced markdown block from git log
45
+ ├── composable-canonical-url.md — useCanonicalUrl: set <link rel="canonical"> from runtimeConfig.public.canonicalHost; layout setup, node types
44
46
  ├── composable-whatsapp.md — useWhatsApp: open pre-filled wa.me link from form payload; runtime config, security, usage
45
47
  ├── composable-zod-validation.md — useZodValidation: schema-driven form validation, error binding, submit flow, API error push
46
48
  ├── composable-colour-scheme.md — useColourScheme: reactive light/dark/auto switching, localStorage persistence, runtime config
@@ -64,7 +66,8 @@ Each skill is a single markdown file named `<area>-<task>.md`.
64
66
  ├── glass-panel.md — GlassPanel props, slots, CSS token API (--glass-panel-bg/border-color/shadow/highlight), theming override
65
67
  ├── navigation-horizontal.md — NavigationHorizontal props, NavItemData type, CSS token API, import path gotcha
66
68
  ├── input-copy-core.md — InputCopyCore: readonly copy-to-clipboard input; props, emits, slots, CSS classes, usage
67
- ├── banner-video.md — BannerVideo: full-width hero video banner, objectFit/objectPosition, responsive max-height, reduced-motion fallback, CSS tokens
69
+ ├── banner-video.md — BannerVideo: full-width hero video banner, depth tier system, objectFit/objectPosition, reduced-motion fallback, CSS tokens
70
+ ├── grid-stack.md — GridStack: CSS Grid z-axis stacking, slot API, z-order rules, sizing, video+overlay and image+text patterns
68
71
  ├── scroll-reveal-frame.md — ScrollRevealFrame: generic parallax clipping frame, slot API, image grid pattern, CSS tokens, browser support
69
72
  ├── scroll-reveal-image.md — ScrollRevealImage: single-image parallax reveal, focalX, imgWidth/imgHeight, responsive frame height
70
73
  ├── site-navigation.md — SiteNavigation: responsive nav with auto-collapse, burger menu, decorator indicators, CSS token API
@@ -0,0 +1,305 @@
1
+ # New App Scaffold
2
+
3
+ ## Overview
4
+
5
+ Scaffold a new Nuxt app that extends `srcdev-nuxt-components` as a layer. Use this skill when
6
+ a user asks to initialise or set up a new consumer app from scratch.
7
+
8
+ ## Prerequisites
9
+
10
+ - An empty (or near-empty) git repo exists at the target path
11
+ - The user provides: repo path, package name, production domain, fonts, and whether to include
12
+ `nuxt-security` + `@nuxtjs/robots`
13
+
14
+ ## Steps
15
+
16
+ ### 1. Gather details
17
+
18
+ Ask for (or confirm from context):
19
+
20
+ | Detail | Example |
21
+ |---|---|
22
+ | Repo path | `/Users/name/websites/my-app` |
23
+ | Package name | `my-app` (used in `package.json` and `bodyAttrs.class`) |
24
+ | Production domain | `myapp.co.uk` |
25
+ | Fonts | `Fraunces, Manrope` — assume bunny CDN unless stated otherwise |
26
+ | Include security modules? | yes / no (nuxt-security + @nuxtjs/robots) |
27
+
28
+ Check for any existing files (`.claude/`, `README.md`, design docs) before writing — read them
29
+ to inform the CLAUDE.md and CSS tokens.
30
+
31
+ ### 2. Create `package.json`
32
+
33
+ ```json
34
+ {
35
+ "name": "{name}",
36
+ "private": true,
37
+ "type": "module",
38
+ "scripts": {
39
+ "clean": "rm -rf .nuxt && rm -rf .output && rm -rf node_modules && rm ./package-lock.json",
40
+ "cleanupandprepare": "npx nuxi cleanup && npx nuxi prepare",
41
+ "reinstall": "npm install",
42
+ "cleaninstall": "npm run clean && npm run reinstall",
43
+ "build": "nuxt build",
44
+ "dev": "nuxt dev",
45
+ "generate": "nuxt generate",
46
+ "preview": "nuxt preview",
47
+ "setup:claude": "mkdir -p .claude/skills/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/skills/. .claude/skills/srcdev-nuxt-components",
48
+ "postinstall": "NUXT_STANDALONE=true nuxt prepare && npm run setup:claude",
49
+ "lint": "eslint .",
50
+ "lint:fix": "eslint . --fix"
51
+ },
52
+ "dependencies": {
53
+ "@iconify-json/gravity-ui": "1.2.12",
54
+ "@iconify-json/ic": "1.2.4",
55
+ "@iconify-json/lucide": "1.2.101",
56
+ "@iconify-json/material-symbols": "1.2.65",
57
+ "@iconify-json/mdi": "1.2.3",
58
+ "modern-normalize": "3.0.1",
59
+ "nuxt-security": "2.5.1"
60
+ },
61
+ "devDependencies": {
62
+ "@nuxt/eslint": "1.15.2",
63
+ "@nuxt/scripts": "0.13.2",
64
+ "@nuxtjs/robots": "6.0.6",
65
+ "eslint": "10.2.0",
66
+ "nuxt": "4.4.2",
67
+ "srcdev-nuxt-components": "{latest version}"
68
+ }
69
+ }
70
+ ```
71
+
72
+ > **`setup:claude` script**: `mkdir -p` is required — the destination folder won't exist on a
73
+ > fresh clone. The trailing `/.` on the source path copies contents into the destination rather
74
+ > than nesting a `skills` subfolder inside it.
75
+
76
+ ### 3. Create `nuxt.config.ts`
77
+
78
+ ```ts
79
+ const PROD_HOST = "{domain}"
80
+ const canonicalHost = process.env.NUXT_PUBLIC_CANONICAL_HOST ?? "{name}.vercel.app"
81
+ const isProduction = canonicalHost === PROD_HOST
82
+
83
+ export default defineNuxtConfig({
84
+ debug: false,
85
+ devServer: { https: false },
86
+ compatibilityDate: "2025-08-04",
87
+ runtimeConfig: {
88
+ public: {
89
+ canonicalHost,
90
+ colourScheme: { enabled: false },
91
+ },
92
+ },
93
+ extends: ["srcdev-nuxt-components"],
94
+ modules: ["@nuxt/eslint", "@nuxt/scripts", "nuxt-security", "@nuxtjs/robots"],
95
+ robots: {
96
+ enabled: isProduction,
97
+ groups: [{ userAgent: ["*"], allow: ["/"] }],
98
+ sitemap: [`https://${PROD_HOST}/sitemap.xml`],
99
+ },
100
+ components: [{ path: "./components", pathPrefix: false }],
101
+ imports: { dirs: ["./stores"] },
102
+ devtools: { enabled: true },
103
+ app: {
104
+ head: {
105
+ htmlAttrs: { lang: "en" },
106
+ titleTemplate: "%s - {App Title}",
107
+ meta: [{ charset: "utf-8" }, { name: "viewport", content: "width=device-width, initial-scale=1" }],
108
+ link: [{ rel: "icon", href: "/icons/favicon.png" }],
109
+ bodyAttrs: { class: "{name}-body" },
110
+ },
111
+ pageTransition: { name: "page", mode: "out-in" },
112
+ layoutTransition: { name: "layout", mode: "out-in" },
113
+ },
114
+ css: ["srcdev-nuxt-components/app/assets/styles/main.css", "./app/assets/styles/main.css"],
115
+ fonts: {
116
+ assets: { prefix: "/_fonts" },
117
+ families: [
118
+ // Add chosen fonts here — provider: "bunny" for Google Fonts via bunny CDN
119
+ { name: "{Font}", weights: [300, 400, 500, 600, 700], styles: ["normal", "italic"], provider: "bunny", display: "optional" },
120
+ ],
121
+ },
122
+ typescript: {
123
+ includeWorkspace: true,
124
+ strict: true,
125
+ shim: true,
126
+ typeCheck: false,
127
+ tsConfig: { compilerOptions: { types: ["srcdev-nuxt-components", "node"] } },
128
+ },
129
+ eslint: { config: {} },
130
+ security: {
131
+ headers: {
132
+ contentSecurityPolicy: {
133
+ "default-src": ["'self'"],
134
+ "script-src": ["'self'", "'unsafe-inline'", "'unsafe-eval'", "https://vercel.live"],
135
+ "style-src": ["'self'", "'unsafe-inline'"],
136
+ "script-src-attr": ["'self'", "'unsafe-inline'"],
137
+ "img-src": ["'self'", "data:"],
138
+ "connect-src": ["'self'", "https://api.iconify.design", "https://vercel.live"],
139
+ "frame-src": ["'self'", "https://vercel.live"],
140
+ },
141
+ },
142
+ },
143
+ vite: {
144
+ optimizeDeps: {
145
+ include: ["@oddbird/css-anchor-positioning", "@vue/devtools-core", "@vue/devtools-kit", "zod"],
146
+ },
147
+ },
148
+ })
149
+ ```
150
+
151
+ ### 4. Create supporting config files
152
+
153
+ **`tsconfig.json`**
154
+ ```json
155
+ {
156
+ "extends": "./.nuxt/tsconfig.json",
157
+ "compilerOptions": { "strict": true }
158
+ }
159
+ ```
160
+
161
+ **`eslint.config.mjs`**
162
+ ```js
163
+ // @ts-check
164
+ import withNuxt from "./.nuxt/eslint.config.mjs"
165
+
166
+ export default withNuxt({
167
+ files: ["**/*.vue"],
168
+ rules: {
169
+ "@stylistic/max-len": "off",
170
+ "vue/max-len": "off",
171
+ "vue/max-attributes-per-line": "off",
172
+ "vue/singleline-html-element-content-newline": "off",
173
+ "vue/html-closing-bracket-newline": "off",
174
+ "vue/html-self-closing": [
175
+ "error",
176
+ { html: { void: "always", normal: "never", component: "always" } },
177
+ ],
178
+ "@stylistic/member-delimiter-style": [
179
+ "error",
180
+ {
181
+ multiline: { delimiter: "semi", requireLast: true },
182
+ singleline: { delimiter: "semi", requireLast: false },
183
+ multilineDetection: "brackets",
184
+ },
185
+ ],
186
+ },
187
+ })
188
+ ```
189
+
190
+ **`.prettierrc`**
191
+ ```json
192
+ {
193
+ "printWidth": 120,
194
+ "singleQuote": false,
195
+ "trailingComma": "es5",
196
+ "bracketSpacing": true,
197
+ "semi": false,
198
+ "vueIndentScriptAndStyle": false,
199
+ "htmlWhitespaceSensitivity": "ignore"
200
+ }
201
+ ```
202
+
203
+ **`.nvmrc`**
204
+ ```
205
+ node 20
206
+ ```
207
+
208
+ **`.gitignore`** — include `.output`, `.nuxt`, `.nitro`, `.cache`, `node_modules`, `.env`, `.DS_Store`, `.vercel`, `storybook-static`.
209
+
210
+ ### 5. Create app directory structure
211
+
212
+ ```
213
+ app/
214
+ ├── assets/styles/
215
+ │ └── main.css ← brand tokens + font vars (loads after layer CSS)
216
+ ├── layouts/
217
+ │ └── default.vue ← minimal shell with <slot>
218
+ ├── pages/
219
+ │ └── index.vue ← placeholder home page with useSeoMeta
220
+ └── error.vue ← 404 / 500 handler with clearError redirect
221
+ ```
222
+
223
+ **`app/assets/styles/main.css`** — declare brand CSS custom properties and set font-family:
224
+
225
+ ```css
226
+ :root {
227
+ /* Add brand colour tokens */
228
+ --font-display: "{Display Font}", serif;
229
+ --font-body: "{Body Font}", sans-serif;
230
+ font-family: var(--font-body);
231
+ }
232
+ ```
233
+
234
+ **`app/layouts/default.vue`** — minimal, with canonical URL head tag:
235
+
236
+ ```vue
237
+ <template>
238
+ <div class="layout-default">
239
+ <main><slot></slot></main>
240
+ </div>
241
+ </template>
242
+
243
+ <script setup lang="ts">
244
+ useCanonicalUrl()
245
+ </script>
246
+ ```
247
+
248
+ **`app/pages/index.vue`** — placeholder with `useSeoMeta`.
249
+
250
+ **`app/error.vue`** — handle `statusCode` 404 vs 500, call `clearError({ redirect: "/" })`.
251
+
252
+ ### 6. Create `.claude/skills/.gitkeep`
253
+
254
+ Ensures `.claude/skills/` exists in the repo on a fresh clone so `setup:claude` never errors.
255
+
256
+ ```bash
257
+ mkdir -p .claude/skills && touch .claude/skills/.gitkeep
258
+ ```
259
+
260
+ ### 7. Create `CLAUDE.md`
261
+
262
+ Brief project context file at repo root:
263
+
264
+ ```md
265
+ # {App Title} — Claude Guidelines
266
+
267
+ This app extends the `srcdev-nuxt-components` Nuxt layer.
268
+
269
+ ## Layer Skills
270
+
271
+ Skills are copied into `.claude/skills/srcdev-nuxt-components/` during `npm install`.
272
+ Run `npm run setup:claude` to refresh after a layer upgrade.
273
+
274
+ ## Project Context
275
+
276
+ - **Design system**: `.claude/design.md` (if it exists)
277
+ - **Production domain**: {domain}
278
+ - **Fonts**: {fonts}
279
+
280
+ ## Key Conventions
281
+
282
+ - Follow all guidelines in the layer's `CLAUDE.md`
283
+ - CSS custom properties for all design tokens — no hardcoded values in component styles
284
+ - Layer CSS loads first; `app/assets/styles/main.css` overrides
285
+ ```
286
+
287
+ ### 8. Confirm next step for the user
288
+
289
+ Tell the user to run:
290
+
291
+ ```bash
292
+ npm install
293
+ ```
294
+
295
+ `postinstall` will run `nuxt prepare` and copy the layer skills automatically.
296
+
297
+ ## Notes
298
+
299
+ - If the repo contains a `.claude/design.md`, read it before writing `main.css` — use any
300
+ brand colours or font choices from it to seed the CSS tokens.
301
+ - If security modules are not wanted, omit `nuxt-security` from `dependencies` and remove it
302
+ from `modules` and the `security` config block in `nuxt.config.ts`. Same for `@nuxtjs/robots`.
303
+ - The `colourScheme.enabled: false` default is correct for most consumer apps — enable only if
304
+ the app needs light/dark switching.
305
+ - Check the latest `srcdev-nuxt-components` version on npm before writing `package.json`.