Skip to content

Commit e998749

Browse files
committed
PageHeroHighlights - Skills, Tests and Stories updated
1 parent a3a09d2 commit e998749

6 files changed

Lines changed: 108 additions & 59 deletions

File tree

.claude/skills/components/page-hero-highlights.md

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ The layout uses a 4-row CSS Grid with `subgrid` — no `translate`, negative mar
1717
| `tag` | `"div" \| "section" \| "main"` | `"div"` | Root element tag |
1818
| `highlightsEqualWidths` | `boolean` | `false` | Equal-width grid columns for highlight items |
1919
| `highlightsJustify` | `"start" \| "center" \| "end" \| "space-between" \| "space-around"` | `"start"` | Alignment of highlight items along the main axis |
20-
| `maxWidth` | `string` | `undefined` | Cap the central content column (e.g. `"1064px"`). Gutters grow to enforce the constraint; below this width they hold at `16px`. |
21-
| `contentAlign` | `"start" \| "center"` | `"center"` | When `maxWidth` is set: `"center"` grows gutters equally; `"start"` pins content to the left with a fixed `16px` left gutter. |
20+
| `maxWidth` | `boolean` | `false` | When `true`, caps the central column at `--max-width` (default `1064px`). Gutters grow responsively to enforce the constraint. |
21+
| `contentAlign` | `"start" \| "center"` | `"center"` | When `maxWidth` is `true`: `"center"` grows gutters equally; `"start"` pins content to the left with a fixed left gutter. |
2222
| `contentPanel` | `boolean` | `true` | When `true`, renders a decorative panel behind the content slot and offsets the highlights strip. Set to `false` for a flat layout with no backdrop. |
2323
| `highlightTitleBaseline`| `boolean` | `false` | When `true`, fixes the highlight title row to a set height so titles align at a common baseline. Override `--highlight-title-height` to tune. |
2424
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root element |
@@ -66,7 +66,7 @@ Each slot accepts any content, but these library components are natural fits:
6666
Example with `HeroText` in the header slot:
6767

6868
```vue
69-
<PageHeroHighlights tag="section" max-width="1064px">
69+
<PageHeroHighlights tag="section" :max-width="true">
7070
<template #header="{ headingId }">
7171
<HeroText :heading-id="headingId" text="Welcome back" accent-text="Simon" />
7272
</template>
@@ -106,11 +106,13 @@ Located at: `app/components/04.templates/page-hero-highlights/PageHeroHighlights
106106

107107
### CSS tokens
108108

109-
| Token | Default | Description |
110-
| ------------------ | -------- | ---------------------------------------- |
111-
| `--phh-padding-block` | `1.6rem` | Block padding on the header |
112-
| `--phh-gap` | `1.6rem` | Gap between `#start` and `#end` areas |
113-
| `--phh-end-gap` | `0.8rem` | Gap between items within `#end` |
109+
| Token | Default | Description |
110+
| -------------------------- | ---------------- | ---------------------------------------------------- |
111+
| `--phh-padding-block-mobile` | `1.6rem 3.2rem` | Block padding (start end) at mobile widths |
112+
| `--phh-padding-block-tablet` | `2.4rem 4.8rem` | Block padding (start end) at ≥768px |
113+
| `--phh-padding-block-desktop` | `3.2rem 6.4rem` | Block padding (start end) at ≥1024px |
114+
| `--phh-gap` | `1.6rem` | Gap between `#start` and `#end` areas |
115+
| `--phh-end-gap` | `0.8rem` | Gap between items within `#end` |
114116

115117
### Usage
116118

