srcdev-nuxt-components 9.1.32 → 9.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/components/banner-video.md +32 -0
- package/.claude/skills/components/hero-text.md +15 -2
- package/.claude/skills/components/profile-section.md +134 -0
- package/.claude/skills/components/scroll-reveal-image.md +6 -4
- package/.claude/skills/components/section-parallax.md +102 -0
- package/app/components/01.atoms/scroll-reveal-frame/ScrollRevealFrame.vue +1 -0
- package/app/components/01.atoms/text-blocks/hero-text/HeroText.vue +1 -1
- package/package.json +1 -1
|
@@ -170,6 +170,38 @@ Use an unscoped style block scoped by a page or section wrapper class. No `:deep
|
|
|
170
170
|
</style>
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
+
## Gotcha: aspect-ratio is overridden by max-height at wide viewports
|
|
174
|
+
|
|
175
|
+
`max-height` from the depth tier silently wins over `aspect-ratio` once the viewport is wide enough. For example, at `depth="md"` the max-height clamp caps at `56rem` — so changing `aspectRatio` from `"21/9"` to `"1/1"` produces no visible change on a wide desktop because `max-height` is the binding constraint.
|
|
176
|
+
|
|
177
|
+
**If you need `aspect-ratio` to dominate**, override all depth tokens to a large fixed value on the parent:
|
|
178
|
+
|
|
179
|
+
```css
|
|
180
|
+
.my-page {
|
|
181
|
+
--theme-banner-video-max-height-xs: 80rem;
|
|
182
|
+
--theme-banner-video-max-height-sm: 80rem;
|
|
183
|
+
--theme-banner-video-max-height-md: 80rem;
|
|
184
|
+
--theme-banner-video-max-height-lg: 80rem;
|
|
185
|
+
--theme-banner-video-max-height-xl: 80rem;
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Or via inline `:style` on the parent element (useful for dev QA exploration):
|
|
190
|
+
|
|
191
|
+
```vue
|
|
192
|
+
<div :style="{
|
|
193
|
+
'--theme-banner-video-max-height-xs': '80rem',
|
|
194
|
+
'--theme-banner-video-max-height-sm': '80rem',
|
|
195
|
+
'--theme-banner-video-max-height-md': '80rem',
|
|
196
|
+
'--theme-banner-video-max-height-lg': '80rem',
|
|
197
|
+
'--theme-banner-video-max-height-xl': '80rem',
|
|
198
|
+
}">
|
|
199
|
+
<BannerVideo aspect-ratio="1/1" depth="lg" ... />
|
|
200
|
+
</div>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
You must override **all five tier tokens** — overriding only the active depth token is not sufficient because the component resolves the token by name.
|
|
204
|
+
|
|
173
205
|
## Notes
|
|
174
206
|
|
|
175
207
|
- `loading="eager"` and `decoding="async"` are hardcoded on the fallback `NuxtImg` — it is above the fold by definition.
|
|
@@ -100,8 +100,21 @@ Override via `styleClassPassthrough` or a parent HOC `<style>` block targeting `
|
|
|
100
100
|
|
|
101
101
|
Key CSS custom properties:
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
| Property | Default | Controls |
|
|
104
|
+
| -------- | ------- | -------- |
|
|
105
|
+
| `--colour-text-accent` | — | Colour of `.accent` spans and the icon |
|
|
106
|
+
| `--hero-text-{scale}` | — | Font size per `fontSize` prop value |
|
|
107
|
+
| `--hero-text-vertical-gap` | `0.4em` | Gap between segments in `axis="vertical"` mode |
|
|
108
|
+
|
|
109
|
+
**`--hero-text-vertical-gap`** controls `gap` on the flex column in vertical axis. Override at theme or page level:
|
|
110
|
+
|
|
111
|
+
```css
|
|
112
|
+
.my-page {
|
|
113
|
+
.hero-text.axis-vertical {
|
|
114
|
+
--hero-text-vertical-gap: 0.6em;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
105
118
|
|
|
106
119
|
## Local style override scaffold
|
|
107
120
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ProfileSection
|
|
3
|
+
description: ProfileSection molecule — props, dynamic profile-info slots, eyebrowText/heroText/profileLinks slots, layout, accessibility
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ProfileSection
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
`ProfileSection` is a molecule that renders a practitioner/author profile: a header area (eyebrow + heading), a profile picture, and a flexible set of bio/info blocks alongside optional profile links. It is landmark-aware — the root element automatically gets `aria-labelledby` wired to the heading inside the `#heroText` slot.
|
|
12
|
+
|
|
13
|
+
## Props
|
|
14
|
+
|
|
15
|
+
| Prop | Type | Default | Description |
|
|
16
|
+
|------|------|---------|-------------|
|
|
17
|
+
| `profilePicture` | `{ src: string; alt: string }` | — | **Required.** Path and alt text for the profile image. Rendered via `NuxtImg`. |
|
|
18
|
+
| `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | HTML element rendered as the root. |
|
|
19
|
+
| `profileInfoCount` | `number` | `3` | Number of `profile-info-N` slots to generate when none are explicitly provided. |
|
|
20
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
|
|
21
|
+
|
|
22
|
+
## Slots
|
|
23
|
+
|
|
24
|
+
| Slot | Description |
|
|
25
|
+
|------|-------------|
|
|
26
|
+
| `#eyebrowText` | Optional eyebrow label above the heading. Use `EyebrowText`. |
|
|
27
|
+
| `#heroText` | Heading slot. Receives `headingId` as a slot prop — bind it to the heading's `id` for accessible `aria-labelledby`. |
|
|
28
|
+
| `#profile-info-1` … `#profile-info-N` | Bio / info blocks. Provide as many numbered slots as needed. They are rendered in ascending numeric order. |
|
|
29
|
+
| `#profileLinks` | Links / actions shown at the bottom-right of the info column (e.g. social icons, booking CTA). |
|
|
30
|
+
|
|
31
|
+
## Basic usage
|
|
32
|
+
|
|
33
|
+
```vue
|
|
34
|
+
<ProfileSection
|
|
35
|
+
tag="section"
|
|
36
|
+
:profile-picture="{ src: '/images/profile/mel.jpg', alt: 'Mel Stafford, reflexologist' }"
|
|
37
|
+
>
|
|
38
|
+
<template #eyebrowText>
|
|
39
|
+
<EyebrowText font-size="large" text="About Mel" />
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<template #heroText="{ headingId }">
|
|
43
|
+
<HeroText
|
|
44
|
+
tag="h2"
|
|
45
|
+
:id="headingId"
|
|
46
|
+
font-size="title"
|
|
47
|
+
:text-content="[
|
|
48
|
+
{ text: 'Mel Stafford', styleClass: 'normal' },
|
|
49
|
+
{ text: 'Reflexologist', styleClass: 'accent' },
|
|
50
|
+
]"
|
|
51
|
+
/>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<template #profile-info-1>
|
|
55
|
+
<p>With over a decade of practice…</p>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<template #profile-info-2>
|
|
59
|
+
<p>Mel is based in Lichfield…</p>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<template #profileLinks>
|
|
63
|
+
<SocialIconsList :items="socialLinks" />
|
|
64
|
+
</template>
|
|
65
|
+
</ProfileSection>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Dynamic profile-info slots
|
|
69
|
+
|
|
70
|
+
The `profile-info-N` slots are discovered at runtime by filtering `useSlots()` for keys matching `/^profile-info-\d+$/`. The slots are rendered in ascending numeric order.
|
|
71
|
+
|
|
72
|
+
- If you provide `#profile-info-1`, `#profile-info-2`, `#profile-info-3` — all three render in order.
|
|
73
|
+
- If you provide no `profile-info-*` slots, the component generates `profileInfoCount` empty placeholder blocks.
|
|
74
|
+
- Gaps are supported — you can provide `#profile-info-1` and `#profile-info-3` without `#profile-info-2`; they sort correctly.
|
|
75
|
+
|
|
76
|
+
## heroText slot prop
|
|
77
|
+
|
|
78
|
+
The `#heroText` slot exposes `headingId` as a slot prop. Bind it to the heading's `id` prop so the component's `aria-labelledby` attribute points to the heading automatically:
|
|
79
|
+
|
|
80
|
+
```vue
|
|
81
|
+
<template #heroText="{ headingId }">
|
|
82
|
+
<HeroText tag="h2" :id="headingId" ... />
|
|
83
|
+
</template>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If you use a different heading component, bind `headingId` to whatever prop renders the `id` attribute on the heading element.
|
|
87
|
+
|
|
88
|
+
## Layout
|
|
89
|
+
|
|
90
|
+
- **Mobile**: single column — picture stacked above info.
|
|
91
|
+
- **768px+**: two columns — `384px` picture column, `1fr` info column, `4rem` gap.
|
|
92
|
+
- Picture frame: `aspect-ratio: 3/4`, `border-radius: 8px`, `overflow: hidden`. The `NuxtImg` fills the frame with `object-fit: cover`.
|
|
93
|
+
- Profile links: `align-items: end; justify-content: flex-end` — right-aligned to the bottom of the info column.
|
|
94
|
+
|
|
95
|
+
## CSS notes
|
|
96
|
+
|
|
97
|
+
- The `.profile-section-header` element has **no default margin-block-end**. If you need spacing between the header and the picture/info grid, add it via a consuming-page style:
|
|
98
|
+
|
|
99
|
+
```css
|
|
100
|
+
.my-page {
|
|
101
|
+
.profile-section-header {
|
|
102
|
+
margin-block-end: 3.2rem;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- Accent-coloured text within `.location` or `.services` info blocks can use the `.highlight` class for `var(--colour-text-accent)` or `var(--colour-link-default)` colouring respectively.
|
|
108
|
+
|
|
109
|
+
## Consumer styling scaffold
|
|
110
|
+
|
|
111
|
+
```vue
|
|
112
|
+
<ProfileSection
|
|
113
|
+
:style-class-passthrough="['my-profile']"
|
|
114
|
+
...
|
|
115
|
+
>
|
|
116
|
+
...
|
|
117
|
+
</ProfileSection>
|
|
118
|
+
|
|
119
|
+
<style>
|
|
120
|
+
.profile-section {
|
|
121
|
+
&.my-profile {
|
|
122
|
+
.profile-section-header {
|
|
123
|
+
margin-block-end: 3.2rem;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
</style>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Notes
|
|
131
|
+
|
|
132
|
+
- `NuxtImg` is used for the profile picture — provide a real `src` path. A placeholder (grey box) is shown if the image 404s.
|
|
133
|
+
- The component does not expose `imgWidth` / `imgHeight` props for the picture. If IPX optimisation is critical, use a fixed-dimension image and rely on the `384px` column cap as the natural size constraint.
|
|
134
|
+
- Component is auto-imported in Nuxt — no import needed.
|
|
@@ -15,7 +15,7 @@ For arbitrary slot content — a grid of images, video, markup — use `ScrollRe
|
|
|
15
15
|
## Props
|
|
16
16
|
|
|
17
17
|
| Prop | Type | Default | Description |
|
|
18
|
-
|
|
18
|
+
| ---- | ---- | ------- | ----------- |
|
|
19
19
|
| `src` | `string` | — | Image source path. **Required.** |
|
|
20
20
|
| `alt` | `string` | `""` | Alt text for the image. |
|
|
21
21
|
| `imgWidth` | `number` | `1920` | Intrinsic width of the source image — required for NuxtImg optimisation. |
|
|
@@ -95,7 +95,7 @@ For arbitrary slot content — a grid of images, video, markup — use `ScrollRe
|
|
|
95
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
96
|
|
|
97
97
|
| Value | Crops toward |
|
|
98
|
-
|
|
98
|
+
| ----- | ----------- |
|
|
99
99
|
| `"0%"` or `"left"` | Left edge |
|
|
100
100
|
| `"50%"` (default) | Centre |
|
|
101
101
|
| `"75%"` | Right of centre — useful for a subject offset to the right |
|
|
@@ -106,13 +106,14 @@ Internally, `focalX` sets `object-position: <focalX> 0%` on the `<img>`. The `Y`
|
|
|
106
106
|
## imgWidth / imgHeight
|
|
107
107
|
|
|
108
108
|
Always provide these to match the intrinsic dimensions of the source file. NuxtImg uses them to:
|
|
109
|
+
|
|
109
110
|
- Generate the correct `srcset` via the IPX pipeline
|
|
110
111
|
- Avoid the `w=1536` fallback (not in Vercel's allowed widths: 640, 750, 828, 1080, 1200, 1920, 2048, 3840)
|
|
111
112
|
|
|
112
113
|
Common pairs:
|
|
113
114
|
|
|
114
115
|
| Image type | imgWidth | imgHeight |
|
|
115
|
-
|
|
116
|
+
| ---------- | -------- | --------- |
|
|
116
117
|
| Portrait (3:4) | `1280` | `1920` |
|
|
117
118
|
| Landscape / banner (16:9) | `1920` | `1080` |
|
|
118
119
|
| Wide banner (12:5) | `1920` | `800` |
|
|
@@ -143,7 +144,7 @@ Override `--_frame-height` in a scoped style block for responsive control:
|
|
|
143
144
|
Set from props via inline `:style` — override in CSS for responsive or contextual control.
|
|
144
145
|
|
|
145
146
|
| Property | Default | Set by prop |
|
|
146
|
-
|
|
147
|
+
| -------- | ------- | ----------- |
|
|
147
148
|
| `--_frame-height` | `540px` | `frameHeight` |
|
|
148
149
|
| `--_parallax-offset` | `36rem` | `parallaxOffset` |
|
|
149
150
|
| `--_radius` | `0px` | `radius` |
|
|
@@ -155,6 +156,7 @@ See `scroll-reveal-frame.md` for the full guide. For portrait images the default
|
|
|
155
156
|
|
|
156
157
|
## Notes
|
|
157
158
|
|
|
159
|
+
- The root `<figure>` has `margin: 0` set in the component — browser default `<figure>` margins are neutralised at source.
|
|
158
160
|
- `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
161
|
- Do not place inside a container with `overflow: hidden` or `overflow: clip` — breaks the `view-timeline` scroll detection inherited from `ScrollRevealFrame`.
|
|
160
162
|
- Reduced-motion: animation is disabled and the image falls back to a static crop centred at `object-position: <focalX> 50%`.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: SectionParallax
|
|
3
|
+
description: SectionParallax CSS fixed-background parallax section — props, browser support, iOS Safari limitation, when to use vs ScrollRevealImage
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# SectionParallax
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
`SectionParallax` is a full-width section that uses CSS `background-attachment: fixed` to create a parallax scrolling effect — the background image stays stationary while the page content scrolls over it. The effect is implemented with `@supports (background-attachment: fixed)` so it degrades gracefully.
|
|
12
|
+
|
|
13
|
+
**Important browser limitation:** `background-attachment: fixed` does not work on iOS Safari (and mobile Chrome on iOS). On those browsers the background renders as a static image with no parallax motion. Use `SectionParallax` for atmospheric/decorative breaks where this is an acceptable trade-off. For cross-browser scroll-driven parallax, use `ScrollRevealImage` or `ScrollRevealFrame` instead.
|
|
14
|
+
|
|
15
|
+
## Props
|
|
16
|
+
|
|
17
|
+
| Prop | Type | Default | Description |
|
|
18
|
+
|------|------|---------|-------------|
|
|
19
|
+
| `tag` | `"div" \| "section" \| "article" \| "aside"` | `"div"` | HTML element rendered as the root. |
|
|
20
|
+
| `backgroundImage` | `string` | `undefined` | Path to the background image. Bound as `url("...")` via `v-bind`. |
|
|
21
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
|
|
22
|
+
|
|
23
|
+
## Slots
|
|
24
|
+
|
|
25
|
+
| Slot | Description |
|
|
26
|
+
|------|-------------|
|
|
27
|
+
| `default` | Optional content rendered inside the section. Often left empty — the component is typically used as a purely decorative atmospheric break. |
|
|
28
|
+
|
|
29
|
+
## Basic usage — decorative atmospheric break
|
|
30
|
+
|
|
31
|
+
```vue
|
|
32
|
+
<SectionParallax
|
|
33
|
+
tag="section"
|
|
34
|
+
background-image="/images/eucalyptus-lavender-and-oil.jpg"
|
|
35
|
+
/>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## With overlay content
|
|
39
|
+
|
|
40
|
+
```vue
|
|
41
|
+
<SectionParallax
|
|
42
|
+
tag="section"
|
|
43
|
+
background-image="/images/candle-and-stones.jpg"
|
|
44
|
+
:style-class-passthrough="['has-overlay']"
|
|
45
|
+
>
|
|
46
|
+
<div class="overlay-text">
|
|
47
|
+
<p>A quiet moment.</p>
|
|
48
|
+
</div>
|
|
49
|
+
</SectionParallax>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## How it works
|
|
53
|
+
|
|
54
|
+
The component sets:
|
|
55
|
+
- `background-image: url(...)` via `v-bind`
|
|
56
|
+
- `background-position: center`
|
|
57
|
+
- `background-size: inherit` (fallback when fixed is not supported)
|
|
58
|
+
- `min-height: 120vh`
|
|
59
|
+
- `background-color: light-dark(var(--slate-01), var(--slate-08))` (visible if image fails to load)
|
|
60
|
+
|
|
61
|
+
Inside `@supports (background-attachment: fixed)`:
|
|
62
|
+
- `background-attachment: fixed` — pins the image to the viewport
|
|
63
|
+
- `background-size: cover` — ensures the image fills the viewport
|
|
64
|
+
- `min-height: 120vh` — ensures enough scroll travel to see the parallax motion
|
|
65
|
+
|
|
66
|
+
## Controlling height
|
|
67
|
+
|
|
68
|
+
Override `min-height` with a consuming-page style:
|
|
69
|
+
|
|
70
|
+
```css
|
|
71
|
+
.my-page {
|
|
72
|
+
.section-parallax {
|
|
73
|
+
min-height: 60vh; /* shorter atmospheric break */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Browser support
|
|
79
|
+
|
|
80
|
+
| Browser | Support |
|
|
81
|
+
|---------|---------|
|
|
82
|
+
| Chrome / Edge (desktop) | ✅ Full parallax |
|
|
83
|
+
| Firefox (desktop) | ✅ Full parallax |
|
|
84
|
+
| Safari (desktop) | ✅ Full parallax |
|
|
85
|
+
| iOS Safari | ❌ Static background (no parallax) |
|
|
86
|
+
| Chrome on iOS | ❌ Static background (no parallax) |
|
|
87
|
+
| Android Chrome | ✅ Usually supported |
|
|
88
|
+
|
|
89
|
+
## When to use SectionParallax vs ScrollRevealImage
|
|
90
|
+
|
|
91
|
+
| Use case | Recommendation |
|
|
92
|
+
|----------|---------------|
|
|
93
|
+
| Atmospheric break between sections, desktop-first site | `SectionParallax` — simpler, no JS |
|
|
94
|
+
| Cross-browser scroll animation, mobile-first site | `ScrollRevealImage` or `ScrollRevealFrame` |
|
|
95
|
+
| Image with rounded corners, specific frame height | `ScrollRevealImage` |
|
|
96
|
+
| Image inlined within a content grid | `ScrollRevealImage` |
|
|
97
|
+
|
|
98
|
+
## Notes
|
|
99
|
+
|
|
100
|
+
- The `@supports` guard means the parallax activates only when the browser supports `background-attachment: fixed`. No JS is involved.
|
|
101
|
+
- Slot content is only rendered when the `default` slot is provided (`v-if="slots.default"`).
|
|
102
|
+
- The component has no built-in overlay or gradient — add one via the slot or a `::before` pseudo-element in your consuming-page styles.
|