srcdev-nuxt-components 9.1.50 → 9.1.52
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 +8 -27
- package/.claude/settings.local.json +24 -52
- package/.claude/skills/components/action-menu.md +141 -0
- package/.claude/skills/components/stepper-list.md +52 -18
- package/.claude/skills/css-nesting-conventions.md +121 -0
- package/.claude/skills/index.md +3 -0
- package/.claude/skills/pull-request-description.md +48 -0
- package/.claude/skills/testing-add-unit-test.md +40 -2
- package/README.md +26 -5
- package/app/components/02.molecules/action-menu/ActionMenu.vue +218 -0
- package/app/components/02.molecules/action-menu/ActionMenuItemCore.vue +123 -0
- package/app/components/02.molecules/action-menu/CONSUMER-STYLING.md +125 -0
- package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +326 -0
- package/app/components/02.molecules/action-menu/tests/ActionMenu.spec.ts +458 -0
- package/app/components/02.molecules/action-menu/tests/ActionMenuItemCore.spec.ts +199 -0
- package/app/components/02.molecules/action-menu/tests/__snapshots__/ActionMenu.spec.ts.snap +28 -0
- package/app/components/02.molecules/action-menu/tests/__snapshots__/ActionMenuItemCore.spec.ts.snap +27 -0
- package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +5 -1
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +2 -1
- package/app/components/02.molecules/stepper-list/CONSUMER-STYLING.md +138 -0
- package/app/components/02.molecules/stepper-list/StepperList.vue +50 -24
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +5 -1
- package/app/components/05.forms/input-button/InputButtonCore.vue +1 -1
- package/app/components/carousels/tests/CarouselFlip.spec.ts +1 -0
- package/app/components/responsive-header/NavigationItems.vue +302 -81
- package/app/components/responsive-header/ResponsiveHeader.vue +360 -56
- package/app/layouts/default.vue +43 -352
- package/app/layouts/site-navigation-demo.vue +36 -34
- package/app/pages/page-hero-highlights.vue +3 -3
- package/app/pages/ui/scroll-reveal-image.vue +3 -3
- package/app/pages/ui/simple-grid.vue +9 -20
- package/modules/colour-scheme.ts +1 -1
- package/nuxt.config.ts +11 -2
- package/package.json +17 -16
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
2
|
+
<div
|
|
3
|
+
ref="navigationWrapper"
|
|
4
|
+
class="navigation"
|
|
5
|
+
:class="[elementClasses, { loaded: navLoaded, 'geometry-ready': isGeometryReady }]"
|
|
6
|
+
role="banner"
|
|
7
|
+
>
|
|
8
|
+
<nav
|
|
9
|
+
ref="mainNav"
|
|
10
|
+
class="main-navigation"
|
|
11
|
+
:class="{ 'is-animated': isAnimated }"
|
|
12
|
+
aria-label="Main navigation"
|
|
13
|
+
@mouseleave="hoveredItemKey = null"
|
|
14
|
+
@focusout="handleNavFocusout"
|
|
15
|
+
>
|
|
4
16
|
<ul
|
|
5
17
|
v-for="(navGroup, groupKey) in responsiveNavLinks"
|
|
6
18
|
:key="groupKey"
|
|
@@ -16,6 +28,8 @@
|
|
|
16
28
|
class="main-navigation-item"
|
|
17
29
|
:class="{
|
|
18
30
|
'visually-hidden': !mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]?.config?.visible,
|
|
31
|
+
'is-hovered': hoveredItemKey === `${String(groupKey)}-${localIndex}`,
|
|
32
|
+
'is-active': isActiveNavItem(link),
|
|
19
33
|
}"
|
|
20
34
|
:style="{
|
|
21
35
|
'--_main-navigation-item-width':
|
|
@@ -23,8 +37,8 @@
|
|
|
23
37
|
}"
|
|
24
38
|
:data-group-key="groupKey"
|
|
25
39
|
:data-local-index="localIndex"
|
|
26
|
-
@mouseenter="handleNavigationItemHover"
|
|
27
|
-
@focusin="handleNavigationItemHover"
|
|
40
|
+
@mouseenter="handleNavigationItemHover(`${String(groupKey)}-${localIndex}`)"
|
|
41
|
+
@focusin="handleNavigationItemHover(`${String(groupKey)}-${localIndex}`)"
|
|
28
42
|
>
|
|
29
43
|
<NuxtLink
|
|
30
44
|
v-if="link.path"
|
|
@@ -61,12 +75,14 @@
|
|
|
61
75
|
</details>
|
|
62
76
|
</li>
|
|
63
77
|
</ul>
|
|
78
|
+
<div aria-hidden="true" class="nav-indicator-hovered"></div>
|
|
79
|
+
<div aria-hidden="true" class="nav-indicator-active"></div>
|
|
64
80
|
</nav>
|
|
65
81
|
<nav ref="secondaryNav" class="secondary-navigation" aria-label="Secondary navigation">
|
|
66
82
|
<details
|
|
67
83
|
ref="overflowDetails"
|
|
68
84
|
class="overflow-details"
|
|
69
|
-
:class="[
|
|
85
|
+
:class="overflowDetailsClass ? [overflowDetailsClass] : []"
|
|
70
86
|
name="overflow-group"
|
|
71
87
|
>
|
|
72
88
|
<summary class="overflow-details-summary has-toggle-icon">
|
|
@@ -93,7 +109,12 @@
|
|
|
93
109
|
</template>
|
|
94
110
|
|
|
95
111
|
<script setup lang="ts">
|
|
96
|
-
import type {
|
|
112
|
+
import type {
|
|
113
|
+
ResponsiveHeaderProp,
|
|
114
|
+
ResponsiveHeaderState,
|
|
115
|
+
IFlooredRect,
|
|
116
|
+
ResponsiveHeaderNavItem,
|
|
117
|
+
} from "../../types/components";
|
|
97
118
|
import { useResizeObserver, onClickOutside } from "@vueuse/core";
|
|
98
119
|
|
|
99
120
|
interface Props {
|
|
@@ -176,19 +197,84 @@ const handleSummaryHover = (event: MouseEvent | FocusEvent) => {
|
|
|
176
197
|
toggleDetailsElement(event);
|
|
177
198
|
};
|
|
178
199
|
|
|
179
|
-
const handleNavigationItemHover = () => {
|
|
200
|
+
const handleNavigationItemHover = (key: string) => {
|
|
201
|
+
hoveredItemKey.value = key;
|
|
180
202
|
if (!props.allowExpandOnGesture) {
|
|
181
203
|
return;
|
|
182
204
|
}
|
|
183
|
-
|
|
184
|
-
// Close all open navigation details when hovering over regular nav items
|
|
185
205
|
closeAllNavigationDetails();
|
|
186
206
|
};
|
|
187
207
|
|
|
208
|
+
const handleNavFocusout = (event: FocusEvent) => {
|
|
209
|
+
// Only clear when focus moves outside the nav entirely, not between nav items.
|
|
210
|
+
const nav = event.currentTarget as HTMLElement;
|
|
211
|
+
if (!event.relatedTarget || !nav.contains(event.relatedTarget as Node)) {
|
|
212
|
+
hoveredItemKey.value = null;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
188
216
|
const handleSummaryAction = (event: MouseEvent | KeyboardEvent) => {
|
|
189
217
|
toggleDetailsElement(event);
|
|
190
218
|
};
|
|
191
219
|
|
|
220
|
+
const hoveredItemKey = ref<string | null>(null);
|
|
221
|
+
|
|
222
|
+
const route = useRoute();
|
|
223
|
+
|
|
224
|
+
const isActiveNavItem = (link: ResponsiveHeaderNavItem): boolean => {
|
|
225
|
+
if (link.path) return route.path === link.path;
|
|
226
|
+
if (link.childLinks) return link.childLinks.some((child) => child.path && route.path === child.path);
|
|
227
|
+
return false;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const isAnimated = ref(true);
|
|
231
|
+
|
|
232
|
+
// Local (non-useState) flag — resets to false on every mount.
|
|
233
|
+
// Gates two things:
|
|
234
|
+
// 1. The `.geometry-ready` wrapper class that enables nav-item visibility transitions,
|
|
235
|
+
// preventing items from animating in on every route change.
|
|
236
|
+
// 2. Whether the overflow button is shown (via overflowDetailsHidden below).
|
|
237
|
+
const isGeometryReady = ref(false);
|
|
238
|
+
|
|
239
|
+
// Set to true only during Phase 2 of the initial geometry pass (see useResizeObserver).
|
|
240
|
+
// Allows the overflow button to be temporarily revealed at its natural size so that
|
|
241
|
+
// secondaryNavRects can be measured accurately before the final visibility pass runs.
|
|
242
|
+
const isOverflowVisibleForMeasurement = ref(false);
|
|
243
|
+
|
|
244
|
+
// Single source of truth for the overflow button's CSS class:
|
|
245
|
+
//
|
|
246
|
+
// 'visually-hidden' → width: 0 — removed from layout entirely (early boot, between
|
|
247
|
+
// the first and second measurement phases)
|
|
248
|
+
// 'is-measuring' → opacity:0 / visibility:hidden, but NATURAL width — button is
|
|
249
|
+
// present in layout so secondaryNavRects captures its real width,
|
|
250
|
+
// but invisible to the user (phase 2 of the geometry pass)
|
|
251
|
+
// null → fully visible (isGeometryReady + showOverflowDetails)
|
|
252
|
+
//
|
|
253
|
+
// Note: we never conditionally skip phase 2 with a peek check. A peek using
|
|
254
|
+
// determineNavigationItemVisibility() after phase 1 gives the wrong answer at the
|
|
255
|
+
// breakpoint because the CSS v-bind(mainNavigationMarginBlockEndStr) hasn't been
|
|
256
|
+
// flushed to the DOM yet — positions lag the reactive update by one tick.
|
|
257
|
+
const overflowDetailsClass = computed<string | null>(() => {
|
|
258
|
+
if (!navLoaded.value) return "visually-hidden";
|
|
259
|
+
if (!isGeometryReady.value) {
|
|
260
|
+
return isOverflowVisibleForMeasurement.value ? "is-measuring" : "visually-hidden";
|
|
261
|
+
}
|
|
262
|
+
return showOverflowDetails.value ? null : "visually-hidden";
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
watch(
|
|
266
|
+
() => route.path,
|
|
267
|
+
() => {
|
|
268
|
+
isAnimated.value = false;
|
|
269
|
+
requestAnimationFrame(() => {
|
|
270
|
+
requestAnimationFrame(() => {
|
|
271
|
+
isAnimated.value = true;
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
},
|
|
275
|
+
{ flush: "pre" }
|
|
276
|
+
);
|
|
277
|
+
|
|
192
278
|
// Initialize main navigation state
|
|
193
279
|
const mainNavigationState = ref<ResponsiveHeaderState>({
|
|
194
280
|
navListVisibility: {
|
|
@@ -348,6 +434,10 @@ const initMainNavigationState = () => {
|
|
|
348
434
|
mainNavigationState.value.navListVisibility[groupKey] = true;
|
|
349
435
|
}
|
|
350
436
|
});
|
|
437
|
+
|
|
438
|
+
// Geometry pass is complete. The overflow button can now appear (if needed)
|
|
439
|
+
// and nav-item transitions can play from this point forward.
|
|
440
|
+
isGeometryReady.value = true;
|
|
351
441
|
};
|
|
352
442
|
|
|
353
443
|
// Function to set up click outside listeners
|
|
@@ -366,26 +456,95 @@ const setupClickOutsideListeners = () => {
|
|
|
366
456
|
};
|
|
367
457
|
|
|
368
458
|
onMounted(async () => {
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
459
|
+
// Always reset on every mount — this is a local ref so it naturally resets,
|
|
460
|
+
// but being explicit here makes the intent clear: geometry must be re-verified
|
|
461
|
+
// on every route change before the overflow button appears or transitions play.
|
|
462
|
+
isGeometryReady.value = false;
|
|
463
|
+
|
|
464
|
+
// Always (re-)populate the local nav refs — firstNavRef / secondNavRef are local
|
|
465
|
+
// refs that reset on unmount, so they must be re-assigned on every mount even
|
|
466
|
+
// when the useState flags say we're already initialized.
|
|
467
|
+
await initTemplateRefs();
|
|
376
468
|
|
|
377
|
-
|
|
469
|
+
if (!navigationInitialized.value || !navLoaded.value) {
|
|
378
470
|
navLoaded.value = true;
|
|
379
471
|
navigationInitialized.value = true;
|
|
380
|
-
}
|
|
472
|
+
}
|
|
381
473
|
|
|
474
|
+
// Geometry recalculation is handled by useResizeObserver which fires when the
|
|
475
|
+
// element first becomes observed. isGeometryReady is set there after the pass.
|
|
382
476
|
setupClickOutsideListeners();
|
|
383
477
|
});
|
|
384
478
|
|
|
479
|
+
// ─── Re-entrancy guard for the ResizeObserver callback ────────────────────
|
|
480
|
+
// The observer fires an async callback that awaits multiple ticks and toggles
|
|
481
|
+
// layout-affecting state. If the observer fires again while the first pass is
|
|
482
|
+
// still in flight, two concurrent passes would race on isOverflowVisibleForMeasurement
|
|
483
|
+
// and mainNavigationState.
|
|
484
|
+
//
|
|
485
|
+
// Pattern: "run-latest" queue.
|
|
486
|
+
// • isMeasuring gates entry — any new observer call while a pass is running
|
|
487
|
+
// sets pendingMeasure = true and returns immediately.
|
|
488
|
+
// • The do/while re-runs once after the pass completes if a new resize arrived,
|
|
489
|
+
// so we never silently drop the final geometry update.
|
|
490
|
+
let isMeasuring = false;
|
|
491
|
+
let pendingMeasure = false;
|
|
492
|
+
|
|
385
493
|
useResizeObserver(navigationWrapperRef, async () => {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
494
|
+
if (isMeasuring) {
|
|
495
|
+
pendingMeasure = true;
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
isMeasuring = true;
|
|
500
|
+
try {
|
|
501
|
+
do {
|
|
502
|
+
pendingMeasure = false;
|
|
503
|
+
|
|
504
|
+
if (!isGeometryReady.value) {
|
|
505
|
+
// ─── Two-pass measurement on initial mount / route change ──────────────
|
|
506
|
+
//
|
|
507
|
+
// We never use a "peek" check to decide whether phase 2 is needed.
|
|
508
|
+
// A peek after phase 1 gives the wrong answer at the breakpoint because:
|
|
509
|
+
// • updateNavigationConfig() sets secondaryNavRects reactively, but
|
|
510
|
+
// v-bind(mainNavigationMarginBlockEndStr) isn't flushed to the DOM until
|
|
511
|
+
// the next Vue render tick — so item rects are still measured against the
|
|
512
|
+
// OLD margin, producing a stale DOM/computed mismatch.
|
|
513
|
+
// • Even with correct timing, checking without the button reserved will
|
|
514
|
+
// always pass marginal items (those that fit only when button is absent).
|
|
515
|
+
//
|
|
516
|
+
// Phase 1: button is 'visually-hidden' (width:0).
|
|
517
|
+
// Measure the wrapper and secondary-nav rects without the button.
|
|
518
|
+
await updateNavigationConfig("phase1");
|
|
519
|
+
|
|
520
|
+
// Phase 2: switch button to 'is-measuring' — opacity:0 / visibility:hidden
|
|
521
|
+
// but natural width — so secondaryNavRects captures the button's real width.
|
|
522
|
+
// The button is invisible to the user; nav items are still visually-hidden.
|
|
523
|
+
isOverflowVisibleForMeasurement.value = true;
|
|
524
|
+
await nextTick(); // let Vue render the 'is-measuring' class (button at natural width)
|
|
525
|
+
await updateNavigationConfig("phase2"); // secondaryNavRects now includes button width
|
|
526
|
+
|
|
527
|
+
// Wait for Vue to flush the updated mainNavigationMarginBlockEndStr via v-bind
|
|
528
|
+
// before reading item positions — without this tick, getBoundingClientRect()
|
|
529
|
+
// in initMainNavigationState would see the pre-phase-2 margin-inline-end.
|
|
530
|
+
await nextTick();
|
|
531
|
+
|
|
532
|
+
// Final visibility pass — reads item rects against the correct margin.
|
|
533
|
+
// Also sets isGeometryReady = true.
|
|
534
|
+
initMainNavigationState();
|
|
535
|
+
// Hand control back to showOverflowDetails; measurement flag no longer needed.
|
|
536
|
+
isOverflowVisibleForMeasurement.value = false;
|
|
537
|
+
} else {
|
|
538
|
+
// ─── Normal resize after geometry is settled ───────────────────────────
|
|
539
|
+
// The button's state is already correct (driven by showOverflowDetails),
|
|
540
|
+
// so a single-pass measurement gives accurate secondaryNavRects.
|
|
541
|
+
await updateNavigationConfig("useResizeObserver");
|
|
542
|
+
initMainNavigationState();
|
|
543
|
+
}
|
|
544
|
+
} while (pendingMeasure);
|
|
545
|
+
} finally {
|
|
546
|
+
isMeasuring = false;
|
|
547
|
+
}
|
|
389
548
|
});
|
|
390
549
|
|
|
391
550
|
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
@@ -415,7 +574,57 @@ watch(
|
|
|
415
574
|
}
|
|
416
575
|
}
|
|
417
576
|
|
|
577
|
+
/* ─── Public CSS tokens ─────────────────────────────────────────────────
|
|
578
|
+
Override these on the consumer's scope class to theme the nav.
|
|
579
|
+
Tokens are read via var(--token, default) — defaults are NOT declared
|
|
580
|
+
on this element to avoid source-order cascade conflicts with consumers.
|
|
581
|
+
|
|
582
|
+
--responsive-header-margin (default: 0)
|
|
583
|
+
--responsive-header-bg (default: transparent)
|
|
584
|
+
--responsive-header-border (default: none)
|
|
585
|
+
--responsive-header-border-radius (default: 0)
|
|
586
|
+
--responsive-header-padding-block (default: 0)
|
|
587
|
+
--responsive-header-padding-inline (default: 0)
|
|
588
|
+
--responsive-header-max-height (default: none)
|
|
589
|
+
--responsive-header-inline-size (default: 100%)
|
|
590
|
+
|
|
591
|
+
--responsive-header-color (default: inherit)
|
|
592
|
+
--responsive-header-link-color (default: inherit)
|
|
593
|
+
|
|
594
|
+
--responsive-header-sub-nav-bg (default: Canvas)
|
|
595
|
+
--responsive-header-sub-nav-border (default: 1px solid #efefef75)
|
|
596
|
+
--responsive-header-sub-nav-border-radius (default: 8px)
|
|
597
|
+
--responsive-header-sub-nav-padding (default: 12px)
|
|
598
|
+
|
|
599
|
+
--responsive-header-overflow-btn-bg (default: Canvas)
|
|
600
|
+
--responsive-header-overflow-btn-size (default: 20px)
|
|
601
|
+
--responsive-header-overflow-btn-border (default: 1px solid #ffffff90)
|
|
602
|
+
--responsive-header-overflow-btn-outline (default: 1px solid #ffffff10)
|
|
603
|
+
--responsive-header-overflow-btn-icon-color (default: inherit)
|
|
604
|
+
--responsive-header-overflow-btn-hover-outline (default: 1px solid #ffffff)
|
|
605
|
+
|
|
606
|
+
--responsive-header-overflow-nav-bg (default: Canvas)
|
|
607
|
+
--responsive-header-overflow-nav-border (default: 1px solid #ffffff90)
|
|
608
|
+
--responsive-header-overflow-nav-border-radius (default: 8px)
|
|
609
|
+
--responsive-header-overflow-nav-padding-block (default: 12px)
|
|
610
|
+
|
|
611
|
+
--responsive-nav-decorator-indicator-color (default: currentColor)
|
|
612
|
+
--responsive-nav-decorator-hovered-indicator-color (default: inherits --responsive-nav-decorator-indicator-color)
|
|
613
|
+
--responsive-nav-decorator-hovered-bg (default: oklch(100% 0 0 / 8%))
|
|
614
|
+
──────────────────────────────────────────────────────────────────────── */
|
|
615
|
+
|
|
418
616
|
--_link-visibility-transition: none;
|
|
617
|
+
position: relative;
|
|
618
|
+
color: var(--responsive-header-color, inherit);
|
|
619
|
+
|
|
620
|
+
margin: var(--responsive-header-margin, 0);
|
|
621
|
+
background-color: var(--responsive-header-bg, transparent);
|
|
622
|
+
border: var(--responsive-header-border, none);
|
|
623
|
+
border-radius: var(--responsive-header-border-radius, 0);
|
|
624
|
+
padding-block: var(--responsive-header-padding-block, 0);
|
|
625
|
+
padding-inline: var(--responsive-header-padding-inline, 0);
|
|
626
|
+
max-height: var(--responsive-header-max-height, none);
|
|
627
|
+
inline-size: var(--responsive-header-inline-size, 100%);
|
|
419
628
|
|
|
420
629
|
&.loaded {
|
|
421
630
|
--_link-visibility-transition: all 0.2s ease-in-out;
|
|
@@ -441,7 +650,6 @@ watch(
|
|
|
441
650
|
flex-grow: 1;
|
|
442
651
|
justify-content: space-between;
|
|
443
652
|
gap: 60px;
|
|
444
|
-
|
|
445
653
|
overflow-x: hidden;
|
|
446
654
|
margin-inline-end: v-bind(mainNavigationMarginBlockEndStr);
|
|
447
655
|
|
|
@@ -464,9 +672,9 @@ watch(
|
|
|
464
672
|
.main-navigation-item {
|
|
465
673
|
/* width: var(--_main-navigation-item-width); */
|
|
466
674
|
overflow: hidden;
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
675
|
+
/* Transition is intentionally absent here — it is only enabled after
|
|
676
|
+
the first geometry pass via .navigation.geometry-ready below, so
|
|
677
|
+
items don't animate in on every route change. */
|
|
470
678
|
padding-block: var(--_link-focus-visible-outline-width);
|
|
471
679
|
padding-inline: var(--_link-focus-visible-outline-width);
|
|
472
680
|
|
|
@@ -474,21 +682,18 @@ watch(
|
|
|
474
682
|
display: flex;
|
|
475
683
|
gap: 6px;
|
|
476
684
|
text-wrap-mode: nowrap;
|
|
477
|
-
color: inherit;
|
|
685
|
+
color: var(--responsive-header-link-color, inherit);
|
|
478
686
|
text-decoration: none;
|
|
687
|
+
cursor: pointer;
|
|
479
688
|
margin-inline-start: 0;
|
|
480
|
-
|
|
689
|
+
position: relative;
|
|
690
|
+
z-index: 4;
|
|
481
691
|
|
|
482
692
|
padding-block: var(--_link-padding-block);
|
|
483
693
|
padding-inline: var(--_link-padding-inline);
|
|
484
694
|
margin-block: var(--_link-margin-block);
|
|
485
695
|
margin-inline: var(--_link-margin-inline);
|
|
486
696
|
border-bottom: var(--_link-border-default);
|
|
487
|
-
|
|
488
|
-
&:hover {
|
|
489
|
-
cursor: pointer;
|
|
490
|
-
border-bottom-color: var(--_link-border-bottom-hover);
|
|
491
|
-
}
|
|
492
697
|
}
|
|
493
698
|
|
|
494
699
|
.main-navigation-details {
|
|
@@ -499,10 +704,6 @@ watch(
|
|
|
499
704
|
|
|
500
705
|
&[open] {
|
|
501
706
|
--_icon-transform: scaleY(-1);
|
|
502
|
-
|
|
503
|
-
.main-navigation-details-summary {
|
|
504
|
-
border-bottom-color: var(--_link-border-bottom-hover);
|
|
505
|
-
}
|
|
506
707
|
}
|
|
507
708
|
|
|
508
709
|
.has-toggle-icon {
|
|
@@ -524,17 +725,16 @@ watch(
|
|
|
524
725
|
margin-inline: var(--_link-margin-inline);
|
|
525
726
|
border-bottom: var(--_link-border-default);
|
|
526
727
|
white-space: nowrap;
|
|
728
|
+
cursor: pointer;
|
|
729
|
+
position: relative;
|
|
730
|
+
z-index: 4;
|
|
731
|
+
color: var(--responsive-header-link-color, inherit);
|
|
527
732
|
|
|
528
733
|
&::-webkit-details-marker,
|
|
529
734
|
&::marker {
|
|
530
735
|
display: none;
|
|
531
736
|
}
|
|
532
737
|
|
|
533
|
-
&:hover {
|
|
534
|
-
cursor: pointer;
|
|
535
|
-
border-bottom-color: var(--_link-border-bottom-hover);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
738
|
.decorator-icon {
|
|
539
739
|
margin-inline-start: 8px;
|
|
540
740
|
}
|
|
@@ -542,10 +742,11 @@ watch(
|
|
|
542
742
|
|
|
543
743
|
.main-navigation-sub-nav {
|
|
544
744
|
position: absolute;
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
border
|
|
548
|
-
|
|
745
|
+
|
|
746
|
+
padding: var(--responsive-header-sub-nav-padding, 12px);
|
|
747
|
+
border: var(--responsive-header-sub-nav-border, 1px solid #efefef75);
|
|
748
|
+
border-radius: var(--responsive-header-sub-nav-border-radius, 8px);
|
|
749
|
+
background-color: var(--responsive-header-sub-nav-bg, Canvas);
|
|
549
750
|
translate: 0 12px;
|
|
550
751
|
|
|
551
752
|
min-width: var(--_main-navigation-item-width);
|
|
@@ -566,7 +767,7 @@ watch(
|
|
|
566
767
|
display: block;
|
|
567
768
|
text-wrap-mode: nowrap;
|
|
568
769
|
text-decoration: none;
|
|
569
|
-
color: inherit;
|
|
770
|
+
color: var(--responsive-header-link-color, inherit);
|
|
570
771
|
}
|
|
571
772
|
}
|
|
572
773
|
}
|
|
@@ -576,6 +777,12 @@ watch(
|
|
|
576
777
|
&.visually-hidden {
|
|
577
778
|
visibility: hidden;
|
|
578
779
|
opacity: 0;
|
|
780
|
+
/* Pin the outer <li> to its originally-measured width so re-measurements
|
|
781
|
+
while hidden don't pick up an inflated value from the shifted inner
|
|
782
|
+
content. Without this, each hide→measure cycle compounds the width
|
|
783
|
+
(feedback loop → 7995px → items never recover). */
|
|
784
|
+
inline-size: var(--_main-navigation-item-width);
|
|
785
|
+
overflow: hidden;
|
|
579
786
|
|
|
580
787
|
.main-navigation-details,
|
|
581
788
|
.main-navigation-link {
|
|
@@ -619,7 +826,7 @@ watch(
|
|
|
619
826
|
display: flex;
|
|
620
827
|
align-items: center;
|
|
621
828
|
font: inherit;
|
|
622
|
-
color: inherit;
|
|
829
|
+
color: var(--responsive-header-link-color, inherit);
|
|
623
830
|
|
|
624
831
|
.icon {
|
|
625
832
|
height: 1.35em;
|
|
@@ -652,9 +859,21 @@ watch(
|
|
|
652
859
|
width: 0;
|
|
653
860
|
}
|
|
654
861
|
|
|
862
|
+
/* Applied during phase 2 of the geometry pass only.
|
|
863
|
+
Button is invisible but at natural width so secondaryNavRects
|
|
864
|
+
captures its real footprint for margin-inline-end calculation.
|
|
865
|
+
transition:none prevents a width animation from 0→natural that
|
|
866
|
+
would make the mid-animation measurement inaccurate. */
|
|
867
|
+
&.is-measuring {
|
|
868
|
+
opacity: 0;
|
|
869
|
+
visibility: hidden;
|
|
870
|
+
pointer-events: none;
|
|
871
|
+
transition: none;
|
|
872
|
+
}
|
|
873
|
+
|
|
655
874
|
.overflow-details-summary {
|
|
656
875
|
--_icon-zoom: 1;
|
|
657
|
-
--_icon-size: 20px;
|
|
876
|
+
--_icon-size: var(--responsive-header-overflow-btn-size, 20px);
|
|
658
877
|
--_border-width: 1px;
|
|
659
878
|
--_outline-width: 1px;
|
|
660
879
|
--_transition-duration: 0.2s;
|
|
@@ -668,9 +887,9 @@ watch(
|
|
|
668
887
|
|
|
669
888
|
aspect-ratio: 1;
|
|
670
889
|
border-radius: 4px;
|
|
671
|
-
border: var(--
|
|
672
|
-
outline: var(--
|
|
673
|
-
background-color: Canvas;
|
|
890
|
+
border: var(--responsive-header-overflow-btn-border, 1px solid #ffffff90);
|
|
891
|
+
outline: var(--responsive-header-overflow-btn-outline, 1px solid #ffffff10);
|
|
892
|
+
background-color: var(--responsive-header-overflow-btn-bg, Canvas);
|
|
674
893
|
|
|
675
894
|
width: var(--_icon-size);
|
|
676
895
|
overflow: hidden;
|
|
@@ -686,12 +905,13 @@ watch(
|
|
|
686
905
|
&:hover,
|
|
687
906
|
&:focus-visible {
|
|
688
907
|
--_icon-zoom: 1.2;
|
|
689
|
-
outline: var(--
|
|
908
|
+
outline: var(--responsive-header-overflow-btn-hover-outline, 1px solid #ffffff);
|
|
690
909
|
}
|
|
691
910
|
|
|
692
911
|
.icon {
|
|
693
912
|
grid-area: icon;
|
|
694
913
|
scale: var(--_icon-zoom);
|
|
914
|
+
color: var(--responsive-header-overflow-btn-icon-color, inherit);
|
|
695
915
|
transition: scale 0.2s ease-in-out;
|
|
696
916
|
width: calc(var(--_icon-size) - var(--_border-width) * 2 - var(--_outline-width) * 2);
|
|
697
917
|
height: calc(var(--_icon-size) - var(--_border-width) * 2 - var(--_outline-width) * 2);
|
|
@@ -711,10 +931,10 @@ watch(
|
|
|
711
931
|
position: absolute;
|
|
712
932
|
top: 135%;
|
|
713
933
|
right: 0;
|
|
714
|
-
background-color:
|
|
715
|
-
border: 1px solid #ffffff90;
|
|
716
|
-
border-radius: 8px;
|
|
717
|
-
padding-block: 12px;
|
|
934
|
+
background-color: var(--responsive-header-overflow-nav-bg, Canvas);
|
|
935
|
+
border: var(--responsive-header-overflow-nav-border, 1px solid #ffffff90);
|
|
936
|
+
border-radius: var(--responsive-header-overflow-nav-border-radius, 8px);
|
|
937
|
+
padding-block: var(--responsive-header-overflow-nav-padding-block, 12px);
|
|
718
938
|
margin: 0;
|
|
719
939
|
z-index: 999;
|
|
720
940
|
min-width: var(--_overflow-drop-down-width, fit-content);
|
|
@@ -725,6 +945,90 @@ watch(
|
|
|
725
945
|
}
|
|
726
946
|
}
|
|
727
947
|
}
|
|
948
|
+
|
|
949
|
+
/* Once geometry has been calculated for this mount, enable item transitions.
|
|
950
|
+
This prevents nav items from animating in on every route change while still
|
|
951
|
+
providing smooth hide/show transitions when the viewport is resized. */
|
|
952
|
+
&.geometry-ready {
|
|
953
|
+
.main-navigation-item {
|
|
954
|
+
transition:
|
|
955
|
+
opacity 0.2s ease-in-out,
|
|
956
|
+
visibility 0.2s ease-in-out;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/* ─── Anchor positioning for main-nav indicators ─────────────────────────
|
|
962
|
+
Single --responsive-main-nav-indicator anchor: sits on is-hovered when
|
|
963
|
+
something is hovered, falls back to is-active when nothing is hovered.
|
|
964
|
+
──────────────────────────────────────────────────────────────────────── */
|
|
965
|
+
|
|
966
|
+
.main-navigation li.is-hovered {
|
|
967
|
+
anchor-name: --responsive-main-nav-indicator;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
.main-navigation:not(:has(li.is-hovered)) li.is-active {
|
|
971
|
+
anchor-name: --responsive-main-nav-indicator;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
.main-navigation .nav-indicator-hovered,
|
|
975
|
+
.main-navigation .nav-indicator-active {
|
|
976
|
+
display: none;
|
|
977
|
+
pointer-events: none;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
.main-navigation {
|
|
981
|
+
.nav-indicator-hovered {
|
|
982
|
+
display: block;
|
|
983
|
+
position: absolute;
|
|
984
|
+
position-anchor: --responsive-main-nav-indicator;
|
|
985
|
+
left: anchor(left);
|
|
986
|
+
right: anchor(right);
|
|
987
|
+
top: anchor(top);
|
|
988
|
+
bottom: anchor(bottom);
|
|
989
|
+
background: var(--responsive-nav-decorator-hovered-bg, oklch(100% 0 0 / 8%));
|
|
990
|
+
border-radius: 4px;
|
|
991
|
+
z-index: 1;
|
|
992
|
+
opacity: 0;
|
|
993
|
+
transition:
|
|
994
|
+
left 200ms ease,
|
|
995
|
+
right 200ms ease,
|
|
996
|
+
opacity 150ms ease;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
&:not(.is-animated) .nav-indicator-hovered {
|
|
1000
|
+
transition: none;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
&:has(li.is-hovered) .nav-indicator-hovered {
|
|
1004
|
+
opacity: 1;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
.nav-indicator-active {
|
|
1008
|
+
display: block;
|
|
1009
|
+
position: absolute;
|
|
1010
|
+
position-anchor: --responsive-main-nav-indicator;
|
|
1011
|
+
left: anchor(left);
|
|
1012
|
+
right: anchor(right);
|
|
1013
|
+
bottom: calc(anchor(bottom) - 2px);
|
|
1014
|
+
height: 2px;
|
|
1015
|
+
background: var(--responsive-nav-decorator-indicator-color, currentColor);
|
|
1016
|
+
z-index: 3;
|
|
1017
|
+
transition:
|
|
1018
|
+
left 200ms ease,
|
|
1019
|
+
right 200ms ease;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
&:not(.is-animated) .nav-indicator-active {
|
|
1023
|
+
transition: none;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
&:has(li.is-hovered) .nav-indicator-active {
|
|
1027
|
+
background: var(
|
|
1028
|
+
--responsive-nav-decorator-hovered-indicator-color,
|
|
1029
|
+
var(--responsive-nav-decorator-indicator-color, currentColor)
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
728
1032
|
}
|
|
729
1033
|
}
|
|
730
1034
|
</style>
|