srcdev-nuxt-components 9.1.28 → 9.1.29
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/settings.json +7 -2
- package/.claude/settings.local.json +2 -1
- package/.claude/skills/components/banner-video.md +186 -0
- package/.claude/skills/components/scroll-reveal-frame.md +136 -0
- package/.claude/skills/components/scroll-reveal-image.md +161 -0
- package/.claude/skills/index.md +7 -1
- package/.claude/skills/setup-postinstall.md +45 -0
- package/.claude/skills/theming-override-default.md +6 -0
- package/.claude/skills/theming-partial-override.md +131 -0
- package/.claude/skills/vue-video-autoplay.md +74 -0
- package/README.md +15 -5
- package/app/components/01.atoms/banner-video/BannerVideo.vue +201 -0
- package/app/components/01.atoms/banner-video/stories/BannerVideo.stories.ts +191 -0
- package/app/components/01.atoms/banner-video/tests/BannerVideo.spec.ts +374 -0
- package/app/components/01.atoms/banner-video/tests/__snapshots__/BannerVideo.spec.ts.snap +13 -0
- package/app/components/01.atoms/scroll-reveal-frame/ScrollRevealFrame.vue +125 -0
- package/app/components/01.atoms/scroll-reveal-frame/stories/ScrollRevealFrame.stories.ts +166 -0
- package/app/components/01.atoms/scroll-reveal-frame/tests/ScrollRevealFrame.spec.ts +148 -0
- package/app/components/01.atoms/scroll-reveal-frame/tests/__snapshots__/ScrollRevealFrame.spec.ts.snap +13 -0
- package/app/components/01.atoms/scroll-reveal-image/ScrollRevealImage.vue +96 -0
- package/app/components/01.atoms/scroll-reveal-image/stories/ScrollRevealImage.stories.ts +181 -0
- package/app/components/01.atoms/scroll-reveal-image/tests/ScrollRevealImage.spec.ts +235 -0
- package/app/components/01.atoms/scroll-reveal-image/tests/__snapshots__/ScrollRevealImage.spec.ts.snap +13 -0
- package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +1 -0
- package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +1 -0
- package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +1 -0
- package/app/components/03.organisms/services/services-grids/stories/ServicesSectionGrid.stories.ts +1 -0
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +1 -0
- package/app/components/03.organisms/services/services-grids/tests/ServicesSectionGrid.spec.ts +1 -0
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesSectionGrid.spec.ts.snap +3 -3
- package/app/components/03.organisms/services/services-section/stories/ServicesSection.stories.ts +1 -0
- package/app/components/03.organisms/services/services-section/tests/ServicesSection.spec.ts +1 -0
- package/app/components/03.organisms/services/services-section/tests/__snapshots__/ServicesSection.spec.ts.snap +1 -1
- package/app/layouts/default.vue +2 -0
- package/app/pages/banner-video.vue +296 -0
- package/app/pages/ui/scroll-reveal-image.vue +483 -0
- package/app/types/types.services.ts +1 -0
- package/package.json +1 -1
- package/app/assets/styles/extends-layer/srcdev-components/components/_display-prompt-core.css +0 -94
- package/app/assets/styles/extends-layer/srcdev-components/components/_display-toast.css +0 -5
- package/app/assets/styles/extends-layer/srcdev-components/components/_expanding-panel.css +0 -37
- package/app/assets/styles/extends-layer/srcdev-components/components/index.css +0 -3
- package/app/assets/styles/extends-layer/srcdev-components/index.css +0 -1
- package/app/assets/styles/extends-layer/srcdev-forms/components/_form-fieldset.css +0 -38
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-button.css +0 -67
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-checkbox-radio-core.css +0 -87
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-checkbox-radio-options-button.css +0 -74
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-checkbox-radio-with-label.css +0 -14
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-description.css +0 -13
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-error.css +0 -54
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-label.css +0 -21
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-select.css +0 -143
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-text.css +0 -190
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-textarea.css +0 -96
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-toggle-switch.css +0 -68
- package/app/assets/styles/extends-layer/srcdev-forms/components/index.css +0 -12
- package/app/assets/styles/extends-layer/srcdev-forms/index.css +0 -2
- package/app/assets/styles/extends-layer/srcdev-forms/setup/_generic.css +0 -21
- package/app/assets/styles/extends-layer/srcdev-forms/setup/index.css +0 -3
- package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_default.css +0 -13
- package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_large.css +0 -12
- package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_medium.css +0 -12
- package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_small.css +0 -12
- package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_x-small.css +0 -11
- package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/index.css +0 -23
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_default.css +0 -62
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_error.css +0 -63
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_ghost.css +0 -63
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_info.css +0 -63
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_primary.css +0 -66
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_secondary.css +0 -64
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_success.css +0 -63
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_tertiary.css +0 -63
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_warning.css +0 -63
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/index.css +0 -7
package/.claude/settings.json
CHANGED
|
@@ -36,13 +36,18 @@
|
|
|
36
36
|
"Bash(git -C /Users/simoncornforth/websites/nuxt-components log --oneline v9.1.22..HEAD)",
|
|
37
37
|
"Bash(git -C /Users/simoncornforth/websites/nuxt-components log --oneline c401cea..HEAD)",
|
|
38
38
|
"Bash(git -C /Users/simoncornforth/websites/nuxt-components show 3058d63 --stat)",
|
|
39
|
-
"Bash(git -C /Users/simoncornforth/websites/nuxt-components show 0f99d7a --stat)"
|
|
39
|
+
"Bash(git -C /Users/simoncornforth/websites/nuxt-components show 0f99d7a --stat)",
|
|
40
|
+
"Bash(node -e ':*)"
|
|
40
41
|
],
|
|
41
42
|
"additionalDirectories": [
|
|
42
43
|
"/Users/simoncornforth/websites/nuxt-components/app/components/01.atoms/content-wrappers/content-width",
|
|
43
44
|
"/Users/simoncornforth/websites/nuxt-components/.claude/skills",
|
|
44
45
|
"/Users/simoncornforth/websites/nuxt-components/app/components/02.molecules/navigation/site-navigation/tests",
|
|
45
|
-
"/Users/simoncornforth/websites/luxury-locs-by-natasha-nuxt3/app/pages"
|
|
46
|
+
"/Users/simoncornforth/websites/luxury-locs-by-natasha-nuxt3/app/pages",
|
|
47
|
+
"/Users/simoncornforth/websites/nuxt-components/app/assets/styles/extends-layer",
|
|
48
|
+
"/Users/simoncornforth/websites/nuxt-components/app/components/01.atoms",
|
|
49
|
+
"/private/tmp",
|
|
50
|
+
"/Users/simoncornforth/websites/nuxt-components/app/components/01.atoms/banner-video"
|
|
46
51
|
]
|
|
47
52
|
}
|
|
48
53
|
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
"WebFetch(domain:drafts.csswg.org)",
|
|
7
7
|
"Bash(grep '\"\"typescript\"\"' package.json)",
|
|
8
8
|
"Bash(grep '\"\"@vue/language-server\\\\|@volar\\\\|vue-tsc\"\"' package.json)",
|
|
9
|
-
"Bash(npx nuxi prepare)"
|
|
9
|
+
"Bash(npx nuxi prepare)",
|
|
10
|
+
"Bash(ls -la /Users/simoncornforth/websites/luxury-locs-by-natasha-nuxt3/.env*)"
|
|
10
11
|
]
|
|
11
12
|
}
|
|
12
13
|
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: BannerVideo
|
|
3
|
+
description: BannerVideo full-width hero video banner — props, verticalPosition/horizontalPosition, responsive max-height, reduced-motion fallback, CSS tokens, consumer styling
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# BannerVideo
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
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
|
+
|
|
13
|
+
The banner is sized via `aspect-ratio` so it scales naturally, with `max-height` props capping height at each breakpoint.
|
|
14
|
+
|
|
15
|
+
### Autoplay mechanism
|
|
16
|
+
|
|
17
|
+
The video uses `autoplay muted loop playsinline preload="auto"` attributes on the `<video>` element with the src in a `<source>` child. A `:key="src"` on the video element forces Vue to remount it on src changes. `@loadeddata` and `@canplay` events call `tryPlay()`, and a `watch(() => props.src, ..., { immediate: true, flush: 'post' })` calls `kickOffLoad()` (which calls `v.load()` then `tryPlay()`) on mount and src change. `onActivated` handles keep-alive re-activation.
|
|
18
|
+
|
|
19
|
+
## Props
|
|
20
|
+
|
|
21
|
+
| Prop | Type | Default | Description |
|
|
22
|
+
|------|------|---------|-------------|
|
|
23
|
+
| `src` | `string` | — | Path to the mp4 video source. **Required.** |
|
|
24
|
+
| `poster` | `string` | — | Path to the fallback/poster image. **Required.** Used as the video `poster` attribute and as the `prefers-reduced-motion` fallback. |
|
|
25
|
+
| `alt` | `string` | `""` | Alt text for the fallback `NuxtImg`. |
|
|
26
|
+
| `imgWidth` | `number` | `1920` | Intrinsic width of the poster image — required for NuxtImg/IPX optimisation. |
|
|
27
|
+
| `imgHeight` | `number` | `1080` | Intrinsic height of the poster image — required for NuxtImg/IPX optimisation. |
|
|
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. |
|
|
32
|
+
| `aspectRatio` | `string` | `"21/9"` | CSS `aspect-ratio` of the container (e.g. `"16/9"`, `"21/9"`, `"4/3"`). |
|
|
33
|
+
| `objectFit` | `"cover" \| "contain" \| "fill" \| "none" \| "scale-down"` | `"cover"` | How the video and fallback image fill the banner frame. |
|
|
34
|
+
| `verticalPosition` | `"start" \| "center" \| "end"` | `"center"` | Vertical crop position. Maps to `align-self` on the video element and `object-position` Y on the fallback image. |
|
|
35
|
+
| `horizontalPosition` | `"start" \| "center" \| "end"` | `"center"` | Horizontal crop position. Maps to `object-position` X on the fallback image. |
|
|
36
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
|
|
37
|
+
|
|
38
|
+
## Basic usage
|
|
39
|
+
|
|
40
|
+
```vue
|
|
41
|
+
<BannerVideo
|
|
42
|
+
src="/videos/hero.mp4"
|
|
43
|
+
poster="/images/hero-poster.jpg"
|
|
44
|
+
alt="Studio interior"
|
|
45
|
+
/>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Common variants
|
|
49
|
+
|
|
50
|
+
### Responsive max-height per breakpoint
|
|
51
|
+
|
|
52
|
+
```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
|
+
```
|
|
62
|
+
|
|
63
|
+
### Tall viewport-filling banner
|
|
64
|
+
|
|
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
|
+
/>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Custom focal point
|
|
76
|
+
|
|
77
|
+
```vue
|
|
78
|
+
<BannerVideo
|
|
79
|
+
src="/videos/hero.mp4"
|
|
80
|
+
poster="/images/hero-poster.jpg"
|
|
81
|
+
alt="Studio interior"
|
|
82
|
+
vertical-position="end"
|
|
83
|
+
horizontal-position="center"
|
|
84
|
+
/>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Full bleed with no block margin
|
|
88
|
+
|
|
89
|
+
```vue
|
|
90
|
+
<BannerVideo
|
|
91
|
+
src="/videos/hero.mp4"
|
|
92
|
+
poster="/images/hero-poster.jpg"
|
|
93
|
+
alt="Studio interior"
|
|
94
|
+
:style-class-passthrough="['full-bleed', 'mbe-0']"
|
|
95
|
+
/>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Positioning explained
|
|
99
|
+
|
|
100
|
+
The video element uses `height: auto; min-height: 100%` so it overflows the container naturally when aspect ratios differ. `overflow: hidden` on the root clips it. `verticalPosition` maps to `align-self` on the video (shifting which portion of the overflow is visible) and to `object-position` Y on the fallback image.
|
|
101
|
+
|
|
102
|
+
`horizontalPosition` only affects the fallback image via `object-position` X — the video fills full width so horizontal alignment is a no-op on the video element itself.
|
|
103
|
+
|
|
104
|
+
| verticalPosition | align-self | object-position Y |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `"start"` | `start` | `top` |
|
|
107
|
+
| `"center"` (default) | `center` | `center` |
|
|
108
|
+
| `"end"` | `end` | `bottom` |
|
|
109
|
+
|
|
110
|
+
| horizontalPosition | object-position X |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `"start"` | `left` |
|
|
113
|
+
| `"center"` (default) | `center` |
|
|
114
|
+
| `"end"` | `right` |
|
|
115
|
+
|
|
116
|
+
## imgWidth / imgHeight
|
|
117
|
+
|
|
118
|
+
Always match the intrinsic dimensions of the poster file. NuxtImg uses them to avoid the `w=1536` fallback (not in Vercel's allowed widths: 640, 750, 828, 1080, 1200, 1920, 2048, 3840).
|
|
119
|
+
|
|
120
|
+
| Image type | imgWidth | imgHeight |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| Landscape / banner (16:9) | `1920` | `1080` |
|
|
123
|
+
| Wide banner (12:5) | `1920` | `800` |
|
|
124
|
+
| Square | `800` | `800` |
|
|
125
|
+
|
|
126
|
+
## CSS custom properties
|
|
127
|
+
|
|
128
|
+
All set from props via inline `:style` on the root element.
|
|
129
|
+
|
|
130
|
+
| Property | Default | Set by prop |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `--_max-height` | `56rem` | `maxHeight` |
|
|
133
|
+
| `--_max-height-tablet` | *(unset)* | `maxHeightTablet` |
|
|
134
|
+
| `--_max-height-mobile` | *(unset)* | `maxHeightMobile` |
|
|
135
|
+
| `--_aspect-ratio` | `21/9` | `aspectRatio` |
|
|
136
|
+
| `--_align-self` | `center` | `verticalPosition` |
|
|
137
|
+
| `--_justify-self` | `center` | `horizontalPosition` |
|
|
138
|
+
|
|
139
|
+
**Responsive override example:**
|
|
140
|
+
|
|
141
|
+
```css
|
|
142
|
+
.my-page {
|
|
143
|
+
.banner-video {
|
|
144
|
+
--_max-height: 56rem;
|
|
145
|
+
|
|
146
|
+
@media (width < 1024px) {
|
|
147
|
+
--_max-height: 40rem;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@media (width < 768px) {
|
|
151
|
+
--_max-height: 24rem;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## CSS classes
|
|
158
|
+
|
|
159
|
+
| Class | Applied when |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `.banner-video` | Always — the root element |
|
|
162
|
+
| `.video` | The `<video>` element |
|
|
163
|
+
| `.fallback` | The `<NuxtImg>` fallback — hidden by default, shown via `prefers-reduced-motion` CSS |
|
|
164
|
+
|
|
165
|
+
## Consumer styling
|
|
166
|
+
|
|
167
|
+
Use an unscoped style block scoped by a page or section wrapper class. No `:deep()` needed — the component's styles live in `@layer components` so page styles win automatically.
|
|
168
|
+
|
|
169
|
+
```vue
|
|
170
|
+
<style>
|
|
171
|
+
.my-page {
|
|
172
|
+
.banner-video {
|
|
173
|
+
@media (width < 900px) {
|
|
174
|
+
--_max-height: 36rem;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
</style>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Notes
|
|
182
|
+
|
|
183
|
+
- `loading="eager"` and `decoding="async"` are hardcoded on the fallback `NuxtImg` — it is above the fold by definition.
|
|
184
|
+
- The video has `autoplay muted loop playsinline preload="auto"` — intentional and not configurable. This component is for ambient background video only, not user-controlled media.
|
|
185
|
+
- `prefers-reduced-motion` is handled in CSS (`.video { display: none }` + `.fallback { display: block }`), not via JS.
|
|
186
|
+
- Storybook: the `"none"` image provider is active, so `poster` paths pass through unchanged. Always provide explicit `img-width` and `img-height` to avoid the `w=1536` fallback in deployed Storybook.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ScrollRevealFrame
|
|
3
|
+
description: ScrollRevealFrame generic parallax clipping frame — props, slot API, CSS tokens, browser support, when to use vs ScrollRevealImage
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ScrollRevealFrame
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
`ScrollRevealFrame` is a generic clipping frame that pans its slot content vertically as it scrolls through the viewport — driven entirely by CSS Scroll-driven Animations. No scroll event listeners, no `requestAnimationFrame`, no `IntersectionObserver`.
|
|
12
|
+
|
|
13
|
+
Use `ScrollRevealFrame` when the content inside the frame is **anything other than a single `NuxtImg`** — a grid of images, a video, a card, arbitrary markup. For a single optimised image with focal-point control, use `ScrollRevealImage` instead (it wraps this component).
|
|
14
|
+
|
|
15
|
+
## How it works
|
|
16
|
+
|
|
17
|
+
- The `<figure>` root is a fixed-height clipping window (`overflow: hidden`) that registers a named `view-timeline`.
|
|
18
|
+
- The inner `.reveal-content` wrapper is taller than the frame by `parallaxOffset` and animates `translateY` as the frame scrolls through the viewport.
|
|
19
|
+
- Slot content fills that wrapper — anything inside pans as a unit.
|
|
20
|
+
|
|
21
|
+
Browser support (as of 2026): Chrome 115+, Edge 115+, Firefox 114+, Safari 17.2+. Older browsers fall back to a static cropped view.
|
|
22
|
+
|
|
23
|
+
## Props
|
|
24
|
+
|
|
25
|
+
| Prop | Type | Default | Description |
|
|
26
|
+
|------|------|---------|-------------|
|
|
27
|
+
| `frameHeight` | `string` | `"540px"` | Height of the visible clipping frame. Any CSS length unit (`px`, `vh`, `rem`). |
|
|
28
|
+
| `parallaxOffset` | `string` | `"36rem"` | Distance the content travels vertically across the full scroll range. Larger = more dramatic reveal. |
|
|
29
|
+
| `radius` | `string` | `"0px"` | `border-radius` applied to the clipping frame. Slot content stays contained within the rounded shape. |
|
|
30
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root `<figure>`. |
|
|
31
|
+
|
|
32
|
+
## Slots
|
|
33
|
+
|
|
34
|
+
| Slot | Description |
|
|
35
|
+
|------|-------------|
|
|
36
|
+
| `default` | Content to pan. It fills a `div.reveal-content` that is taller than the frame by `parallaxOffset`. |
|
|
37
|
+
|
|
38
|
+
## Basic usage — single image
|
|
39
|
+
|
|
40
|
+
```vue
|
|
41
|
+
<ScrollRevealFrame frame-height="540px" parallax-offset="36rem">
|
|
42
|
+
<img
|
|
43
|
+
src="/images/hero.jpg"
|
|
44
|
+
alt="Hero"
|
|
45
|
+
style="width: 100%; height: 100%; object-fit: cover; display: block;"
|
|
46
|
+
/>
|
|
47
|
+
</ScrollRevealFrame>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Grid of images
|
|
51
|
+
|
|
52
|
+
Wrap each `<img>` / `<NuxtImg>` in a `<div>` cell — `object-fit` on an `<img>` that is a direct grid item still lets the intrinsic dimensions influence the cell size. The wrapper takes the grid sizing; the image fills it.
|
|
53
|
+
|
|
54
|
+
```vue
|
|
55
|
+
<ScrollRevealFrame frame-height="480px" parallax-offset="36rem">
|
|
56
|
+
<div class="image-grid">
|
|
57
|
+
<div class="image-grid__cell">
|
|
58
|
+
<NuxtImg src="/images/a.jpg" alt="A" :width="800" :height="800" class="image-grid__img" />
|
|
59
|
+
</div>
|
|
60
|
+
<div class="image-grid__cell">
|
|
61
|
+
<NuxtImg src="/images/b.jpg" alt="B" :width="800" :height="800" class="image-grid__img" />
|
|
62
|
+
</div>
|
|
63
|
+
<div class="image-grid__cell">
|
|
64
|
+
<NuxtImg src="/images/c.jpg" alt="C" :width="800" :height="800" class="image-grid__img" />
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</ScrollRevealFrame>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```css
|
|
71
|
+
.image-grid {
|
|
72
|
+
display: grid;
|
|
73
|
+
grid-template-columns: repeat(3, 1fr);
|
|
74
|
+
grid-template-rows: repeat(1, 1fr); /* adjust for row count */
|
|
75
|
+
gap: 4px;
|
|
76
|
+
height: 100%;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.image-grid__cell {
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
min-height: 0; /* prevent grid blowout from intrinsic image size */
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.image-grid__img {
|
|
85
|
+
display: block;
|
|
86
|
+
width: 100%;
|
|
87
|
+
height: 100%;
|
|
88
|
+
object-fit: cover;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Responsive frame height
|
|
93
|
+
|
|
94
|
+
The `frameHeight` prop sets `--_frame-height` as an inline style. To vary it responsively, override the CSS custom property in a scoped style block:
|
|
95
|
+
|
|
96
|
+
```css
|
|
97
|
+
.my-page {
|
|
98
|
+
.reveal-frame {
|
|
99
|
+
--_frame-height: 320px;
|
|
100
|
+
|
|
101
|
+
@media (width >= 768px) {
|
|
102
|
+
--_frame-height: 540px;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## CSS custom properties
|
|
109
|
+
|
|
110
|
+
These are set from props via inline `:style` but can be overridden in CSS for responsive or contextual control.
|
|
111
|
+
|
|
112
|
+
| Property | Default | Set by prop |
|
|
113
|
+
|----------|---------|-------------|
|
|
114
|
+
| `--_frame-height` | `540px` | `frameHeight` |
|
|
115
|
+
| `--_parallax-offset` | `36rem` | `parallaxOffset` |
|
|
116
|
+
| `--_radius` | `0px` | `radius` |
|
|
117
|
+
|
|
118
|
+
## Choosing parallaxOffset
|
|
119
|
+
|
|
120
|
+
The animation spans the full time the frame is in the viewport. On a typical desktop (~900px viewport, 540px frame) the total scroll travel is ~1440px. A rule of thumb:
|
|
121
|
+
|
|
122
|
+
| Frame height | Recommended parallaxOffset |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `320px` | `20rem–24rem` |
|
|
125
|
+
| `480px` | `28rem–36rem` |
|
|
126
|
+
| `540px` | `36rem` (default) |
|
|
127
|
+
| `70vh` | `48rem–60rem` |
|
|
128
|
+
|
|
129
|
+
Values below `20rem` tend to look static at normal scroll speeds.
|
|
130
|
+
|
|
131
|
+
## Notes
|
|
132
|
+
|
|
133
|
+
- `overflow: hidden` is on the root `<figure>` — content that needs to escape (dropdowns, tooltips) must be portalled outside.
|
|
134
|
+
- The named `view-timeline` (`--reveal-frame-timeline`) is scoped to the component. Multiple `ScrollRevealFrame` instances on the same page are independent.
|
|
135
|
+
- Reduced-motion: the animation is disabled and the content falls back to a static centred crop via `@media (prefers-reduced-motion: reduce)`.
|
|
136
|
+
- Do not put `ScrollRevealFrame` inside a container with `overflow: hidden` or `overflow: clip` — this breaks the `view-timeline` scroll detection.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ScrollRevealImage
|
|
3
|
+
description: ScrollRevealImage single-image parallax reveal — props, focalX usage, CSS tokens, browser support, when to use vs ScrollRevealFrame
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ScrollRevealImage
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
`ScrollRevealImage` is a convenience wrapper around `ScrollRevealFrame` for the common case of a single optimised image. It adds `NuxtImg` (with intrinsic dimension props for Vercel/IPX optimisation) and a `focalX` prop to pin the horizontal crop axis as the image pans vertically.
|
|
12
|
+
|
|
13
|
+
For arbitrary slot content — a grid of images, video, markup — use `ScrollRevealFrame` directly.
|
|
14
|
+
|
|
15
|
+
## Props
|
|
16
|
+
|
|
17
|
+
| Prop | Type | Default | Description |
|
|
18
|
+
|------|------|---------|-------------|
|
|
19
|
+
| `src` | `string` | — | Image source path. **Required.** |
|
|
20
|
+
| `alt` | `string` | `""` | Alt text for the image. |
|
|
21
|
+
| `imgWidth` | `number` | `1920` | Intrinsic width of the source image — required for NuxtImg optimisation. |
|
|
22
|
+
| `imgHeight` | `number` | `1080` | Intrinsic height of the source image — required for NuxtImg optimisation. |
|
|
23
|
+
| `frameHeight` | `string` | `"540px"` | Height of the visible clipping frame. Any CSS length unit (`px`, `vh`, `rem`). |
|
|
24
|
+
| `parallaxOffset` | `string` | `"36rem"` | Distance the image travels vertically across the full scroll range. Larger = more dramatic reveal. |
|
|
25
|
+
| `focalX` | `string` | `"50%"` | Horizontal focal point — CSS `object-position` x-axis value. Controls which horizontal slice stays in view. |
|
|
26
|
+
| `radius` | `string` | `"0px"` | `border-radius` applied to the clipping frame. |
|
|
27
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root `<figure>`. |
|
|
28
|
+
|
|
29
|
+
## Basic usage
|
|
30
|
+
|
|
31
|
+
```vue
|
|
32
|
+
<ScrollRevealImage
|
|
33
|
+
src="/images/hero.jpg"
|
|
34
|
+
alt="Hair portrait"
|
|
35
|
+
:img-width="1280"
|
|
36
|
+
:img-height="1920"
|
|
37
|
+
/>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Common variants
|
|
41
|
+
|
|
42
|
+
### Short banner frame
|
|
43
|
+
|
|
44
|
+
```vue
|
|
45
|
+
<ScrollRevealImage
|
|
46
|
+
src="/images/banner.webp"
|
|
47
|
+
alt="Banner"
|
|
48
|
+
:img-width="1920"
|
|
49
|
+
:img-height="800"
|
|
50
|
+
frame-height="320px"
|
|
51
|
+
parallax-offset="24rem"
|
|
52
|
+
/>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Rounded corners
|
|
56
|
+
|
|
57
|
+
```vue
|
|
58
|
+
<ScrollRevealImage
|
|
59
|
+
src="/images/hero.jpg"
|
|
60
|
+
alt="Portrait"
|
|
61
|
+
:img-width="1280"
|
|
62
|
+
:img-height="1920"
|
|
63
|
+
frame-height="480px"
|
|
64
|
+
radius="2.4rem"
|
|
65
|
+
/>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Horizontal focal point
|
|
69
|
+
|
|
70
|
+
```vue
|
|
71
|
+
<ScrollRevealImage
|
|
72
|
+
src="/images/hero.jpg"
|
|
73
|
+
alt="Portrait"
|
|
74
|
+
:img-width="1280"
|
|
75
|
+
:img-height="1920"
|
|
76
|
+
focal-x="75%"
|
|
77
|
+
/>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Tall frame — maximum travel
|
|
81
|
+
|
|
82
|
+
```vue
|
|
83
|
+
<ScrollRevealImage
|
|
84
|
+
src="/images/hero.jpg"
|
|
85
|
+
alt="Portrait"
|
|
86
|
+
:img-width="1280"
|
|
87
|
+
:img-height="1920"
|
|
88
|
+
frame-height="70vh"
|
|
89
|
+
parallax-offset="60rem"
|
|
90
|
+
/>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## focalX explained
|
|
94
|
+
|
|
95
|
+
The vertical position of the image is driven by the scroll animation (`translateY`). `focalX` controls the **horizontal** crop so the subject stays centred when the frame is narrower than the image.
|
|
96
|
+
|
|
97
|
+
| Value | Crops toward |
|
|
98
|
+
|-------|-------------|
|
|
99
|
+
| `"0%"` or `"left"` | Left edge |
|
|
100
|
+
| `"50%"` (default) | Centre |
|
|
101
|
+
| `"75%"` | Right of centre — useful for a subject offset to the right |
|
|
102
|
+
| `"100%"` or `"right"` | Right edge |
|
|
103
|
+
|
|
104
|
+
Internally, `focalX` sets `object-position: <focalX> 0%` on the `<img>`. The `Y` is always `0%` because the animation handles vertical travel.
|
|
105
|
+
|
|
106
|
+
## imgWidth / imgHeight
|
|
107
|
+
|
|
108
|
+
Always provide these to match the intrinsic dimensions of the source file. NuxtImg uses them to:
|
|
109
|
+
- Generate the correct `srcset` via the IPX pipeline
|
|
110
|
+
- Avoid the `w=1536` fallback (not in Vercel's allowed widths: 640, 750, 828, 1080, 1200, 1920, 2048, 3840)
|
|
111
|
+
|
|
112
|
+
Common pairs:
|
|
113
|
+
|
|
114
|
+
| Image type | imgWidth | imgHeight |
|
|
115
|
+
|-----------|----------|-----------|
|
|
116
|
+
| Portrait (3:4) | `1280` | `1920` |
|
|
117
|
+
| Landscape / banner (16:9) | `1920` | `1080` |
|
|
118
|
+
| Wide banner (12:5) | `1920` | `800` |
|
|
119
|
+
| Square | `800` | `800` |
|
|
120
|
+
|
|
121
|
+
## Responsive frame height
|
|
122
|
+
|
|
123
|
+
Override `--_frame-height` in a scoped style block for responsive control:
|
|
124
|
+
|
|
125
|
+
```css
|
|
126
|
+
.my-page {
|
|
127
|
+
.reveal-frame {
|
|
128
|
+
--_frame-height: 320px;
|
|
129
|
+
|
|
130
|
+
@media (width >= 768px) {
|
|
131
|
+
--_frame-height: 480px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@media (width >= 1024px) {
|
|
135
|
+
--_frame-height: 540px;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## CSS custom properties
|
|
142
|
+
|
|
143
|
+
Set from props via inline `:style` — override in CSS for responsive or contextual control.
|
|
144
|
+
|
|
145
|
+
| Property | Default | Set by prop |
|
|
146
|
+
|----------|---------|-------------|
|
|
147
|
+
| `--_frame-height` | `540px` | `frameHeight` |
|
|
148
|
+
| `--_parallax-offset` | `36rem` | `parallaxOffset` |
|
|
149
|
+
| `--_radius` | `0px` | `radius` |
|
|
150
|
+
| `--_focal-x` | `50%` | `focalX` |
|
|
151
|
+
|
|
152
|
+
## Choosing parallaxOffset
|
|
153
|
+
|
|
154
|
+
See `scroll-reveal-frame.md` for the full guide. For portrait images the default `36rem` is a reliable starting point. Landscape/banner images in shorter frames work better with `20rem–24rem`.
|
|
155
|
+
|
|
156
|
+
## Notes
|
|
157
|
+
|
|
158
|
+
- `loading="lazy"` and `decoding="async"` are hardcoded on the `<img>`. If this component is the LCP image, override with `loading="eager"` via a CSS-only approach is not possible — use `ScrollRevealFrame` with a manual `NuxtImg` instead and set `:loading="'eager'"`.
|
|
159
|
+
- Do not place inside a container with `overflow: hidden` or `overflow: clip` — breaks the `view-timeline` scroll detection inherited from `ScrollRevealFrame`.
|
|
160
|
+
- Reduced-motion: animation is disabled and the image falls back to a static crop centred at `object-position: <focalX> 50%`.
|
|
161
|
+
- Storybook: the `"none"` image provider is active (`nuxt.config.ts`), so `src` paths pass through unchanged. Always provide explicit `img-width` and `img-height` props to avoid the `w=1536` fallback in deployed Storybook.
|
package/.claude/skills/index.md
CHANGED
|
@@ -26,7 +26,9 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
26
26
|
├── storybook-add-font.md — add a new font to Storybook
|
|
27
27
|
├── testing-add-unit-test.md — create a Vitest unit test with snapshots
|
|
28
28
|
├── testing-add-playwright.md — create a Playwright visual regression test
|
|
29
|
-
├──
|
|
29
|
+
├── setup-postinstall.md — automate nuxt prepare + Claude skills copy via postinstall so neither is forgotten after npm install
|
|
30
|
+
├── theming-override-default.md — replace the entire default theme with a custom colour scale (full palette swap)
|
|
31
|
+
├── theming-partial-override.md — override a specific token category (forms, buttons, colours) without a full theme replacement
|
|
30
32
|
├── colour-scheme-disable.md — disable light/dark scheme support in a consumer app
|
|
31
33
|
├── component-dynamic-slots.md — named dynamic slots ($slots iteration) vs indexed dynamic slots (itemCount pattern)
|
|
32
34
|
├── component-local-style-override.md — styleClassPassthrough + scoped style block for per-usage visual customisation
|
|
@@ -35,6 +37,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
35
37
|
├── component-aria-landmark.md — useAriaLabelledById composable: aria-labelledby for section/main/article/aside tags
|
|
36
38
|
├── component-export-types.md — move inline component types to app/types/components/ barrel for consumer imports
|
|
37
39
|
├── component-inline-action-button.md — InputButtonCore variant="inline" pattern for buttons embedded in custom input wrappers
|
|
40
|
+
├── vue-video-autoplay.md — autoplay on client-side navigation: use <source> child (not :src on <video>), :key, and explicit v.load()
|
|
38
41
|
├── icon-sets.md — icon set packages required by layer components, FOUC prevention, component→package map
|
|
39
42
|
├── robots-env-aware.md — @nuxtjs/robots: allow crawling on prod domain only, block on preview/staging via env var
|
|
40
43
|
├── release-notes.md — produce release notes as a fenced markdown block from git log
|
|
@@ -61,6 +64,9 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
61
64
|
├── glass-panel.md — GlassPanel props, slots, CSS token API (--glass-panel-bg/border-color/shadow/highlight), theming override
|
|
62
65
|
├── navigation-horizontal.md — NavigationHorizontal props, NavItemData type, CSS token API, import path gotcha
|
|
63
66
|
├── 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
|
+
├── scroll-reveal-frame.md — ScrollRevealFrame: generic parallax clipping frame, slot API, image grid pattern, CSS tokens, browser support
|
|
69
|
+
├── scroll-reveal-image.md — ScrollRevealImage: single-image parallax reveal, focalX, imgWidth/imgHeight, responsive frame height
|
|
64
70
|
├── site-navigation.md — SiteNavigation: responsive nav with auto-collapse, burger menu, decorator indicators, CSS token API
|
|
65
71
|
└── social-icons-list.md — SocialIconsList: data-driven social icon links, ISocialIcon type, logos: icon names, CSS tokens
|
|
66
72
|
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Automate nuxt prepare and Claude Skills with postinstall
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Prevent two common "forgot to run after install" problems in a consuming app:
|
|
6
|
+
|
|
7
|
+
1. `nuxt prepare` — generates Nuxt type declarations. Skipping it causes TypeScript errors after install or package updates.
|
|
8
|
+
2. `npm run setup:claude` — copies the latest `srcdev-nuxt-components` skills into `.claude/skills/srcdev-nuxt-components/`. Skipping it leaves Claude working from stale skill docs after a package update.
|
|
9
|
+
|
|
10
|
+
A `postinstall` script runs both automatically after every `npm install`.
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
### 1. Add the scripts to package.json
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
"scripts": {
|
|
18
|
+
"setup:claude": "cp -r node_modules/srcdev-nuxt-components/.claude/skills .claude/skills/srcdev-nuxt-components",
|
|
19
|
+
"postinstall": "nuxt prepare && npm run setup:claude"
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 2. Check whether your app needs an env flag for nuxt prepare
|
|
24
|
+
|
|
25
|
+
Some apps set an env var to switch `nuxt.config.ts` behaviour when running outside the full dev server (e.g. `NUXT_STANDALONE=true`). Check your own `nuxt.config.ts` — if it gates any config behind such a variable, add it to the `postinstall` command:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
"postinstall": "NUXT_STANDALONE=true nuxt prepare && npm run setup:claude"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If your `nuxt.config.ts` has no such conditional, plain `nuxt prepare` is sufficient.
|
|
32
|
+
|
|
33
|
+
### 3. Run once manually to bootstrap
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm run setup:claude
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
From this point on, `npm install` and `npm ci` trigger both steps automatically.
|
|
40
|
+
|
|
41
|
+
## Notes
|
|
42
|
+
|
|
43
|
+
- Skills land in `.claude/skills/srcdev-nuxt-components/` — safe to re-run without overwriting your own project's skills.
|
|
44
|
+
- `postinstall` also fires on `npm ci`, so CI environments get the skills too if they have a `.claude/` directory in the project.
|
|
45
|
+
- If you do not want `postinstall` running in CI, guard it: `"postinstall": "[ \"$CI\" = \"true\" ] || (nuxt prepare && npm run setup:claude)"`.
|
|
@@ -241,6 +241,12 @@ The consuming app's CSS loads after the layer's, so your token overrides win via
|
|
|
241
241
|
| 24px | 2.4rem |
|
|
242
242
|
| 32px | 3.2rem |
|
|
243
243
|
|
|
244
|
+
## Token reference
|
|
245
|
+
|
|
246
|
+
See `CONSUMER-STYLING.md` in the layer package root for the full list of available tokens grouped
|
|
247
|
+
by category. This is the authoritative reference when deciding which tokens to include in your
|
|
248
|
+
override files.
|
|
249
|
+
|
|
244
250
|
## Notes
|
|
245
251
|
|
|
246
252
|
- The `--slate-*` scale comes from the layer and does not need to be redefined — keep all neutral/background tokens pointing at `--slate-*`.
|