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.
Files changed (34) hide show
  1. package/.claude/settings.json +8 -27
  2. package/.claude/settings.local.json +24 -52
  3. package/.claude/skills/components/action-menu.md +141 -0
  4. package/.claude/skills/components/stepper-list.md +52 -18
  5. package/.claude/skills/css-nesting-conventions.md +121 -0
  6. package/.claude/skills/index.md +3 -0
  7. package/.claude/skills/pull-request-description.md +48 -0
  8. package/.claude/skills/testing-add-unit-test.md +40 -2
  9. package/README.md +26 -5
  10. package/app/components/02.molecules/action-menu/ActionMenu.vue +218 -0
  11. package/app/components/02.molecules/action-menu/ActionMenuItemCore.vue +123 -0
  12. package/app/components/02.molecules/action-menu/CONSUMER-STYLING.md +125 -0
  13. package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +326 -0
  14. package/app/components/02.molecules/action-menu/tests/ActionMenu.spec.ts +458 -0
  15. package/app/components/02.molecules/action-menu/tests/ActionMenuItemCore.spec.ts +199 -0
  16. package/app/components/02.molecules/action-menu/tests/__snapshots__/ActionMenu.spec.ts.snap +28 -0
  17. package/app/components/02.molecules/action-menu/tests/__snapshots__/ActionMenuItemCore.spec.ts.snap +27 -0
  18. package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +5 -1
  19. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +2 -1
  20. package/app/components/02.molecules/stepper-list/CONSUMER-STYLING.md +138 -0
  21. package/app/components/02.molecules/stepper-list/StepperList.vue +50 -24
  22. package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +5 -1
  23. package/app/components/05.forms/input-button/InputButtonCore.vue +1 -1
  24. package/app/components/carousels/tests/CarouselFlip.spec.ts +1 -0
  25. package/app/components/responsive-header/NavigationItems.vue +302 -81
  26. package/app/components/responsive-header/ResponsiveHeader.vue +360 -56
  27. package/app/layouts/default.vue +43 -352
  28. package/app/layouts/site-navigation-demo.vue +36 -34
  29. package/app/pages/page-hero-highlights.vue +3 -3
  30. package/app/pages/ui/scroll-reveal-image.vue +3 -3
  31. package/app/pages/ui/simple-grid.vue +9 -20
  32. package/modules/colour-scheme.ts +1 -1
  33. package/nuxt.config.ts +11 -2
  34. package/package.json +17 -16
@@ -1,5 +1,14 @@
1
1
  <template>
2
- <div class="overflow-navigation-wrapper" :class="elementClasses" role="menu" aria-label="Overflow navigation menu">
2
+ <div
3
+ class="overflow-navigation-wrapper"
4
+ :class="[elementClasses, { 'is-panel-animating': isPanelAnimating }]"
5
+ role="menu"
6
+ aria-label="Overflow navigation menu"
7
+ @mouseleave="
8
+ hoveredItemKey = null;
9
+ hoveredChildKey = null;
10
+ "
11
+ >
3
12
  <ul
4
13
  v-for="(navGroup, groupKey) in mainNavigationState.clonedNavLinks"
5
14
  :key="groupKey"
@@ -12,7 +21,11 @@
12
21
  <li
13
22
  v-if="link.path"
14
23
  class="overflow-navigation-item"
15
- :class="{ visible: !mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]?.config?.visible }"
24
+ :class="{
25
+ visible: !mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]?.config?.visible,
26
+ 'is-hovered': hoveredItemKey === `${String(groupKey)}-${localIndex}`,
27
+ 'is-active': isActiveNavItem(link),
28
+ }"
16
29
  :style="{
17
30
  '--_main-navigation-item-width':
18
31
  mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]?.config?.width + 'px',
@@ -20,6 +33,7 @@
20
33
  :data-group-key="groupKey"
21
34
  :data-local-index="localIndex"
22
35
  role="none"
