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.
Files changed (75) hide show
  1. package/.claude/settings.json +7 -2
  2. package/.claude/settings.local.json +2 -1
  3. package/.claude/skills/components/banner-video.md +186 -0
  4. package/.claude/skills/components/scroll-reveal-frame.md +136 -0
  5. package/.claude/skills/components/scroll-reveal-image.md +161 -0
  6. package/.claude/skills/index.md +7 -1
  7. package/.claude/skills/setup-postinstall.md +45 -0
  8. package/.claude/skills/theming-override-default.md +6 -0
  9. package/.claude/skills/theming-partial-override.md +131 -0
  10. package/.claude/skills/vue-video-autoplay.md +74 -0
  11. package/README.md +15 -5
  12. package/app/components/01.atoms/banner-video/BannerVideo.vue +201 -0
  13. package/app/components/01.atoms/banner-video/stories/BannerVideo.stories.ts +191 -0
  14. package/app/components/01.atoms/banner-video/tests/BannerVideo.spec.ts +374 -0
  15. package/app/components/01.atoms/banner-video/tests/__snapshots__/BannerVideo.spec.ts.snap +13 -0
  16. package/app/components/01.atoms/scroll-reveal-frame/ScrollRevealFrame.vue +125 -0
  17. package/app/components/01.atoms/scroll-reveal-frame/stories/ScrollRevealFrame.stories.ts +166 -0
  18. package/app/components/01.atoms/scroll-reveal-frame/tests/ScrollRevealFrame.spec.ts +148 -0
  19. package/app/components/01.atoms/scroll-reveal-frame/tests/__snapshots__/ScrollRevealFrame.spec.ts.snap +13 -0
  20. package/app/components/01.atoms/scroll-reveal-image/ScrollRevealImage.vue +96 -0
  21. package/app/components/01.atoms/scroll-reveal-image/stories/ScrollRevealImage.stories.ts +181 -0
  22. package/app/components/01.atoms/scroll-reveal-image/tests/ScrollRevealImage.spec.ts +235 -0
  23. package/app/components/01.atoms/scroll-reveal-image/tests/__snapshots__/ScrollRevealImage.spec.ts.snap +13 -0
  24. package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +1 -0
  25. package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +1 -0
  26. package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +1 -0
  27. package/app/components/03.organisms/services/services-grids/stories/ServicesSectionGrid.stories.ts +1 -0
  28. package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +1 -0
  29. package/app/components/03.organisms/services/services-grids/tests/ServicesSectionGrid.spec.ts +1 -0
  30. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesSectionGrid.spec.ts.snap +3 -3
  31. package/app/components/03.organisms/services/services-section/stories/ServicesSection.stories.ts +1 -0
  32. package/app/components/03.organisms/services/services-section/tests/ServicesSection.spec.ts +1 -0
  33. package/app/components/03.organisms/services/services-section/tests/__snapshots__/ServicesSection.spec.ts.snap +1 -1
  34. package/app/layouts/default.vue +2 -0
  35. package/app/pages/banner-video.vue +296 -0
  36. package/app/pages/ui/scroll-reveal-image.vue +483 -0
  37. package/app/types/types.services.ts +1 -0
  38. package/package.json +1 -1
  39. package/app/assets/styles/extends-layer/srcdev-components/components/_display-prompt-core.css +0 -94
  40. package/app/assets/styles/extends-layer/srcdev-components/components/_display-toast.css +0 -5
  41. package/app/assets/styles/extends-layer/srcdev-components/components/_expanding-panel.css +0 -37
  42. package/app/assets/styles/extends-layer/srcdev-components/components/index.css +0 -3
  43. package/app/assets/styles/extends-layer/srcdev-components/index.css +0 -1
  44. package/app/assets/styles/extends-layer/srcdev-forms/components/_form-fieldset.css +0 -38
  45. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-button.css +0 -67
  46. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-checkbox-radio-core.css +0 -87
  47. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-checkbox-radio-options-button.css +0 -74
  48. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-checkbox-radio-with-label.css +0 -14
  49. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-description.css +0 -13
  50. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-error.css +0 -54
  51. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-label.css +0 -21
  52. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-select.css +0 -143
  53. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-text.css +0 -190
  54. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-textarea.css +0 -96
  55. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-toggle-switch.css +0 -68
  56. package/app/assets/styles/extends-layer/srcdev-forms/components/index.css +0 -12
  57. package/app/assets/styles/extends-layer/srcdev-forms/index.css +0 -2
  58. package/app/assets/styles/extends-layer/srcdev-forms/setup/_generic.css +0 -21
  59. package/app/assets/styles/extends-layer/srcdev-forms/setup/index.css +0 -3
  60. package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_default.css +0 -13
  61. package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_large.css +0 -12
  62. package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_medium.css +0 -12
  63. package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_small.css +0 -12
  64. package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_x-small.css +0 -11
  65. package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/index.css +0 -23
  66. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_default.css +0 -62
  67. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_error.css +0 -63
  68. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_ghost.css +0 -63
  69. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_info.css +0 -63
  70. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_primary.css +0 -66
  71. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_secondary.css +0 -64
  72. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_success.css +0 -63
  73. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_tertiary.css +0 -63
  74. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_warning.css +0 -63
  75. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/index.css +0 -7
@@ -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.
@@ -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
- ├── theming-override-default.md override the default theme with a custom colour scale
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-*`.