srcdev-nuxt-components 9.1.2 → 9.1.4
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/index.md +6 -0
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +98 -94
- package/app/layouts/site-navigation-demo.vue +118 -1
- package/app/pages/ui/navigation/site-navigation/about.vue +27 -2
- package/app/pages/ui/navigation/site-navigation/contact.vue +29 -1
- package/app/pages/ui/navigation/site-navigation/index.vue +29 -2
- package/app/pages/ui/navigation/site-navigation/portfolio.vue +34 -2
- package/app/pages/ui/navigation/site-navigation/services.vue +32 -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.
|
package/.claude/skills/index.md
CHANGED
|
@@ -49,6 +49,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
49
49
|
├── contact-section.md — ContactSection props (stepperIndicatorSize pass-through), 3-item info+form layout, slot API
|
|
50
50
|
├── stepper-list.md — StepperList dynamic slots (item-{n}/indicator-{n}), props, connector behaviour
|
|
51
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
|
|
52
53
|
├── navigation-horizontal.md — NavigationHorizontal props, NavItemData type, CSS token API, import path gotcha
|
|
53
54
|
├── input-copy-core.md — InputCopyCore: readonly copy-to-clipboard input; props, emits, slots, CSS classes, usage
|
|
54
55
|
└── site-navigation.md — SiteNavigation: responsive nav with auto-collapse, burger menu, decorator indicators, CSS token API
|
|
@@ -60,19 +61,24 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
60
61
|
# <Title>
|
|
61
62
|
|
|
62
63
|
## Overview
|
|
64
|
+
|
|
63
65
|
Brief description of what this skill does and why it exists.
|
|
64
66
|
|
|
65
67
|
## Prerequisites
|
|
68
|
+
|
|
66
69
|
What needs to be in place before starting (optional section).
|
|
67
70
|
|
|
68
71
|
## Steps
|
|
69
72
|
|
|
70
73
|
### 1. <Step name>
|
|
74
|
+
|
|
71
75
|
...
|
|
72
76
|
|
|
73
77
|
### 2. <Step name>
|
|
78
|
+
|
|
74
79
|
...
|
|
75
80
|
|
|
76
81
|
## Notes
|
|
82
|
+
|
|
77
83
|
Edge cases, gotchas, or links to related files (optional section).
|
|
78
84
|
```
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
{{ item.text }}
|
|
24
24
|
</NuxtLink>
|
|
25
25
|
</li>
|
|
26
|
+
<li aria-hidden="true" role="none" class="nav-indicator-li"><div class="nav__hovered"></div></li>
|
|
27
|
+
<li aria-hidden="true" role="none" class="nav-indicator-li"><div class="nav__active"></div></li>
|
|
28
|
+
<li aria-hidden="true" role="none" class="nav-indicator-li"><div class="nav__active-indicator"></div></li>
|
|
26
29
|
</ul>
|
|
27
30
|
|
|
28
31
|
<InputButtonCore
|
|
@@ -79,6 +82,9 @@
|
|
|
79
82
|
{{ item.text }}
|
|
80
83
|
</NuxtLink>
|
|
81
84
|
</li>
|
|
85
|
+
<li aria-hidden="true" role="none" class="nav-indicator-li"><div class="nav__hovered"></div></li>
|
|
86
|
+
<li aria-hidden="true" role="none" class="nav-indicator-li"><div class="nav__active"></div></li>
|
|
87
|
+
<li aria-hidden="true" role="none" class="nav-indicator-li"><div class="nav__active-indicator"></div></li>
|
|
82
88
|
</ul>
|
|
83
89
|
</div>
|
|
84
90
|
</div>
|
|
@@ -108,17 +114,17 @@ const isLoaded = useState("site-nav-loaded", () => false);
|
|
|
108
114
|
const isMenuOpen = ref(false);
|
|
109
115
|
|
|
110
116
|
// Stored natural width of the list — used when the list is not in the DOM
|
|
111
|
-
|
|
117
|
+
let navListNaturalWidth = 0;
|
|
112
118
|
|
|
113
119
|
const checkOverflow = () => {
|
|
114
120
|
if (!navRef.value) return;
|
|
115
121
|
|
|
116
122
|
// Measure and store the list width whenever it's in the DOM
|
|
117
123
|
if (navListRef.value) {
|
|
118
|
-
navListNaturalWidth
|
|
124
|
+
navListNaturalWidth = navListRef.value.scrollWidth;
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
isCollapsed.value = navListNaturalWidth
|
|
127
|
+
isCollapsed.value = navListNaturalWidth > navRef.value.clientWidth;
|
|
122
128
|
};
|
|
123
129
|
|
|
124
130
|
const toggleMenu = () => {
|
|
@@ -137,36 +143,43 @@ const NAV_DECORATOR_DURATION = 200;
|
|
|
137
143
|
let navSnapTimer: ReturnType<typeof setTimeout> | null = null;
|
|
138
144
|
let panelSnapTimer: ReturnType<typeof setTimeout> | null = null;
|
|
139
145
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
146
|
+
let currentActiveNavLink: HTMLElement | null = null;
|
|
147
|
+
let currentHoveredNavLink: HTMLElement | null = null;
|
|
148
|
+
let previousHoveredNavLink: HTMLElement | null = null;
|
|
143
149
|
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
// Cached after first init — invalidated when nav list is re-rendered (collapse toggle)
|
|
151
|
+
let cachedNavLinks: HTMLElement[] = [];
|
|
152
|
+
|
|
153
|
+
const getNavLinks = (): HTMLElement[] => {
|
|
154
|
+
if (cachedNavLinks.length) return cachedNavLinks;
|
|
155
|
+
if (!navListRef.value) return [];
|
|
156
|
+
cachedNavLinks = Array.from(navListRef.value.querySelectorAll<HTMLElement>("[data-nav-item]"));
|
|
157
|
+
return cachedNavLinks;
|
|
158
|
+
};
|
|
146
159
|
|
|
147
160
|
const setFinalNavActivePositions = (instant = false) => {
|
|
148
|
-
if (!navListRef.value || !currentActiveNavLink
|
|
161
|
+
if (!navListRef.value || !currentActiveNavLink) return;
|
|
149
162
|
const list = navListRef.value;
|
|
150
|
-
const el = currentActiveNavLink
|
|
163
|
+
const el = currentActiveNavLink;
|
|
151
164
|
list.style.setProperty("--_transition-duration", instant ? "0ms" : NAV_DECORATOR_DURATION + "ms");
|
|
152
165
|
list.style.setProperty("--_x-active", el.offsetLeft + "px");
|
|
153
166
|
list.style.setProperty("--_width-active", String(el.offsetWidth / list.offsetWidth));
|
|
154
167
|
};
|
|
155
168
|
|
|
156
169
|
const setFinalNavHoveredPositions = (instant = false) => {
|
|
157
|
-
if (!navListRef.value || !currentHoveredNavLink
|
|
170
|
+
if (!navListRef.value || !currentHoveredNavLink) return;
|
|
158
171
|
const list = navListRef.value;
|
|
159
|
-
const el = currentHoveredNavLink
|
|
172
|
+
const el = currentHoveredNavLink;
|
|
160
173
|
list.style.setProperty("--_transition-duration", instant ? "0ms" : NAV_DECORATOR_DURATION + "ms");
|
|
161
174
|
list.style.setProperty("--_x-hovered", el.offsetLeft + "px");
|
|
162
175
|
list.style.setProperty("--_width-hovered", String(el.offsetWidth / list.offsetWidth));
|
|
163
176
|
};
|
|
164
177
|
|
|
165
178
|
const moveNavHoveredIndicator = () => {
|
|
166
|
-
if (!navListRef.value || !currentHoveredNavLink
|
|
179
|
+
if (!navListRef.value || !currentHoveredNavLink || !previousHoveredNavLink) return;
|
|
167
180
|
const list = navListRef.value;
|
|
168
|
-
const curr = currentHoveredNavLink
|
|
169
|
-
const prev = previousHoveredNavLink
|
|
181
|
+
const curr = currentHoveredNavLink;
|
|
182
|
+
const prev = previousHoveredNavLink;
|
|
170
183
|
list.style.setProperty("--_transition-duration", NAV_DECORATOR_DURATION + "ms");
|
|
171
184
|
const isMovingRight = prev.compareDocumentPosition(curr) === 4;
|
|
172
185
|
let transitionWidth: number;
|
|
@@ -187,23 +200,23 @@ const moveNavHoveredIndicator = () => {
|
|
|
187
200
|
const handleNavLinkClick = (event: MouseEvent) => {
|
|
188
201
|
const target = (event.target as HTMLElement).closest<HTMLElement>("[data-nav-item]");
|
|
189
202
|
if (!target) return;
|
|
190
|
-
currentActiveNavLink
|
|
191
|
-
currentHoveredNavLink
|
|
192
|
-
previousHoveredNavLink
|
|
203
|
+
currentActiveNavLink = target;
|
|
204
|
+
currentHoveredNavLink = target;
|
|
205
|
+
previousHoveredNavLink = target;
|
|
193
206
|
};
|
|
194
207
|
|
|
195
208
|
const handleNavHover = (event: MouseEvent) => {
|
|
196
209
|
const target = (event.target as HTMLElement).closest<HTMLElement>("[data-nav-item]");
|
|
197
|
-
if (!target || target === currentHoveredNavLink
|
|
198
|
-
previousHoveredNavLink
|
|
199
|
-
currentHoveredNavLink
|
|
210
|
+
if (!target || target === currentHoveredNavLink) return;
|
|
211
|
+
previousHoveredNavLink = currentHoveredNavLink;
|
|
212
|
+
currentHoveredNavLink = target;
|
|
200
213
|
moveNavHoveredIndicator();
|
|
201
214
|
};
|
|
202
215
|
|
|
203
216
|
const resetHoverNavToActive = () => {
|
|
204
|
-
if (!currentActiveNavLink
|
|
205
|
-
previousHoveredNavLink
|
|
206
|
-
currentHoveredNavLink
|
|
217
|
+
if (!currentActiveNavLink || currentHoveredNavLink === currentActiveNavLink) return;
|
|
218
|
+
previousHoveredNavLink = currentHoveredNavLink;
|
|
219
|
+
currentHoveredNavLink = currentActiveNavLink;
|
|
207
220
|
moveNavHoveredIndicator();
|
|
208
221
|
};
|
|
209
222
|
|
|
@@ -212,71 +225,66 @@ const initNavDecorators = () => {
|
|
|
212
225
|
const links = getNavLinks();
|
|
213
226
|
if (!links.length) return;
|
|
214
227
|
|
|
215
|
-
// Cancel any in-flight snap timer before resetting positions
|
|
216
228
|
if (navSnapTimer !== null) {
|
|
217
229
|
clearTimeout(navSnapTimer);
|
|
218
230
|
navSnapTimer = null;
|
|
219
231
|
}
|
|
220
232
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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];
|
|
224
237
|
if (!activeLink) return;
|
|
225
238
|
|
|
226
|
-
currentActiveNavLink
|
|
227
|
-
currentHoveredNavLink
|
|
228
|
-
previousHoveredNavLink
|
|
239
|
+
currentActiveNavLink = activeLink;
|
|
240
|
+
currentHoveredNavLink = activeLink;
|
|
241
|
+
previousHoveredNavLink = activeLink;
|
|
229
242
|
|
|
230
243
|
setFinalNavActivePositions(true);
|
|
231
244
|
setFinalNavHoveredPositions(true);
|
|
232
|
-
|
|
233
|
-
// Wrap each indicator in a <li> so the <ul> contains only valid children
|
|
234
|
-
["nav__active-indicator", "nav__active", "nav__hovered"].forEach((cls) => {
|
|
235
|
-
const li = document.createElement("li");
|
|
236
|
-
li.classList.add("nav-indicator-li");
|
|
237
|
-
li.setAttribute("aria-hidden", "true");
|
|
238
|
-
li.setAttribute("role", "none");
|
|
239
|
-
const div = document.createElement("div");
|
|
240
|
-
div.classList.add(cls);
|
|
241
|
-
li.appendChild(div);
|
|
242
|
-
navListRef.value!.appendChild(li);
|
|
243
|
-
});
|
|
244
245
|
};
|
|
245
246
|
|
|
246
247
|
// ─── Panel decorators (y-axis active / hover indicators) ─────────────────────
|
|
247
248
|
|
|
248
249
|
const panelListRef = ref<HTMLUListElement | null>(null);
|
|
249
250
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
251
|
+
let currentActivePanelLink: HTMLElement | null = null;
|
|
252
|
+
let currentHoveredPanelLink: HTMLElement | null = null;
|
|
253
|
+
let previousHoveredPanelLink: HTMLElement | null = null;
|
|
253
254
|
|
|
254
|
-
|
|
255
|
-
|
|
255
|
+
// Cached after first init — invalidated when panel list is re-rendered (menu open/close)
|
|
256
|
+
let cachedPanelLinks: HTMLElement[] = [];
|
|
257
|
+
|
|
258
|
+
const getPanelLinks = (): HTMLElement[] => {
|
|
259
|
+
if (cachedPanelLinks.length) return cachedPanelLinks;
|
|
260
|
+
if (!panelListRef.value) return [];
|
|
261
|
+
cachedPanelLinks = Array.from(panelListRef.value.querySelectorAll<HTMLElement>("[data-panel-nav-item]"));
|
|
262
|
+
return cachedPanelLinks;
|
|
263
|
+
};
|
|
256
264
|
|
|
257
265
|
const setFinalPanelActivePositions = (instant = false) => {
|
|
258
|
-
if (!panelListRef.value || !currentActivePanelLink
|
|
266
|
+
if (!panelListRef.value || !currentActivePanelLink) return;
|
|
259
267
|
const list = panelListRef.value;
|
|
260
|
-
const el = currentActivePanelLink
|
|
268
|
+
const el = currentActivePanelLink;
|
|
261
269
|
list.style.setProperty("--_panel-transition-duration", instant ? "0ms" : NAV_DECORATOR_DURATION + "ms");
|
|
262
270
|
list.style.setProperty("--_panel-y-active", el.offsetTop + "px");
|
|
263
271
|
list.style.setProperty("--_panel-height-active", String(el.offsetHeight / list.offsetHeight));
|
|
264
272
|
};
|
|
265
273
|
|
|
266
274
|
const setFinalPanelHoveredPositions = (instant = false) => {
|
|
267
|
-
if (!panelListRef.value || !currentHoveredPanelLink
|
|
275
|
+
if (!panelListRef.value || !currentHoveredPanelLink) return;
|
|
268
276
|
const list = panelListRef.value;
|
|
269
|
-
const el = currentHoveredPanelLink
|
|
277
|
+
const el = currentHoveredPanelLink;
|
|
270
278
|
list.style.setProperty("--_panel-transition-duration", instant ? "0ms" : NAV_DECORATOR_DURATION + "ms");
|
|
271
279
|
list.style.setProperty("--_panel-y-hovered", el.offsetTop + "px");
|
|
272
280
|
list.style.setProperty("--_panel-height-hovered", String(el.offsetHeight / list.offsetHeight));
|
|
273
281
|
};
|
|
274
282
|
|
|
275
283
|
const movePanelHoveredIndicator = () => {
|
|
276
|
-
if (!panelListRef.value || !currentHoveredPanelLink
|
|
284
|
+
if (!panelListRef.value || !currentHoveredPanelLink || !previousHoveredPanelLink) return;
|
|
277
285
|
const list = panelListRef.value;
|
|
278
|
-
const curr = currentHoveredPanelLink
|
|
279
|
-
const prev = previousHoveredPanelLink
|
|
286
|
+
const curr = currentHoveredPanelLink;
|
|
287
|
+
const prev = previousHoveredPanelLink;
|
|
280
288
|
list.style.setProperty("--_panel-transition-duration", NAV_DECORATOR_DURATION + "ms");
|
|
281
289
|
const isMovingDown = prev.compareDocumentPosition(curr) === 4;
|
|
282
290
|
let transitionHeight: number;
|
|
@@ -297,28 +305,29 @@ const movePanelHoveredIndicator = () => {
|
|
|
297
305
|
const handlePanelLinkClick = (event: MouseEvent) => {
|
|
298
306
|
const target = (event.target as HTMLElement).closest<HTMLElement>("[data-panel-nav-item]");
|
|
299
307
|
if (!target) return;
|
|
300
|
-
currentActivePanelLink
|
|
301
|
-
currentHoveredPanelLink
|
|
302
|
-
previousHoveredPanelLink
|
|
308
|
+
currentActivePanelLink = target;
|
|
309
|
+
currentHoveredPanelLink = target;
|
|
310
|
+
previousHoveredPanelLink = target;
|
|
303
311
|
};
|
|
304
312
|
|
|
305
313
|
const handlePanelHover = (event: MouseEvent) => {
|
|
306
314
|
const target = (event.target as HTMLElement).closest<HTMLElement>("[data-panel-nav-item]");
|
|
307
|
-
if (!target || target === currentHoveredPanelLink
|
|
308
|
-
previousHoveredPanelLink
|
|
309
|
-
currentHoveredPanelLink
|
|
315
|
+
if (!target || target === currentHoveredPanelLink) return;
|
|
316
|
+
previousHoveredPanelLink = currentHoveredPanelLink;
|
|
317
|
+
currentHoveredPanelLink = target;
|
|
310
318
|
movePanelHoveredIndicator();
|
|
311
319
|
};
|
|
312
320
|
|
|
313
321
|
const resetHoverPanelToActive = () => {
|
|
314
|
-
if (!currentActivePanelLink
|
|
315
|
-
previousHoveredPanelLink
|
|
316
|
-
currentHoveredPanelLink
|
|
322
|
+
if (!currentActivePanelLink || currentHoveredPanelLink === currentActivePanelLink) return;
|
|
323
|
+
previousHoveredPanelLink = currentHoveredPanelLink;
|
|
324
|
+
currentHoveredPanelLink = currentActivePanelLink;
|
|
317
325
|
movePanelHoveredIndicator();
|
|
318
326
|
};
|
|
319
327
|
|
|
320
328
|
const initPanelDecorators = () => {
|
|
321
329
|
if (!panelListRef.value) return;
|
|
330
|
+
cachedPanelLinks = []; // panel DOM is re-rendered each open — invalidate cache
|
|
322
331
|
const links = getPanelLinks();
|
|
323
332
|
if (!links.length) return;
|
|
324
333
|
|
|
@@ -327,27 +336,18 @@ const initPanelDecorators = () => {
|
|
|
327
336
|
panelSnapTimer = null;
|
|
328
337
|
}
|
|
329
338
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
339
|
+
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];
|
|
333
343
|
if (!activeLink) return;
|
|
334
344
|
|
|
335
|
-
currentActivePanelLink
|
|
336
|
-
currentHoveredPanelLink
|
|
337
|
-
previousHoveredPanelLink
|
|
345
|
+
currentActivePanelLink = activeLink;
|
|
346
|
+
currentHoveredPanelLink = activeLink;
|
|
347
|
+
previousHoveredPanelLink = activeLink;
|
|
338
348
|
|
|
339
349
|
setFinalPanelActivePositions(true);
|
|
340
350
|
setFinalPanelHoveredPositions(true);
|
|
341
|
-
["nav__active-indicator", "nav__active", "nav__hovered"].forEach((cls) => {
|
|
342
|
-
const li = document.createElement("li");
|
|
343
|
-
li.classList.add("nav-indicator-li");
|
|
344
|
-
li.setAttribute("aria-hidden", "true");
|
|
345
|
-
li.setAttribute("role", "none");
|
|
346
|
-
const div = document.createElement("div");
|
|
347
|
-
div.classList.add(cls);
|
|
348
|
-
li.appendChild(div);
|
|
349
|
-
panelListRef.value!.appendChild(li);
|
|
350
|
-
});
|
|
351
351
|
};
|
|
352
352
|
|
|
353
353
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -389,6 +389,7 @@ onMounted(async () => {
|
|
|
389
389
|
|
|
390
390
|
watch(isCollapsed, async (collapsed) => {
|
|
391
391
|
if (!collapsed) {
|
|
392
|
+
cachedNavLinks = []; // nav <ul> was re-rendered — invalidate cache
|
|
392
393
|
await nextTick();
|
|
393
394
|
initNavDecorators();
|
|
394
395
|
}
|
|
@@ -437,23 +438,23 @@ watch(
|
|
|
437
438
|
/* Decorators — horizontal nav */
|
|
438
439
|
--_decorator-hovered-bg: transparent;
|
|
439
440
|
--_decorator-active-bg: transparent;
|
|
440
|
-
--_decorator-indicator-color: var(--site-nav-decorator-indicator-color, var(--
|
|
441
|
+
--_decorator-indicator-color: var(--site-nav-decorator-indicator-color, var(--slate-01, currentColor));
|
|
441
442
|
|
|
442
443
|
/* Decorators — panel */
|
|
443
444
|
--_panel-decorator-hovered-bg: var(--site-nav-panel-decorator-hovered-bg, transparent);
|
|
444
445
|
--_panel-decorator-active-bg: var(--site-nav-panel-decorator-active-bg, transparent);
|
|
445
|
-
--_panel-decorator-indicator-color: var(--site-nav-panel-decorator-indicator-color, var(--
|
|
446
|
+
--_panel-decorator-indicator-color: var(--site-nav-panel-decorator-indicator-color, var(--slate-01, currentColor));
|
|
446
447
|
--_panel-indicator-left: var(--site-nav-panel-indicator-left, 0);
|
|
447
448
|
--_panel-indicator-right: var(--site-nav-panel-indicator-right, auto);
|
|
448
449
|
|
|
449
450
|
/* Horizontal nav */
|
|
450
|
-
--_link-color: var(--site-nav-link-color, var(--
|
|
451
|
-
--_link-hover-color: var(--site-nav-link-hover-color, var(--
|
|
452
|
-
--_link-active-color: var(--site-nav-link-active-color, var(--
|
|
451
|
+
--_link-color: var(--site-nav-link-color, var(--slate-01, currentColor));
|
|
452
|
+
--_link-hover-color: var(--site-nav-link-hover-color, var(--slate-04, var(--_link-color)));
|
|
453
|
+
--_link-active-color: var(--site-nav-link-active-color, var(--slate-01, var(--_link-color)));
|
|
453
454
|
--_link-size: var(--site-nav-link-size, 1.6rem);
|
|
454
455
|
--_link-tracking: var(--site-nav-link-tracking, 0.06em);
|
|
455
456
|
--_link-weight: var(--site-nav-link-weight, 400);
|
|
456
|
-
--_link-accent: var(--site-nav-link-accent, var(--
|
|
457
|
+
--_link-accent: var(--site-nav-link-accent, var(--slate-01, currentColor));
|
|
457
458
|
--_nav-gap: var(--site-nav-gap, 2.2rem);
|
|
458
459
|
--_nav-transition: var(--site-nav-transition, 250ms ease);
|
|
459
460
|
|
|
@@ -461,12 +462,15 @@ watch(
|
|
|
461
462
|
--_panel-bg: var(--site-nav-panel-bg, var(--page-bg, #1a1614));
|
|
462
463
|
--_panel-border-color: var(
|
|
463
464
|
--site-nav-panel-border-color,
|
|
464
|
-
color-mix(in oklch, var(--
|
|
465
|
+
color-mix(in oklch, var(--slate-01, #c0847a) 35%, transparent)
|
|
466
|
+
);
|
|
467
|
+
--_panel-item-border: var(
|
|
468
|
+
--site-nav-panel-item-border,
|
|
469
|
+
color-mix(in oklch, var(--slate-01, white) 8%, transparent)
|
|
465
470
|
);
|
|
466
|
-
--_panel-
|
|
467
|
-
--_panel-link-color: var(--site-nav-panel-link-color, var(--
|
|
468
|
-
--_panel-link-
|
|
469
|
-
--_panel-link-active-color: var(--site-nav-panel-link-active-color, var(--rose-05, var(--_panel-link-color)));
|
|
471
|
+
--_panel-link-color: var(--site-nav-panel-link-color, var(--slate-01, currentColor));
|
|
472
|
+
--_panel-link-hover-color: var(--site-nav-panel-link-hover-color, var(--slate-04, var(--_panel-link-color)));
|
|
473
|
+
--_panel-link-active-color: var(--site-nav-panel-link-active-color, var(--slate-01, var(--_panel-link-color)));
|
|
470
474
|
--_panel-padding-block: var(--site-nav-panel-padding-block, 1.4rem);
|
|
471
475
|
--_panel-padding-inline: var(--site-nav-panel-padding-inline, 1.5rem);
|
|
472
476
|
--_panel-slide-duration: var(--site-nav-panel-slide-duration, 350ms);
|
|
@@ -476,7 +480,7 @@ watch(
|
|
|
476
480
|
--_burger-bar-width: var(--site-nav-burger-width, 22px);
|
|
477
481
|
--_burger-bar-height: var(--site-nav-burger-height, 1.5px);
|
|
478
482
|
--_burger-bar-gap: var(--site-nav-burger-gap, 5px);
|
|
479
|
-
--_burger-color: var(--site-nav-burger-color, var(--
|
|
483
|
+
--_burger-color: var(--site-nav-burger-color, var(--slate-01, currentColor));
|
|
480
484
|
--_burger-transition: var(--site-nav-burger-transition, 300ms ease);
|
|
481
485
|
|
|
482
486
|
/* ─────────────────────────────────────────────────────────────────── */
|
|
@@ -576,7 +580,7 @@ watch(
|
|
|
576
580
|
outline: none;
|
|
577
581
|
}
|
|
578
582
|
|
|
579
|
-
&.router-link-active {
|
|
583
|
+
&.router-link-exact-active {
|
|
580
584
|
color: var(--_link-active-color);
|
|
581
585
|
}
|
|
582
586
|
}
|
|
@@ -769,7 +773,7 @@ watch(
|
|
|
769
773
|
outline: none;
|
|
770
774
|
}
|
|
771
775
|
|
|
772
|
-
&.router-link-active {
|
|
776
|
+
&.router-link-exact-active {
|
|
773
777
|
color: var(--_panel-link-active-color);
|
|
774
778
|
}
|
|
775
779
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="site-nav-demo-layout">
|
|
3
3
|
<header class="site-nav-demo-header">
|
|
4
|
-
<NuxtLink to="/
|
|
4
|
+
<NuxtLink to="/" class="site-nav-demo-logo">SiteNav Demo</NuxtLink>
|
|
5
5
|
<SiteNavigation :nav-item-data="navItemData" nav-align="right" />
|
|
6
6
|
</header>
|
|
7
7
|
<main class="site-nav-demo-main">
|
|
@@ -66,4 +66,121 @@ const navItemData: NavItemData = {
|
|
|
66
66
|
width: 100%;
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
+
|
|
70
|
+
/* Shared demo page styles */
|
|
71
|
+
.snav-demo-page {
|
|
72
|
+
h1,
|
|
73
|
+
h2,
|
|
74
|
+
h3 {
|
|
75
|
+
margin-block-end: 1.6rem;
|
|
76
|
+
}
|
|
77
|
+
h2 {
|
|
78
|
+
margin-block-start: 4rem;
|
|
79
|
+
}
|
|
80
|
+
> p {
|
|
81
|
+
margin-block-end: 2.4rem;
|
|
82
|
+
line-height: 1.6;
|
|
83
|
+
}
|
|
84
|
+
ul {
|
|
85
|
+
padding-inline-start: 2rem;
|
|
86
|
+
margin-block-end: 2.4rem;
|
|
87
|
+
line-height: 2;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.snav-demo-hero {
|
|
92
|
+
width: 100%;
|
|
93
|
+
height: auto;
|
|
94
|
+
aspect-ratio: 2 / 1;
|
|
95
|
+
object-fit: cover;
|
|
96
|
+
border-radius: 0.8rem;
|
|
97
|
+
display: block;
|
|
98
|
+
margin-block-end: 2.4rem;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.snav-demo-grid {
|
|
102
|
+
display: grid;
|
|
103
|
+
grid-template-columns: repeat(3, 1fr);
|
|
104
|
+
gap: 1.6rem;
|
|
105
|
+
|
|
106
|
+
&--wide {
|
|
107
|
+
grid-template-columns: repeat(2, 1fr);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@media (max-width: 600px) {
|
|
111
|
+
grid-template-columns: repeat(2, 1fr);
|
|
112
|
+
&--wide {
|
|
113
|
+
grid-template-columns: 1fr;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.snav-demo-img {
|
|
119
|
+
width: 100%;
|
|
120
|
+
height: auto;
|
|
121
|
+
aspect-ratio: 3 / 2;
|
|
122
|
+
object-fit: cover;
|
|
123
|
+
border-radius: 0.4rem;
|
|
124
|
+
display: block;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.snav-demo-split {
|
|
128
|
+
display: grid;
|
|
129
|
+
grid-template-columns: 1fr 1fr;
|
|
130
|
+
gap: 3.2rem;
|
|
131
|
+
align-items: start;
|
|
132
|
+
margin-block-end: 4rem;
|
|
133
|
+
|
|
134
|
+
&__text {
|
|
135
|
+
h2,
|
|
136
|
+
h3 {
|
|
137
|
+
margin-block-start: 2.4rem;
|
|
138
|
+
}
|
|
139
|
+
p {
|
|
140
|
+
line-height: 1.6;
|
|
141
|
+
margin-block-end: 1.6rem;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
&__img {
|
|
146
|
+
width: 100%;
|
|
147
|
+
height: auto;
|
|
148
|
+
aspect-ratio: 3 / 4;
|
|
149
|
+
object-fit: cover;
|
|
150
|
+
border-radius: 0.4rem;
|
|
151
|
+
display: block;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@media (max-width: 650px) {
|
|
155
|
+
grid-template-columns: 1fr;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.snav-demo-services {
|
|
160
|
+
display: grid;
|
|
161
|
+
grid-template-columns: repeat(3, 1fr);
|
|
162
|
+
gap: 2.4rem;
|
|
163
|
+
|
|
164
|
+
@media (max-width: 650px) {
|
|
165
|
+
grid-template-columns: 1fr;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.snav-demo-service-card {
|
|
170
|
+
border-radius: 0.8rem;
|
|
171
|
+
overflow: hidden;
|
|
172
|
+
background: oklch(100% 0 0 / 5%);
|
|
173
|
+
|
|
174
|
+
&__body {
|
|
175
|
+
padding: 1.6rem;
|
|
176
|
+
h3 {
|
|
177
|
+
margin-block-end: 0.8rem;
|
|
178
|
+
}
|
|
179
|
+
p {
|
|
180
|
+
font-size: 1.4rem;
|
|
181
|
+
line-height: 1.6;
|
|
182
|
+
opacity: 0.8;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
69
186
|
</style>
|
|
@@ -1,7 +1,32 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
2
|
+
<div class="snav-demo-page">
|
|
3
3
|
<h1>About</h1>
|
|
4
|
-
<p>This is the About page. The active indicator in the navigation above should be sitting under "About".</p>
|
|
4
|
+
<p>This is the About page. The active indicator in the navigation above should be sitting under "About". Lots of images below to simulate a content-heavy page.</p>
|
|
5
|
+
|
|
6
|
+
<div class="snav-demo-split">
|
|
7
|
+
<div class="snav-demo-split__text">
|
|
8
|
+
<h2>Our Story</h2>
|
|
9
|
+
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.</p>
|
|
10
|
+
<p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
|
11
|
+
<h2>Our Team</h2>
|
|
12
|
+
<p>We are a team of dedicated professionals committed to delivering the best experience. Our combined expertise spans design, development, and user experience research.</p>
|
|
13
|
+
<p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis.</p>
|
|
14
|
+
</div>
|
|
15
|
+
<NuxtImg src="/images/rotating-carousel/image-1.webp" alt="About hero" width="600" height="800" loading="eager" class="snav-demo-split__img" />
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<h2>Gallery</h2>
|
|
19
|
+
<div class="snav-demo-grid">
|
|
20
|
+
<NuxtImg v-for="n in 9" :key="n" :src="`/images/rotating-carousel/image-${n}.webp`" :alt="`About gallery ${n}`" width="600" height="400" loading="lazy" class="snav-demo-img" />
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<h2>Behind the scenes</h2>
|
|
24
|
+
<div class="snav-demo-grid snav-demo-grid--wide">
|
|
25
|
+
<NuxtImg src="/images/banners/banner-ginger.jpeg" alt="Behind the scenes 1" width="800" height="500" loading="lazy" class="snav-demo-img" />
|
|
26
|
+
<NuxtImg src="/images/banners/banner-mid-brown.webp" alt="Behind the scenes 2" width="800" height="500" loading="lazy" class="snav-demo-img" />
|
|
27
|
+
<NuxtImg src="/images/page/hero/hero-dark.jpg" alt="Behind the scenes 3" width="800" height="500" loading="lazy" class="snav-demo-img" />
|
|
28
|
+
<NuxtImg src="/images/page/hero/hero-red.jpg" alt="Behind the scenes 4" width="800" height="500" loading="lazy" class="snav-demo-img" />
|
|
29
|
+
</div>
|
|
5
30
|
</div>
|
|
6
31
|
</template>
|
|
7
32
|
|
|
@@ -1,7 +1,35 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
2
|
+
<div class="snav-demo-page">
|
|
3
3
|
<h1>Contact</h1>
|
|
4
4
|
<p>This is the Contact page. The active indicator in the navigation above should be sitting under "Contact".</p>
|
|
5
|
+
|
|
6
|
+
<div class="snav-demo-split">
|
|
7
|
+
<div class="snav-demo-split__text">
|
|
8
|
+
<h2>Get in Touch</h2>
|
|
9
|
+
<p>We'd love to hear from you. Whether you have a question about our services, want to book an appointment, or just want to say hello — our door is always open.</p>
|
|
10
|
+
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
|
|
11
|
+
<h3>Opening Hours</h3>
|
|
12
|
+
<ul>
|
|
13
|
+
<li>Monday – Friday: 9am – 7pm</li>
|
|
14
|
+
<li>Saturday: 9am – 5pm</li>
|
|
15
|
+
<li>Sunday: Closed</li>
|
|
16
|
+
</ul>
|
|
17
|
+
<h3>Address</h3>
|
|
18
|
+
<p>123 Example Street<br />London, UK<br />EC1A 1BB</p>
|
|
19
|
+
</div>
|
|
20
|
+
<NuxtImg src="/images/rotating-carousel/55-woman-garden-backdrop.webp" alt="Contact hero" width="600" height="800" loading="eager" class="snav-demo-split__img" />
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<h2>Find Us</h2>
|
|
24
|
+
<div class="snav-demo-grid snav-demo-grid--wide">
|
|
25
|
+
<NuxtImg src="/images/rotating-carousel/image-5.webp" alt="Location 1" width="800" height="500" loading="lazy" class="snav-demo-img" />
|
|
26
|
+
<NuxtImg src="/images/rotating-carousel/image-6.webp" alt="Location 2" width="800" height="500" loading="lazy" class="snav-demo-img" />
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<h2>Instagram</h2>
|
|
30
|
+
<div class="snav-demo-grid">
|
|
31
|
+
<NuxtImg v-for="n in [1, 2, 3, 7, 8, 9]" :key="n" :src="`/images/rotating-carousel/image-${n}.webp`" :alt="`Instagram post ${n}`" width="600" height="600" loading="lazy" class="snav-demo-img snav-demo-img--square" />
|
|
32
|
+
</div>
|
|
5
33
|
</div>
|
|
6
34
|
</template>
|
|
7
35
|
|
|
@@ -1,7 +1,34 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
2
|
+
<div class="snav-demo-page">
|
|
3
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>
|
|
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. This page is intentionally heavy with images to stress-test nav behaviour during loading.</p>
|
|
5
|
+
|
|
6
|
+
<NuxtImg
|
|
7
|
+
src="/images/banners/banner-ginger.jpeg"
|
|
8
|
+
alt="Hero banner"
|
|
9
|
+
width="1200"
|
|
10
|
+
height="600"
|
|
11
|
+
loading="eager"
|
|
12
|
+
class="snav-demo-hero"
|
|
13
|
+
/>
|
|
14
|
+
|
|
15
|
+
<h2>Featured Gallery</h2>
|
|
16
|
+
<div class="snav-demo-grid">
|
|
17
|
+
<NuxtImg v-for="n in 9" :key="n" :src="`/images/rotating-carousel/image-${n}.webp`" :alt="`Gallery image ${n}`" width="600" height="400" loading="lazy" class="snav-demo-img" />
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<h2>More Images</h2>
|
|
21
|
+
<div class="snav-demo-grid snav-demo-grid--wide">
|
|
22
|
+
<NuxtImg src="/images/banners/banner-light-brunette.webp" alt="Banner light brunette" width="800" height="500" loading="lazy" class="snav-demo-img" />
|
|
23
|
+
<NuxtImg src="/images/banners/banner-mid-brown.webp" alt="Banner mid brown" width="800" height="500" loading="lazy" class="snav-demo-img" />
|
|
24
|
+
<NuxtImg src="/images/rotating-carousel/35-light-brown-highlights.webp" alt="Light brown highlights" width="800" height="500" loading="lazy" class="snav-demo-img" />
|
|
25
|
+
<NuxtImg src="/images/rotating-carousel/55-woman-garden-backdrop.webp" alt="Woman garden backdrop" width="800" height="500" loading="lazy" class="snav-demo-img" />
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<h2>Hero Images</h2>
|
|
29
|
+
<div class="snav-demo-grid">
|
|
30
|
+
<NuxtImg v-for="hero in ['hero-blonde', 'hero-dark', 'hero-hair', 'hero-red']" :key="hero" :src="`/images/page/hero/${hero}.jpg`" :alt="hero" width="600" height="400" loading="lazy" class="snav-demo-img" />
|
|
31
|
+
</div>
|
|
5
32
|
</div>
|
|
6
33
|
</template>
|
|
7
34
|
|
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
2
|
+
<div class="snav-demo-page">
|
|
3
3
|
<h1>Portfolio</h1>
|
|
4
|
-
<p>This is the Portfolio page. The active indicator in the navigation above should be sitting under "Portfolio".</p>
|
|
4
|
+
<p>This is the Portfolio page. The active indicator in the navigation above should be sitting under "Portfolio". Maximum image load to stress-test navigation behaviour.</p>
|
|
5
|
+
|
|
6
|
+
<h2>Spotlight Work</h2>
|
|
7
|
+
<div class="snav-demo-grid snav-demo-grid--dense">
|
|
8
|
+
<NuxtImg v-for="n in 9" :key="n" :src="`/images/rotating-carousel/image-${n}.webp`" :alt="`Portfolio piece ${n}`" width="600" height="600" loading="lazy" class="snav-demo-img snav-demo-img--square" />
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<h2>Featured Looks</h2>
|
|
12
|
+
<div class="snav-demo-grid snav-demo-grid--wide">
|
|
13
|
+
<NuxtImg src="/images/banners/banner-ginger.jpeg" alt="Ginger look" width="800" height="500" loading="lazy" class="snav-demo-img" />
|
|
14
|
+
<NuxtImg src="/images/banners/banner-light-brunette.webp" alt="Light brunette look" width="800" height="500" loading="lazy" class="snav-demo-img" />
|
|
15
|
+
<NuxtImg src="/images/banners/banner-mid-brown.webp" alt="Mid brown look" width="800" height="500" loading="lazy" class="snav-demo-img" />
|
|
16
|
+
<NuxtImg src="/images/rotating-carousel/35-light-brown-highlights.webp" alt="Light brown highlights" width="800" height="500" loading="lazy" class="snav-demo-img" />
|
|
17
|
+
<NuxtImg src="/images/rotating-carousel/55-woman-garden-backdrop.webp" alt="Garden backdrop" width="800" height="500" loading="lazy" class="snav-demo-img" />
|
|
18
|
+
<NuxtImg src="/images/rotating-carousel/indian-pink-stripe.webp" alt="Pink stripe" width="800" height="500" loading="lazy" class="snav-demo-img" />
|
|
19
|
+
<NuxtImg src="/images/rotating-carousel/spanish-green-stripe.webp" alt="Green stripe" width="800" height="500" loading="lazy" class="snav-demo-img" />
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<h2>Hero Series</h2>
|
|
23
|
+
<div class="snav-demo-grid">
|
|
24
|
+
<NuxtImg v-for="hero in ['hero-blonde', 'hero-dark', 'hero-hair', 'hero-red']" :key="hero" :src="`/images/page/hero/${hero}.jpg`" :alt="hero" width="600" height="400" loading="lazy" class="snav-demo-img" />
|
|
25
|
+
</div>
|
|
5
26
|
</div>
|
|
6
27
|
</template>
|
|
7
28
|
|
|
@@ -9,3 +30,14 @@
|
|
|
9
30
|
definePageMeta({ layout: "site-navigation-demo" });
|
|
10
31
|
useHead({ title: "SiteNavigation Demo — Portfolio" });
|
|
11
32
|
</script>
|
|
33
|
+
|
|
34
|
+
<style lang="css">
|
|
35
|
+
.snav-demo-img--square {
|
|
36
|
+
aspect-ratio: 1 / 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.snav-demo-grid--dense {
|
|
40
|
+
grid-template-columns: repeat(3, 1fr);
|
|
41
|
+
gap: 0.8rem;
|
|
42
|
+
}
|
|
43
|
+
</style>
|
|
@@ -1,11 +1,42 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
2
|
+
<div class="snav-demo-page">
|
|
3
3
|
<h1>Services</h1>
|
|
4
4
|
<p>This is the Services page. The active indicator in the navigation above should be sitting under "Services".</p>
|
|
5
|
+
|
|
6
|
+
<NuxtImg
|
|
7
|
+
src="/images/banners/banner-light-brunette.webp"
|
|
8
|
+
alt="Services banner"
|
|
9
|
+
width="1200"
|
|
10
|
+
height="500"
|
|
11
|
+
loading="eager"
|
|
12
|
+
class="snav-demo-hero"
|
|
13
|
+
/>
|
|
14
|
+
|
|
15
|
+
<h2>What We Offer</h2>
|
|
16
|
+
<div class="snav-demo-services">
|
|
17
|
+
<div v-for="(service, i) in services" :key="i" class="snav-demo-service-card">
|
|
18
|
+
<NuxtImg :src="service.img" :alt="service.title" width="600" height="400" loading="lazy" class="snav-demo-img" />
|
|
19
|
+
<div class="snav-demo-service-card__body">
|
|
20
|
+
<h3>{{ service.title }}</h3>
|
|
21
|
+
<p>{{ service.description }}</p>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<h2>Our Work</h2>
|
|
27
|
+
<div class="snav-demo-grid">
|
|
28
|
+
<NuxtImg v-for="n in [4, 5, 6, 7, 8, 9]" :key="n" :src="`/images/rotating-carousel/image-${n}.webp`" :alt="`Work sample ${n}`" width="600" height="400" loading="lazy" class="snav-demo-img" />
|
|
29
|
+
</div>
|
|
5
30
|
</div>
|
|
6
31
|
</template>
|
|
7
32
|
|
|
8
33
|
<script setup lang="ts">
|
|
9
34
|
definePageMeta({ layout: "site-navigation-demo" });
|
|
10
35
|
useHead({ title: "SiteNavigation Demo — Services" });
|
|
36
|
+
|
|
37
|
+
const services = [
|
|
38
|
+
{ title: "Colour", img: "/images/rotating-carousel/image-1.webp", description: "From subtle highlights to bold transformations. Our colour specialists use only the finest products for vibrant, lasting results." },
|
|
39
|
+
{ title: "Cut & Style", img: "/images/rotating-carousel/image-2.webp", description: "Precision cuts tailored to your face shape and lifestyle. Walk out feeling confident every single time." },
|
|
40
|
+
{ title: "Treatment", img: "/images/rotating-carousel/image-3.webp", description: "Restore shine and health with our premium treatment range. Ideal for damaged, dry, or colour-treated hair." },
|
|
41
|
+
];
|
|
11
42
|
</script>
|