36
+ @mouseenter="hoveredItemKey = `${String(groupKey)}-${localIndex}`"
23
37
  >
24
38
  <NuxtLink class="overflow-navigation-link" :to="link.path" role="menuitem">
25
39
  <span class="overflow-navigation-text">{{ link.name }}</span>
@@ -28,7 +42,11 @@
28
42
  <li
29
43
  v-else
30
44
  class="overflow-navigation-item"
31
- :class="{ visible: !mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]?.config?.visible }"
45
+ :class="{
46
+ visible: !mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]?.config?.visible,
47
+ 'is-hovered': hoveredItemKey === `${String(groupKey)}-${localIndex}`,
48
+ 'is-active': isActiveNavItem(link),
49
+ }"
32
50
  :style="{
33
51
  '--_main-navigation-item-width':
34
52
  mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]?.config?.width + 'px',
@@ -36,8 +54,10 @@
36
54
  :data-group-key="groupKey"
37
55
  :data-local-index="localIndex"
38
56
  role="none"
57
+ @mouseenter="hoveredItemKey = `${String(groupKey)}-${localIndex}`"
39
58
  >
40
59
  <ExpandingPanel
60
+ v-model="panelOpenStates[`${String(groupKey)}-${localIndex}`]"
41
61
  name="overflow-navigation-group"
42
62
  :animation-duration="DETAILS_ANIMATION_DURATION"
43
63
  icon-size="medium"
@@ -58,28 +78,36 @@
58
78
  </template>
59
79
  <template #content>
60
80
  <div class="overflow-navigation-sub-nav-inner">
61
- <ul class="overflow-navigation-sub-nav-list">
81
+ <ul class="overflow-navigation-sub-nav-list" @mouseleave="hoveredChildKey = null">
62
82
  <li
63
- v-for="childLink in link.childLinks"
83
+ v-for="(childLink, childIndex) in link.childLinks"
64
84
  :key="childLink.name"
65
85
  class="overflow-navigation-sub-nav-item"
86
+ :class="{
87
+ 'is-hovered': hoveredChildKey === `${String(groupKey)}-${localIndex}-${childIndex}`,
88
+ 'is-active': isActiveNavItem(childLink),
89
+ }"
90
+ @mouseenter="hoveredChildKey = `${String(groupKey)}-${localIndex}-${childIndex}`"
66
91
  >
67
92
  <NuxtLink :to="childLink.path" class="overflow-navigation-sub-nav-link" role="menuitem">
68
93
  <span class="overflow-navigation-sub-nav-text">{{ childLink.name }}</span>
69
94
  </NuxtLink>
70
95
  </li>
71
96
  </ul>
97
+ <div aria-hidden="true" class="overflow-sub-nav-indicator-hovered"></div>
72
98
  </div>
73
99
  </template>
74
100
  </ExpandingPanel>
75
101
  </li>
76
102
  </template>
77
103
  </ul>
104
+ <div aria-hidden="true" class="overflow-nav-indicator-hovered"></div>
105
+ <div aria-hidden="true" class="overflow-nav-indicator-active"></div>
78
106
  </div>
79
107
  </template>
80
108
 
81
109
  <script setup lang="ts">
82
- import type { ResponsiveHeaderState } from "../../types/components";
110
+ import type { ResponsiveHeaderState, ResponsiveHeaderNavItem } from "../../types/components";
83
111
 
