srcdev-nuxt-components 9.1.0 → 9.1.2
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/settings.json +2 -1
- package/.claude/skills/components/page-hero-highlights.md +60 -0
- package/.claude/skills/components/services-card-grid.md +110 -0
- package/.claude/skills/components/services-card.md +65 -30
- package/.claude/skills/components/site-navigation.md +120 -0
- package/.claude/skills/index.md +3 -2
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +780 -0
- package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +335 -0
- package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +328 -0
- package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +30 -0
- package/app/components/03.organisms/services/services-card/ServicesCard.vue +26 -10
- package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +27 -3
- package/app/components/04.templates/page-hero-highlights/PageHeroHighlights.vue +36 -21
- package/app/components/04.templates/page-hero-highlights/PageHeroHighlightsHeader.vue +66 -0
- package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlights.stories.ts +50 -3
- package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlightsHeader.stories.ts +77 -0
- package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlights.spec.ts +15 -7
- package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlightsHeader.spec.ts +51 -0
- package/app/components/04.templates/page-hero-highlights/tests/__snapshots__/PageHeroHighlights.spec.ts.snap +1 -1
- package/app/layouts/default.vue +2 -0
- package/app/layouts/site-navigation-demo.vue +69 -0
- package/app/pages/page-hero-highlights.vue +15 -11
- package/app/pages/ui/navigation/site-navigation/about.vue +11 -0
- package/app/pages/ui/navigation/site-navigation/contact.vue +11 -0
- package/app/pages/ui/navigation/site-navigation/index.vue +11 -0
- package/app/pages/ui/navigation/site-navigation/portfolio.vue +11 -0
- package/app/pages/ui/navigation/site-navigation/services.vue +11 -0
- package/app/pages/ui/services/services-cards.vue +16 -1
- package/nuxt.config.ts +7 -0
- package/package.json +6 -6
- package/.claude/skills/components/treatment-consultant.md +0 -128
package/.claude/settings.json
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
],
|
|
21
21
|
"additionalDirectories": [
|
|
22
22
|
"/Users/simoncornforth/websites/nuxt-components/app/components/01.atoms/content-wrappers/content-width",
|
|
23
|
-
"/Users/simoncornforth/websites/nuxt-components/.claude/skills"
|
|
23
|
+
"/Users/simoncornforth/websites/nuxt-components/.claude/skills",
|
|
24
|
+
"/Users/simoncornforth/websites/nuxt-components/app/components/02.molecules/navigation/site-navigation/tests"
|
|
24
25
|
]
|
|
25
26
|
}
|
|
26
27
|
}
|
|
@@ -19,6 +19,8 @@ The layout uses a 4-row CSS Grid with `subgrid` — no `translate`, negative mar
|
|
|
19
19
|
| `highlightsJustify` | `"start" \| "center" \| "end" \| "space-between" \| "space-around"` | `"start"` | Alignment of highlight items along the main axis |
|
|
20
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
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. |
|
|
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
|
+
| `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. |
|
|
22
24
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root element |
|
|
23
25
|
|
|
24
26
|
## Slots
|
|
@@ -83,6 +85,64 @@ Example with `HeroText` in the header slot:
|
|
|
83
85
|
</PageHeroHighlights>
|
|
84
86
|
```
|
|
85
87
|
|
|
88
|
+
## PageHeroHighlightsHeader companion component
|
|
89
|
+
|
|
90
|
+
A co-located companion component for laying out the `#header` slot. Provides a responsive two-area layout: `#start` (title/description) and `#end` (action buttons), stacking vertically on mobile and sitting side-by-side on wider viewports.
|
|
91
|
+
|
|
92
|
+
Located at: `app/components/04.templates/page-hero-highlights/PageHeroHighlightsHeader.vue`
|
|
93
|
+
|
|
94
|
+
### PageHeroHighlightsHeader props
|
|
95
|
+
|
|
96
|
+
| Prop | Type | Default | Description |
|
|
97
|
+
| ----------------------- | -------------------- | ------- | --------------------------------- |
|
|
98
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root element |
|
|
99
|
+
|
|
100
|
+
### PageHeroHighlightsHeader slots
|
|
101
|
+
|
|
102
|
+
| Slot | Purpose |
|
|
103
|
+
| ------- | --------------------------------------------------------- |
|
|
104
|
+
| `start` | Title and description — always rendered, fills full width when `#end` is absent |
|
|
105
|
+
| `end` | Action buttons — `.phh-end` is only mounted when this slot is provided |
|
|
106
|
+
|
|
107
|
+
### CSS tokens
|
|
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` |
|
|
114
|
+
|
|
115
|
+
### Usage
|
|
116
|
+
|
|
117
|
+
```vue
|
|
118
|
+
<PageHeroHighlights tag="section">
|
|
119
|
+
<template #header="{ headingId }">
|
|
120
|
+
<PageHeroHighlightsHeader>
|
|
121
|
+
<template #start>
|
|
122
|
+
<h1 :id="headingId" class="page-heading-1">Surplus needs</h1>
|
|
123
|
+
<p class="page-body-normal">Let us know what you need help with.</p>
|
|
124
|
+
</template>
|
|
125
|
+
<template #end>
|
|
126
|
+
<HelpButton />
|
|
127
|
+
<Button>Create new need</Button>
|
|
128
|
+
</template>
|
|
129
|
+
</PageHeroHighlightsHeader>
|
|
130
|
+
</template>
|
|
131
|
+
...
|
|
132
|
+
</PageHeroHighlights>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Omit `#end` for a single-element header — `#start` fills full width with no layout change needed:
|
|
136
|
+
|
|
137
|
+
```vue
|
|
138
|
+
<PageHeroHighlightsHeader>
|
|
139
|
+
<template #start>
|
|
140
|
+
<h1 :id="headingId">Dashboard</h1>
|
|
141
|
+
<p>Overview of your account activity.</p>
|
|
142
|
+
</template>
|
|
143
|
+
</PageHeroHighlightsHeader>
|
|
144
|
+
```
|
|
145
|
+
|
|
86
146
|
## With aria-labelledby (section tag)
|
|
87
147
|
|
|
88
148
|
When `tag="section"`, `aria-labelledby` is set automatically. Wire the heading id via the scoped slot prop:
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# ServicesCardGrid Component
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`ServicesCardGrid` renders a responsive auto-fit CSS grid of `ServicesCard` components from a `Service[]` array. It owns the grid layout; card-level presentation (eyebrow/hero typography) and slot content are configured via pass-through props and a scoped `#actions` slot.
|
|
6
|
+
|
|
7
|
+
## Props
|
|
8
|
+
|
|
9
|
+
| Prop | Type | Default | Required |
|
|
10
|
+
| ----------------------- | --------------------------------- | -------- | -------- |
|
|
11
|
+
| `servicesData` | `Service[]` | — | **yes** |
|
|
12
|
+
| `tag` | `"div" \| "section" \| "main"` | `"div"` | no |
|
|
13
|
+
| `eyebrowConfig` | `EyebrowConfig` | `{}` | no |
|
|
14
|
+
| `heroConfig` | `HeroConfig` | `{}` | no |
|
|
15
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | no |
|
|
16
|
+
|
|
17
|
+
Both config props are passed through to every `ServicesCard` in the grid unchanged. See [services-card.md](./services-card.md) for the full `EyebrowConfig` / `HeroConfig` key reference.
|
|
18
|
+
|
|
19
|
+
## Slots
|
|
20
|
+
|
|
21
|
+
| Slot | Slot props | Purpose |
|
|
22
|
+
| --------- | -------------------------- | ------------------------------------------------ |
|
|
23
|
+
| `actions` | `{ serviceData: Service }` | CTA rendered inside each card below description |
|
|
24
|
+
|
|
25
|
+
## CSS custom properties
|
|
26
|
+
|
|
27
|
+
Set on `.services-card-grid` (or scoped to a page class):
|
|
28
|
+
|
|
29
|
+
| Token | Default | Controls |
|
|
30
|
+
| ---------------------- | --------- | ------------------------------------- |
|
|
31
|
+
| `--_gap` | `4rem` | Gap between grid cells |
|
|
32
|
+
| `--_column-min-width` | `250px` | Minimum column width before wrapping |
|
|
33
|
+
|
|
34
|
+
## Consumer page boilerplate
|
|
35
|
+
|
|
36
|
+
```vue
|
|
37
|
+
<template>
|
|
38
|
+
<div>
|
|
39
|
+
<NuxtLayout name="default">
|
|
40
|
+
<template #layout-content>
|
|
41
|
+
<LayoutRow tag="div" variant="content" :style-class-passthrough="['mbe-20']">
|
|
42
|
+
<h1 class="page-heading-1">Services</h1>
|
|
43
|
+
</LayoutRow>
|
|
44
|
+
|
|
45
|
+
<LayoutRow tag="div" variant="content" :style-class-passthrough="['mbe-20']">
|
|
46
|
+
<ServicesCardGrid
|
|
47
|
+
:services-data="servicesData ?? []"
|
|
48
|
+
:eyebrow-config="{ fontSize: 'large' }"
|
|
49
|
+
:hero-config="{ tag: 'h2', fontSize: 'heading' }"
|
|
50
|
+
>
|
|
51
|
+
<template #actions="{ serviceData }">
|
|
52
|
+
<InputButtonCore
|
|
53
|
+
variant="secondary"
|
|
54
|
+
:button-text="`Enquire about ${serviceData.title}`"
|
|
55
|
+
:href="`/services/${serviceData.slug}`"
|
|
56
|
+
:style-class-passthrough="['mbs-24']"
|
|
57
|
+
>
|
|
58
|
+
<template #right>
|
|
59
|
+
<Icon name="mdi:arrow-right" class="icon" />
|
|
60
|
+
</template>
|
|
61
|
+
</InputButtonCore>
|
|
62
|
+
</template>
|
|
63
|
+
</ServicesCardGrid>
|
|
64
|
+
</LayoutRow>
|
|
65
|
+
</template>
|
|
66
|
+
</NuxtLayout>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<script setup lang="ts">
|
|
71
|
+
definePageMeta({ layout: false });
|
|
72
|
+
|
|
73
|
+
useHead({
|
|
74
|
+
title: "Services",
|
|
75
|
+
meta: [{ name: "description", content: "Our services" }],
|
|
76
|
+
bodyAttrs: { class: "page-services" },
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const store = useServicesStore();
|
|
80
|
+
const { servicesData } = storeToRefs(store);
|
|
81
|
+
|
|
82
|
+
if (servicesData.value.length === 0) {
|
|
83
|
+
await store.fetchServicesData();
|
|
84
|
+
}
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<style lang="css">
|
|
88
|
+
.page-services {
|
|
89
|
+
/* Page-level CSS token overrides — delete any you don't need */
|
|
90
|
+
.services-card-grid {
|
|
91
|
+
--_gap: 4rem;
|
|
92
|
+
--_column-min-width: 250px;
|
|
93
|
+
|
|
94
|
+
.services-card {
|
|
95
|
+
--_eyebrow-text-margin-block: 0.8rem 0;
|
|
96
|
+
--_hero-text-margin-block: 2rem 1rem;
|
|
97
|
+
--_description-text-colour: var(--colour-text-secondary);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
</style>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Notes
|
|
105
|
+
|
|
106
|
+
- Component is auto-imported in Nuxt — no import needed.
|
|
107
|
+
- The `Service` type is imported from `~/types/types.services`.
|
|
108
|
+
- Uses `repeat(auto-fit, minmax(var(--_column-min-width), 1fr))` — columns grow to fill available space and wrap when below the minimum width.
|
|
109
|
+
- The `#actions` slot template is passed down into each `ServicesCard`; `serviceData` is the scoped prop for the current iteration item.
|
|
110
|
+
- Data fetching is the page's responsibility — pass an empty array as fallback while loading (`servicesData ?? []`).
|
|
@@ -10,8 +10,26 @@
|
|
|
10
10
|
| ----------------------- | --------------------------------- | ------- | -------- |
|
|
11
11
|
| `serviceData` | `Service` | — | **yes** |
|
|
12
12
|
| `tag` | `"div" \| "section" \| "article"` | `"div"` | no |
|
|
13
|
+
| `eyebrowConfig` | `EyebrowConfig` | `{}` | no |
|
|
14
|
+
| `heroConfig` | `HeroConfig` | `{}` | no |
|
|
13
15
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | no |
|
|
14
16
|
|
|
17
|
+
### EyebrowConfig
|
|
18
|
+
|
|
19
|
+
| Key | Type | Default |
|
|
20
|
+
| ---------- | --------------------------------- | ---------- |
|
|
21
|
+
| `tag` | `"p" \| "div" \| "span"` | `"div"` |
|
|
22
|
+
| `fontSize` | `"large" \| "medium" \| "small"` | `"large"` |
|
|
23
|
+
|
|
24
|
+
### HeroConfig
|
|
25
|
+
|
|
26
|
+
| Key | Type | Default |
|
|
27
|
+
| ---------- | -------------------------------------------------------------- | ----------- |
|
|
28
|
+
| `tag` | `"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6"` | `"h2"` |
|
|
29
|
+
| `fontSize` | `"display" \| "title" \| "heading" \| "subheading" \| "label"` | `"heading"` |
|
|
30
|
+
|
|
31
|
+
Config objects are partial — only specify the keys you want to override. Unset keys fall back to the defaults shown above.
|
|
32
|
+
|
|
15
33
|
## Slots
|
|
16
34
|
|
|
17
35
|
| Slot | Slot props | Purpose |
|
|
@@ -20,6 +38,16 @@
|
|
|
20
38
|
|
|
21
39
|
The `actions` slot receives `serviceData` as a scoped prop so the consumer can construct routes or labels from the service data without additional props.
|
|
22
40
|
|
|
41
|
+
## CSS custom properties
|
|
42
|
+
|
|
43
|
+
Set on `.services-card` (or scoped to a page class):
|
|
44
|
+
|
|
45
|
+
| Token | Default | Controls |
|
|
46
|
+
| ------------------------------ | ----------------------------- | ------------------------------- |
|
|
47
|
+
| `--_eyebrow-text-margin-block` | `0.8rem 0` | Space above/below the eyebrow |
|
|
48
|
+
| `--_hero-text-margin-block` | `2rem 1rem` | Space above/below the title |
|
|
49
|
+
| `--_description-text-colour` | `var(--colour-text-secondary)` | Description paragraph colour |
|
|
50
|
+
|
|
23
51
|
## Basic usage
|
|
24
52
|
|
|
25
53
|
```vue
|
|
@@ -38,44 +66,50 @@ The `actions` slot receives `serviceData` as a scoped prop so the consumer can c
|
|
|
38
66
|
</ServicesCard>
|
|
39
67
|
```
|
|
40
68
|
|
|
41
|
-
##
|
|
69
|
+
## With config overrides
|
|
42
70
|
|
|
43
71
|
```vue
|
|
44
|
-
<ServicesCard
|
|
72
|
+
<ServicesCard
|
|
73
|
+
:service-data="service"
|
|
74
|
+
:eyebrow-config="{ fontSize: 'small' }"
|
|
75
|
+
:hero-config="{ tag: 'h3', fontSize: 'title' }"
|
|
76
|
+
>
|
|
45
77
|
<template #actions="{ serviceData }">
|
|
46
|
-
<InputButtonCore
|
|
47
|
-
variant="secondary"
|
|
48
|
-
:button-text="`More about ${serviceData.title}`"
|
|
49
|
-
:href="`/services/${serviceData.slug}`"
|
|
50
|
-
/>
|
|
51
|
-
<NuxtLink :to="`/services/${serviceData.slug}/contact`">Get in touch</NuxtLink>
|
|
78
|
+
<InputButtonCore variant="secondary" :button-text="`Enquire`" :href="`/services/${serviceData.slug}`" />
|
|
52
79
|
</template>
|
|
53
80
|
</ServicesCard>
|
|
54
81
|
```
|
|
55
82
|
|
|
56
|
-
##
|
|
57
|
-
|
|
58
|
-
When consuming this component, scaffold a style block using `styleClassPassthrough`. Delete the block if unused.
|
|
59
|
-
|
|
60
|
-
See [component-local-style-override.md](../component-local-style-override.md) for the full pattern.
|
|
83
|
+
## Consumer page boilerplate
|
|
61
84
|
|
|
62
85
|
```vue
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
<template>
|
|
87
|
+
<ServicesCard
|
|
88
|
+
:service-data="service"
|
|
89
|
+
:eyebrow-config="{ fontSize: 'large' }"
|
|
90
|
+
:hero-config="{ tag: 'h2', fontSize: 'heading' }"
|
|
91
|
+
>
|
|
92
|
+
<template #actions="{ serviceData }">
|
|
93
|
+
<InputButtonCore
|
|
94
|
+
variant="secondary"
|
|
95
|
+
:button-text="`Enquire about ${serviceData.title}`"
|
|
96
|
+
:href="`/services/${serviceData.slug}`"
|
|
97
|
+
:style-class-passthrough="['mbs-24']"
|
|
98
|
+
>
|
|
99
|
+
<template #right>
|
|
100
|
+
<Icon name="mdi:arrow-right" class="icon" />
|
|
101
|
+
</template>
|
|
102
|
+
</InputButtonCore>
|
|
103
|
+
</template>
|
|
104
|
+
</ServicesCard>
|
|
105
|
+
</template>
|
|
106
|
+
|
|
107
|
+
<style lang="css">
|
|
108
|
+
.page-my-page {
|
|
109
|
+
.services-card {
|
|
110
|
+
--_eyebrow-text-margin-block: 0.8rem 0;
|
|
111
|
+
--_hero-text-margin-block: 2rem 1rem;
|
|
112
|
+
--_description-text-colour: var(--colour-text-secondary);
|
|
79
113
|
}
|
|
80
114
|
}
|
|
81
115
|
</style>
|
|
@@ -85,5 +119,6 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
|
|
|
85
119
|
|
|
86
120
|
- Component is auto-imported in Nuxt — no import needed.
|
|
87
121
|
- The `Service` type is imported from `~/types/types.services`.
|
|
88
|
-
- Grid rows are sized `auto
|
|
122
|
+
- Grid rows are sized `auto auto auto 5lh auto` — the `5lh` row locks description height so actions align across cards.
|
|
89
123
|
- Image has a `3/4` aspect ratio with a subtle scale-on-hover effect.
|
|
124
|
+
- Usually consumed via `ServicesCardGrid` rather than directly.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# SiteNavigation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`SiteNavigation` is a responsive site-wide navigation component. It renders a horizontal link list on wide viewports and automatically collapses to a burger-menu + slide-down panel on narrow viewports. Collapse is driven by a `ResizeObserver` that compares the list's natural `scrollWidth` against the nav container's `clientWidth` — no breakpoint prop needed.
|
|
6
|
+
|
|
7
|
+
Both the horizontal list and the panel include animated active/hover indicator decorators (underline indicator + background highlight) that snap into position using CSS custom properties set via JavaScript.
|
|
8
|
+
|
|
9
|
+
## Component location
|
|
10
|
+
|
|
11
|
+
`app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue`
|
|
12
|
+
|
|
13
|
+
## Props
|
|
14
|
+
|
|
15
|
+
| Prop | Type | Default | Description |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| `navItemData` | `NavItemData` | — (required) | Navigation items — see type below |
|
|
18
|
+
| `navAlign` | `"left" \| "center" \| "right"` | `"left"` | Alignment of the horizontal nav list |
|
|
19
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root `<nav>` |
|
|
20
|
+
|
|
21
|
+
## NavItemData type
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import type { NavItemData } from "~/types/components/navigation-horizontal.d";
|
|
25
|
+
|
|
26
|
+
const navItemData: NavItemData = {
|
|
27
|
+
main: [
|
|
28
|
+
{ text: "Home", href: "/" },
|
|
29
|
+
{ text: "About", href: "/about" },
|
|
30
|
+
{ text: "Services", href: "/services", cssName: "is-featured" },
|
|
31
|
+
{ text: "Contact", href: "/contact", iconName: "heroicons:envelope", isExternal: false },
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`NavItem` fields:
|
|
37
|
+
|
|
38
|
+
| Field | Type | Description |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `text` | `string` | Link label |
|
|
41
|
+
| `href` | `string` | Link destination |
|
|
42
|
+
| `isExternal` | `boolean?` | Passed to NuxtLink `:external` — opens in new tab |
|
|
43
|
+
| `iconName` | `string?` | Iconify icon name rendered before the label |
|
|
44
|
+
| `cssName` | `string?` | CSS class applied to the `<li>` element |
|
|
45
|
+
|
|
46
|
+
## Basic usage
|
|
47
|
+
|
|
48
|
+
```vue
|
|
49
|
+
<SiteNavigation :nav-item-data="navItemData" nav-align="left" />
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Always use hyphenated prop names in templates (ESLint enforces this).
|
|
53
|
+
|
|
54
|
+
## CSS token API
|
|
55
|
+
|
|
56
|
+
Set these tokens on a parent element (e.g. your `<header>`) to theme the navigation.
|
|
57
|
+
|
|
58
|
+
```css
|
|
59
|
+
.your-header {
|
|
60
|
+
/* ── Decorators ────────────────────────────────────── */
|
|
61
|
+
--site-nav-decorator-indicator-color: var(--rose-05); /* underline bar */
|
|
62
|
+
|
|
63
|
+
/* ── Horizontal nav links ──────────────────────────── */
|
|
64
|
+
--site-nav-link-color: var(--warm-01);
|
|
65
|
+
--site-nav-link-hover-color: var(--rose-04);
|
|
66
|
+
--site-nav-link-active-color: var(--rose-05);
|
|
67
|
+
--site-nav-link-size: 1.6rem;
|
|
68
|
+
--site-nav-link-weight: 400;
|
|
69
|
+
--site-nav-link-tracking: 0.06em;
|
|
70
|
+
--site-nav-gap: 2.2rem;
|
|
71
|
+
--site-nav-transition: 250ms ease;
|
|
72
|
+
|
|
73
|
+
/* ── Mobile panel ──────────────────────────────────── */
|
|
74
|
+
--site-nav-panel-bg: var(--page-bg, #1a1614);
|
|
75
|
+
--site-nav-panel-border-color: color-mix(in oklch, var(--rose-05) 35%, transparent);
|
|
76
|
+
--site-nav-panel-link-color: var(--warm-01);
|
|
77
|
+
--site-nav-panel-link-hover-color: var(--rose-04);
|
|
78
|
+
--site-nav-panel-link-active-color: var(--rose-05);
|
|
79
|
+
--site-nav-panel-padding-block: 1.4rem;
|
|
80
|
+
--site-nav-panel-padding-inline: 1.5rem;
|
|
81
|
+
--site-nav-panel-slide-duration: 350ms;
|
|
82
|
+
--site-nav-panel-slide-easing: cubic-bezier(0.4, 0, 0.2, 1);
|
|
83
|
+
--site-nav-panel-decorator-indicator-color: var(--rose-05);
|
|
84
|
+
--site-nav-panel-indicator-left: 0; /* position the panel indicator bar */
|
|
85
|
+
--site-nav-panel-indicator-right: auto;
|
|
86
|
+
|
|
87
|
+
/* ── Burger button ─────────────────────────────────── */
|
|
88
|
+
--site-nav-burger-color: var(--warm-01);
|
|
89
|
+
--site-nav-burger-width: 22px;
|
|
90
|
+
--site-nav-burger-height: 1.5px;
|
|
91
|
+
--site-nav-burger-gap: 5px;
|
|
92
|
+
--site-nav-burger-transition: 300ms ease;
|
|
93
|
+
|
|
94
|
+
/* ── Backdrop (teleported to <body>) ───────────────── */
|
|
95
|
+
--site-nav-backdrop-bg: oklch(0% 0 0 / 55%);
|
|
96
|
+
--site-nav-backdrop-blur: 3px;
|
|
97
|
+
--site-nav-backdrop-duration: 350ms;
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Behaviour notes
|
|
102
|
+
|
|
103
|
+
- **Collapse detection**: `ResizeObserver` fires on every container resize. The list's `scrollWidth` is cached whenever the list is in the DOM; that cached value is compared against the container's `clientWidth` to set `isCollapsed`.
|
|
104
|
+
- **isLoaded state**: The component uses `useState("site-nav-loaded")` — a Nuxt shared state — to gate visibility until the first measurement is complete, preventing a flash of the wrong nav state on load. The nav renders `opacity: 0` until `is-loaded` is applied.
|
|
105
|
+
- **Teleport**: The backdrop overlay is teleported to `<body>` via `<Teleport>` and is only mounted when `isCollapsed && isLoaded`.
|
|
106
|
+
- **Panel inert**: The `#site-nav-panel` div receives `:inert="!isMenuOpen ? true : undefined"` — it is inert (keyboard/pointer-inaccessible) when closed.
|
|
107
|
+
- **Decorator init**: `initNavDecorators()` and `initPanelDecorators()` inject `<li>` elements with CSS-driven indicator `<div>`s. They query `[data-nav-item]` / `[data-panel-nav-item]` to find links, and look for `router-link-active` to set the initial active position.
|
|
108
|
+
|
|
109
|
+
## Accessibility
|
|
110
|
+
|
|
111
|
+
- Root `<nav>` has `aria-label="Site navigation"`.
|
|
112
|
+
- Burger button uses `aria-expanded` (string `"true"/"false"`) and `aria-controls="site-nav-panel"`.
|
|
113
|
+
- Backdrop and indicator `<li>` elements are `aria-hidden="true"`.
|
|
114
|
+
- Panel div is `inert` when closed.
|
|
115
|
+
|
|
116
|
+
## Related files
|
|
117
|
+
|
|
118
|
+
- Type: `app/types/components/navigation-horizontal.d.ts`
|
|
119
|
+
- Tests: `app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts`
|
|
120
|
+
- Story: `app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts`
|
package/.claude/skills/index.md
CHANGED
|
@@ -43,14 +43,15 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
43
43
|
├── layout-row.md — LayoutRow variant guide, width/margin decisions, usage patterns
|
|
44
44
|
├── link-text.md — LinkText props, slots, usage patterns, styling
|
|
45
45
|
├── page-hero-highlights.md — PageHeroHighlights template: hero + highlights strip grid, CSS custom property theming
|
|
46
|
-
├── services-card.md — ServicesCard props, actions slot,
|
|
46
|
+
├── services-card.md — ServicesCard props (incl. eyebrowConfig/heroConfig), actions slot, CSS tokens, page boilerplate
|
|
47
|
+
├── services-card-grid.md — ServicesCardGrid props, config pass-through, CSS tokens, full page boilerplate
|
|
47
48
|
├── services-section.md — ServicesSection props, summary-link/cta slots, summary vs full mode
|
|
48
49
|
├── contact-section.md — ContactSection props (stepperIndicatorSize pass-through), 3-item info+form layout, slot API
|
|
49
50
|
├── stepper-list.md — StepperList dynamic slots (item-{n}/indicator-{n}), props, connector behaviour
|
|
50
51
|
├── expanding-panel.md — ExpandingPanel v-model, forceOpened, slots (summary/icon/content), ARIA wiring
|
|
51
52
|
├── navigation-horizontal.md — NavigationHorizontal props, NavItemData type, CSS token API, import path gotcha
|
|
52
53
|
├── input-copy-core.md — InputCopyCore: readonly copy-to-clipboard input; props, emits, slots, CSS classes, usage
|
|
53
|
-
└──
|
|
54
|
+
└── site-navigation.md — SiteNavigation: responsive nav with auto-collapse, burger menu, decorator indicators, CSS token API
|
|
54
55
|
```
|
|
55
56
|
|
|
56
57
|
## Skill file template
|