srcdev-nuxt-components 9.1.28 → 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.
Files changed (82) 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 +178 -0
  4. package/.claude/skills/components/grid-stack.md +133 -0
  5. package/.claude/skills/components/scroll-reveal-frame.md +136 -0
  6. package/.claude/skills/components/scroll-reveal-image.md +161 -0
  7. package/.claude/skills/index.md +9 -1
  8. package/.claude/skills/new-app-scaffold.md +301 -0
  9. package/.claude/skills/setup-postinstall.md +45 -0
  10. package/.claude/skills/theming-override-default.md +6 -0
  11. package/.claude/skills/theming-partial-override.md +131 -0
  12. package/.claude/skills/vue-video-autoplay.md +74 -0
  13. package/README.md +30 -5
  14. package/app/components/01.atoms/banner-video/BannerVideo.vue +207 -0
  15. package/app/components/01.atoms/banner-video/stories/BannerVideo.stories.ts +180 -0
  16. package/app/components/01.atoms/banner-video/tests/BannerVideo.spec.ts +350 -0
  17. package/app/components/01.atoms/banner-video/tests/__snapshots__/BannerVideo.spec.ts.snap +13 -0
  18. package/app/components/01.atoms/grid-stack/GridStack.vue +34 -0
  19. package/app/components/01.atoms/grid-stack/stories/GridStack.stories.ts +162 -0
  20. package/app/components/01.atoms/grid-stack/tests/GridStack.spec.ts +66 -0
  21. package/app/components/01.atoms/scroll-reveal-frame/ScrollRevealFrame.vue +125 -0
  22. package/app/components/01.atoms/scroll-reveal-frame/stories/ScrollRevealFrame.stories.ts +166 -0
  23. package/app/components/01.atoms/scroll-reveal-frame/tests/ScrollRevealFrame.spec.ts +148 -0
  24. package/app/components/01.atoms/scroll-reveal-frame/tests/__snapshots__/ScrollRevealFrame.spec.ts.snap +13 -0
  25. package/app/components/01.atoms/scroll-reveal-image/ScrollRevealImage.vue +96 -0
  26. package/app/components/01.atoms/scroll-reveal-image/stories/ScrollRevealImage.stories.ts +181 -0
  27. package/app/components/01.atoms/scroll-reveal-image/tests/ScrollRevealImage.spec.ts +235 -0
  28. package/app/components/01.atoms/scroll-reveal-image/tests/__snapshots__/ScrollRevealImage.spec.ts.snap +13 -0
  29. package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +1 -0
  30. package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +1 -0
  31. package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +1 -0
  32. package/app/components/03.organisms/services/services-grids/stories/ServicesSectionGrid.stories.ts +1 -0
  33. package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +1 -0
  34. package/app/components/03.organisms/services/services-grids/tests/ServicesSectionGrid.spec.ts +1 -0
  35. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesSectionGrid.spec.ts.snap +3 -3
  36. package/app/components/03.organisms/services/services-section/stories/ServicesSection.stories.ts +1 -0
  37. package/app/components/03.organisms/services/services-section/tests/ServicesSection.spec.ts +1 -0
  38. package/app/components/03.organisms/services/services-section/tests/__snapshots__/ServicesSection.spec.ts.snap +1 -1
  39. package/app/layouts/default.vue +2 -0
  40. package/app/pages/banner-video.vue +238 -0
  41. package/app/pages/grid-stack.vue +252 -0
  42. package/app/pages/index.vue +5 -0
  43. package/app/pages/ui/scroll-reveal-image.vue +483 -0
  44. package/app/types/types.services.ts +1 -0
  45. package/package.json +1 -1
  46. package/app/assets/styles/extends-layer/srcdev-components/components/_display-prompt-core.css +0 -94
  47. package/app/assets/styles/extends-layer/srcdev-components/components/_display-toast.css +0 -5
  48. package/app/assets/styles/extends-layer/srcdev-components/components/_expanding-panel.css +0 -37
  49. package/app/assets/styles/extends-layer/srcdev-components/components/index.css +0 -3
  50. package/app/assets/styles/extends-layer/srcdev-components/index.css +0 -1
  51. package/app/assets/styles/extends-layer/srcdev-forms/components/_form-fieldset.css +0 -38
  52. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-button.css +0 -67
  53. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-checkbox-radio-core.css +0 -87
  54. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-checkbox-radio-options-button.css +0 -74
  55. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-checkbox-radio-with-label.css +0 -14
  56. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-description.css +0 -13
  57. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-error.css +0 -54
  58. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-label.css +0 -21
  59. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-select.css +0 -143
  60. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-text.css +0 -190
  61. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-textarea.css +0 -96
  62. package/app/assets/styles/extends-layer/srcdev-forms/components/_input-toggle-switch.css +0 -68
  63. package/app/assets/styles/extends-layer/srcdev-forms/components/index.css +0 -12
  64. package/app/assets/styles/extends-layer/srcdev-forms/index.css +0 -2
  65. package/app/assets/styles/extends-layer/srcdev-forms/setup/_generic.css +0 -21
  66. package/app/assets/styles/extends-layer/srcdev-forms/setup/index.css +0 -3
  67. package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_default.css +0 -13
  68. package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_large.css +0 -12
  69. package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_medium.css +0 -12
  70. package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_small.css +0 -12
  71. package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_x-small.css +0 -11
  72. package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/index.css +0 -23
  73. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_default.css +0 -62
  74. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_error.css +0 -63
  75. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_ghost.css +0 -63
  76. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_info.css +0 -63
  77. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_primary.css +0 -66
  78. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_secondary.css +0 -64
  79. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_success.css +0 -63
  80. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_tertiary.css +0 -63
  81. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_warning.css +0 -63
  82. 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,178 @@
