srcdev-nuxt-components 9.4.6 → 9.4.8

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 (109) hide show
  1. package/.claude/commands/migrate-component.md +34 -3
  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/component-dynamic-slots.md +1 -1
  7. package/.claude/skills/components/column-flow-grid.md +93 -0
  8. package/.claude/skills/components/entry-animation.md +88 -0
  9. package/.claude/skills/components/header-block.md +92 -0
  10. package/.claude/skills/components/input-copy.md +2 -0
  11. package/.claude/skills/components/input-text-core.md +186 -0
  12. package/.claude/skills/components/masonry-grid.md +153 -0
  13. package/.claude/skills/components/pop-over.md +100 -0
  14. package/.claude/skills/components/rotating-carousel-image.md +101 -0
  15. package/.claude/skills/components/skip-links.md +82 -0
  16. package/.claude/skills/components/tabs-core.md +187 -0
  17. package/.claude/skills/index.md +10 -1
  18. package/.vscode/srcdev-component-column-flow-grid.code-snippets +39 -0
  19. package/.vscode/srcdev-component-entry-animation.code-snippets +29 -0
  20. package/.vscode/srcdev-component-header-block.code-snippets +41 -0
  21. package/.vscode/srcdev-component-input-text.code-snippets +107 -0
  22. package/.vscode/srcdev-component-masonry-grid.code-snippets +51 -0
  23. package/.vscode/srcdev-component-pop-over.code-snippets +44 -0
  24. package/.vscode/srcdev-component-rotating-carousel-image.code-snippets +74 -0
  25. package/.vscode/srcdev-component-skip-links.code-snippets +55 -0
  26. package/.vscode/srcdev-component-tabs-core.code-snippets +78 -0
  27. package/app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-heading.css +2 -1
  28. package/app/components/01.atoms/animations/entry/EntryAnimation.vue +7 -1
  29. package/app/components/01.atoms/animations/entry/stories/EntryAnimation.stories.ts +47 -0
  30. package/app/components/01.atoms/animations/entry/tests/EntryAnimation.spec.ts +57 -0
  31. package/app/components/01.atoms/animations/rotating-carousel-image/CONSUMER-STYLING.md +54 -0
  32. package/app/components/01.atoms/animations/rotating-carousel-image/RotatingCarouselImage.vue +315 -0
  33. package/app/components/01.atoms/animations/rotating-carousel-image/stories/RotatingCarouselImage.stories.ts +156 -0
  34. package/app/components/01.atoms/animations/rotating-carousel-image/tests/RotatingCarouselImage.spec.ts +230 -0
  35. package/app/components/01.atoms/animations/rotating-carousel-image/tests/__snapshots__/RotatingCarouselImage.spec.ts.snap +19 -0
  36. package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +9 -10
  37. package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +12 -13
  38. package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +33 -0
  39. package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +55 -0
  40. package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +178 -0
  41. package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +75 -0
  42. package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +35 -0
  43. package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +178 -0
  44. package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +199 -0
  45. package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +158 -0
  46. package/app/components/01.atoms/navigation/skip-links/CONSUMER-STYLING.md +31 -0
  47. package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +92 -0
  48. package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +113 -0
  49. package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +71 -0
  50. package/app/components/01.atoms/navigation/skip-links/tests/__snapshots__/SkipLinks.spec.ts.snap +15 -0
  51. package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +99 -0
  52. package/app/components/01.atoms/navigation/tabs/TabsCore.vue +272 -0
  53. package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +199 -0
  54. package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +274 -0
  55. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +54 -0
  56. package/app/components/01.atoms/pop-over/PopOver.vue +201 -0
  57. package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +141 -0
  58. package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +195 -0
  59. package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +11 -0
  60. package/app/components/01.atoms/text-blocks/header-block/CONSUMER-STYLING.md +43 -0
  61. package/app/components/01.atoms/text-blocks/header-block/HeaderBlock.vue +50 -0
  62. package/app/components/01.atoms/text-blocks/header-block/stories/HeaderBlock.stories.ts +111 -0
  63. package/app/components/01.atoms/text-blocks/header-block/tests/HeaderBlock.spec.ts +119 -0
  64. package/app/components/01.atoms/text-blocks/header-block/tests/__snapshots__/HeaderBlock.spec.ts.snap +5 -0
  65. package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +8 -2
  66. package/app/components/02.molecules/input-copy/InputCopy.vue +14 -0
  67. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +15 -0
  68. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +41 -0
  69. package/app/components/03.organisms/site-header/SiteHeader.vue +1 -1
  70. package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +1 -1
  71. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +3 -3
  72. package/app/components/05.forms/input-select/InputSelectCore.vue +1 -1
  73. package/app/components/05.forms/input-text/InputTextCore.vue +6 -0
  74. package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +49 -20
  75. package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +41 -22
  76. package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +48 -22
  77. package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +43 -17
  78. package/app/components/05.forms/input-text/tests/InputPasswordWithLabel.spec.ts +56 -0
  79. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +61 -0
  80. package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +60 -0
  81. package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +46 -0
  82. package/app/components/05.forms/input-text/variants/InputPasswordWithLabel.vue +7 -1
  83. package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +8 -2
  84. package/app/components/05.forms/input-text/variants/InputTextWithLabel.vue +6 -0
  85. package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +20 -20
  86. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +32 -26
  87. package/app/composables/useTabs.ts +225 -207
  88. package/app/types/components/index.ts +2 -0
  89. package/app/types/components/rotating-carousel-image.d.ts +4 -0
  90. package/app/types/components/skip-links.d.ts +4 -0
  91. package/app/types/forms/types.forms.d.ts +1 -1
  92. package/package.json +1 -1
  93. package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/DataGrid.spec.ts.snap +0 -11
  94. package/app/components/masonry-grid/MasonryGrid.vue +0 -68
  95. package/app/components/masonry-grid-ordered/MasonryGridOrdered.vue +0 -163
  96. package/app/components/masonry-grid-ordered/MasonryGridOrderedGridExperiment.vue +0 -259
  97. package/app/components/masonry-grid-ordered/stories/MasonryGridOrdered.stories.ts +0 -354
  98. package/app/components/masonry-grid-sorted/MasonryGridSorted.vue +0 -120
  99. package/app/components/pop-over/PopOver.vue +0 -90
  100. package/app/components/rotating-carousel/RotatingCarouselImage.vue +0 -216
  101. package/app/components/skip-links/SkipLinks.vue +0 -60
  102. package/app/components/tabs/TabsCore.vue +0 -306
  103. package/app/components/typography/HeaderBlock.vue +0 -35
  104. package/app/layouts/default.vue +0 -308
  105. package/app/layouts/site-navigation-demo.vue +0 -188
  106. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/AutoGrid.vue +0 -0
  107. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/stories/AutoGrid.stories.ts +0 -0
  108. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/tests/AutoGrid.spec.ts +0 -0
  109. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/tests/__snapshots__/AutoGrid.spec.ts.snap +0 -0
