srcdev-nuxt-components 9.1.7 → 9.1.8

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.
@@ -17,8 +17,8 @@ The layout uses a 4-row CSS Grid with `subgrid` — no `translate`, negative mar
17
17
  | `tag` | `"div" \| "section" \| "main"` | `"div"` | Root element tag |
18
18
  | `highlightsEqualWidths` | `boolean` | `false` | Equal-width grid columns for highlight items |
19
19
  | `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
+ | `widthConstrained` | `boolean` | `false` | When `true`, caps the central column at `--width-constrained` (default `1064px`). Gutters grow responsively to enforce the constraint. |
21
+ | `contentAlign` | `"start" \| "center"` | `"center"` | When `widthConstrained` is `true`: `"center"` grows gutters equally; `"start"` pins content to the left with a fixed left gutter. |
22
22
  | `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. |
23
23
  | `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. |
24
24
  | `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:
66
66
  Example with `HeroText` in the header slot:
67
67
 
68
68
  ```vue
69
- <PageHeroHighlights tag="section" max-width="1064px">
69
+ <PageHeroHighlights tag="section" :width-constrained="true">
70
70
  <template #header="{ headingId }">
71
71
  <HeroText :heading-id="headingId" text="Welcome back" accent-text="Simon" />
72
72
  </template>
@@ -106,11 +106,13 @@ Located at: `app/components/04.templates/page-hero-highlights/PageHeroHighlights
106
106
 
107
107
  ### CSS tokens
108
108
 
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` |
114
116
 
115
117
  ### Usage
116
118
 
@@ -170,17 +172,26 @@ By default, highlight items size to their content (`flex-wrap`). Pass `:highligh
170
172
 
171
173
  ## Constraining the central column width
172
174
 
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 `:width-constrained="true"` to cap the content column at `--width-constrained` (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:
174
176
 
175
177
  ```vue
176
- <!-- Centred, capped at 1064px -->
177
- <PageHeroHighlights max-width="1064px" content-align="center">...</PageHeroHighlights>
178
+ <!-- Centred, capped at --width-constrained (1064px) -->
179
+ <PageHeroHighlights :width-constrained="true" content-align="center">...</PageHeroHighlights>
178
180
 
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 :width-constrained="true" content-align="start">...</PageHeroHighlights>
181
183
  ```
182
184
 
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
+ --width-constrained: 1200px;
190
+ --page-hero-highlights-gutter-desktop: 48px;
191
+ }
192
+ ```
193
+
194
+ See [css-grid-width-constrained-gutters.md](../css-grid-width-constrained-gutters.md) for the underlying pattern explanation.
184
195
 
185
196
  ## Local style override scaffold
186
197
 
@@ -199,6 +210,12 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
199
210
  ─────────────────────────────────────────────────────────────────── */
200
211
  .page-hero-highlights {
201
212
  &.my-page-hero {
213
+ /* Grid layout */
214
+ /* --width-constrained: 1064px; */
215
+ /* --page-hero-highlights-gutter-mobile: 16px; */
216
+ /* --page-hero-highlights-gutter-tablet: 40px; */
217
+ /* --page-hero-highlights-gutter-desktop: 32px; */
218
+
202
219
  /* Header zone */
203
220
  /* --header-row-background-colour: darkblue; */
204
221
 
@@ -239,8 +256,6 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
239
256
  </style>
240
257
  ```
241
258
 
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
-
244
259
  ## Grid structure (reference)
245
260
 
246
261
  ```text
@@ -254,6 +269,8 @@ row4: page content (never underflows highlights)
254
269
  `.header-row` spans cols 1–3, rows 1–2 (edge-to-edge bg). `.header-slot` is placed in row 1 only.
255
270
  `.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.
256
271
 
272
+ Grid columns are determined entirely by CSS — no `v-bind`. The `widthConstrained` and `contentAlign` props add CSS classes (`width-constrained`, `start`, `center`) which select the appropriate `grid-template-columns` rule.
273
+
257
274
  ## Layout pitfall: do not use `grid-template-rows: subgrid` inside `.highlights-row`
