srcdev-nuxt-components 9.4.5 → 9.4.7

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 (104) hide show
  1. package/.claude/commands/migrate-component.md +74 -5
  2. package/.claude/component-ledger/audit.json +1 -1
  3. package/.claude/component-ledger/build.mjs +27 -0
  4. package/.claude/component-ledger/output.html +8 -3
  5. package/.claude/component-ledger/template.html +7 -2
  6. package/.claude/skills/components/breadcrumb.md +1 -0
  7. package/.claude/skills/components/column-flow-grid.md +93 -0
  8. package/.claude/skills/components/content-docs.md +2 -0
  9. package/.claude/skills/components/display-tooltip-defined.md +3 -0
  10. package/.claude/skills/components/display-tooltip.md +1 -0
  11. package/.claude/skills/components/entry-animation.md +88 -0
  12. package/.claude/skills/components/input-copy.md +2 -0
  13. package/.claude/skills/components/input-text-core.md +186 -0
  14. package/.claude/skills/components/marquee-scroller.md +130 -0
  15. package/.claude/skills/components/masonry-grid.md +153 -0
  16. package/.claude/skills/components/navigation-items.md +1 -0
  17. package/.claude/skills/components/pop-over.md +100 -0
  18. package/.claude/skills/components/responsive-header.md +3 -0
  19. package/.claude/skills/components/site-header.md +4 -0
  20. package/.claude/skills/components/site-navigation.md +1 -0
  21. package/.claude/skills/components/tab-navigation.md +1 -0
  22. package/.claude/skills/index.md +7 -1
  23. package/.vscode/srcdev-component-breadcrumb.code-snippets +13 -0
  24. package/.vscode/srcdev-component-column-flow-grid.code-snippets +39 -0
  25. package/.vscode/srcdev-component-content-docs.code-snippets +18 -0
  26. package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +15 -0
  27. package/.vscode/srcdev-component-display-tooltip.code-snippets +11 -0
  28. package/.vscode/srcdev-component-entry-animation.code-snippets +29 -0
  29. package/.vscode/srcdev-component-input-text.code-snippets +107 -0
  30. package/.vscode/srcdev-component-marquee-scroller.code-snippets +93 -0
  31. package/.vscode/srcdev-component-masonry-grid.code-snippets +51 -0
  32. package/.vscode/srcdev-component-pop-over.code-snippets +44 -0
  33. package/.vscode/srcdev-component-responsive-header.code-snippets +12 -0
  34. package/.vscode/srcdev-component-site-header.code-snippets +17 -0
  35. package/.vscode/srcdev-component-site-navigation.code-snippets +30 -0
  36. package/.vscode/srcdev-component-slider-gallery.code-snippets +38 -0
  37. package/.vscode/srcdev-component-tab-navigation.code-snippets +30 -0
  38. package/app/components/01.atoms/animations/entry/EntryAnimation.vue +7 -1
  39. package/app/components/01.atoms/animations/entry/stories/EntryAnimation.stories.ts +47 -0
  40. package/app/components/01.atoms/animations/entry/tests/EntryAnimation.spec.ts +57 -0
  41. package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +94 -0
  42. package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +331 -0
  43. package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +151 -0
  44. package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +315 -0
  45. package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +28 -0
  46. package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +9 -10
  47. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +8 -2
  48. package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +12 -13
  49. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +4 -1
  50. package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +33 -0
  51. package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +55 -0
  52. package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +178 -0
  53. package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +75 -0
  54. package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +35 -0
  55. package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +178 -0
  56. package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +199 -0
  57. package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +158 -0
  58. package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +4 -1
  59. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +54 -0
  60. package/app/components/01.atoms/pop-over/PopOver.vue +201 -0
  61. package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +141 -0
  62. package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +195 -0
  63. package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +11 -0
  64. package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +8 -2
  65. package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +16 -3
  66. package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +1 -1
  67. package/app/components/02.molecules/input-copy/InputCopy.vue +14 -0
  68. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +15 -0
  69. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +41 -0
  70. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +4 -1
  71. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +4 -1
  72. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +16 -4
  73. package/app/components/03.organisms/responsive-header/NavigationItems.vue +4 -1
  74. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +16 -3
  75. package/app/components/03.organisms/site-header/SiteHeader.vue +16 -1
  76. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
  77. package/app/components/05.forms/input-select/InputSelectCore.vue +1 -1
  78. package/app/components/05.forms/input-text/InputTextCore.vue +6 -0
  79. package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +49 -20
  80. package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +41 -22
  81. package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +48 -22
  82. package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +43 -17
  83. package/app/components/05.forms/input-text/tests/InputPasswordWithLabel.spec.ts +56 -0
  84. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +61 -0
  85. package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +60 -0
  86. package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +46 -0
  87. package/app/components/05.forms/input-text/variants/InputPasswordWithLabel.vue +7 -1
  88. package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +8 -2
  89. package/app/components/05.forms/input-text/variants/InputTextWithLabel.vue +6 -0
  90. package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +20 -20
  91. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +32 -26
  92. package/app/types/components/index.ts +1 -0
  93. package/app/types/components/marquee-scroller.d.ts +10 -0
  94. package/app/types/forms/types.forms.d.ts +1 -1
  95. package/package.json +1 -1
  96. package/app/components/marquee-scroller/MarqueeScroller.vue +0 -289
  97. package/app/components/masonry-grid/MasonryGrid.vue +0 -68
  98. package/app/components/masonry-grid-ordered/MasonryGridOrdered.vue +0 -163
  99. package/app/components/masonry-grid-ordered/MasonryGridOrderedGridExperiment.vue +0 -259
  100. package/app/components/masonry-grid-ordered/stories/MasonryGridOrdered.stories.ts +0 -354
  101. package/app/components/masonry-grid-sorted/MasonryGridSorted.vue +0 -120
  102. package/app/components/pop-over/PopOver.vue +0 -90
  103. package/app/layouts/default.vue +0 -308
  104. package/app/layouts/site-navigation-demo.vue +0 -188