@@ -0,0 +1,107 @@
1
+ {
2
+ "SRCDEV InputTextCore Basic": {
3
+ "description": "Basic InputTextCore native input (no label/error wrapper)",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<InputTextCore",
7
+ " id=\"$1fieldId\"",
8
+ " name=\"$2fieldName\"",
9
+ " v-model=\"$3fieldValue\"",
10
+ " type=\"$4text\"",
11
+ " placeholder=\"$5Enter value...\"",
12
+ "/>"
13
+ ]
14
+ },
15
+ "SRCDEV InputTextCore Date with min/max": {
16
+ "description": "InputTextCore date input constrained to a min/max range (e.g. no dates in the past)",
17
+ "scope": "vue,html",
18
+ "body": [
19
+ "<InputTextCore",
20
+ " id=\"$1appointment-date\"",
21
+ " name=\"$2appointmentDate\"",
22
+ " v-model=\"$3appointmentDate\"",
23
+ " type=\"date\"",
24
+ " :min=\"new Date().toISOString().split('T')[0]\"",
25
+ " max=\"$42027-12-31\"",
26
+ "/>"
27
+ ]
28
+ },
29
+ "SRCDEV InputTextWithLabel Basic": {
30
+ "description": "InputTextWithLabel: labelled text field with error/description support",
31
+ "scope": "vue,html",
32
+ "body": [
33
+ "<InputTextWithLabel",
34
+ " v-model=\"$1fieldValue\"",
35
+ " id=\"$2fieldId\"",
36
+ " name=\"$3fieldName\"",
37
+ " type=\"$4text\"",
38
+ " label=\"$5Field label\"",
39
+ " :error-message=\"$6errors.fieldName\"",
40
+ " :field-has-error=\"!!$6errors.fieldName\"",
41
+ "/>"
42
+ ]
43
+ },
44
+ "SRCDEV InputTextWithLabel Date with min/max": {
45
+ "description": "InputTextWithLabel date field constrained to a min/max range",
46
+ "scope": "vue,html",
47
+ "body": [
48
+ "<InputTextWithLabel",
49
+ " v-model=\"$1appointmentDate\"",
50
+ " id=\"$2appointment-date\"",
51
+ " name=\"$2appointmentDate\"",
52
+ " type=\"date\"",
53
+ " label=\"$3Appointment date\"",
54
+ " :min=\"new Date().toISOString().split('T')[0]\"",
55
+ " max=\"$42027-12-31\"",
56
+ " :error-message=\"$5errors.appointmentDate\"",
57
+ " :field-has-error=\"!!$5errors.appointmentDate\"",
58
+ "/>"
59
+ ]
60
+ },
61
+ "SRCDEV InputPasswordWithLabel": {
62
+ "description": "InputPasswordWithLabel with show/hide toggle",
63
+ "scope": "vue,html",
64
+ "body": [
65
+ "<InputPasswordWithLabel",
66
+ " v-model=\"$1password\"",
67
+ " name=\"$2password\"",
68
+ " label=\"$3Password\"",
69
+ " :error-message=\"$4errors.password\"",
70
+ " :field-has-error=\"!!$4errors.password\"",
71
+ "/>"
72
+ ]
73
+ },
74
+ "SRCDEV InputTextAsNumberWithLabel": {
75
+ "description": "InputTextAsNumberWithLabel numeric stepper with min/max/step",
76
+ "scope": "vue,html",
77
+ "body": [
78
+ "<InputTextAsNumberWithLabel",
79
+ " v-model=\"$1quantity\"",
80
+ " name=\"$2quantity\"",
81
+ " label=\"$3Quantity\"",
82
+ " :min=\"$41\"",
83
+ " :max=\"$599\"",
84
+ " :step=\"$61\"",
85
+ " :error-message=\"$7errors.quantity\"",
86
+ " :field-has-error=\"!!$7errors.quantity\"",
87
+ ">",
88
+ " <template #left>−</template>",
89
+ " <template #right>+</template>",
90
+ "</InputTextAsNumberWithLabel>"
91
+ ]
92
+ },
93
+ "SRCDEV InputText CSS Override": {
94
+ "description": "CSS override scaffold for InputTextCore tokens",
95
+ "scope": "css",
96
+ "body": [
97
+ ".$1my-page {",
98
+ " .input-text-wrapper {",
99
+ " --input-text-surface: $2;",
100
+ " --input-text-border: $3;",
101
+ " --input-text-border-focus: $4;",
102
+ " --input-text-color: $5;",
103
+ " }",
104
+ "}"
105
+ ]
106
+ }
107
+ }
@@ -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
+ }
@@ -0,0 +1,74 @@
1
+ {
2
+ "SRCDEV RotatingCarouselImage Basic": {
3
+ "description": "RotatingCarouselImage — 3D rotating image carousel with scroll-parallax tilt",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<RotatingCarouselImage",
7
+ " :data=\"[",
8
+ " { src: '${1:/carousel/1.jpg}', alt: '${2:Image 1}' },",
9
+ " { src: '${3:/carousel/2.jpg}', alt: '${4:Image 2}' },",
10
+ " ]\"",
11
+ ">$5</RotatingCarouselImage>"
12
+ ]
13
+ },
14
+ "SRCDEV RotatingCarouselImage With Controls": {
15
+ "description": "RotatingCarouselImage with the visible pause/play control button (WCAG 2.2.2)",
16
+ "scope": "vue,html",
17
+ "body": [
18
+ "<RotatingCarouselImage",
19
+ " :data=\"$1images\"",
20
+ " show-controls",
21
+ " aria-label=\"$2Rotating product photos\"",
22
+ "/>"
23
+ ]
24
+ },
25
+ "SRCDEV RotatingCarouselImage Static Tilt": {
26
+ "description": "RotatingCarouselImage with scroll-parallax disabled, using a fixed rotateX instead",
27
+ "scope": "vue,html",
28
+ "body": [
29
+ "<RotatingCarouselImage",
30
+ " :data=\"$1images\"",
31
+ " :use-parallax-effect=\"false\"",
32
+ " :rotate-x=\"${2:10}\"",
33
+ " show-controls",
34
+ "/>"
35
+ ]
36
+ },
37
+ "SRCDEV RotatingCarouselImage Custom Toggle Icon Slot": {
38
+ "description": "RotatingCarouselImage with the toggle-icon slot fully replacing the control button's icon",
39
+ "scope": "vue,html",
40
+ "body": [
41
+ "<RotatingCarouselImage :data=\"$1images\" show-controls>",
42
+ " <template #toggle-icon=\"{ isPaused }\">",
43
+ " $2",
44
+ " </template>",
45
+ "</RotatingCarouselImage>"
46
+ ]
47
+ },
48
+ "SRCDEV RotatingCarouselImage CSS Override": {
49
+ "description": "CSS override scaffold for RotatingCarouselImage — scope to a page or section class",
50
+ "scope": "css",
51
+ "body": [
52
+ ".$1my-page {",
53
+ " .rotating-carousel {",
54
+ " --rotating-carousel-height: ;",
55
+ " --rotating-carousel-item-width: ;",
56
+ " --rotating-carousel-item-height: ;",
57
+ " --rotating-carousel-offset-bottom: ;",
58
+ " --rotating-carousel-rotation-duration: ;",
59
+ " --rotating-carousel-z-index: ;",
60
+ " --rotating-carousel-focus-outline-width: ;",
61
+ " --rotating-carousel-focus-outline-offset: ;",
62
+ " --rotating-carousel-control-offset: ;",
63
+ " --rotating-carousel-control-background-colour: ;",
64
+ " --rotating-carousel-control-background-colour-hover: ;",
65
+ " --rotating-carousel-control-text-colour: ;",
66
+ " --rotating-carousel-control-border-radius: ;",
67
+ " --rotating-carousel-control-border-width: ;",
68
+ " --rotating-carousel-control-padding: ;",
69
+ " --rotating-carousel-control-font-size: ;",
70
+ " }",
71
+ "}"
72
+ ]
73
+ }
74
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "SRCDEV SkipLinks Basic": {
3
+ "description": "SkipLinks — accessibility skip-navigation utility, default main-content/footer links",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<SkipLinks />"
7
+ ]
8
+ },
9
+ "SRCDEV SkipLinks Custom Links": {
10
+ "description": "SkipLinks with a custom list of skip targets",
11
+ "scope": "vue,html",
12
+ "body": [
13
+ "<SkipLinks",
14
+ " :links=\"[",
15
+ " { href: '${1:#site-navigation}', label: '${2:Skip to navigation}' },",
16
+ " { href: '${3:#main-content}', label: '${4:Skip to main content}' },",
17
+ " { href: '${5:#footer-content}', label: '${6:Skip to footer}' },",
18
+ " ]\"",
19
+ "/>"
20
+ ]
21
+ },
22
+ "SRCDEV SkipLinks With Home Link Slot": {
23
+ "description": "SkipLinks with the homeLink slot for a logo/home link before the skip-nav",
24
+ "scope": "vue,html",
25
+ "body": [
26
+ "<SkipLinks>",
27
+ " <template #homeLink>",
28
+ " $1",
29
+ " </template>",
30
+ "</SkipLinks>"
31
+ ]
32
+ },
33
+ "SRCDEV SkipLinks CSS Override": {
34
+ "description": "CSS override scaffold for SkipLinks — scope to a page or section class",
35
+ "scope": "css",
36
+ "body": [
37
+ ".$1my-page {",
38
+ " .skip-links {",
39
+ " --skip-links-z-index: ;",
40
+ " --skip-links-gap: ;",
41
+ " --skip-links-background-colour: ;",
42
+ " --skip-links-border-colour: ;",
43
+ " --skip-links-border-width: ;",
44
+ " --skip-links-padding: ;",
45
+ " --skip-links-text-colour: ;",
46
+ " --skip-links-link-padding-block: ;",
47
+ " --skip-links-link-padding-inline: ;",
48
+ " --skip-links-transition-duration: ;",
49
+ " --skip-links-focus-outline-width: ;",
50
+ " --skip-links-focus-outline-colour: ;",
51
+ " }",
52
+ "}"
53
+ ]
54
+ }
55
+ }
@@ -0,0 +1,78 @@
1
+ {
2
+ "SRCDEV TabsCore Basic": {
3
+ "description": "TabsCore — horizontal tablist with indexed trigger/content slots",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<TabsCore :item-count=\"${1:3}\">",
7
+ " <template #tab-0-trigger>${2:Overview}</template>",
8
+ " <template #tab-0-content>",
9
+ " $3",
10
+ " </template>",
11
+ " <template #tab-1-trigger>${4:Specs}</template>",
12
+ " <template #tab-1-content>",
13
+ " $5",
14
+ " </template>",
15
+ " <template #tab-2-trigger>${6:Reviews}</template>",
16
+ " <template #tab-2-content>",
17
+ " $7",
18
+ " </template>",
19
+ "</TabsCore>"
20
+ ]
21
+ },
22
+ "SRCDEV TabsCore Vertical": {
23
+ "description": "TabsCore with axis=\"y\" for a vertical tab column",
24
+ "scope": "vue,html",
25
+ "body": [
26
+ "<TabsCore axis=\"y\" :item-count=\"${1:3}\" aria-label=\"${2:Settings}\">",
27
+ " <template #tab-0-trigger>$3</template>",
28
+ " <template #tab-0-content>$4</template>",
29
+ "</TabsCore>"
30
+ ]
31
+ },
32
+ "SRCDEV TabsCore Indicators Off": {
33
+ "description": "TabsCore with the moving hover/active/underline decorators all disabled",
34
+ "scope": "vue,html",
35
+ "body": [
36
+ "<TabsCore",
37
+ " :item-count=\"${1:3}\"",
38
+ " :track-hover=\"false\"",
39
+ " :track-active=\"false\"",
40
+ " :track-indicator=\"false\"",
41
+ ">",
42
+ " $2",
43
+ "</TabsCore>"
44
+ ]
45
+ },
46
+ "SRCDEV TabsCore CSS Override": {
47
+ "description": "CSS override scaffold for TabsCore — scope to a page or section class",
48
+ "scope": "css",
49
+ "body": [
50
+ ".$1my-page {",
51
+ " .tabs-core {",
52
+ " --tabs-nav-border: ;",
53
+ " --tabs-hover-indicator-colour: ;",
54
+ " --tabs-hover-indicator-text-colour: ;",
55
+ " --tabs-active-indicator-colour: ;",
56
+ " --tabs-active-indicator-text-colour: ;",
57
+ " --tabs-underline-indicator-colour: ;",
58
+ " --tabs-underline-indicator-height: ;",
59
+ " --tabs-list-item-opacity: ;",
60
+ " --tabs-list-item-colour: ;",
61
+ " --tabs-list-item-colour-selected: ;",
62
+ " --tabs-list-item-text-transform: ;",
63
+ " --tabs-list-item-font-weight: ;",
64
+ " --tabs-list-item-padding-block: ;",
65
+ " --tabs-list-item-padding-inline: ;",
66
+ " --tabs-list-item-colour-transition-duration: ;",
67
+ " --tabs-list-item-focus-outline-width: ;",
68
+ " --tabs-list-item-focus-outline-offset: ;",
69
+ " --tabs-content-background-colour: ;",
70
+ " --tabs-content-border-width: ;",
71
+ " --tabs-content-border-colour: ;",
72
+ " --tabs-content-border-radius: ;",
73
+ " --tabs-axis-y-gap: ;",
74
+ " }",
75
+ "}"
76
+ ]
77
+ }
78
+ }
@@ -31,5 +31,6 @@
31
31
  font-weight: 600;