258
275
 
259
276
  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
280
297
  - Component is auto-imported in Nuxt — no import needed.
281
298
  - Lives in `app/components/04.templates/page-hero-highlights/`.
282
299
  - 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 `widthConstrained` 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.
@@ -31,10 +31,47 @@ Left gutter stays fixed, content column is capped at `MAX_WIDTH`, remaining spac
31
31
 
32
32
  ## In Vue with a prop
33
33
 
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
+
34
72
  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:
35
73
 
36
74
  ```ts
37
- // Props
38
75
  interface Props {
39
76
  maxWidth?: string; // e.g. "1064px"
40
77
  contentAlign?: "start" | "center";
@@ -45,7 +82,6 @@ const props = withDefaults(defineProps<Props>(), {
45
82
  contentAlign: "center",
46
83
  });
47
84
 
48
- // Computed column string
49
85
  const gridColumns = computed(() => {
50
86
  if (!props.maxWidth) return "16px 1fr 16px";
51
87
  if (props.contentAlign === "start") return `16px minmax(0, ${props.maxWidth}) 1fr`;
@@ -15,19 +15,31 @@ html {
15
15
  transition:
16
16
  background-color 0.4s ease,
17
17
  color 0.4s ease;
18
- /* scrollbar-gutter: stable; */
18
+ scrollbar-gutter: stable;
19
19
 
20
20
  overflow-x: clip;
21
- }
22
- body {
23
- background-color: var(--page-bg, lightgray);
24
- color: var(--colour-text-default);
25
- font-family: var(--font-family);
26
- font-size: var(--step-4);
27
- min-height: 100dvh;
28
- transition:
29
- background-color 0.4s ease,
30
- color 0.4s ease;
31
21
 
32
- overflow-x: clip;
22
+ body {
23
+ background-color: var(--page-bg, lightgray);
24
+ color: var(--colour-text-default);
25
+ font-family: var(--font-family);
26
+ font-size: var(--step-4);
27
+ /* min-height: 100dvh; */
28
+ transition:
29
+ background-color 0.4s ease,
30
+ color 0.4s ease;
31
+
32
+ overflow-x: clip;
33
+
34
+ #__nuxt {
35
+ height: 100%;
36
+ div {
37
+ .page-layout {
38
+ min-block-size: 100svh;
39
+ display: grid;
40
+ grid-template-rows: auto 1fr auto;
41
+ }
42
+ }
43
+ }
44
+ }
33
45
  }
@@ -379,12 +379,16 @@ useResizeObserver(navRef, () => {
379
379
 
380
380
  onClickOutside(navRef, closeMenu);
381
381
 
382
+ const router = useRouter();
383
+
382
384
  onMounted(async () => {
383
385
  await nextTick();
384
386
  checkOverflow();
385
387
  isLoaded.value = true;
386
- await nextTick();
387
- initNavDecorators();
388
+ await router.isReady();
389
+ requestAnimationFrame(() => {
390
+ initNavDecorators();
391
+ });
388
392
  });
389
393
 
390
394
  watch(isCollapsed, async (collapsed) => {
@@ -7,7 +7,7 @@
7
7
  <div class="info-wrapper" :class="infoWrapperClasses">
8
8
  <EyebrowText font-size="large" :text-content="serviceData.subtitle" />
9
9
  <HeroText
10
- tag="h1"
10
+ :tag="headerTag"
11
11
  font-size="title"
12
12
  :text-content="[
13
13
  {
@@ -33,7 +33,7 @@
33
33
 
34
34
  <HeroText
35
35
  v-if="!isSummary"
36
- tag="h2"
36
+ :tag="headerTag"
37
37
  axis="horizontal"
38
38
  font-size="subheading"
39
39
  :text-content="serviceData.heroHeading"
@@ -47,7 +47,7 @@
47
47
 
48
48
  <HeroText
49
49
  v-if="!isSummary"
50
- tag="h2"
50
+ :tag="headerTag"
51
51
  axis="horizontal"
52
52
  font-size="subheading"
53
53
  :text-content="[{ text: 'The Process ', styleClass: 'normal' }]"
@@ -69,7 +69,7 @@
69
69
 
70
70
  <HeroText
71
71
  v-if="!isSummary"
72
- tag="h2"
72
+ :tag="headerTag"
73
73
  axis="horizontal"
74
74
  font-size="subheading"
75
75
  :text-content="[{ text: 'Ideal For', styleClass: 'normal' }]"
@@ -87,7 +87,7 @@
87
87
 
88
88
  <HeroText
89
89
  v-if="!isSummary"
90
- tag="h2"
90
+ :tag="headerTag"
91
91
  axis="horizontal"
92
92
  font-size="subheading"
93
93
  :text-content="[{ text: 'Aftercare &amp; Maintenance', styleClass: 'normal' }]"
@@ -98,7 +98,7 @@
98
98
 
99
99
  <HeroText
100
100
  v-if="!isSummary"
101
- tag="h2"
101
+ :tag="headerTag"
102
102
  axis="horizontal"
103
103
  font-size="subheading"
104
104
  :text-content="[{ text: 'Frequently Asked Questions', styleClass: 'normal' }]"
@@ -144,6 +144,7 @@ import type { Service } from "~/types/types.services";
144
144
 
145
145
  interface Props {
146
146
  tag?: "div" | "section" | "article" | "main";
147
+ headerTag?: "h1" | "h2" | "h3";
147
148
  index?: number;
148
149
  serviceData: Service;
149
150
  isSummary?: boolean;
@@ -153,6 +154,7 @@ interface Props {
153
154
  }
154
155
  const props = withDefaults(defineProps<Props>(), {
155
156
  tag: "div",
157
+ headerTag: "h2",
156
158
  index: 0,
157
159
  isSummary: false,
158
160
  summaryAlignment: "center",
@@ -2,7 +2,7 @@
2
2
  <component
3
3
  :is="tag"
4
4
  class="page-hero-highlights"
5
- :class="[elementClasses, componentClasses, { 'has-content-panel': contentPanel }]"
5
+ :class="[elementClasses, componentClasses]"
6
6
  :aria-labelledby="ariaLabelledby"
7
7
  >
8
8
  <div class="header-row">
@@ -26,7 +26,7 @@ interface Props {
26
26
  tag?: "div" | "section" | "main";
27
27
  highlightsEqualWidths?: boolean;
28
28
  highlightsJustify?: "start" | "center" | "end" | "space-between" | "space-around";
29
- maxWidth?: string;
29
+ widthConstrained?: boolean;
30
30
  contentAlign?: "start" | "center";
31
31
  contentPanel?: boolean;
32
32
  highlightTitleBaseline?: boolean;
@@ -37,22 +37,19 @@ const props = withDefaults(defineProps<Props>(), {
37
37
  tag: "div",
38
38
  highlightsEqualWidths: false,
39
39
  highlightsJustify: "start",
40
- maxWidth: undefined,
40
+ widthConstrained: false,
41
41
  contentAlign: "center",
42
42
  contentPanel: true,
43
43
  highlightTitleBaseline: false,
44
44
  styleClassPassthrough: () => [],
45
45
  });
46
46
 
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
-
53
47
  const { headingId, ariaLabelledby } = useAriaLabelledById(() => props.tag);
54
48
  const componentClasses = computed(() => ({
55
49
  "highlight-title-baseline": props.highlightTitleBaseline,
50
+ [props.contentAlign]: true,
51
+ "width-constrained": props.widthConstrained,
52
+ "has-content-panel": props.contentPanel,
56
53
  }));
57
54
 
58
55
  const highlightClasses = computed(() => ({
@@ -73,7 +70,14 @@ watch(
73
70
 
74
71
  <style lang="css">
75
72
  .page-hero-highlights {
73
+ /* Layout tokens */
74
+ --max-width: 1064px;
75
+ --page-hero-highlights-gutter-mobile: 16px;
76
+ --page-hero-highlights-gutter-tablet: 40px;
77
+ --page-hero-highlights-gutter-desktop: 32px;
78
+
76
79
  /* User themable tokens */
80
+
77
81
  --header-row-background-colour: darkblue;
78
82
 
79
83
  --highlights-row-item-gap: 1rem;
@@ -105,11 +109,46 @@ watch(
105
109
  --highlight-padding-block-start: 0; /* We're setting the title height via row height, so this should be exposed for override in consuming page */
106
110
  }
107
111
 
112
+ /* Internal layout variables */
113
+ --page-hero-highlights-gutter: var(--page-hero-highlights-gutter-mobile);
114
+
115
+ @container (width >= 768px) {
116
+ --page-hero-highlights-gutter: var(--page-hero-highlights-gutter-tablet);
117
+ }
118
+ @container (width >= 1024px) {
119
+ --page-hero-highlights-gutter: var(--page-hero-highlights-gutter-desktop);
120
+ }
121
+
122
+ /* Private geometry */
123
+ --header-slot-grid-row: 1;
124
+ &.highlight-title-baseline {
125
+ --header-slot-grid-row: 1 / span 2;
126
+ }
127
+
108
128
  display: grid;
109
- grid-template-columns: v-bind(gridColumns);
129
+ /* grid-template-columns: v-bind(gridColumns); */
110
130
  grid-template-rows: auto var(--highlight-title-height) 1fr auto;
111
131
  gap: 0;
112
132
 
133
+ &.width-constrained {
134
+ grid-template-columns: var(--page-hero-highlights-gutter) 1fr var(--page-hero-highlights-gutter);
135
+ }
136
+
137
+ &:not(.width-constrained) {
138
+ &.start {
139
+ grid-template-columns: var(--page-hero-highlights-gutter) minmax(0, var(--max-width)) minmax(
140
+ var(--page-hero-highlights-gutter),
141
+ 1fr
142
+ );
143
+ }
144
+ &.center {
145
+ grid-template-columns: max(var(--page-hero-highlights-gutter), (100% - var(--max-width)) / 2) 1fr max(
146
+ var(--page-hero-highlights-gutter),
147
+ (100% - var(--max-width)) / 2
148
+ );
149
+ }
150
+ }
151
+
113
152
  .header-row {
114
153
  /* Element geometry */
115
154
  grid-column: 1 / -1;
@@ -123,7 +162,7 @@ watch(
123
162
 
124
163
  .header-slot {
125
164
  grid-column: 2;
126
- grid-row: 1;
165
+ grid-row: var(--header-slot-grid-row);
127
166
  container-type: inline-size;
128
167
  }
129
168
  }
@@ -34,14 +34,24 @@ watch(
34
34
  <style lang="css">
35
35
  .page-hero-highlights-header {
36
36
  /* User themable tokens */
37
- --phh-padding-block: 1.6rem;
37
+ --phh-padding-block-mobile: 1.6rem 3.2rem;
38
+ --phh-padding-block-tablet: 2.4rem 4.8rem;
39
+ --phh-padding-block-desktop: 3.2rem 6.4rem;
38
40
  --phh-gap: 1.6rem;
39
41
  --phh-end-gap: 0.8rem;
40
42
 
41
43
  display: flex;
42
44
  flex-direction: column;
43
45
  gap: var(--phh-gap);
44
- padding-block: var(--phh-padding-block);
46
+
47
+ padding-block: var(--phh-padding-block-mobile);
48
+
49
+ @container (width >= 768px) {
50
+ padding-block: var(--phh-padding-block-tablet);
51
+ }
52
+ @container (width >= 1024px) {
53
+ padding-block: var(--phh-padding-block-desktop);
54
+ }
45
55
 
46
56
  &:has(.phh-end) {
47
57
  @container (width >= 768px) {
@@ -6,7 +6,7 @@ type StoryArgs = {
6
6
  tag?: "div" | "section" | "main";
7
7
  highlightsEqualWidths?: boolean;
8
8
  highlightsJustify?: "start" | "center" | "end" | "space-between" | "space-around";
9
- maxWidth?: string;
9
+ widthConstrained?: boolean;
10
10
  contentAlign?: "start" | "center";
11
11
  contentPanel?: boolean;
12
12
  highlightTitleBaseline?: boolean;
@@ -34,11 +34,10 @@ const meta: Meta<StoryArgs> = {
34
34
  options: ["start", "center", "end", "space-between", "space-around"],
35
35
  description: "Justification of highlight items along the main axis",
36
36
  },
37
- maxWidth: {
38
- control: { type: "select" },
39
- options: ["", "600px", "800px", "1024px", "1064px", "1200px", "1440px"],
37
+ widthConstrained: {
38
+ control: "boolean",
40
39
  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.",
42
41
  },
43
42
  contentAlign: {
44
43
  control: { type: "inline-radio" },
@@ -78,7 +77,7 @@ const meta: Meta<StoryArgs> = {
78
77
  tag: "div",
79
78
  highlightsEqualWidths: false,
80
79
  highlightsJustify: "start",
81
- maxWidth: "",
80
+ widthConstrained: false,
82
81
  contentAlign: "center",
83
82
  contentPanel: true,
84
83
  highlightTitleBaseline: false,
@@ -119,6 +118,12 @@ All layout and visual properties are customisable via CSS custom properties. Set
119
118
 
120
119
  \`\`\`css
121
120
  .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
+
122
127
  /* Header zone */
123
128
  --header-row-background-colour: darkblue;
124
129
 
@@ -196,6 +201,12 @@ All layout and visual properties are customisable via CSS custom properties. Set
196
201
  ─────────────────────────────────────────────────────────────────── */
197
202
  .page-hero-highlights {
198
203
 
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
+
199
210
  /* Header zone */
200
211
  --header-row-background-colour: darkblue;
201
212
 
@@ -330,10 +341,10 @@ export const NoSlotContent: Story = {
330
341
  }),
331
342
  };
332
343
 
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. */
334
345
  export const MaxWidthCentered: Story = {
335
346
  name: "Max Width — Centered",
336
- args: { maxWidth: "800px", contentAlign: "center" },
347
+ args: { widthConstrained: true, contentAlign: "center" },
337
348
  render: (args: StoryArgs) => ({
338
349
  components: { PageHeroHighlights },
339
350
  setup() {
@@ -343,7 +354,7 @@ export const MaxWidthCentered: Story = {
343
354
  <PageHeroHighlights v-bind="componentArgs" :style="bgStyles">
344
355
  <template #header>
345
356
  <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>
347
358
  </template>
348
359
 
349
360
  <template #highlights>
@@ -370,10 +381,10 @@ export const MaxWidthCentered: Story = {
370
381
  }),
371
382
  };
372
383
 
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. */
374
385
  export const MaxWidthStart: Story = {
375
386
  name: "Max Width — Start",
376
- args: { maxWidth: "800px", contentAlign: "start" },
387
+ args: { widthConstrained: true, contentAlign: "start" },
377
388
  render: (args: StoryArgs) => ({
378
389
  components: { PageHeroHighlights },
379
390
  setup() {
@@ -383,7 +394,7 @@ export const MaxWidthStart: Story = {
383
394
  <PageHeroHighlights v-bind="componentArgs" :style="bgStyles">
384
395
  <template #header>
385
396
  <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>
387
398
  </template>
388
399
 
389
400
  <template #highlights>
@@ -168,39 +168,30 @@ describe("PageHeroHighlights", () => {
168
168
  expect(el.classes()).toContain("another-class");
169
169
  });
170
170
 
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 () => {
177
173
  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");
180
175
  });
181
176
 
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 width-constrained class by default", async () => {
178
+ const wrapper = await mountSuspended(PageHeroHighlights);
179
+ expect(wrapper.find(".page-hero-highlights").classes()).not.toContain("width-constrained");
188
180
  });
189
181
 
190
- it("returns start-aligned columns when maxWidth is set and contentAlign is start", async () => {
182
+ it("applies width-constrained class when widthConstrained is true", async () => {
191
183
  const wrapper = await mountSuspended(PageHeroHighlights, {
192
- props: { maxWidth: "1064px", contentAlign: "start" },
184
+ props: { widthConstrained: true },
193
185
  });
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("width-constrained");
196
187
  });
197
188
 
198
- it("ignores contentAlign when maxWidth is not set", async () => {
189
+ it("applies start class when contentAlign is start", async () => {
199
190
  const wrapper = await mountSuspended(PageHeroHighlights, {
200
191
  props: { contentAlign: "start" },
201
192
  });
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");
204
195
  });
205
196
  });
206
197
  });
@@ -1,7 +1,7 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  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">
5
5
  <div class="header-row">
6
6
  <div class="header-slot">
7
7
  <h1>Page Title</h1>
@@ -4,12 +4,12 @@
4
4
  <template #layout-content>
5
5
  <PageHeroHighlights
6
6
  tag="section"
7
- max-width="1064px"
8
- content-align="start"
9
- :content-panel="true"
10
- :highlights-equal-widths="false"
11
- highlights-justify="start"
12
- :highlight-title-baseline="true"
7
+ :width-constrained="widthConstrained"
8
+ :content-align="contentAlign"
9
+ :content-panel="contentPanel"
10
+ :highlights-equal-widths="highlightsEqualWidths"
11
+ :highlights-justify="highlightsJustify"
12
+ :highlight-title-baseline="highlightTitleBaseline"
13
13
  :style-class-passthrough="['mbe-32']"
14
14
  >
15
15
  <template #header="{ headingId }">
@@ -38,6 +38,54 @@
38
38
  <p class="page-heading-2">Recent Activity</p>
39
39
  <p class="page-body-normal">Your most recent transactions and events will appear here.</p>
40
40
  </div>
41
+
42
+ <div class="demo-controls">
43
+ <p class="demo-controls__heading">Props</p>
44
+ <div class="demo-controls__toggles">
45
+ <ToggleSwitchWithLabelInline
46
+ v-model="widthConstrained"
47
+ name="widthConstrained"
48
+ label="widthConstrained"
49
+ />
50
+ <ToggleSwitchWithLabelInline v-model="contentPanel" name="contentPanel" label="contentPanel" />
51
+ <ToggleSwitchWithLabelInline
52
+ v-model="highlightsEqualWidths"
53
+ name="highlightsEqualWidths"
54
+ label="highlightsEqualWidths"
55
+ />
56
+ <ToggleSwitchWithLabelInline
57
+ v-model="highlightTitleBaseline"
58
+ name="highlightTitleBaseline"
59
+ label="highlightTitleBaseline"
60
+ />
61
+ </div>
62
+ <div class="demo-controls__selects">
63
+ <MultipleRadiobuttons
64
+ v-model="contentAlign"
65
+ v-model:field-data="contentAlignData"
66
+ name="contentAlign"
67
+ legend="contentAlign"
68
+ label="contentAlign"
69
+ error-message=""
70
+ :is-button="true"
71
+ :is-pill="true"
72
+ :field-has-error="false"
73
+ options-layout="inline"
74
+ />
75
+ <MultipleRadiobuttons
76
+ v-model="highlightsJustify"
77
+ v-model:field-data="highlightsJustifyData"
78
+ name="highlightsJustify"
79
+ legend="highlightsJustify"
80
+ label="highlightsJustify"
81
+ error-message=""
82
+ :is-button="true"
83
+ :is-pill="true"
84
+ :field-has-error="false"
85
+ options-layout="inline"
86
+ />
87
+ </div>
88
+ </div>
41
89
  </template>
42
90
  </PageHeroHighlights>
43
91
  </template>
@@ -46,6 +94,8 @@
46
94
  </template>
47
95
 
48
96
  <script setup lang="ts">
97
+ import type { IFormMultipleOptions } from "~/types/forms/types.forms";
98
+
49
99
  definePageMeta({
50
100
  layout: false,
51
101
  });
@@ -62,13 +112,52 @@ useHead({
62
112
  class: "page-hero-highlights-page",
63
113
  },
64
114
  });
115
+
116
+ const widthConstrained = ref(false);
117
+ const contentAlign = ref<"start" | "center">("start");
118
+ const contentPanel = ref(true);
119
+ const highlightsEqualWidths = ref(false);
120
+ const highlightsJustify = ref<"start" | "center" | "end" | "space-between" | "space-around">("start");
121
+ const highlightTitleBaseline = ref(true);
122
+
123
+ const contentAlignData = ref<IFormMultipleOptions>({
124
+ data: [
125
+ { id: "align-start", name: "contentAlign", value: "start", label: "start" },
126
+ { id: "align-center", name: "contentAlign", value: "center", label: "center" },
127
+ ],
128
+ total: 2,
129
+ skip: 0,
130
+ limit: 2,
131
+ });
132
+
133
+ const highlightsJustifyData = ref<IFormMultipleOptions>({
134
+ data: [
135
+ { id: "justify-start", name: "highlightsJustify", value: "start", label: "start" },
136
+ { id: "justify-center", name: "highlightsJustify", value: "center", label: "center" },
137
+ { id: "justify-end", name: "highlightsJustify", value: "end", label: "end" },
138
+ { id: "justify-space-between", name: "highlightsJustify", value: "space-between", label: "space-between" },
139
+ { id: "justify-space-around", name: "highlightsJustify", value: "space-around", label: "space-around" },
140
+ ],
141
+ total: 5,
142
+ skip: 0,
143
+ limit: 5,
144
+ });
65
145
  </script>
66
146
 
67
147
  <style lang="css">
68
148
  .page-hero-highlights-page {
69
149
  .page-hero-highlights {
150
+ /* Layout tokens */
151
+ --max-width: 1064px;
152
+ --page-hero-highlights-gutter-mobile: 16px;
153
+ --page-hero-highlights-gutter-tablet: 40px;
154
+ --page-hero-highlights-gutter-desktop: 32px;
155
+
70
156
  .page-hero-highlights-header {
71
- padding-block: 4rem 3rem;
157
+ /* User themable tokens */
158
+ --phh-padding-block-mobile: 1.6rem 6rem;
159
+ --phh-padding-block-tablet: 2.4rem 6rem;
160
+ --phh-padding-block-desktop: 4.2rem 7.4rem;
72
161
  }
73
162
 
74
163
  .content-row {
@@ -76,10 +165,40 @@ useHead({
76
165
  color: black;
77
166
 
78
167
  .some-class {
79
- min-height: 40svh;
168
+ padding-block-end: 2.4rem;
80
169
  }
81
170
  }
82
171
  }
83
172
  }
173
+
174
+ .demo-controls {
175
+ margin-block-start: 2.4rem;
176
+ padding: 1.6rem;
177
+ border: 1px solid var(--slate-06);
178
+ border-radius: 0.8rem;
179
+ background: var(--slate-01);
180
+
181
+ &__heading {
182
+ font-size: 1.1rem;
183
+ font-weight: 600;
184
+ text-transform: uppercase;
185
+ letter-spacing: 0.08em;
186
+ color: var(--slate-08);
187
+ margin-block-end: 1.2rem;
188
+ }
189
+
190
+ &__toggles {
191
+ display: flex;
192
+ flex-wrap: wrap;
193
+ gap: 0.4rem 2.4rem;
194
+ margin-block-end: 1.6rem;
195
+ }
196
+
197
+ &__selects {
198
+ display: flex;
199
+ flex-direction: column;
200
+ gap: 0.8rem;
201
+ }
202
+ }
84
203
  }
85
204
  </style>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.1.7",
4
+ "version": "9.1.8",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",