srcdev-nuxt-components 9.1.29 → 9.1.30

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.
@@ -40,6 +40,7 @@ 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
44
45
  ├── composable-whatsapp.md — useWhatsApp: open pre-filled wa.me link from form payload; runtime config, security, usage
45
46
  ├── composable-zod-validation.md — useZodValidation: schema-driven form validation, error binding, submit flow, API error push
@@ -64,7 +65,8 @@ Each skill is a single markdown file named `<area>-<task>.md`.
64
65
  ├── glass-panel.md — GlassPanel props, slots, CSS token API (--glass-panel-bg/border-color/shadow/highlight), theming override
65
66
  ├── navigation-horizontal.md — NavigationHorizontal props, NavItemData type, CSS token API, import path gotcha
66
67
  ├── 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
68
+ ├── banner-video.md — BannerVideo: full-width hero video banner, depth tier system, objectFit/objectPosition, reduced-motion fallback, CSS tokens
69
+ ├── grid-stack.md — GridStack: CSS Grid z-axis stacking, slot API, z-order rules, sizing, video+overlay and image+text patterns
68
70
  ├── scroll-reveal-frame.md — ScrollRevealFrame: generic parallax clipping frame, slot API, image grid pattern, CSS tokens, browser support
69
71
  ├── scroll-reveal-image.md — ScrollRevealImage: single-image parallax reveal, focalX, imgWidth/imgHeight, responsive frame height
70
72
  ├── site-navigation.md — SiteNavigation: responsive nav with auto-collapse, burger menu, decorator indicators, CSS token API
@@ -0,0 +1,301 @@
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"] } },
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:
235
+
236
+ ```vue
237
+ <template>
238
+ <div class="layout-default">
239
+ <main><slot></slot></main>
240
+ </div>
241
+ </template>
242
+ ```
243
+
244
+ **`app/pages/index.vue`** — placeholder with `useSeoMeta`.
245
+
246
+ **`app/error.vue`** — handle `statusCode` 404 vs 500, call `clearError({ redirect: "/" })`.
247
+
248
+ ### 6. Create `.claude/skills/.gitkeep`
249
+
250
+ Ensures `.claude/skills/` exists in the repo on a fresh clone so `setup:claude` never errors.
251
+
252
+ ```bash
253
+ mkdir -p .claude/skills && touch .claude/skills/.gitkeep
254
+ ```
255
+
256
+ ### 7. Create `CLAUDE.md`
257
+
258
+ Brief project context file at repo root:
259
+
260
+ ```md
261
+ # {App Title} — Claude Guidelines
262
+
263
+ This app extends the `srcdev-nuxt-components` Nuxt layer.
264
+
265
+ ## Layer Skills
266
+
267
+ Skills are copied into `.claude/skills/srcdev-nuxt-components/` during `npm install`.
268
+ Run `npm run setup:claude` to refresh after a layer upgrade.
269
+
270
+ ## Project Context
271
+
272
+ - **Design system**: `.claude/design.md` (if it exists)
273
+ - **Production domain**: {domain}
274
+ - **Fonts**: {fonts}
275
+
276
+ ## Key Conventions
277
+
278
+ - Follow all guidelines in the layer's `CLAUDE.md`
279
+ - CSS custom properties for all design tokens — no hardcoded values in component styles
280
+ - Layer CSS loads first; `app/assets/styles/main.css` overrides
281
+ ```
282
+
283
+ ### 8. Confirm next step for the user
284
+
285
+ Tell the user to run:
286
+
287
+ ```bash
288
+ npm install
289
+ ```
290
+
291
+ `postinstall` will run `nuxt prepare` and copy the layer skills automatically.
292
+
293
+ ## Notes
294
+
295
+ - If the repo contains a `.claude/design.md`, read it before writing `main.css` — use any
296
+ brand colours or font choices from it to seed the CSS tokens.
297
+ - If security modules are not wanted, omit `nuxt-security` from `dependencies` and remove it
298
+ from `modules` and the `security` config block in `nuxt.config.ts`. Same for `@nuxtjs/robots`.
299
+ - The `colourScheme.enabled: false` default is correct for most consumer apps — enable only if
300
+ the app needs light/dark switching.
301
+ - Check the latest `srcdev-nuxt-components` version on npm before writing `package.json`.
package/README.md CHANGED
@@ -56,6 +56,21 @@ To ensure skills are always up to date and `nuxt prepare` is never forgotten, co
56
56
 
57
57
  ---
58
58
 
59
+ ## Scaffolding a New App
60
+
61
+ A Claude Code skill is included to scaffold a new Nuxt consumer app from scratch. It generates
62
+ `package.json`, `nuxt.config.ts`, ESLint/Prettier config, the full `app/` directory structure,
63
+ and a `CLAUDE.md` — all pre-wired to extend this layer correctly.
64
+
65
+ **Trigger it by saying to Claude Code:**
66
+
67
+ > "Scaffold a new layer consumer app. Repo: `/path/to/repo`, name: `my-app`, domain: `myapp.co.uk`, fonts: `Fraunces, Manrope`."
68
+
69
+ The skill is available at `.claude/skills/new-app-scaffold.md` once copied into your project
70
+ via `npm run setup:claude`.
71
+
72
+ ---
73
+
59
74
  ## Consumer App Configuration
60
75
 
61
76
  Configuration options for apps extending this layer. All options go in the consumer's `nuxt.config.ts` under `runtimeConfig.public` and can also be set via environment variable.