32
32
  font-variation-settings: "wght" 600;
33
33
  line-height: 1;
34
- margin: 0;
34
+ margin-block: 0rem;
35
+ padding-block: 0rem;
35
36
  }
@@ -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,54 @@
1
+ # RotatingCarouselImage — Consumer Styling Guide
2
+
3
+ ## Public token API
4
+
5
+ | Token | Default | Controls |
6
+ |---|---|---|
7
+ | `--rotating-carousel-height` | `70svh` | Height of the carousel's root element |
8
+ | `--rotating-carousel-item-width` | `200px` | Width of each rotating item (and the slider box itself) |
9
+ | `--rotating-carousel-item-height` | `250px` | Height of each rotating item (and the slider box itself) |
10
+ | `--rotating-carousel-offset-bottom` | `35%` | Vertical position of the slider box within the root element |
11
+ | `--rotating-carousel-rotation-duration` | `30s` | Duration of one full 360° rotation |
12
+ | `--rotating-carousel-z-index` | `2` | Stacking context of the slider box |
13
+ | `--rotating-carousel-focus-outline-width` | `2px` | Outline width on the root region and the control button when keyboard-focused (colour is `--theme-ring`) |
14
+ | `--rotating-carousel-focus-outline-offset` | `2px` | Outline offset on the same two focus states |
15
+ | `--rotating-carousel-control-offset` | `8px` | Distance of the pause/play button from the top and end edges |
16
+ | `--rotating-carousel-control-background-colour` | `rgba(0, 0, 0, 0.7)` | Pause/play button background |
17
+ | `--rotating-carousel-control-background-colour-hover` | `rgba(0, 0, 0, 0.9)` | Pause/play button background on hover |
18
+ | `--rotating-carousel-control-text-colour` | `white` | Pause/play button icon colour |
19
+ | `--rotating-carousel-control-border-radius` | `4px` | Pause/play button border radius |
20
+ | `--rotating-carousel-control-padding` | `8px` | Pause/play button padding |
21
+ | `--rotating-carousel-control-font-size` | `14px` | Pause/play button icon font size |
22
+ | `--rotating-carousel-control-transition-duration` | `0.2s` | Pause/play button background-colour transition duration |
23
+ | `--rotating-carousel-control-border-width` | `1px` | Pause/play button border width in forced-colors/high-contrast mode (`prefers-contrast: high`) |
24
+
25
+ ```css
26
+ .my-page {
27
+ --rotating-carousel-item-width: 260px;
28
+ --rotating-carousel-item-height: 320px;
29
+ --rotating-carousel-rotation-duration: 45s;
30
+ }
31
+ ```
32
+
33
+ ## High-contrast mode colours are not tokenised
34
+
35
+ Under `prefers-contrast: high`, the control button switches to the system colour keywords
36
+ `ButtonFace`/`ButtonText` rather than the component's own background/text tokens, so it tracks the
37
+ user's OS-level high-contrast theme. Only the border *width* in that mode is a token
38
+ (`--rotating-carousel-control-border-width`).
39
+
40
+ ## Rotation geometry is prop-driven, not tokens
41
+
42
+ `rotateX`, `perspective`, and `translateZ` control the 3D tilt and radius of the rotation and are
43
+ set via props (not CSS custom properties), since they also drive the component's own scroll-based
44
+ parallax math:
45
+
46
+ ```vue
47
+ <RotatingCarouselImage :data="images" :perspective="1200" :translate-z="1200" />
48
+ ```
49
+
50
+ ## Control button icon and copy
51
+
52
+ The pause/play button's icon and its `aria-label` copy are overridable via `playIcon`/`pauseIcon`/
53
+ `playLabel`/`pauseLabel` props, or the `toggle-icon` slot for a fully custom icon (scoped with
54
+ `isPaused`) — same pattern as `MarqueeScroller`.