srcdev-nuxt-components 9.1.7 → 9.1.9

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
  }
@@ -17,7 +17,7 @@
17
17
  @mouseleave="resetHoverNavToActive"
18
18
  @mouseover="handleNavHover"
19
19
  >
20
- <li v-for="item in navItemData.main" :key="item.href" :class="item.cssName">
20
+ <li v-for="item in navItemData.main" :key="item.href" :class="[item.cssName, { 'is-active': activeHref === item.href }]">
21
21
  <NuxtLink :href="item.href" :external="item.isExternal || undefined" class="site-nav-link" data-nav-item>
22
22
  <Icon v-if="item.iconName" :name="item.iconName" aria-hidden="true" />
23
23
  {{ item.text }}
@@ -70,7 +70,7 @@
70
70
  @mouseover="handlePanelHover"
71
71
  @mouseleave="resetHoverPanelToActive"
72
72
  >
73
- <li v-for="item in navItemData.main" :key="item.href" :class="item.cssName">
73
+ <li v-for="item in navItemData.main" :key="item.href" :class="[item.cssName, { 'is-active': activeHref === item.href }]">
74
74
  <NuxtLink
75
75
  :href="item.href"
76
76
  :external="item.isExternal || undefined"
@@ -231,9 +231,7 @@ const initNavDecorators = () => {
231
231
  }
232
232
 
233
233
  const activeLink =
234
- links.find((el) => el.classList.contains("router-link-exact-active")) ??
235
- links.find((el) => el.classList.contains("router-link-active")) ??
236
- links[0];
234
+ links.find((el) => el.getAttribute("href") === activeHref.value) ?? links[0];
237
235
  if (!activeLink) return;
238
236
 
239
237
  currentActiveNavLink = activeLink;
@@ -337,9 +335,7 @@ const initPanelDecorators = () => {
337
335
  }
338
336
 
339
337
  const activeLink =
340
- links.find((el) => el.classList.contains("router-link-exact-active")) ??
341
- links.find((el) => el.classList.contains("router-link-active")) ??
342
- links[0];
338
+ links.find((el) => el.getAttribute("href") === activeHref.value) ?? links[0];
343
339
  if (!activeLink) return;
344
340
 
345
341
  currentActivePanelLink = activeLink;
@@ -352,11 +348,20 @@ const initPanelDecorators = () => {
352
348
 
353
349
  // ─────────────────────────────────────────────────────────────────────────────
354
350
 
355
- // Close panel on navigation and re-init decorators.
356
- // flush: 'post' ensures router-link-active classes are applied.
357
- // requestAnimationFrame defers the measurement until after browser layout,
358
- // preventing stale offsetLeft reads and racing with hover setTimeouts.
351
+ // Compute active href from route directly — avoids racing against Vue Router's
352
+ // class application timing when reading router-link-exact-active from the DOM.
359
353
  const route = useRoute();
354
+
355
+ const activeHref = computed(() => {
356
+ const items = props.navItemData.main ?? [];
357
+ const exact = items.find((item) => item.href && route.path === item.href);
358
+ if (exact) return exact.href ?? null;
359
+ return (
360
+ items
361
+ .filter((item) => item.href && route.path.startsWith(item.href + "/"))
362
+ .sort((a, b) => (b.href?.length ?? 0) - (a.href?.length ?? 0))[0]?.href ?? null
363
+ );
364
+ });
360
365
  watch(
361
366
  () => route.path,
362
367
  () => {
@@ -379,12 +384,16 @@ useResizeObserver(navRef, () => {
379
384
 
380
385
  onClickOutside(navRef, closeMenu);
381
386
 
387
+ const router = useRouter();
388
+
382
389
  onMounted(async () => {
383
390
  await nextTick();
384
391
  checkOverflow();
385
392
  isLoaded.value = true;
386
- await nextTick();
387
- initNavDecorators();
393
+ await router.isReady();
394
+ requestAnimationFrame(() => {
395
+ initNavDecorators();
396
+ });
388
397
  });
389
398
 
390
399
  watch(isCollapsed, async (collapsed) => {
@@ -580,9 +589,9 @@ watch(
580
589
  outline: none;
581
590
  }
582
591
 
583
- &.router-link-exact-active {
584
- color: var(--_link-active-color);
585
- }
592
+ }
593
+ li.is-active .site-nav-link {
594
+ color: var(--_link-active-color);
586
595
  }
587
596
  }
588
597
 
@@ -773,9 +782,9 @@ watch(
773
782
  outline: none;
774
783
  }
775
784
 
776
- &.router-link-exact-active {
777
- color: var(--_panel-link-active-color);
778
- }
785
+ }
786
+ li.is-active .site-nav-panel-link {
787
+ color: var(--_panel-link-active-color);
779
788
  }
780
789
  }
781
790
  }