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.
Files changed (45) hide show
  1. package/.claude/commands/migrate-component.md +40 -2
  2. package/.claude/component-ledger/audit.json +1 -1
  3. package/.claude/component-ledger/output.html +1 -1
  4. package/.claude/skills/components/breadcrumb.md +1 -0
  5. package/.claude/skills/components/content-docs.md +2 -0
  6. package/.claude/skills/components/display-tooltip-defined.md +3 -0
  7. package/.claude/skills/components/display-tooltip.md +1 -0
  8. package/.claude/skills/components/marquee-scroller.md +130 -0
  9. package/.claude/skills/components/navigation-items.md +1 -0
  10. package/.claude/skills/components/responsive-header.md +3 -0
  11. package/.claude/skills/components/site-header.md +4 -0
  12. package/.claude/skills/components/site-navigation.md +1 -0
  13. package/.claude/skills/components/tab-navigation.md +1 -0
  14. package/.claude/skills/index.md +1 -0
  15. package/.vscode/srcdev-component-breadcrumb.code-snippets +13 -0
  16. package/.vscode/srcdev-component-content-docs.code-snippets +18 -0
  17. package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +15 -0
  18. package/.vscode/srcdev-component-display-tooltip.code-snippets +11 -0
  19. package/.vscode/srcdev-component-marquee-scroller.code-snippets +93 -0
  20. package/.vscode/srcdev-component-responsive-header.code-snippets +12 -0
  21. package/.vscode/srcdev-component-site-header.code-snippets +17 -0
  22. package/.vscode/srcdev-component-site-navigation.code-snippets +30 -0
  23. package/.vscode/srcdev-component-slider-gallery.code-snippets +38 -0
  24. package/.vscode/srcdev-component-tab-navigation.code-snippets +30 -0
  25. package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +94 -0
  26. package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +331 -0
  27. package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +151 -0
  28. package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +315 -0
  29. package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +28 -0
  30. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +8 -2
  31. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +4 -1
  32. package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +4 -1
  33. package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +16 -3
  34. package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +1 -1
  35. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +4 -1
  36. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +4 -1
  37. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +16 -4
  38. package/app/components/03.organisms/responsive-header/NavigationItems.vue +4 -1
  39. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +16 -3
  40. package/app/components/03.organisms/site-header/SiteHeader.vue +16 -1
  41. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
  42. package/app/types/components/index.ts +1 -0
  43. package/app/types/components/marquee-scroller.d.ts +10 -0
  44. package/package.json +1 -1
  45. 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="Main navigation"
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="Secondary navigation">
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 :main-navigation-state="mainNavigationState" :panel-variant="panelVariant" />
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="Home Navigation">
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
  `;
@@ -17,3 +17,4 @@ export * from "./content-docs.d"
17
17
  export * from "./breadcrumb"
18
18
  export * from "./cookie-consent-banner.d"
19
19
  export * from "./display-tooltip-defined.d"
20
+ export * from "./marquee-scroller.d"
@@ -0,0 +1,10 @@
1
+ export interface MarqueeItem {
2
+ id: number
3
+ content: string
4
+ }
5
+
6
+ export interface MarqueeItemConfig {
7
+ width?: string
8
+ height?: string
9
+ gap?: string
10
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.4.5",
4
+ "version": "9.4.6",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",
@@ -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>