srcdev-nuxt-components 9.1.1 → 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.
@@ -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
- ## Multiple actions
69
+ ## With config overrides
42
70
 
43
71
  ```vue
44
- <ServicesCard :service-data="service">
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
- ## Local style override scaffold
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
- <ServicesCard :style-class-passthrough="['my-card']" :service-data="service">
64
- ...
65
- </ServicesCard>
66
-
67
- <style>
68
- /* ─── ServicesCard local overrides ─────────────────────────────────
69
- Colours, borders, geometry only — do not override behaviour.
70
- Delete this block if no overrides are needed.
71
- ─────────────────────────────────────────────────────────────────── */
72
- .services-card {
73
- &.my-card {
74
- /* Geometry — image */
75
- /* .image-wrapper { border-radius: 1.2rem; } */
76
-
77
- /* Colours */
78
- /* .description { color: var(--brand-text-secondary); } */
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 2ch auto 5lh auto` — the last row (`auto`) accommodates the `actions` slot at any height.
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.
@@ -43,7 +43,8 @@ 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, usage patterns
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
@@ -3,10 +3,10 @@
3
3
  <div class="image-wrapper">
4
4
  <NuxtImg :src="serviceData.image" :alt="serviceData.title" loading="lazy" class="image" />
5
5
  </div>
6
- <EyebrowText :text-content="serviceData.subtitle" />
6
+ <EyebrowText :font-size="eyebrowConfig.fontSize ?? 'large'" :tag="eyebrowConfig.tag ?? 'div'" :text-content="serviceData.subtitle" />
7
7
  <HeroText
8
- tag="h2"
9
- font-size="heading"
8
+ :tag="heroConfig.tag ?? 'h2'"
9
+ :font-size="heroConfig.fontSize ?? 'heading'"
10
10
  :text-content="[