@@ -170,17 +172,26 @@ By default, highlight items size to their content (`flex-wrap`). Pass `:highligh
170172

171173
## Constraining the central column width
172174

173-
Pass `max-width` to cap the content column. The gutters grow to enforce it — full-bleed backgrounds are unaffected and `subgrid` continues to work. Use `content-align` to pin to the left or centre:
175+
Pass `:max-width="true"` to cap the content column at `--max-width` (default `1064px`). The gutters grow responsively to enforce it — full-bleed backgrounds are unaffected and `subgrid` continues to work. Use `content-align` to pin to the left or centre:
174176

175177
```vue
176-
<!-- Centred, capped at 1064px -->
177-
<PageHeroHighlights max-width="1064px" content-align="center">...</PageHeroHighlights>
178+
<!-- Centred, capped at --max-width (1064px) -->
179+
<PageHeroHighlights :max-width="true" content-align="center">...</PageHeroHighlights>
178180
179-
<!-- Left-pinned, capped at 1064px (right side takes remaining space) -->
180-
<PageHeroHighlights max-width="1064px" content-align="start">...</PageHeroHighlights>
181+
<!-- Left-pinned (right side takes remaining space) -->
182+
<PageHeroHighlights :max-width="true" content-align="start">...</PageHeroHighlights>
181183
```
182184

183-
See [css-grid-max-width-gutters.md](../css-grid-max-width-gutters.md) for the full pattern explanation.
185+
The maximum width value and gutter sizes are all CSS tokens — override them via `styleClassPassthrough` if you need different values:
186+
187+
```css
188+
.my-page-hero {
189+
--max-width: 1200px;
190+
--page-hero-highlights-gutter-desktop: 48px;
191+
}
192+
```
193+
194+
See [css-grid-max-width-gutters.md](../css-grid-max-width-gutters.md) for the underlying pattern explanation.
184195

185196
## Local style override scaffold
186197

@@ -199,6 +210,12 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
199210
─────────────────────────────────────────────────────────────────── */
200211
.page-hero-highlights {
201212
&.my-page-hero {
213+
/* Grid layout */
214+
/* --max-width: 1064px; */
215+
/* --page-hero-highlights-gutter-mobile: 16px; */
216+
/* --page-hero-highlights-gutter-tablet: 40px; */
217+
/* --page-hero-highlights-gutter-desktop: 32px; */
218+
202219
/* Header zone */
203220
/* --header-row-background-colour: darkblue; */
204221
@@ -239,8 +256,6 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
239256
</style>
240257
```
241258

242-
> **Note:** The minimum gutter width (`16px`) and layout behaviour are not overridable via CSS custom properties. Use the `max-width` and `content-align` props to control column constraints.
243-
244259
## Grid structure (reference)
245260