1
+ ---
2
+ name: BannerVideo
3
+ description: BannerVideo full-width hero video banner — props, depth tier system, verticalPosition/horizontalPosition, 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. 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
+
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
+ | `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}`. |
30
+ | `aspectRatio` | `string` | `"21/9"` | CSS `aspect-ratio` of the container (e.g. `"16/9"`, `"21/9"`, `"4/3"`). |
31
+ | `objectFit` | `"cover" \| "contain" \| "fill" \| "none" \| "scale-down"` | `"cover"` | How the video and fallback image fill the banner frame. |
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. |
33
+ | `horizontalPosition` | `"start" \| "center" \| "end"` | `"center"` | Horizontal crop position. Maps to `object-position` X on the fallback image. |
34
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
35
+
36
+ ## Basic usage
37
+
38
+ ```vue
39
+ <BannerVideo
40
+ src="/videos/hero.mp4"
41
+ poster="/images/hero-poster.jpg"
42
+ alt="Studio interior"
43
+ />
44
+ ```
45
+
46
+ ## Common variants
47
+
48
+ ### Depth tiers
49
+
50
+ ```vue
51
+ <!-- xs: clamp(12rem, 15vw, 24rem) — thin strip -->
52
+ <BannerVideo src="…" poster="…" depth="xs" />
53
+
54
+ <!-- sm: clamp(18rem, 22vw, 36rem) -->
55
+ <BannerVideo src="…" poster="…" depth="sm" />
56
+
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" />
65
+ ```
66
+
67
+ ### Custom focal point
68
+
69
+ ```vue
70
+ <BannerVideo
71
+ src="/videos/hero.mp4"
72
+ poster="/images/hero-poster.jpg"
73
+ alt="Studio interior"
74
+ vertical-position="end"
75
+ horizontal-position="center"
76
+ />
77
+ ```
78
+
79
+ ### Full bleed with no block margin
80
+
81
+ ```vue
82
+ <BannerVideo
83
+ src="/videos/hero.mp4"
84
+ poster="/images/hero-poster.jpg"
85
+ alt="Studio interior"
86
+ :style-class-passthrough="['full-bleed', 'mbe-0']"
87
+ />
88
+ ```
89
+
90
+ ## Positioning explained
91
+
92
+ 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.
93
+
94
+ `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.
95
+
96
+ | verticalPosition | align-self | object-position Y |
97
+ |---|---|---|
98
+ | `"start"` | `start` | `top` |
99
+ | `"center"` (default) | `center` | `center` |
100
+ | `"end"` | `end` | `bottom` |
101
+
102
+ | horizontalPosition | object-position X |
103
+ |---|---|
104
+ | `"start"` | `left` |
105
+ | `"center"` (default) | `center` |
106
+ | `"end"` | `right` |
107
+
108
+ ## imgWidth / imgHeight
109
+
110
+ 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).
111
+
112
+ | Image type | imgWidth | imgHeight |
113
+ |---|---|---|
114
+ | Landscape / banner (16:9) | `1920` | `1080` |
115
+ | Wide banner (12:5) | `1920` | `800` |
116
+ | Square | `800` | `800` |
117
+
118
+ ## CSS custom properties
119
+
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):
127
+
128
+ | Property | Default | Set by prop |
129
+ |---|---|---|
130
+ | `--_aspect-ratio` | `21/9` | `aspectRatio` |
131
+ | `--_align-self` | `center` | `verticalPosition` |
132
+ | `--_justify-self` | `center` | `horizontalPosition` |
133
+
134
+ ### Depth token defaults
135
+
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)` |
143
+
144
+ **Override a tier in a consuming page:**
145
+
146
+ ```css
147
+ .my-page {
148
+ --theme-banner-video-max-height-md: clamp(32rem, 45vw, 64rem);
149
+ }
150
+ ```
151
+
152
+ ## CSS classes
153
+
154
+ | Class | Applied when |
155
+ |---|---|
156
+ | `.banner-video` | Always — the root element |
157
+ | `.video` | The `<video>` element |
158
+ | `.fallback` | The `<NuxtImg>` fallback — hidden by default, shown via `prefers-reduced-motion` CSS |
159
+
160
+ ## Consumer styling
161
+
162
+ 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.
163
+
164
+ ```vue
165
+ <style>
166
+ .my-page {
167
+ /* Override the md tier's clamp range for this page */
168
+ --theme-banner-video-max-height-md: clamp(32rem, 45vw, 64rem);
169
+ }
170
+ </style>
171
+ ```
172
+
173
+ ## Notes
174
+
175
+ - `loading="eager"` and `decoding="async"` are hardcoded on the fallback `NuxtImg` — it is above the fold by definition.
176
+ - 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.
177
+ - `prefers-reduced-motion` is handled in CSS (`.video { display: none }` + `.fallback { display: block }`), not via JS.
178
+ - 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,133 @@
1
+ ---
2
+ name: GridStack
3
+ description: GridStack CSS Grid z-axis stacking component — slot API, z-order rules, sizing behaviour, consumer patterns (video+overlay, image+text)
4
+ type: reference
5
+ ---
6
+
7
+ # GridStack
8
+
9
+ ## Overview
10
+
11
+ `GridStack` stacks slot content in the z-axis using a single `grid-template-areas: "stack"` — no `position: absolute` needed. Every slot is wrapped in a `.grid-stack__layer` div sharing that grid area. The container sizes itself from the tallest layer; all layers stretch to fill that height.
12
+
13
+ ## Props
14
+
15
+ | Prop | Type | Default | Description |
16
+ |------|------|---------|-------------|
17
+ | `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | HTML element rendered as the root. |
18
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
19
+
20
+ ## Slot API
21
+
22
+ Any named slot is accepted — there are no declared slot names. Convention is `layer-1`, `layer-2`, `layer-3` etc., but any name works. The component iterates `$slots` and wraps each in a `.grid-stack__layer`.
23
+
24
+ **Z-order rule: DOM order = z-order. The last slot is on top.**
25
+
26
+ ```vue
27
+ <GridStack>
28
+ <template #layer-1><!-- base, behind everything --></template>
29
+ <template #layer-2><!-- middle --></template>
30
+ <template #layer-3><!-- on top --></template>
31
+ </GridStack>
32
+ ```
33
+
34
+ ## Basic usage
35
+
36
+ ```vue
37
+ <GridStack>
38
+ <template #layer-1>
39
+ <img src="/images/hero.jpg" alt="" />
40
+ </template>
41
+ <template #layer-2>
42
+ <div class="hero-overlay">
43
+ <h1>Heading over image</h1>
44
+ </div>
45
+ </template>
46
+ </GridStack>
47
+ ```
48
+
49
+ ## Common patterns
50
+
51
+ ### Video background + overlay
52
+
53
+ ```vue
54
+ <GridStack>
55
+ <template #layer-1>
56
+ <BannerVideo
57
+ src="/videos/hero.mp4"
58
+ poster="/images/hero-poster.jpg"
59
+ alt=""
60
+ depth="lg"
61
+ />
62
+ </template>
63
+ <template #layer-2>
64
+ <div class="video-overlay">
65
+ <h1>Content over video</h1>
66
+ </div>
67
+ </template>
68
+ </GridStack>
69
+ ```
70
+
71
+ ### Decorative background + content
72
+
73
+ ```vue
74
+ <GridStack tag="section">
75
+ <template #layer-1>
76
+ <div class="decorative-bg" aria-hidden="true"></div>
77
+ </template>
78
+ <template #layer-2>
79
+ <div class="section-content">
80
+ <p>Real content here</p>
81
+ </div>
82
+ </template>
83
+ </GridStack>
84
+ ```
85
+
86
+ ## Sizing
87
+
88
+ The container height is determined by the tallest child layer. All layers stretch to match. If layers have different intrinsic heights, the shorter ones will stretch — use `align-self` on the layer's inner content to control vertical position within the stretched space.
89
+
90
+ To pin the stack to a fixed height, set it on the root from the consuming page:
91
+
92
+ ```css
93
+ .my-page {
94
+ .grid-stack {
95
+ block-size: 60rem;
96
+ }
97
+ }
98
+ ```
99
+
100
+ ## CSS classes
101
+
102
+ | Class | Element |
103
+ |---|---|
104
+ | `.grid-stack` | Root element |
105
+ | `.grid-stack__layer` | Wrapper div around each slot — all share `grid-area: stack` |
106
+
107
+ ## Consumer styling
108
+
109
+ No `:deep()` needed — `@layer components` means page styles win automatically.
110
+
111
+ ```vue
112
+ <style>
113
+ .my-page {
114
+ .grid-stack {
115
+ border-radius: 1.2rem;
116
+ overflow: hidden; /* clips layers to rounded corners */
117
+ }
118
+
119
+ /* Style the overlay layer by targeting content inside it */
120
+ .my-overlay {
121
+ display: grid;
122
+ place-items: center;
123
+ pointer-events: none; /* let clicks through to the layer below */
124
+ }
125
+ }
126
+ </style>
127
+ ```
128
+
129
+ ## Notes
130
+
131
+ - `pointer-events: none` on overlay layers (and `pointer-events: auto` on interactive children within them) is the standard pattern for overlays that shouldn't block interaction with layers behind them.
132
+ - There is no built-in `z-index` — stacking is handled purely by DOM order. If a consumer applies `z-index` on a layer for other reasons, be aware it creates a new stacking context.
133
+ - The slot name is used as the Vue `:key` on the layer wrapper, so slot names must be unique.
@@ -0,0 +1,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,8 +37,10 @@ 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
43
+ ├── new-app-scaffold.md — scaffold a new Nuxt consumer app extending this layer (package.json, nuxt.config, app structure, CLAUDE.md)
40
44
  ├── release-notes.md — produce release notes as a fenced markdown block from git log
41
45
  ├── composable-whatsapp.md — useWhatsApp: open pre-filled wa.me link from form payload; runtime config, security, usage
42
46
  ├── composable-zod-validation.md — useZodValidation: schema-driven form validation, error binding, submit flow, API error push
@@ -61,6 +65,10 @@ Each skill is a single markdown file named `<area>-<task>.md`.
61
65
  ├── glass-panel.md — GlassPanel props, slots, CSS token API (--glass-panel-bg/border-color/shadow/highlight), theming override
62
66
  ├── navigation-horizontal.md — NavigationHorizontal props, NavItemData type, CSS token API, import path gotcha
63
67
  ├── input-copy-core.md — InputCopyCore: readonly copy-to-clipboard input; props, emits, slots, CSS classes, usage
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
70
+ ├── scroll-reveal-frame.md — ScrollRevealFrame: generic parallax clipping frame, slot API, image grid pattern, CSS tokens, browser support
71
+ ├── scroll-reveal-image.md — ScrollRevealImage: single-image parallax reveal, focalX, imgWidth/imgHeight, responsive frame height
64
72
  ├── site-navigation.md — SiteNavigation: responsive nav with auto-collapse, burger menu, decorator indicators, CSS token API
65
73
  └── social-icons-list.md — SocialIconsList: data-driven social icon links, ISocialIcon type, logos: icon names, CSS tokens
66
74
  ```