@@ -0,0 +1,93 @@
1
+ {
2
+ "SRCDEV MarqueeScroller Basic": {
3
+ "description": "MarqueeScroller — infinite horizontal logo/badge scroller with per-item slots",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<MarqueeScroller",
7
+ " :marquee-data=\"[",
8
+ " { id: 1, content: '$1logo-a' },",
9
+ " { id: 2, content: '$2logo-b' },",
10
+ " ]\"",
11
+ " :item-config=\"{ width: '${3:120px}', height: '${4:60px}', gap: '${5:24px}' }\"",
12
+ ">",
13
+ " <template #1>",
14
+ " $6",
15
+ " </template>",
16
+ " <template #2>",
17
+ " $7",
18
+ " </template>",
19
+ "</MarqueeScroller>"
20
+ ]
21
+ },
22
+ "SRCDEV MarqueeScroller With Controls": {
23
+ "description": "MarqueeScroller with the visible pause/play control button (WCAG 2.2.2)",
24
+ "scope": "vue,html",
25
+ "body": [
26
+ "<MarqueeScroller",
27
+ " :marquee-data=\"$1items\"",
28
+ " show-controls",
29
+ " aria-label=\"$2Scrolling client logos\"",
30
+ ">",
31
+ " <template v-for=\"item in $1items\" #[item.id] :key=\"item.id\">",
32
+ " $3",
33
+ " </template>",
34
+ "</MarqueeScroller>"
35
+ ]
36
+ },
37
+ "SRCDEV MarqueeScroller Custom Control Icon/Copy": {
38
+ "description": "MarqueeScroller control button with custom icons and localised aria-labels",
39
+ "scope": "vue,html",
40
+ "body": [
41
+ "<MarqueeScroller",
42
+ " :marquee-data=\"$1items\"",
43
+ " show-controls",
44
+ " play-icon=\"${2:mdi:play-circle}\"",
45
+ " pause-icon=\"${3:mdi:pause-circle}\"",
46
+ " play-label=\"${4:Play}\"",
47
+ " pause-label=\"${5:Pause}\"",
48
+ ">",
49
+ " <template v-for=\"item in $1items\" #[item.id] :key=\"item.id\">",
50
+ " $6",
51
+ " </template>",
52
+ "</MarqueeScroller>"
53
+ ]
54
+ },
55
+ "SRCDEV MarqueeScroller Custom Toggle Icon Slot": {
56
+ "description": "MarqueeScroller with the toggle-icon slot fully replacing the control button's icon",
57
+ "scope": "vue,html",
58
+ "body": [
59
+ "<MarqueeScroller :marquee-data=\"$1items\" show-controls>",
60
+ " <template #toggle-icon=\"{ isPaused }\">",
61
+ " $2",
62
+ " </template>",
63
+ "</MarqueeScroller>"
64
+ ]
65
+ },
66
+ "SRCDEV MarqueeScroller CSS Override": {
67
+ "description": "CSS override scaffold for MarqueeScroller — scope to a page or section class",
68
+ "scope": "css",
69
+ "body": [
70
+ ".$1my-page {",
71
+ " .marquee-scroller {",
72
+ " --marquee-scroller-fade-width: ;",
73
+ " --marquee-scroller-focus-outline-width: ;",
74
+ " --marquee-scroller-focus-outline-offset: ;",
75
+ " --marquee-scroller-item-border-colour: ;",
76
+ " --marquee-scroller-item-border-width: ;",
77
+ " --marquee-scroller-item-border-radius: ;",
78
+ " --marquee-scroller-item-transition-duration: ;",
79
+ " --marquee-scroller-group-hover-filter: ;",
80
+ " --marquee-scroller-item-hover-filter: ;",
81
+ " --marquee-scroller-control-offset: ;",
82
+ " --marquee-scroller-control-background-colour: ;",
83
+ " --marquee-scroller-control-background-colour-hover: ;",
84
+ " --marquee-scroller-control-text-colour: ;",
85
+ " --marquee-scroller-control-border-radius: ;",
86
+ " --marquee-scroller-control-border-width: ;",
87
+ " --marquee-scroller-control-padding: ;",
88
+ " --marquee-scroller-control-font-size: ;",
89
+ " }",
90
+ "}"
91
+ ]
92
+ }
93
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "SRCDEV MasonryGrid Basic": {
3
+ "description": "Basic MasonryGrid with a few named item slots",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<MasonryGrid :item-min-width=\"$1300\" :gap=\"$212\">",
7
+ " <template #item-1>",
8
+ " $3",
9
+ " </template>",
10
+ " <template #item-2>",
11
+ " $4",
12
+ " </template>",
13
+ "</MasonryGrid>"
14
+ ]
15
+ },
16
+ "SRCDEV MasonryGrid Looped": {
17
+ "description": "MasonryGrid rendering a v-for list via dynamic slot names (no count prop needed)",
18
+ "scope": "vue,html",
19
+ "body": [
20
+ "<MasonryGrid :item-min-width=\"$1280\" :gap=\"$216\">",
21
+ " <template v-for=\"($3item, $4index) in $5items\" :key=\"$3item.id\" #[`item-${$4index}`]>",
22
+ " $6",
23
+ " </template>",
24
+ "</MasonryGrid>"
25
+ ]
26
+ },
27
+ "SRCDEV MasonryGrid Fixed Width Centered": {
28
+ "description": "MasonryGrid with fixed-width tiles centered in the wrapper",
29
+ "scope": "vue,html",
30
+ "body": [
31
+ "<MasonryGrid :fixed-width=\"true\" justify=\"center\" :item-min-width=\"$1240\" :gap=\"$212\">",
32
+ " <template v-for=\"($3item, $4index) in $5items\" :key=\"$3item.id\" #[`item-${$4index}`]>",
33
+ " $6",
34
+ " </template>",
35
+ "</MasonryGrid>"
36
+ ]
37
+ },
38
+ "SRCDEV MasonryGrid CSS Override": {
39
+ "description": "CSS override scaffold for MasonryGrid tokens",
40
+ "scope": "css",
41
+ "body": [
42
+ ".$1my-page {",
43
+ " .masonry-grid-wrapper {",
44
+ " --masonry-grid-item-border-colour: $2;",
45
+ " --masonry-grid-item-padding: $3;",
46
+ " --masonry-grid-transition-duration: $4;",
47
+ " }",
48
+ "}"
49
+ ]
50
+ }
51
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "SRCDEV PopOver Basic": {
3
+ "description": "PopOver — generic anchor-positioned disclosure panel",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<PopOver>",
7
+ " <template #trigger>",
8
+ " <span>$1</span>",
9
+ " </template>",
10
+ " <template #content>",
11
+ " <p>$0</p>",
12
+ " </template>",
13
+ "</PopOver>"
14
+ ]
15
+ },
16
+ "SRCDEV PopOver Placement": {
17
+ "description": "PopOver with an explicit placement",
18
+ "scope": "vue,html",
19
+ "body": [
20
+ "<PopOver placement=\"${1|top,right,bottom,left|}\">",
21
+ " <template #trigger>",
22
+ " <span>$2</span>",
23
+ " </template>",
24
+ " <template #content>",
25
+ " <p>$0</p>",
26
+ " </template>",
27
+ "</PopOver>"
28
+ ]
29
+ },
30
+ "SRCDEV PopOver Icon Trigger": {
31
+ "description": "PopOver with an icon-only trigger and localised labels",
32
+ "scope": "vue,html",
33
+ "body": [
34
+ "<PopOver trigger-aria-label=\"${1:Show more}\" popover-aria-label=\"${2:More information}\">",
35
+ " <template #trigger>",
36
+ " <Icon name=\"${3:lucide:info}\" aria-hidden=\"true\" />",
37
+ " </template>",
38
+ " <template #content>",
39
+ " <p>$0</p>",
40
+ " </template>",
41
+ "</PopOver>"
42
+ ]
43
+ }
44
+ }
@@ -15,6 +15,18 @@
15
15
  "</ResponsiveHeader>"
