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.
- package/.claude/skills/components/banner-video.md +37 -45
- package/.claude/skills/components/grid-stack.md +133 -0
- package/.claude/skills/index.md +3 -1
- package/.claude/skills/new-app-scaffold.md +301 -0
- package/README.md +15 -0
- package/app/components/01.atoms/banner-video/BannerVideo.vue +25 -19
- package/app/components/01.atoms/banner-video/stories/BannerVideo.stories.ts +25 -36
- package/app/components/01.atoms/banner-video/tests/BannerVideo.spec.ts +15 -39
- package/app/components/01.atoms/banner-video/tests/__snapshots__/BannerVideo.spec.ts.snap +2 -2
- package/app/components/01.atoms/grid-stack/GridStack.vue +34 -0
- package/app/components/01.atoms/grid-stack/stories/GridStack.stories.ts +162 -0
- package/app/components/01.atoms/grid-stack/tests/GridStack.spec.ts +66 -0
- package/app/pages/banner-video.vue +13 -71
- package/app/pages/grid-stack.vue +252 -0
- package/app/pages/index.vue +5 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: BannerVideo
|
|
3
|
-
description: BannerVideo full-width hero video banner — props, verticalPosition/horizontalPosition,
|
|
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
|
|
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
|
-
| `
|
|
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
|
-
###
|
|
48
|
+
### Depth tiers
|
|
51
49
|
|
|
52
50
|
```vue
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
54
|
+
<!-- sm: clamp(18rem, 22vw, 36rem) -->
|
|
55
|
+
<BannerVideo src="…" poster="…" depth="sm" />
|
|
64
56
|
|
|
65
|
-
|
|
66
|
-
<BannerVideo
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
+
### Depth token defaults
|
|
140
135
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
--_max-height: 40rem;
|
|
148
|
-
}
|
|
144
|
+
**Override a tier in a consuming page:**
|
|
149
145
|
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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.
|
package/.claude/skills/index.md
CHANGED
|
@@ -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,
|
|
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.
|