srcdev-nuxt-components 9.4.5 → 9.4.6
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/commands/migrate-component.md +40 -2
- package/.claude/component-ledger/audit.json +1 -1
- package/.claude/component-ledger/output.html +1 -1
- package/.claude/skills/components/breadcrumb.md +1 -0
- package/.claude/skills/components/content-docs.md +2 -0
- package/.claude/skills/components/display-tooltip-defined.md +3 -0
- package/.claude/skills/components/display-tooltip.md +1 -0
- package/.claude/skills/components/marquee-scroller.md +130 -0
- package/.claude/skills/components/navigation-items.md +1 -0
- package/.claude/skills/components/responsive-header.md +3 -0
- package/.claude/skills/components/site-header.md +4 -0
- package/.claude/skills/components/site-navigation.md +1 -0
- package/.claude/skills/components/tab-navigation.md +1 -0
- package/.claude/skills/index.md +1 -0
- package/.vscode/srcdev-component-breadcrumb.code-snippets +13 -0
- package/.vscode/srcdev-component-content-docs.code-snippets +18 -0
- package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +15 -0
- package/.vscode/srcdev-component-display-tooltip.code-snippets +11 -0
- package/.vscode/srcdev-component-marquee-scroller.code-snippets +93 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +12 -0
- package/.vscode/srcdev-component-site-header.code-snippets +17 -0
- package/.vscode/srcdev-component-site-navigation.code-snippets +30 -0
- package/.vscode/srcdev-component-slider-gallery.code-snippets +38 -0
- package/.vscode/srcdev-component-tab-navigation.code-snippets +30 -0
- package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +94 -0
- package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +331 -0
- package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +151 -0
- package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +315 -0
- package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +28 -0
- package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +8 -2
- package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +4 -1
- package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +4 -1
- package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +16 -3
- package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +1 -1
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +4 -1
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +4 -1
- package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +16 -4
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +4 -1
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +16 -3
- package/app/components/03.organisms/site-header/SiteHeader.vue +16 -1
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
- package/app/types/components/index.ts +1 -0
- package/app/types/components/marquee-scroller.d.ts +10 -0
- package/package.json +1 -1
- package/app/components/marquee-scroller/MarqueeScroller.vue +0 -289
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
ref="mainNav"
|
|
10
10
|
class="main-navigation"
|
|
11
11
|
:class="{ 'is-animated': isAnimated }"
|
|
12
|
-
aria-label="
|
|
12
|
+
:aria-label="mainNavAriaLabel"
|
|
13
13
|
@mouseleave="hoveredItemKey = null"
|
|
14
14
|
@focusout="handleNavFocusout"
|
|
15
15
|
>
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
<div aria-hidden="true" class="nav-indicator-hovered"></div>
|
|
79
79
|
<div aria-hidden="true" class="nav-indicator-active"></div>
|
|
80
80
|
</nav>
|
|
81
|
-
<nav ref="secondaryNav" class="secondary-navigation" aria-label="
|
|
81
|
+
<nav ref="secondaryNav" class="secondary-navigation" :aria-label="secondaryNavAriaLabel">
|
|
82
82
|
<details
|
|
83
83
|
ref="overflowDetails"
|
|
84
84
|
class="overflow-details"
|
|
@@ -100,7 +100,11 @@
|
|
|
100
100
|
/>
|
|
101
101
|
</summary>
|
|
102
102
|
<div class="overflow-details-nav" role="menu">
|
|
103
|
-
<NavigationItems
|
|
103
|
+
<NavigationItems
|
|
104
|
+
:main-navigation-state="mainNavigationState"
|
|
105
|
+
:panel-variant="panelVariant"
|
|
106
|
+
:aria-label="overflowMenuAriaLabel"
|
|
107
|
+
/>
|
|
104
108
|
</div>
|
|
105
109
|
</details>
|
|
106
110
|
<slot v-if="slots.secondaryNavigation" name="secondaryNavigation"></slot>
|
|
@@ -126,6 +130,12 @@ interface Props {
|
|
|
126
130
|
styleClassPassthrough?: string | string[];
|
|
127
131
|
allowExpandOnGesture?: boolean;
|
|
128
132
|
panelVariant?: "modern" | "classic";
|
|
133
|
+
/** aria-label on the primary nav landmark — override for localisation. */
|
|
134
|
+
mainNavAriaLabel?: string;
|
|
135
|
+
/** aria-label on the secondary (overflow) nav landmark — override for localisation. */
|
|
136
|
+
secondaryNavAriaLabel?: string;
|
|
137
|
+
/** aria-label on the overflow menu itself (forwarded to NavigationItems) — override for localisation. */
|
|
138
|
+
overflowMenuAriaLabel?: string;
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -142,6 +152,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
142
152
|
// Forwarded to NavigationItems' overflow submenu panels — see its own default for why
|
|
143
153
|
// "classic" is the safe default (CLAUDE.md pitfall #19).
|
|
144
154
|
panelVariant: "classic",
|
|
155
|
+
mainNavAriaLabel: "Main navigation",
|
|
156
|
+
secondaryNavAriaLabel: "Secondary navigation",
|
|
157
|
+
overflowMenuAriaLabel: "Overflow navigation menu",
|
|
145
158
|
});
|
|
146
159
|
|
|
147
160
|
const collapseNavigationBelowWidth = computed(
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<PageRow tag="div" :variant="pageRowVariant" :style-class-passthrough="styleClassPassthrough">
|
|
3
3
|
<template #default>
|
|
4
4
|
<header class="site-header">
|
|
5
|
-
<nav class="home-navigation" aria-label="
|
|
5
|
+
<nav class="home-navigation" :aria-label="homeNavAriaLabel">
|
|
6
6
|
<SkipLinks>
|
|
7
7
|
<template #homeLink>
|
|
8
8
|
<slot name="branding"></slot>
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
:collapse-at-main-nav-intersection="collapseAtMainNavIntersection"
|
|
18
18
|
:allow-expand-on-gesture="allowExpandOnGesture"
|
|
19
19
|
:style-class-passthrough="navStyleClassPassthrough"
|
|
20
|
+
:main-nav-aria-label="mainNavAriaLabel"
|
|
21
|
+
:secondary-nav-aria-label="secondaryNavAriaLabel"
|
|
22
|
+
:overflow-menu-aria-label="overflowMenuAriaLabel"
|
|
20
23
|
>
|
|
21
24
|
<template v-if="slots.secondaryNavigation" #secondaryNavigation>
|
|
22
25
|
<slot name="secondaryNavigation"></slot>
|
|
@@ -43,6 +46,14 @@ interface Props {
|
|
|
43
46
|
pageRowVariant?: "full" | "popout" | "content" | "inset-content";
|
|
44
47
|
styleClassPassthrough?: string | string[];
|
|
45
48
|
navStyleClassPassthrough?: string | string[];
|
|
49
|
+
/** aria-label on the home/branding nav landmark — override for localisation. */
|
|
50
|
+
homeNavAriaLabel?: string;
|
|
51
|
+
/** Forwarded to ResponsiveHeader's mainNavAriaLabel — override for localisation. */
|
|
52
|
+
mainNavAriaLabel?: string;
|
|
53
|
+
/** Forwarded to ResponsiveHeader's secondaryNavAriaLabel — override for localisation. */
|
|
54
|
+
secondaryNavAriaLabel?: string;
|
|
55
|
+
/** Forwarded to ResponsiveHeader's overflowMenuAriaLabel — override for localisation. */
|
|
56
|
+
overflowMenuAriaLabel?: string;
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
withDefaults(defineProps<Props>(), {
|
|
@@ -58,6 +69,10 @@ withDefaults(defineProps<Props>(), {
|
|
|
58
69
|
pageRowVariant: "content",
|
|
59
70
|
styleClassPassthrough: () => [],
|
|
60
71
|
navStyleClassPassthrough: () => [],
|
|
72
|
+
homeNavAriaLabel: "Home Navigation",
|
|
73
|
+
mainNavAriaLabel: "Main navigation",
|
|
74
|
+
secondaryNavAriaLabel: "Secondary navigation",
|
|
75
|
+
overflowMenuAriaLabel: "Overflow navigation menu",
|
|
61
76
|
});
|
|
62
77
|
|
|
63
78
|
const slots = useSlots();
|
|
@@ -9,7 +9,7 @@ exports[`SiteHeader > renders correct HTML structure with default props 1`] = `
|
|
|
9
9
|
<nav class="skip-links-nav" aria-label="Skip navigation"><a href="#main-content" class="skip-link">Skip to main content</a><a href="#footer-content" class="skip-link">Skip to footer</a></nav>
|
|
10
10
|
</div>
|
|
11
11
|
</nav>
|
|
12
|
-
<div class="responsive-header-stub"></div>
|
|
12
|
+
<div class="responsive-header-stub" main-nav-aria-label="Main navigation" secondary-nav-aria-label="Secondary navigation" overflow-menu-aria-label="Overflow navigation menu"></div>
|
|
13
13
|
</header>
|
|
14
14
|
</div>"
|
|
15
15
|
`;
|
package/package.json
CHANGED
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div
|
|
3
|
-
v-if="displayComponent"
|
|
4
|
-
class="marquee-scroller"
|
|
5
|
-
:class="{ reverse: reverse, paused: isPaused, 'reduced-motion': prefersReducedMotion }"
|
|
6
|
-
role="region"
|
|
7
|
-
:aria-label="ariaLabel || 'Scrolling content'"
|
|
8
|
-
:aria-live="isPaused ? 'polite' : 'off'"
|
|
9
|
-
tabindex="0"
|
|
10
|
-
@keydown="handleKeydown"
|
|
11
|
-
@focus="handleFocus"
|
|
12
|
-
@blur="handleBlur"
|
|
13
|
-
>
|
|
14
|
-
<!-- Screen reader instructions -->
|
|
15
|
-
<div class="sr-only">
|
|
16
|
-
{{ ariaDescription || "Use spacebar to pause/play animation, arrow keys when focused for manual control" }}
|
|
17
|
-
</div>
|
|
18
|
-
|
|
19
|
-
<!-- Pause/Play button -->
|
|
20
|
-
<button
|
|
21
|
-
v-if="showControls"
|
|
22
|
-
class="control-btn"
|
|
23
|
-
@click="togglePause"
|
|
24
|
-
:aria-label="isPaused ? 'Play animation' : 'Pause animation'"
|
|
25
|
-
type="button"
|
|
26
|
-
>
|
|
27
|
-
<span aria-hidden="true">{{ isPaused ? "▶" : "⏸" }}</span>
|
|
28
|
-
</button>
|
|
29
|
-
|
|
30
|
-
<div class="marquee-track" :aria-hidden="!isPaused">
|
|
31
|
-
<div class="marquee-group">
|
|
32
|
-
<div v-for="item in marqueeData" :key="item.id" class="item">
|
|
33
|
-
<slot :name="item.id"></slot>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
<div class="marquee-group" aria-hidden="true">
|
|
37
|
-
<div v-for="item in marqueeData" :key="`duplicate-${item.id}`" class="item">
|
|
38
|
-
<slot :name="item.id"></slot>
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
43
|
-
</template>
|
|
44
|
-
|
|
45
|
-
<script setup lang="ts">
|
|
46
|
-
const props = defineProps({
|
|
47
|
-
animationRuntime: {
|
|
48
|
-
type: String,
|
|
49
|
-
default: "40s",
|
|
50
|
-
},
|
|
51
|
-
reverse: {
|
|
52
|
-
type: Boolean,
|
|
53
|
-
default: false,
|
|
54
|
-
},
|
|
55
|
-
marqueeData: {
|
|
56
|
-
type: Array as PropType<Array<{ id: number; content: string }>>,
|
|
57
|
-
default: () => [],
|
|
58
|
-
},
|
|
59
|
-
itemConfig: {
|
|
60
|
-
type: Object,
|
|
61
|
-
default: () => ({
|
|
62
|
-
width: "50px",
|
|
63
|
-
height: "50px",
|
|
64
|
-
gap: "16px",
|
|
65
|
-
}),
|
|
66
|
-
required: true,
|
|
67
|
-
},
|
|
68
|
-
// Accessibility props
|
|
69
|
-
ariaLabel: {
|
|
70
|
-
type: String,
|
|
71
|
-
default: null,
|
|
72
|
-
},
|
|
73
|
-
ariaDescription: {
|
|
74
|
-
type: String,
|
|
75
|
-
default: null,
|
|
76
|
-
},
|
|
77
|
-
showControls: {
|
|
78
|
-
type: Boolean,
|
|
79
|
-
default: false,
|
|
80
|
-
},
|
|
81
|
-
respectReducedMotion: {
|
|
82
|
-
type: Boolean,
|
|
83
|
-
default: true,
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const displayComponent = ref(false);
|
|
88
|
-
const isPaused = ref(false);
|
|
89
|
-
const isFocused = ref(false);
|
|
90
|
-
const prefersReducedMotion = ref(false);
|
|
91
|
-
|
|
92
|
-
const height = computed(() => props.itemConfig.height);
|
|
93
|
-
const width = computed(() => props.itemConfig.width);
|
|
94
|
-
const gap = computed(() => props.itemConfig.gap || "16px");
|
|
95
|
-
|
|
96
|
-
// Check for reduced motion preference
|
|
97
|
-
const checkReducedMotion = () => {
|
|
98
|
-
if (typeof window !== "undefined" && props.respectReducedMotion) {
|
|
99
|
-
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
100
|
-
prefersReducedMotion.value = mediaQuery.matches;
|
|
101
|
-
|
|
102
|
-
// Listen for changes
|
|
103
|
-
mediaQuery.addEventListener("change", (e) => {
|
|
104
|
-
prefersReducedMotion.value = e.matches;
|
|
105
|
-
if (e.matches) {
|
|
106
|
-
isPaused.value = true;
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const togglePause = () => {
|
|
113
|
-
isPaused.value = !isPaused.value;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const handleKeydown = (event: KeyboardEvent) => {
|
|
117
|
-
switch (event.key) {
|
|
118
|
-
case " ":
|
|
119
|
-
case "Spacebar":
|
|
120
|
-
event.preventDefault();
|
|
121
|
-
togglePause();
|
|
122
|
-
break;
|
|
123
|
-
case "ArrowLeft":
|
|
124
|
-
case "ArrowRight":
|
|
125
|
-
// Could add manual stepping functionality here
|
|
126
|
-
event.preventDefault();
|
|
127
|
-
break;
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const handleFocus = () => {
|
|
132
|
-
isFocused.value = true;
|
|
133
|
-
if (props.respectReducedMotion) {
|
|
134
|
-
isPaused.value = true;
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const handleBlur = () => {
|
|
139
|
-
isFocused.value = false;
|
|
140
|
-
if (!prefersReducedMotion.value) {
|
|
141
|
-
isPaused.value = false;
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
onMounted(() => {
|
|
146
|
-
displayComponent.value = true;
|
|
147
|
-
checkReducedMotion();
|
|
148
|
-
|
|
149
|
-
// Auto-pause if user prefers reduced motion
|
|
150
|
-
if (prefersReducedMotion.value) {
|
|
151
|
-
isPaused.value = true;
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
</script>
|
|
155
|
-
|
|
156
|
-
<style lang="css">
|
|
157
|
-
@layer components {
|
|
158
|
-
.marquee-scroller {
|
|
159
|
-
width: 100%;
|
|
160
|
-
height: v-bind(height);
|
|
161
|
-
overflow: hidden;
|
|
162
|
-
mask-image: linear-gradient(to right, transparent, #000 10% 90%, transparent);
|
|
163
|
-
position: relative;
|
|
164
|
-
|
|
165
|
-
/* Focus styles */
|
|
166
|
-
&:focus {
|
|
167
|
-
outline: 2px solid var(--color-focus, #0066cc);
|
|
168
|
-
outline-offset: 2px;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/* Paused state */
|
|
172
|
-
&.paused .marquee-track {
|
|
173
|
-
animation-play-state: paused;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/* Reduced motion - disable animation completely */
|
|
177
|
-
&.reduced-motion .marquee-track {
|
|
178
|
-
animation: none !important;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
&:hover .marquee-track {
|
|
182
|
-
animation-play-state: paused;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
&:hover .item {
|
|
186
|
-
filter: grayscale(1);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/* Screen reader only text */
|
|
190
|
-
.sr-only {
|
|
191
|
-
position: absolute;
|
|
192
|
-
width: 1px;
|
|
193
|
-
height: 1px;
|
|
194
|
-
padding: 0;
|
|
195
|
-
margin: -1px;
|
|
196
|
-
overflow: hidden;
|
|
197
|
-
clip: rect(0, 0, 0, 0);
|
|
198
|
-
white-space: nowrap;
|
|
199
|
-
border: 0;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/* Control button */
|
|
203
|
-
.control-btn {
|
|
204
|
-
position: absolute;
|
|
205
|
-
inset-block-start: 8px;
|
|
206
|
-
inset-inline-end: 8px;
|
|
207
|
-
z-index: 10;
|
|
208
|
-
background: rgba(0, 0, 0, 0.7);
|
|
209
|
-
color: white;
|
|
210
|
-
border: none;
|
|
211
|
-
border-radius: 4px;
|
|
212
|
-
padding: 8px;
|
|
213
|
-
cursor: pointer;
|
|
214
|
-
font-size: 14px;
|
|
215
|
-
transition: background-color 0.2s;
|
|
216
|
-
|
|
217
|
-
&:hover {
|
|
218
|
-
background-color: rgba(0, 0, 0, 0.9);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
&:focus {
|
|
222
|
-
outline: 2px solid var(--color-focus, #0066cc);
|
|
223
|
-
outline-offset: 2px;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
.marquee-track {
|
|
228
|
-
display: flex;
|
|
229
|
-
width: fit-content;
|
|
230
|
-
gap: v-bind(gap);
|
|
231
|
-
animation: marqueeMove v-bind(animationRuntime) linear infinite;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
&.reverse .marquee-track {
|
|
235
|
-
animation-direction: reverse;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
.marquee-group {
|
|
239
|
-
display: flex;
|
|
240
|
-
gap: v-bind(gap);
|
|
241
|
-
flex-shrink: 0;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
.item {
|
|
245
|
-
width: v-bind(width);
|
|
246
|
-
height: v-bind(height);
|
|
247
|
-
display: grid;
|
|
248
|
-
place-items: center;
|
|
249
|
-
aspect-ratio: 1 / 1;
|
|
250
|
-
transition: filter 0.5s;
|
|
251
|
-
flex-shrink: 0;
|
|
252
|
-
|
|
253
|
-
border: 1px solid light-dark(var(--slate-10), var(--slate-00));
|
|
254
|
-
border-radius: 4px;
|
|
255
|
-
|
|
256
|
-
&:hover {
|
|
257
|
-
filter: grayscale(0);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
@keyframes marqueeMove {
|
|
263
|
-
from {
|
|
264
|
-
transform: translateX(0);
|
|
265
|
-
}
|
|
266
|
-
to {
|
|
267
|
-
transform: translateX(-50%);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/* High contrast mode support */
|
|
272
|
-
@media (prefers-contrast: high) {
|
|
273
|
-
.marquee-scroller {
|
|
274
|
-
.control-btn {
|
|
275
|
-
background: ButtonFace;
|
|
276
|
-
color: ButtonText;
|
|
277
|
-
border: 1px solid ButtonText;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/* Respect user's motion preferences */
|
|
283
|
-
@media (prefers-reduced-motion: reduce) {
|
|
284
|
-
.marquee-scroller .marquee-track {
|
|
285
|
-
animation: none !important;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
</style>
|