16
16
  ]
17
17
  },
18
+ "SRCDEV ResponsiveHeader Localised Nav Labels": {
19
+ "description": "ResponsiveHeader with localised aria-labels on the main/secondary nav landmarks and overflow menu",
20
+ "scope": "vue,html",
21
+ "body": [
22
+ "<ResponsiveHeader",
23
+ " :responsive-nav-links=\"$1responsiveNavLinks\"",
24
+ " main-nav-aria-label=\"${2:Main navigation}\"",
25
+ " secondary-nav-aria-label=\"${3:Secondary navigation}\"",
26
+ " overflow-menu-aria-label=\"${4:Overflow navigation menu}\"",
27
+ "/>"
28
+ ]
29
+ },
18
30
  "SRCDEV ResponsiveHeader Nav Data (with dropdown)": {
19
31
  "description": "responsiveNavLinks data with a dropdown group",
20
32
  "scope": "typescript,vue",
@@ -30,6 +30,23 @@
30
30
  "</SiteHeader>"
31
31
  ]
32
32
  },
33
+ "SRCDEV SiteHeader Localised Nav Labels": {
34
+ "description": "SiteHeader with localised aria-labels on all its nav landmarks (home, main, secondary, overflow)",
35
+ "scope": "vue,html",
36
+ "body": [
37
+ "<SiteHeader",
38
+ " :responsive-nav-links=\"$1responsiveNavLinks\"",
39
+ " home-nav-aria-label=\"${2:Home Navigation}\"",
40
+ " main-nav-aria-label=\"${3:Main navigation}\"",
41
+ " secondary-nav-aria-label=\"${4:Secondary navigation}\"",
42
+ " overflow-menu-aria-label=\"${5:Overflow navigation menu}\"",
43
+ ">",
44
+ " <template #branding>",
45
+ " <NuxtLink to=\"/\" class=\"home-link\">$6Brand</NuxtLink>",
46
+ " </template>",
47
+ "</SiteHeader>"
48
+ ]
49
+ },
33
50
  "SRCDEV SiteHeader Nav Data (flat)": {
34
51
  "description": "Simple flat responsiveNavLinks data with no dropdowns, for a few-page site",
35
52
  "scope": "typescript,vue",
@@ -0,0 +1,30 @@
1
+ {
2
+ "SRCDEV SiteNavigation Basic": {
3
+ "description": "SiteNavigation horizontal nav with sliding active/hover indicator",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<SiteNavigation :nav-item-data=\"$1navData\" nav-align=\"${2:left}\" />"
7
+ ]
8
+ },
9
+ "SRCDEV SiteNavigation Localised": {
10
+ "description": "SiteNavigation with a localised aria-label",
11
+ "scope": "vue,html",
12
+ "body": [
13
+ "<SiteNavigation :nav-item-data=\"$1navData\" aria-label=\"${2:Site navigation}\" />"
14
+ ]
15
+ },
16
+ "SRCDEV SiteNavigation Nav Data Script": {
17
+ "description": "navItemData for SiteNavigation",
18
+ "scope": "typescript,vue",
19
+ "body": [
20
+ "import type { NavItemData } from \"srcdev-nuxt-components\";",
21
+ "",
22
+ "const $1navData: NavItemData = {",
23
+ " main: [",
24
+ " { text: \"$2Home\", href: \"$3/\" },",
25
+ " { text: \"$4About\", href: \"$5/about\" },",
26
+ " ],",
27
+ "};"
28
+ ]
29
+ }
30
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "SRCDEV SliderGallery Basic": {
3
+ "description": "SliderGallery full-screen image slider with thumbnails and prev/next arrows",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<SliderGallery v-model:gallery-data=\"$1galleryData\" />"
7
+ ]
8
+ },
9
+ "SRCDEV SliderGallery Localised": {
10
+ "description": "SliderGallery with localised loading/CTA/arrow copy",
11
+ "scope": "vue,html",
12
+ "body": [
13
+ "<SliderGallery",
14
+ " v-model:gallery-data=\"$1galleryData\"",
15
+ " loading-text=\"${2:Loading gallery...}\"",
16
+ " see-more-text=\"${3:SEE MORE}\"",
17
+ " prev-aria-label=\"${4:Previous image}\"",
18
+ " next-aria-label=\"${5:Next image}\"",
19
+ "/>"
20
+ ]
21
+ },
22
+ "SRCDEV SliderGallery Data Script": {
23
+ "description": "galleryData ref for SliderGallery",
24
+ "scope": "typescript,vue",
25
+ "body": [
26
+ "import type { IGalleryData } from \"srcdev-nuxt-components\";",
27
+ "",
28
+ "const $1galleryData = ref<IGalleryData[]>([",
29
+ " {",
30
+ " src: \"$2/images/slide-1.jpg\",",
31
+ " alt: \"$3\",",
32
+ " title: \"$4\",",
33
+ " textBrightness: \"${5:light}\",",
34
+ " },",
35
+ "]);"
36
+ ]
37
+ }
38
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "SRCDEV TabNavigation Basic": {
3
+ "description": "TabNavigation with route/hash links",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<TabNavigation :nav-item-data=\"$1navData\" nav-align=\"${2:center}\" />"
7
+ ]
8
+ },
9
+ "SRCDEV TabNavigation Localised": {
10
+ "description": "TabNavigation with a localised aria-label",
11
+ "scope": "vue,html",
12
+ "body": [
13
+ "<TabNavigation :nav-item-data=\"$1navData\" aria-label=\"${2:Site navigation}\" />"
14
+ ]
15
+ },
16
+ "SRCDEV TabNavigation Nav Data Script": {
17
+ "description": "navItemData for TabNavigation",
18
+ "scope": "typescript,vue",
19
+ "body": [
20
+ "import type { NavItemData } from \"srcdev-nuxt-components\";",
21
+ "",
22
+ "const $1navData: NavItemData = {",
23
+ " main: [",
24
+ " { text: \"$2Home\", href: \"$3/\" },",
25
+ " { text: \"$4Overview\", href: \"$5#overview\" },",
26
+ " ],",
27
+ "};"
28
+ ]
29
+ }
30
+ }
@@ -8,15 +8,21 @@
8
8
  interface Props {
9
9
  tag?: "div" | "section" | "article" | "aside";
10
10
  animationType?: "entry-slide-in" | "entry-zoom-reveal" | "entry-exit-blur";
11
+ /** Renders as a plain element with no animation class — e.g. the first item in a looped list
12
+ * that's already above the fold, where an entry animation would just delay visible content. */
13
+ skipAnimation?: boolean;
11
14
  styleClassPassthrough?: string | string[];
12
15
  }