11
11
  {
12
12
  text: serviceData.title,
@@ -24,20 +24,31 @@
24
24
  <script setup lang="ts">
25
25
  import type { Service } from "~/types/types.services";
26
26
 
27
+ interface EyebrowConfig {
28
+ tag?: "p" | "div" | "span";
29
+ fontSize?: "large" | "medium" | "small";
30
+ }
31
+
32
+ interface HeroConfig {
33
+ tag?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
34
+ fontSize?: "display" | "title" | "heading" | "subheading" | "label";
35
+ }
36
+
27
37
  interface Props {
28
38
  tag?: "div" | "section" | "article";
29
39
  serviceData: Service;
40
+ eyebrowConfig?: EyebrowConfig;
41
+ heroConfig?: HeroConfig;
30
42
  styleClassPassthrough?: string | string[];
31
43
  }
32
44
 
33
45
  const props = withDefaults(defineProps<Props>(), {
34
46
  tag: "div",
47
+ eyebrowConfig: () => ({}),
48
+ heroConfig: () => ({}),
35
49
  styleClassPassthrough: () => [],
36
50
  });
37
51
 
38
- // const slots = useSlots();
39
- // const hasDefaultSlot = computed(() => Boolean(slots.default));
40
-
41
52
  const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
42
53
 
43
54
  watch(
@@ -51,8 +62,13 @@ watch(
51
62
  <style lang="css">
52
63
  @layer components {
53
64
  .services-card {
65
+ /* Consumer definable css tokens */
66
+ --_eyebrow-text-margin-block: 0.8rem 0;
67
+ --_hero-text-margin-block: 2rem 1rem;
68
+ --_description-text-colour: var(--colour-text-secondary);
69
+
54
70
  display: grid;
55
- grid-template-rows: auto 2ch auto 5lh auto;
71
+ grid-template-rows: auto auto auto 5lh auto;
56
72
  gap: 1rem;
57
73
 
58
74
  .image-wrapper {
@@ -74,15 +90,15 @@ watch(
74
90
  }
75
91
 
76
92
  .eyebrow-text {
77
- margin-block: 0.8rem;
93
+ margin-block: var(--_eyebrow-text-margin-block);
78
94
  }
79
95
 
80
96
  .hero-text {
81
- margin-block: 2rem 1rem;
97
+ margin-block: var(--_hero-text-margin-block);
82
98
  }
83
99
 
84
100
  .description {
85
- color: var(--colour-text-secondary);
101
+ color: var(--_description-text-colour);
86
102
 
87
103
  /* display: -webkit-box;
88
104
  -webkit-box-orient: vertical;
@@ -1,6 +1,12 @@
1
1
  <template>
2
2
  <component :is="tag" class="services-card-grid" :class="[elementClasses]">
3
- <ServicesCard v-for="(item, index) in servicesData" :key="index" :service-data="item">
3
+ <ServicesCard
4
+ v-for="(item, index) in servicesData"
5
+ :key="index"
6
+ :service-data="item"
7
+ :eyebrow-config="eyebrowConfig"
8
+ :hero-config="heroConfig"
9
+ >
4
10
  <template #actions="{ serviceData }">
5
11
  <InputButtonCore
6
12
  variant="secondary"
@@ -20,14 +26,28 @@
20
26
  <script setup lang="ts">
21
27
  import type { Service } from "~/types/types.services";
22
28
 
29
+ interface EyebrowConfig {
30
+ tag?: "p" | "div" | "span";
31
+ fontSize?: "large" | "medium" | "small";
32
+ }
33
+
34
+ interface HeroConfig {
35
+ tag?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
36
+ fontSize?: "display" | "title" | "heading" | "subheading" | "label";
37
+ }
38
+
23
39
  interface Props {
24
40
  tag?: "div" | "section" | "main";
25
41
  servicesData: Service[];
42
+ eyebrowConfig?: EyebrowConfig;
43
+ heroConfig?: HeroConfig;
26
44
  styleClassPassthrough?: string | string[];
27
45
  }
28
46
 
29
47
  const props = withDefaults(defineProps<Props>(), {
30
48
  tag: "div",
49
+ eyebrowConfig: () => ({}),
50
+ heroConfig: () => ({}),
31
51
  styleClassPassthrough: () => [],
32
52
  });
33
53
 
@@ -44,9 +64,13 @@ watch(
44
64
  <style lang="css">
45
65
  @layer components {
46
66
  .services-card-grid {
67
+ /* Consumer definable css tokens */
68
+ --_gap: 4rem;
69
+ --_column-min-width: 250px;
70
+
47
71
  display: grid;
48
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
49
- gap: 4rem;
72
+ grid-template-columns: repeat(auto-fit, minmax(var(--_column-min-width), 1fr));
73
+ gap: var(--_gap);
50
74
  }
51
75
  }
52
76
  </style>
@@ -97,6 +97,7 @@ const responsiveNavLinks = {
97
97
  childLinks: [
98
98
  { name: "Block Decorators", path: "/ui/block-decorators" },
99
99
  { name: "Navigation Horizontal", path: "/ui/navigation/navigation-horizontal" },
100
+ { name: "Site Navigation", path: "/ui/navigation/site-navigation" },
100
101
  { name: "Magnetic Navigation", path: "/ui/magnetic-navigation" },
101
102
  { name: "Layout Row", path: "/ui/layout-row" },
102
103
  { name: "Layout Grid A", path: "/ui/layout-grid-a" },
@@ -0,0 +1,69 @@
1
+ <template>
2
+ <div class="site-nav-demo-layout">
3
+ <header class="site-nav-demo-header">
4
+ <NuxtLink to="/ui/navigation/site-navigation" class="site-nav-demo-logo">SiteNav Demo</NuxtLink>
5
+ <SiteNavigation :nav-item-data="navItemData" nav-align="right" />
6
+ </header>
7
+ <main class="site-nav-demo-main">
8
+ <slot></slot>
9
+ </main>
10
+ </div>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import type { NavItemData } from "~/types/components/navigation-horizontal.d";
15
+
16
+ const navItemData: NavItemData = {
17
+ main: [
18
+ { text: "Home", href: "/ui/navigation/site-navigation" },
19
+ { text: "About", href: "/ui/navigation/site-navigation/about" },
20
+ { text: "Services", href: "/ui/navigation/site-navigation/services" },
21
+ { text: "Portfolio", href: "/ui/navigation/site-navigation/portfolio" },
22
+ { text: "Contact", href: "/ui/navigation/site-navigation/contact" },
23
+ ],
24
+ };
25
+ </script>
26
+
27
+ <style lang="css">
28
+ .site-nav-demo-layout {
29
+ min-height: 100dvh;
30
+ display: flex;
31
+ flex-direction: column;
32
+
33
+ .site-nav-demo-header {
34
+ display: flex;
35
+ align-items: center;
36
+ gap: 2.4rem;
37
+ padding-block: 1.2rem;
38
+ padding-inline: 2.4rem;
39
+ background-color: #0e0e0e;
40
+ border-block-end: 1px solid oklch(100% 0 0 / 10%);
41
+ position: sticky;
42
+ top: 0;
43
+ z-index: 10;
44
+ }
45
+
46
+ .site-nav-demo-logo {
47
+ font-size: 1.6rem;
48
+ font-weight: 600;
49
+ letter-spacing: 0.05em;
50
+ color: var(--warm-01, #fff);
51
+ text-decoration: none;
52
+ text-wrap: nowrap;
53
+ flex-shrink: 0;
54
+ }
55
+
56
+ .site-navigation {
57
+ flex: 1;
58
+ min-width: 0;
59
+ }
60
+
61
+ .site-nav-demo-main {
62
+ flex: 1;
63
+ padding: 4rem 2.4rem;
64
+ max-width: 80rem;
65
+ margin-inline: auto;
66
+ width: 100%;
67
+ }
68
+ }
69
+ </style>
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <div>
3
+ <h1>About</h1>
4
+ <p>This is the About page. The active indicator in the navigation above should be sitting under "About".</p>
5
+ </div>
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ definePageMeta({ layout: "site-navigation-demo" });
10
+ useHead({ title: "SiteNavigation Demo — About" });
11
+ </script>
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <div>
3
+ <h1>Contact</h1>
4
+ <p>This is the Contact page. The active indicator in the navigation above should be sitting under "Contact".</p>
5
+ </div>
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ definePageMeta({ layout: "site-navigation-demo" });
10
+ useHead({ title: "SiteNavigation Demo — Contact" });
11
+ </script>
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <div>
3
+ <h1>Home</h1>
4
+ <p>Welcome to the SiteNavigation component demo. Resize the window to see the burger menu collapse, and navigate between pages to see the active indicator update.</p>
5
+ </div>
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ definePageMeta({ layout: "site-navigation-demo" });
10
+ useHead({ title: "SiteNavigation Demo — Home" });
11
+ </script>
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <div>
3
+ <h1>Portfolio</h1>
4
+ <p>This is the Portfolio page. The active indicator in the navigation above should be sitting under "Portfolio".</p>
5
+ </div>
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ definePageMeta({ layout: "site-navigation-demo" });
10
+ useHead({ title: "SiteNavigation Demo — Portfolio" });
11
+ </script>
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <div>
3
+ <h1>Services</h1>
4
+ <p>This is the Services page. The active indicator in the navigation above should be sitting under "Services".</p>
5
+ </div>
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ definePageMeta({ layout: "site-navigation-demo" });
10
+ useHead({ title: "SiteNavigation Demo — Services" });
11
+ </script>
@@ -8,7 +8,11 @@
8
8
  </LayoutRow>
9
9
 
10
10
  <LayoutRow tag="div" variant="content" :style-class-passthrough="['mbe-20']">
11
- <ServicesCardGrid :services-data="servicesData ?? []" />
11
+ <ServicesCardGrid
12
+ :services-data="servicesData ?? []"
13
+ :eyebrow-config="{ fontSize: 'large' }"
14
+ :hero-config="{ tag: 'h2', fontSize: 'heading' }"
15
+ />
12
16
  </LayoutRow>
13
17
  </template>
14
18
  </NuxtLayout>
@@ -38,5 +42,16 @@ if (servicesData.value.length === 0) {
38
42
 
39
43
  <style lang="css">
40
44
  .page-services-card-grid {
45
+ /* Page specific styles here */
46
+ .services-card-grid {
47
+ --_gap: 4rem;
48
+ --_column-min-width: 250px;
49
+
50
+ .services-card {
51
+ --_eyebrow-text-margin-block: 0.8rem 0;
52
+ --_hero-text-margin-block: 2rem 1rem;
53
+ --_description-text-colour: var(--colour-text-secondary);
54
+ }
55
+ }
41
56
  }
42
57
  </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.1",
4
+ "version": "9.1.2",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",