246261
```text
@@ -254,6 +269,8 @@ row4: page content (never underflows highlights)
254269
`.header-row` spans cols 1–3, rows 1–2 (edge-to-edge bg). `.header-slot` is placed in row 1 only.
255270
`.content-row` spans cols 1–3, rows 3–4 (bg fills behind highlights; `.content-slot` is placed in row 4 only). The decorative border behind `.content-slot` is rendered via `.content-row:before` — there is no separate DOM element for it.
256271

272+
Grid columns are determined entirely by CSS — no `v-bind`. The `maxWidth` and `contentAlign` props add CSS classes (`max-width`, `start`, `center`) which select the appropriate `grid-template-columns` rule.
273+
257274
## Layout pitfall: do not use `grid-template-rows: subgrid` inside `.highlights-row`
258275

259276
The `.highlights-row` element spans rows 2–3 of the parent grid (the "straddle"). If you add an inner grid to `.highlights-row` (e.g. to extend `equal-widths` behaviour) and include `grid-template-rows: subgrid`, auto-placed items will only occupy row 1 of the subgrid (= parent row 2). Parent row 3 collapses to 0-height, destroying the straddle effect — `.content-row` appears immediately below the highlights instead of overlapping it.
@@ -280,5 +297,5 @@ The `.highlights-row` element spans rows 2–3 of the parent grid (the "straddle
280297
- Component is auto-imported in Nuxt — no import needed.
281298
- Lives in `app/components/04.templates/page-hero-highlights/`.
282299
- Storybook title: `"Templates/PageHeroHighlights"`.
283-
- **Minimum gutter is fixed at `16px`** — it is baked into the `gridColumns` computed and cannot be overridden by a CSS custom property. If a consumer needs a different minimum (e.g. `24px`), it requires a prop or a fork of the component.
284-
- **`contentAlign` has no effect without `maxWidth`**both sides always hold `16px` when `maxWidth` is not set.
300+
- **`contentAlign` has no effect when `maxWidth` is `false`** — both gutters hold their responsive default.
301+
- **Gutter sizes are CSS tokens**`--page-hero-highlights-gutter-mobile/tablet/desktop` are all overridable. The responsive switching between them (via `@container`) is handled internally and cannot be overridden.

.claude/skills/css-grid-max-width-gutters.md

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,47 @@ Left gutter stays fixed, content column is capped at `MAX_WIDTH`, remaining spac
3131

3232
## In Vue with a prop
3333

34+
### Option A — CSS tokens + class selectors (preferred when max-width is a fixed design value)
35+
36+
When the max-width and gutter values are fixed design tokens (not arbitrary consumer strings), express the logic entirely in CSS using a boolean `maxWidth` prop that adds a class:
37+
38+
```ts
39+
interface Props {
40+
maxWidth?: boolean;
41+
contentAlign?: "start" | "center";
42+
}
43+
```
44+
45+
```css
46+
.component {
47+
--max-width: 1064px;
48+
--gutter: 16px;
49+
50+
display: grid;
51+
52+
&.max-width {
53+
grid-template-columns: var(--gutter) 1fr var(--gutter);
54+
}
55+
56+
&:not(.max-width) {
57+
&.start {
58+
grid-template-columns: var(--gutter) minmax(0, var(--max-width)) minmax(var(--gutter), 1fr);
59+
}
60+
&.center {
61+
grid-template-columns: max(var(--gutter), (100% - var(--max-width)) / 2) 1fr
62+
max(var(--gutter), (100% - var(--max-width)) / 2);
63+
}
64+
}
65+
}
66+
```
67+
68+
Consumers can override `--max-width` and `--gutter` via `styleClassPassthrough` without touching the prop. This is the approach used by `PageHeroHighlights`.
69+
70+
### Option B — computed string with `v-bind` (use when max-width is a dynamic consumer prop)
71+
3472
Because `v-bind()` in `<style>` can't be nested inside CSS functions like `max()`, build the column string as a computed and bind the whole value:
3573

3674
```ts
37-
// Props
3875
interface Props {
3976
maxWidth?: string; // e.g. "1064px"
4077
contentAlign?: "start" | "center";
@@ -45,7 +82,6 @@ const props = withDefaults(defineProps<Props>(), {
4582
contentAlign: "center",
4683
});
4784

48-
// Computed column string
4985
const gridColumns = computed(() => {
5086
if (!props.maxWidth) return "16px 1fr 16px";
5187
if (props.contentAlign === "start") return `16px minmax(0, ${props.maxWidth}) 1fr`;

app/components/04.templates/page-hero-highlights/PageHeroHighlights.vue

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,6 @@ const props = withDefaults(defineProps<Props>(), {
4444
styleClassPassthrough: () => [],
4545
});
4646
47-
// const gridColumns = computed(() => {
48-
// if (!props.maxWidth) return "16px 1fr 16px";
49-
// if (props.contentAlign === "start") return `16px minmax(0, ${props.maxWidth}) minmax(16px, 1fr)`;
50-
// return `max(16px, (100% - ${props.maxWidth}) / 2) 1fr max(16px, (100% - ${props.maxWidth}) / 2)`;
51-
// });
52-
5347
const { headingId, ariaLabelledby } = useAriaLabelledById(() => props.tag);
5448
const componentClasses = computed(() => ({
5549
"highlight-title-baseline": props.highlightTitleBaseline,

app/components/04.templates/page-hero-highlights/stories/PageHeroHighlights.stories.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ type StoryArgs = {
66
tag?: "div" | "section" | "main";
77
highlightsEqualWidths?: boolean;
88
highlightsJustify?: "start" | "center" | "end" | "space-between" | "space-around";
9-
maxWidth?: string;
9+
maxWidth?: boolean;
1010
contentAlign?: "start" | "center";
1111
contentPanel?: boolean;
1212
highlightTitleBaseline?: boolean;
@@ -35,10 +35,9 @@ const meta: Meta<StoryArgs> = {
3535
description: "Justification of highlight items along the main axis",
3636
},
3737
maxWidth: {
38-
control: { type: "select" },
39-
options: ["", "600px", "800px", "1024px", "1064px", "1200px", "1440px"],
38+
control: "boolean",
4039
description:
41-
"Max width of the central content column. Gutters grow to enforce the constraint; below this width they hold at 16px.",
40+
"When true, caps the central column at --max-width (default 1064px). Gutters grow responsively to enforce the constraint. Override --max-width via styleClassPassthrough to change the cap value.",
4241
},
4342
contentAlign: {
4443
control: { type: "inline-radio" },
@@ -78,7 +77,7 @@ const meta: Meta<StoryArgs> = {
7877
tag: "div",
7978
highlightsEqualWidths: false,
8079
highlightsJustify: "start",
81-
maxWidth: "",
80+
maxWidth: false,
8281
contentAlign: "center",
8382
contentPanel: true,
8483
highlightTitleBaseline: false,
@@ -119,6 +118,12 @@ All layout and visual properties are customisable via CSS custom properties. Set
119118
120119
\`\`\`css
121120
.page-hero-highlights {
121+
/* Grid layout */
122+
--max-width: 1064px;
123+
--page-hero-highlights-gutter-mobile: 16px;
124+
--page-hero-highlights-gutter-tablet: 40px;
125+
--page-hero-highlights-gutter-desktop: 32px;
126+
122127
/* Header zone */
123128
--header-row-background-colour: darkblue;
124129
@@ -196,6 +201,12 @@ All layout and visual properties are customisable via CSS custom properties. Set
196201
─────────────────────────────────────────────────────────────────── */
197202
.page-hero-highlights {
198203
204+
/* Grid layout */
205+
--max-width: 1064px;
206+
--page-hero-highlights-gutter-mobile: 16px;
207+
--page-hero-highlights-gutter-tablet: 40px;
208+
--page-hero-highlights-gutter-desktop: 32px;
209+
199210
/* Header zone */
200211
--header-row-background-colour: darkblue;
201212
@@ -330,10 +341,10 @@ export const NoSlotContent: Story = {
330341
}),
331342
};
332343

333-
/** Max width centered — content column capped at 800px with equal growing gutters either side. */
344+
/** Max width centered — content column capped at --max-width (1064px) with equal growing gutters either side. */
334345
export const MaxWidthCentered: Story = {
335346
name: "Max Width — Centered",
336-
args: { maxWidth: "800px", contentAlign: "center" },
347+
args: { maxWidth: true, contentAlign: "center" },
337348
render: (args: StoryArgs) => ({
338349
components: { PageHeroHighlights },
339350
setup() {
@@ -343,7 +354,7 @@ export const MaxWidthCentered: Story = {
343354
<PageHeroHighlights v-bind="componentArgs" :style="bgStyles">
344355
<template #header>
345356
<p class="page-heading-1">Dashboard</p>
346-
<p class="page-body-normal">Content column is capped at 800px — gutters grow equally on both sides.</p>
357+
<p class="page-body-normal">Content column is capped at --max-width (1064px by default) — gutters grow equally on both sides.</p>
347358
</template>
348359
349360
<template #highlights>
@@ -370,10 +381,10 @@ export const MaxWidthCentered: Story = {
370381
}),
371382
};
372383

373-
/** Max width start — content column capped at 800px, pinned to the left with a fixed 16px gutter. */
384+
/** Max width start — content column capped at --max-width (1064px), pinned to the left. */
374385
export const MaxWidthStart: Story = {
375386
name: "Max Width — Start",
376-
args: { maxWidth: "800px", contentAlign: "start" },
387+
args: { maxWidth: true, contentAlign: "start" },
377388
render: (args: StoryArgs) => ({
378389
components: { PageHeroHighlights },
379390
setup() {
@@ -383,7 +394,7 @@ export const MaxWidthStart: Story = {
383394
<PageHeroHighlights v-bind="componentArgs" :style="bgStyles">
384395
<template #header>
385396
<p class="page-heading-1">Dashboard</p>
386-
<p class="page-body-normal">Content column is capped at 800px, aligned to the start — right side takes the remaining space.</p>
397+
<p class="page-body-normal">Content column is capped at --max-width (1064px by default), aligned to the start — right side takes the remaining space.</p>
387398
</template>
388399
389400
<template #highlights>

app/components/04.templates/page-hero-highlights/tests/PageHeroHighlights.spec.ts

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -168,39 +168,30 @@ describe("PageHeroHighlights", () => {
168168
expect(el.classes()).toContain("another-class");
169169
});
170170

171-
describe("gridColumns", () => {
172-
interface ComponentInstance {
173-
gridColumns: string;
174-
}
175-
176-
it("defaults to fixed 16px gutters with no maxWidth", async () => {
171+
describe("grid layout classes", () => {
172+
it("applies center class by default", async () => {
177173
const wrapper = await mountSuspended(PageHeroHighlights);
178-
const vm = wrapper.vm as unknown as ComponentInstance;
179-
expect(vm.gridColumns).toBe("16px 1fr 16px");
174+
expect(wrapper.find(".page-hero-highlights").classes()).toContain("center");
180175
});
181176

182-
it("returns centered max-width columns when maxWidth is set and contentAlign is center", async () => {
183-
const wrapper = await mountSuspended(PageHeroHighlights, {
184-
props: { maxWidth: "1064px", contentAlign: "center" },
185-
});
186-
const vm = wrapper.vm as unknown as ComponentInstance;
187-
expect(vm.gridColumns).toBe("max(16px, (100% - 1064px) / 2) 1fr max(16px, (100% - 1064px) / 2)");
177+
it("does not apply max-width class by default", async () => {
178+
const wrapper = await mountSuspended(PageHeroHighlights);
179+
expect(wrapper.find(".page-hero-highlights").classes()).not.toContain("max-width");
188180
});
189181

190-
it("returns start-aligned columns when maxWidth is set and contentAlign is start", async () => {
182+
it("applies max-width class when maxWidth is true", async () => {
191183
const wrapper = await mountSuspended(PageHeroHighlights, {
192-
props: { maxWidth: "1064px", contentAlign: "start" },
184+
props: { maxWidth: true },
193185
});
194-
const vm = wrapper.vm as unknown as ComponentInstance;
195-
expect(vm.gridColumns).toBe("16px minmax(0, 1064px) minmax(16px, 1fr)");
186+
expect(wrapper.find(".page-hero-highlights").classes()).toContain("max-width");
196187
});
197188

198-
it("ignores contentAlign when maxWidth is not set", async () => {
189+
it("applies start class when contentAlign is start", async () => {
199190
const wrapper = await mountSuspended(PageHeroHighlights, {
200191
props: { contentAlign: "start" },
201192
});
202-
const vm = wrapper.vm as unknown as ComponentInstance;
203-
expect(vm.gridColumns).toBe("16px 1fr 16px");
193+
expect(wrapper.find(".page-hero-highlights").classes()).toContain("start");
194+
expect(wrapper.find(".page-hero-highlights").classes()).not.toContain("center");
204195
});
205196
});
206197
});

app/components/04.templates/page-hero-highlights/tests/__snapshots__/PageHeroHighlights.spec.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

33
exports[`PageHeroHighlights > renders correct HTML structure 1`] = `
4-
"<div class="page-hero-highlights has-content-panel">
4+
"<div class="page-hero-highlights center has-content-panel">
55
<div class="header-row">
66
<div class="header-slot">
77
<h1>Page Title</h1>

0 commit comments

Comments
 (0)