13
16
  const props = withDefaults(defineProps<Props>(), {
14
17
  tag: "div",
15
18
  animationType: "entry-slide-in",
19
+ skipAnimation: false,
16
20
  styleClassPassthrough: () => [],
17
21
  });
18
22
 
19
23
  const { elementClasses, updateElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
20
24
 
21
- updateElementClasses(props.animationType);
25
+ if (!props.skipAnimation) {
26
+ updateElementClasses(props.animationType);
27
+ }
22
28
  </script>
@@ -17,6 +17,12 @@ const meta: Meta<typeof EntryAnimation> = {
17
17
  description: "Type of entry animation",
18
18
  defaultValue: "entry-slide-in",
19
19
  },
20
+ skipAnimation: {
21
+ control: { type: "boolean" },
22
+ description:
23
+ "Render as a plain element with no animation class — e.g. the first item in a looped list that's already above the fold",
24
+ defaultValue: false,
25
+ },
20
26
  styleClassPassthrough: {
21
27
  control: { type: "text" },
22
28
  description: "Additional classes for styling",
@@ -72,3 +78,44 @@ export const EntryExitBlur: StoryObj<typeof EntryAnimation> = {
72
78
  },
73
79
  render: EntrySlideIn.render,
74
80
  };
81
+
82
+ export const SkippedInALoop: StoryObj<typeof EntryAnimation> = {
83
+ name: "Skip first item (looped list, above the fold)",
84
+ parameters: {
85
+ docs: {
86
+ description: {
87
+ story:
88
+ "The common `v-for` case: the first item is already above the fold, so animating it in " +
89
+ "would just delay content the visitor can already see. Pass `skip-animation` for that one " +
90
+ "item instead of swapping the whole component out for a plain tag.",
91
+ },
92
+ },
93
+ },
94
+ render: () => ({
95
+ components: { EntryAnimation },
96
+ setup() {
97
+ const items = [0, 1, 2];
98
+ return { items };
99
+ },
100
+ template: `
101
+ <div style="padding-block: 400px; background-color: green; margin-bottom: 1000px;">
102
+ <label style="display: block; font-weight: bold; margin-bottom: 1rem; color: #fff;">
103
+ Item 0 renders immediately (skip-animation) — scroll to see items 1 and 2 animate in
104
+ </label>
105
+ <div
106
+ v-for="index in items"
107
+ :key="index"
108
+ style="margin-bottom: 2rem;"
109
+ >
110
+ <EntryAnimation :skip-animation="index === 0" animation-type="entry-slide-in">
111
+ <template #default>
112
+ <div style="padding: 2rem; background: #f0f0f0; border-radius: 8px;">
113
+ Item {{ index }} {{ index === 0 ? '(skipped)' : '(animated)' }}
114
+ </div>
115
+ </template>
116
+ </EntryAnimation>
117
+ </div>
118
+ </div>
119
+ `,
120
+ }),
121
+ };
@@ -0,0 +1,57 @@
1
+ // https://nuxt.com/docs/getting-started/testing#unit-testing
2
+ import type { VueWrapper } from "@vue/test-utils";
3
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
4
+ import ComponentUnderTest from "../EntryAnimation.vue";
5
+
6
+ let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
7
+ const wrapperFactory = (propsData = {}, slotsData = {}) => {
8
+ return mountSuspended(ComponentUnderTest, {
9
+ attachTo: document.body,
10
+ props: propsData,
11
+ slots: {
12
+ default: () => "Content",
13
+ ...slotsData,
14
+ },
15
+ });
16
+ };
17
+
18
+ describe("EntryAnimation", () => {
19
+ it("mounts without error", async () => {
20
+ wrapper = await wrapperFactory();
21
+ expect(wrapper.vm).toBeTruthy();
22
+ });
23
+
24
+ it("renders the given tag", async () => {
25
+ wrapper = await wrapperFactory({ tag: "section" });
26
+ expect(wrapper.element.tagName).toBe("SECTION");
27
+ });
28
+
29
+ it("applies the default animation class", async () => {
30
+ wrapper = await wrapperFactory();
31
+ expect(wrapper.classes()).toContain("entry-slide-in");
32
+ });
33
+
34
+ it("applies the requested animationType class", async () => {
35
+ wrapper = await wrapperFactory({ animationType: "entry-zoom-reveal" });
36
+ expect(wrapper.classes()).toContain("entry-zoom-reveal");
37
+ expect(wrapper.classes()).not.toContain("entry-slide-in");
38
+ });
39
+
40
+ it("applies no animation class when skipAnimation is true", async () => {
41
+ wrapper = await wrapperFactory({ skipAnimation: true });
42
+ expect(wrapper.classes()).not.toContain("entry-slide-in");
43
+ expect(wrapper.classes()).not.toContain("entry-zoom-reveal");
44
+ expect(wrapper.classes()).not.toContain("entry-exit-blur");
45
+ });
46
+
47
+ it("still applies styleClassPassthrough classes when skipAnimation is true", async () => {
48
+ wrapper = await wrapperFactory({ skipAnimation: true, styleClassPassthrough: ["custom-class"] });
49
+ expect(wrapper.classes()).toContain("custom-class");
50
+ expect(wrapper.classes()).not.toContain("entry-slide-in");
51
+ });
52
+
53
+ it("renders slot content", async () => {
54
+ wrapper = await wrapperFactory();
55
+ expect(wrapper.text()).toContain("Content");
56
+ });
57
+ });
@@ -0,0 +1,94 @@
1
+ # MarqueeScroller — Consumer Styling Guide
2
+
3
+ ## Public token API
4
+
5
+ | Token | Default | Controls |
6
+ |---|---|---|
7
+ | `--marquee-scroller-fade-width` | `10%` | Width of the transparent fade at each edge of the track (`mask-image`) |
8
+ | `--marquee-scroller-focus-outline-width` | `2px` | Outline width on the root region and the control button when keyboard-focused (colour is `--theme-ring`) |
9
+ | `--marquee-scroller-focus-outline-offset` | `2px` | Outline offset on the same two focus states |
10
+ | `--marquee-scroller-group-hover-filter` | `grayscale(1)` | Filter applied to every item when the pointer hovers anywhere over the scroller |
11
+ | `--marquee-scroller-item-hover-filter` | `grayscale(0)` | Filter applied to a single item when it is individually hovered (overrides the group filter for that item) |
12
+ | `--marquee-scroller-item-transition-duration` | `0.5s` | Transition duration for the item hover filter change |
13
+ | `--marquee-scroller-item-border-width` | `1px` | Item border width |
14
+ | `--marquee-scroller-item-border-colour` | `light-dark(var(--slate-10), var(--slate-00))` | Item border colour |
15
+ | `--marquee-scroller-item-border-radius` | `4px` | Item border radius |
16
+ | `--marquee-scroller-control-offset` | `8px` | Distance of the pause/play button from the top and end edges |
17
+ | `--marquee-scroller-control-background-colour` | `rgba(0, 0, 0, 0.7)` | Pause/play button background |
18
+ | `--marquee-scroller-control-background-colour-hover` | `rgba(0, 0, 0, 0.9)` | Pause/play button background on hover |
19
+ | `--marquee-scroller-control-text-colour` | `white` | Pause/play button icon colour |
20
+ | `--marquee-scroller-control-border-radius` | `4px` | Pause/play button border radius |
21
+ | `--marquee-scroller-control-padding` | `8px` | Pause/play button padding |
22
+ | `--marquee-scroller-control-font-size` | `14px` | Pause/play button icon font size |
23
+ | `--marquee-scroller-control-transition-duration` | `0.2s` | Pause/play button background-colour transition duration |
24
+ | `--marquee-scroller-control-border-width` | `1px` | Pause/play button border width in forced-colors/high-contrast mode (`prefers-contrast: high`). The border colour itself stays the system `ButtonText` keyword — see note below. |
25
+
26
+ ```css
27
+ .my-page {
28
+ --marquee-scroller-item-border-colour: #b5651d;
29
+ --marquee-scroller-control-background-colour: #1a1a1a;
30
+ --marquee-scroller-fade-width: 20%;
31
+ }
32
+ ```
33
+
34
+ ## High-contrast mode colours are not tokenised
35
+
36
+ Under `prefers-contrast: high`, the control button switches to the system colour keywords
37
+ `ButtonFace`/`ButtonText` rather than the component's own background/text tokens. These aren't
38
+ exposed as overridable tokens on purpose — they're meant to track the user's OS-level high-contrast
39
+ theme, which a component-level override would defeat. Only the border *width* in that mode is a
40
+ token (`--marquee-scroller-control-border-width`), since it's a plain dimension with no such
41
+ constraint.
42
+
43
+ ## Item size and spacing are prop-driven, not tokens
44
+
45
+ Item `width`/`height`/`gap` and the animation duration are set via the `itemConfig` and
46
+ `animationRuntime` props (not CSS custom properties), since they also drive the component's own
47
+ layout math (`aspect-ratio`, track width). Override those through props:
48
+
49
+ ```vue
50
+ <MarqueeScroller
51
+ :item-config="{ width: '80px', height: '80px', gap: '24px' }"
52
+ animation-runtime="60s"
53
+ />
54
+ ```
55
+
56
+ ## Item content via slots
57
+
58
+ Each entry in `marqueeData` (`{ id, content }`) renders a slot named after its `id`. Provide the
59
+ markup for each item via that dynamically-named slot:
60
+
61
+ ```vue
62
+ <MarqueeScroller :marquee-data="[{ id: 1, content: 'logo-a' }, { id: 2, content: 'logo-b' }]">
63
+ <template #1>
64
+ <img src="/logos/a.svg" alt="Logo A" />
65
+ </template>
66
+ <template #2>
67
+ <img src="/logos/b.svg" alt="Logo B" />
68
+ </template>
69
+ </MarqueeScroller>
70
+ ```
71
+
72
+ ## Control button icon and copy
73
+
74
+ The pause/play button's icon and its `aria-label` copy are all overridable — see `playIcon`/
75
+ `pauseIcon`/`playLabel`/`pauseLabel` props, or the `toggle-icon` slot for a fully custom icon
76
+ (scoped with `isPaused`):
77
+
78
+ ```vue
79
+ <MarqueeScroller
80
+ show-controls
81
+ play-icon="mdi:play-circle"
82
+ pause-icon="mdi:pause-circle"
83
+ play-label="Reproduire"
84
+ pause-label="Suspendre"
85
+ />
86
+ ```
87
+
88
+ ```vue
89
+ <MarqueeScroller show-controls>
90
+ <template #toggle-icon="{ isPaused }">
91
+ <Icon :name="isPaused ? 'lucide:play' : 'lucide:pause'" />
92
+ </template>
93
+ </MarqueeScroller>
94
+ ```