srcdev-nuxt-components 9.1.1 → 9.1.3
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/skills/components/glass-panel.md +89 -0
- package/.claude/skills/components/services-card-grid.md +110 -0
- package/.claude/skills/components/services-card.md +65 -30
- package/.claude/skills/index.md +8 -1
- 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/layouts/default.vue +1 -0
- package/app/layouts/site-navigation-demo.vue +69 -0
- 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/package.json +1 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: GlassPanel
|
|
3
|
+
description: GlassPanel props, slots, CSS token API, and theming override
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# GlassPanel
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
`GlassPanel` is a semantic container with a frosted-glass visual effect — blurred background, border, drop shadow, and an angled specular highlight on the top-left corner. All visual properties are driven by CSS custom properties so they can be overridden per-theme.
|
|
12
|
+
|
|
13
|
+
## Props
|
|
14
|
+
|
|
15
|
+
| Prop | Type | Default | Description |
|
|
16
|
+
|------|------|---------|-------------|
|
|
17
|
+
| `tag` | `"div" \| "section" \| "article" \| "main" \| "header" \| "footer"` | `"div"` | Rendered HTML element |
|
|
18
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element |
|
|
19
|
+
|
|
20
|
+
## Slots
|
|
21
|
+
|
|
22
|
+
| Slot | Description |
|
|
23
|
+
|------|-------------|
|
|
24
|
+
| `default` | Panel content |
|
|
25
|
+
|
|
26
|
+
## Basic usage
|
|
27
|
+
|
|
28
|
+
```vue
|
|
29
|
+
<GlassPanel tag="section" :style-class-passthrough="['p-8']">
|
|
30
|
+
<p>Content here</p>
|
|
31
|
+
</GlassPanel>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## CSS token API
|
|
35
|
+
|
|
36
|
+
All four tokens must be defined — the component has no built-in fallbacks.
|
|
37
|
+
|
|
38
|
+
| Token | What it controls |
|
|
39
|
+
|-------|-----------------|
|
|
40
|
+
| `--glass-panel-bg` | Panel background (use `rgba` / `oklch` with alpha for the glass effect) |
|
|
41
|
+
| `--glass-panel-border-color` | 1px border colour (typically a translucent white) |
|
|
42
|
+
| `--glass-panel-shadow` | `box-shadow` value |
|
|
43
|
+
| `--glass-panel-highlight` | Colour of the `135deg` specular gradient overlay (`::before`) |
|
|
44
|
+
|
|
45
|
+
## Layer defaults
|
|
46
|
+
|
|
47
|
+
**Light mode** (`:root` / `[data-color-scheme="light"]`):
|
|
48
|
+
```css
|
|
49
|
+
--glass-panel-bg: rgba(255, 255, 255, 0.55);
|
|
50
|
+
--glass-panel-border-color: rgba(255, 255, 255, 0.8);
|
|
51
|
+
--glass-panel-shadow: 0 8px 32px rgba(0, 0, 0, 0.08), 0 2px 8px rgba(0, 0, 0, 0.04);
|
|
52
|
+
--glass-panel-highlight: rgba(255, 255, 255, 0.9);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Dark mode** (`[data-color-scheme="dark"]`):
|
|
56
|
+
```css
|
|
57
|
+
--glass-panel-bg: rgba(12, 12, 20, 0.45);
|
|
58
|
+
--glass-panel-border-color: rgba(255, 255, 255, 0.07);
|
|
59
|
+
--glass-panel-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
60
|
+
--glass-panel-highlight: rgba(255, 255, 255, 0.04);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Overriding in a consuming app
|
|
64
|
+
|
|
65
|
+
Add overrides to `app/assets/styles/setup/03.theming/default/_light.css` and `_dark.css`.
|
|
66
|
+
|
|
67
|
+
For this site the background is dark/warm, so the dark-mode values are the primary brand values. Warm rose tints work well for the border and highlight:
|
|
68
|
+
|
|
69
|
+
```css
|
|
70
|
+
/* _dark.css — example warm-rose override */
|
|
71
|
+
--glass-panel-bg: oklch(12% 0.01 15 / 0.5); /* warm near-black, semi-transparent */
|
|
72
|
+
--glass-panel-border-color: oklch(60% 0.06 15 / 0.15); /* muted rose border */
|
|
73
|
+
--glass-panel-shadow: 0 8px 32px oklch(0% 0 0 / 0.6), 0 2px 8px oklch(0% 0 0 / 0.4);
|
|
74
|
+
--glass-panel-highlight: oklch(80% 0.04 15 / 0.06); /* faint rose specular */
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```css
|
|
78
|
+
/* _light.css — example warm-rose override */
|
|
79
|
+
--glass-panel-bg: oklch(98% 0.005 15 / 0.6);
|
|
80
|
+
--glass-panel-border-color: oklch(100% 0 0 / 0.75);
|
|
81
|
+
--glass-panel-shadow: 0 8px 32px oklch(0% 0 0 / 0.06), 0 2px 8px oklch(0% 0 0 / 0.03);
|
|
82
|
+
--glass-panel-highlight: oklch(100% 0 0 / 0.85);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Notes
|
|
86
|
+
|
|
87
|
+
- The `backdrop-filter: blur(14px) saturate(180%)` is baked into the component — the blurred-background effect only looks right when the panel is layered over an image or textured background.
|
|
88
|
+
- `overflow: hidden` is set on the root — ensure content that needs to escape (e.g. tooltips, dropdowns) is portalled outside.
|
|
89
|
+
- The `::before` highlight overlay is `pointer-events: none` so it never blocks clicks.
|
|
@@ -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.
|
package/.claude/skills/index.md
CHANGED
|
@@ -43,11 +43,13 @@ 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
|
|
52
|
+
├── glass-panel.md — GlassPanel props, slots, CSS token API (--glass-panel-bg/border-color/shadow/highlight), theming override
|
|
51
53
|
├── navigation-horizontal.md — NavigationHorizontal props, NavItemData type, CSS token API, import path gotcha
|
|
52
54
|
├── input-copy-core.md — InputCopyCore: readonly copy-to-clipboard input; props, emits, slots, CSS classes, usage
|
|
53
55
|
└── site-navigation.md — SiteNavigation: responsive nav with auto-collapse, burger menu, decorator indicators, CSS token API
|
|
@@ -59,19 +61,24 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
59
61
|
# <Title>
|
|
60
62
|
|
|
61
63
|
## Overview
|
|
64
|
+
|
|
62
65
|
Brief description of what this skill does and why it exists.
|
|
63
66
|
|
|
64
67
|
## Prerequisites
|
|
68
|
+
|
|
65
69
|
What needs to be in place before starting (optional section).
|
|
66
70
|
|
|
67
71
|
## Steps
|
|
68
72
|
|
|
69
73
|
### 1. <Step name>
|
|
74
|
+
|
|
70
75
|
...
|
|
71
76
|
|
|
72
77
|
### 2. <Step name>
|
|
78
|
+
|
|
73
79
|
...
|
|
74
80
|
|
|
75
81
|
## Notes
|
|
82
|
+
|
|
76
83
|
Edge cases, gotchas, or links to related files (optional section).
|
|
77
84
|
```
|
|
@@ -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
|
|
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:
|
|
93
|
+
margin-block: var(--_eyebrow-text-margin-block);
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
.hero-text {
|
|
81
|
-
margin-block:
|
|
97
|
+
margin-block: var(--_hero-text-margin-block);
|
|
82
98
|
}
|
|
83
99
|
|
|
84
100
|
.description {
|
|
85
|
-
color: var(--
|
|
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
|
|
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(
|
|
49
|
-
gap:
|
|
72
|
+
grid-template-columns: repeat(auto-fit, minmax(var(--_column-min-width), 1fr));
|
|
73
|
+
gap: var(--_gap);
|
|
50
74
|
}
|
|
51
75
|
}
|
|
52
76
|
</style>
|
package/app/layouts/default.vue
CHANGED
|
@@ -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
|
|
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>
|