84
112
  interface Props {
85
113
  mainNavigationState?: ResponsiveHeaderState;
@@ -91,8 +119,38 @@ const props = withDefaults(defineProps<Props>(), {
91
119
  styleClassPassthrough: () => [],
92
120
  });
93
121
 
94
- // Performance: Use const assertion for static values
122
+ const hoveredItemKey = ref<string | null>(null);
123
+ const hoveredChildKey = ref<string | null>(null);
124
+
95
125
  const DETAILS_ANIMATION_DURATION = 200 as const;
126
+
127
+ const panelOpenStates = reactive<Record<string, boolean>>({});
128
+ const isPanelAnimating = ref(false);
129
+ let panelAnimationTimer: ReturnType<typeof setTimeout> | null = null;
130
+
131
+ watch(
132
+ panelOpenStates,
133
+ () => {
134
+ isPanelAnimating.value = true;
135
+ if (panelAnimationTimer) clearTimeout(panelAnimationTimer);
136
+ panelAnimationTimer = setTimeout(() => {
137
+ isPanelAnimating.value = false;
138
+ }, DETAILS_ANIMATION_DURATION);
139
+ },
140
+ { deep: true }
141
+ );
142
+
143
+ onUnmounted(() => {
144
+ if (panelAnimationTimer) clearTimeout(panelAnimationTimer);
145
+ });
146
+
147
+ const route = useRoute();
148
+
149
+ const isActiveNavItem = (link: ResponsiveHeaderNavItem): boolean => {
150
+ if (link.path) return route.path === link.path;
151
+ if (link.childLinks) return link.childLinks.some((child) => child.path && route.path === child.path);
152
+ return false;
153
+ };
96
154
  // const DETAILS_ANIMATION_DURATION_STRING = `${DETAILS_ANIMATION_DURATION}ms` as const;
97
155
 
98
156
  // Performance: Memoize expensive computed with proper dependencies
@@ -122,100 +180,120 @@ watch(
122
180
 
123
181
  <style lang="css">
124
182
  @layer components {
125
- .overflow-navigation-wrapper {
126
- --overflow-nav-padding-inline: 0.8rem;
127
- --overflow-nav-items-gap: 0px;
128
- --overflow-nav-items-padding-block: 0.8rem;
129
- display: flex;
130
- flex-direction: column;
131
- gap: var(--overflow-nav-items-gap);
132
-
133
- .overflow-navigation-list {
134
- display: none;
183
+ .overflow-navigation-wrapper {
184
+ /* ─── Public CSS tokens ─────────────────────────────────────────────────
185
+ Override on the consumer's scope class to theme the overflow nav.
186
+ Tokens are read via var(--token, default) — defaults are NOT declared
187
+ on this element to avoid cascade conflicts with ancestor overrides.
135
188
 
136
- &.visible {
137
- display: flex;
138
- flex-direction: column;
139
- gap: var(--overflow-nav-items-gap);
140
- min-width: var(--_overflow-navigation-list-min-width, auto);
141
- }
189
+ --overflow-nav-padding-inline (default: 0.8rem)
190
+ --overflow-nav-items-gap (default: 0px)
191
+ --overflow-nav-items-padding-block (default: 0.8rem)
192
+
193
+ --overflow-nav-link-color (default: inherit)
194
+ --overflow-nav-link-border-color (default: #efefef75)
195
+ --overflow-nav-sub-item-color (default: inherit)
196
+ --overflow-nav-sub-item-font-size (default: inherit)
142
197
 
143
- .overflow-navigation-item {
198
+ --overflow-nav-decorator-indicator-color (default: currentColor)
199
+ --overflow-nav-decorator-hovered-indicator-color (default: inherits --overflow-nav-decorator-indicator-color)
200
+ --overflow-nav-decorator-hovered-bg (default: oklch(100% 0 0 / 6%))
201
+ ──────────────────────────────────────────────────────────────────────── */
202
+
203
+ display: flex;
204
+ flex-direction: column;
205
+ gap: var(--overflow-nav-items-gap, 0px);
206
+ position: relative;
207
+
208
+ .overflow-navigation-list {
144
209
  display: none;
145
210
 
146
211
  &.visible {
147
- display: block;
148
- }
149
-
150
- .overflow-navigation-link {
151
- text-decoration: none;
152
- color: inherit;
153
- padding-block: var(--overflow-nav-items-padding-block);
154
- padding-inline: var(--overflow-nav-padding-inline);
155
212
  display: flex;
156
- /* background-color: red; */
157
- border-bottom: 0.1rem solid #efefef75;
213
+ flex-direction: column;
214
+ gap: var(--overflow-nav-items-gap, 0px);
215
+ min-width: var(--_overflow-navigation-list-min-width, auto);
158
216
  }
159
217
 
160
- .overflow-navigation-details {
161
- &.expanding-panel {
162
- margin-block-end: 0;
163
-
164
- .expanding-panel-details {
165
- .expanding-panel-summary {
166
- padding-block: var(--overflow-nav-items-padding-block);
167
- padding-inline: var(--overflow-nav-padding-inline);
168
- gap: 1rem;
169
- /* background-color: red; */
170
- border-bottom: 0.1rem solid #efefef75;
171
-
172
- .label-wrapper {
173
- .overflow-navigation-text {
174
- text-wrap: nowrap;
175
- }
176
- }
177
- .icon-wrapper {
178
- padding: 0;
179
- }
180
- }
218
+ .overflow-navigation-item {
219
+ display: none;
220
+
221
+ &.visible {
222
+ display: block;
223
+ }
224
+
225
+ .overflow-navigation-link {
226
+ text-decoration: none;
227
+ color: var(--overflow-nav-link-color, inherit);
228
+ padding-block: var(--overflow-nav-items-padding-block, 0.8rem);
229
+ padding-inline: var(--overflow-nav-padding-inline, 0.8rem);
230
+ display: flex;
231
+ border-bottom: 0.1rem solid var(--overflow-nav-link-border-color, #efefef75);
232
+ }
233
+
234
+ .overflow-navigation-details {
235
+ &.expanding-panel {
236
+ margin-block-end: 0;
181
237
 
182
- &[open] {
238
+ .expanding-panel-details {
183
239
  .expanding-panel-summary {
184
- border-bottom: 0.1rem solid transparent;
240
+ padding-block: var(--overflow-nav-items-padding-block, 0.8rem);
241
+ padding-inline: var(--overflow-nav-padding-inline, 0.8rem);
242
+ gap: 1rem;
243
+ color: var(--overflow-nav-link-color, inherit);
244
+ border-bottom: 0.1rem solid var(--overflow-nav-link-border-color, #efefef75);
245
+
246
+ .label-wrapper {
247
+ .overflow-navigation-text {
248
+ text-wrap: nowrap;
249
+ }
250
+ }
251
+ .icon-wrapper {
252
+ padding: 0;
253
+ }
185
254
  }
186
- + .expanding-panel-content {
187
- border-bottom: 0.1rem solid #efefef75;
188
- .inner {
189
- .overflow-navigation-sub-nav-inner {
190
- margin-top: var(--overflow-nav-items-gap);
255
+
256
+ &[open] {
257
+ .expanding-panel-summary {
258
+ border-bottom: 0.1rem solid transparent;
259
+ }
260
+ + .expanding-panel-content {
261
+ border-bottom: 0.1rem solid var(--overflow-nav-link-border-color, #efefef75);
262
+ .inner {
263
+ .overflow-navigation-sub-nav-inner {
264
+ margin-top: var(--overflow-nav-items-gap, 0px);
265
+ }
191
266
  }
192
267
  }
193
268
  }
194
269
  }
195
- }
196
-
197
- .expanding-panel-content {
198
- border-bottom: 0.1rem solid transparent;
199
270
 
200
- .inner {
201
- margin-top: 0;
271
+ .expanding-panel-content {
272
+ border-bottom: 0.1rem solid transparent;
202
273
 
203
- .overflow-navigation-sub-nav-inner {
274
+ .inner {
204
275
  margin-top: 0;
205
276
 
206
- .overflow-navigation-sub-nav-list {
207
- display: flex;
208
- flex-direction: column;
209
- gap: 2px;
277
+ .overflow-navigation-sub-nav-inner {
278
+ margin-top: 0;
279
+ position: relative;
210
280
 
211
- .overflow-navigation-sub-nav-item {
212
- padding-block: var(--overflow-nav-items-padding-block);
213
- padding-inline: var(--overflow-nav-padding-inline);
281
+ .overflow-navigation-sub-nav-list {
282
+ display: flex;
283
+ flex-direction: column;
284
+ gap: 2px;
214
285
 
215
- .overflow-navigation-sub-nav-link {
216
- display: block;
217
- text-decoration: none;
218
- color: inherit;
286
+ .overflow-navigation-sub-nav-item {
287
+ padding-block: var(--overflow-nav-items-padding-block, 0.8rem);
288
+ padding-inline: var(--overflow-nav-padding-inline, 0.8rem);
289
+ font-size: var(--overflow-nav-sub-item-font-size, inherit);
290
+ color: var(--overflow-nav-sub-item-color, inherit);
291
+
292
+ .overflow-navigation-sub-nav-link {
293
+ display: block;
294
+ text-decoration: none;
295
+ color: inherit;
296
+ }
219
297
  }
220
298
  }
221
299
  }
@@ -226,6 +304,149 @@ watch(
226
304
  }
227
305
  }
228
306
  }
229
- }
307
+
308
+ /* Freeze all indicator transitions for the duration of a panel open/close animation
309
+ so the indicators snap to position rather than sliding through intermediate states. */
310
+ .overflow-navigation-wrapper.is-panel-animating .overflow-nav-indicator-hovered,
311
+ .overflow-navigation-wrapper.is-panel-animating .overflow-nav-indicator-active,
312
+ .overflow-navigation-wrapper.is-panel-animating .overflow-sub-nav-indicator-hovered {
313
+ transition: none;
314
+ }
315
+
316
+ /* ─── Anchor positioning for overflow-nav indicators ─────────────────────
317
+ Single --overflow-nav-indicator: sits on is-hovered, falls back to
318
+ is-active. Vertical list so indicators slide up/down rather than left/right.
319
+ ──────────────────────────────────────────────────────────────────────── */
320
+
321
+ /* Anchor to the inner row element, not the <li>, so an open submenu panel
322
+ doesn't stretch the indicator down into the expanded content. */
323
+
324
+ /* Plain link — hovered */
325
+ .overflow-navigation-wrapper
326
+ .overflow-navigation-item.is-hovered:not(:has(.overflow-navigation-details))
327
+ .overflow-navigation-link {
328
+ anchor-name: --overflow-nav-indicator;
329
+ }
330
+
331
+ /* Submenu — hovered: anchor to summary row only */
332
+ .overflow-navigation-wrapper
333
+ .overflow-navigation-item.is-hovered
334
+ .overflow-navigation-details
335
+ .expanding-panel-summary {
336
+ anchor-name: --overflow-nav-indicator;
337
+ }
338
+
339
+ /* Plain link — active (no hover) */
340
+ .overflow-navigation-wrapper:not(:has(.overflow-navigation-item.is-hovered))
341
+ .overflow-navigation-item.is-active:not(:has(.overflow-navigation-details))
342
+ .overflow-navigation-link {
343
+ anchor-name: --overflow-nav-indicator;
344
+ }
345
+
346
+ /* Submenu — active (no hover): anchor to summary row only */
347
+ .overflow-navigation-wrapper:not(:has(.overflow-navigation-item.is-hovered))
348
+ .overflow-navigation-item.is-active
349
+ .overflow-navigation-details
350
+ .expanding-panel-summary {
351
+ anchor-name: --overflow-nav-indicator;
352
+ }
353
+
354
+ .overflow-navigation-wrapper .overflow-nav-indicator-hovered,
355
+ .overflow-navigation-wrapper .overflow-nav-indicator-active {
356
+ display: none;
357
+ pointer-events: none;
358
+ }
359
+
360
+ .overflow-navigation-wrapper .overflow-nav-indicator-hovered {
361
+ display: block;
362
+ position: absolute;
363
+ position-anchor: --overflow-nav-indicator;
364
+ left: 0;
365
+ right: 0;
366
+ top: anchor(top);
367
+ bottom: anchor(bottom);
368
+ background: var(--overflow-nav-decorator-hovered-bg, oklch(100% 0 0 / 6%));
369
+ z-index: 1;
370
+ opacity: 0;
371
+ transition:
372
+ top 200ms ease-in-out,
373
+ bottom 200ms ease-in-out,
374
+ opacity 150ms ease;
375
+ }
376
+
377
+ .overflow-navigation-wrapper:has(.overflow-navigation-item.is-hovered) .overflow-nav-indicator-hovered {
378
+ opacity: 1;
379
+ }
380
+
381
+ .overflow-navigation-wrapper .overflow-nav-indicator-active {
382
+ display: block;
383
+ position: absolute;
384
+ position-anchor: --overflow-nav-indicator;
385
+ left: 0;
386
+ width: 2px;
387
+ top: anchor(top);
388
+ bottom: anchor(bottom);
389
+ background: var(--overflow-nav-decorator-indicator-color, currentColor);
390
+ z-index: 3;
391
+ transition:
392
+ top 200ms ease-in-out,
393
+ bottom 200ms ease-in-out;
394
+ }
395
+
396
+ /* When something is hovered the active bar tracks the hovered item — use hover colour. */
397
+ .overflow-navigation-wrapper:has(.overflow-navigation-item.is-hovered) .overflow-nav-indicator-active {
398
+ background: var(
399
+ --overflow-nav-decorator-hovered-indicator-color,
400
+ var(--overflow-nav-decorator-indicator-color, currentColor)
401
+ );
402
+ }
403
+
404
+ /* ─── Anchor positioning for sub-nav child indicators ────────────────────
405
+ Separate --overflow-sub-nav-indicator scoped to the inner wrapper so
406
+ parent and child indicators are fully independent.
407
+ ──────────────────────────────────────────────────────────────────────── */
408
+
409
+ .overflow-navigation-sub-nav-item.is-hovered {
410
+ anchor-name: --overflow-sub-nav-indicator;
411
+ }
412
+
413
+ .overflow-navigation-sub-nav-list:not(:has(.overflow-navigation-sub-nav-item.is-hovered))
414
+ .overflow-navigation-sub-nav-item.is-active {
415
+ anchor-name: --overflow-sub-nav-indicator;
416
+ }
417
+
418
+ .overflow-navigation-sub-nav-item.is-active {
419
+ border-inline-start: 2px solid var(--overflow-nav-decorator-indicator-color, currentColor);
420
+ }
421
+
422
+ .overflow-sub-nav-indicator-hovered {
423
+ /* pointer-events: none is critical — without it the indicator (z-index:1) sits
424
+ over the list items and swallows mouse events, clearing hoveredChildKey and
425
+ causing a flicker loop. */
426
+ pointer-events: none;
427
+ display: block;
428
+ position: absolute;
429
+ position-anchor: --overflow-sub-nav-indicator;
430
+ left: 0;
431
+ right: 0;
432
+ top: anchor(top);
433
+ bottom: anchor(bottom);
434
+ background: var(--overflow-nav-decorator-hovered-bg, oklch(100% 0 0 / 6%));
435
+ border-inline-start: 2px solid var(
436
+ --overflow-nav-decorator-hovered-indicator-color,
437
+ var(--overflow-nav-decorator-indicator-color, currentColor)
438
+ );
439
+ z-index: 1;
440
+ opacity: 0;
441
+ transition:
442
+ top 200ms ease-in-out,
443
+ bottom 200ms ease-in-out,
444
+ opacity 150ms ease;
445
+ }
446
+
447
+ .overflow-navigation-sub-nav-inner:has(.overflow-navigation-sub-nav-item.is-hovered)
448
+ .overflow-sub-nav-indicator-hovered {
449
+ opacity: 1;
450
+ }
230
451
  }
231
452
  </style>