srcdev-nuxt-components 9.4.4 → 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 (116) hide show
  1. package/.claude/commands/migrate-component.md +106 -0
  2. package/.claude/component-ledger/artifact-url.txt +1 -0
  3. package/.claude/component-ledger/audit.json +1 -0
  4. package/.claude/component-ledger/build.mjs +111 -0
  5. package/.claude/component-ledger/output.html +559 -0
  6. package/.claude/component-ledger/template.html +559 -0
  7. package/.claude/hooks/refresh-component-ledger.sh +32 -0
  8. package/.claude/settings.json +10 -0
  9. package/.claude/skills/components/animated-svg-text.md +78 -0
  10. package/.claude/skills/components/breadcrumb.md +1 -0
  11. package/.claude/skills/components/canvas-switcher.md +61 -0
  12. package/.claude/skills/components/clip-element.md +67 -0
  13. package/.claude/skills/components/clipped-panel.md +64 -0
  14. package/.claude/skills/components/content-docs.md +2 -0
  15. package/.claude/skills/components/deep-expanding-menu-classic.md +94 -0
  16. package/.claude/skills/components/deep-expanding-menu.md +92 -0
  17. package/.claude/skills/components/display-banner.md +60 -0
  18. package/.claude/skills/components/display-tooltip-defined.md +139 -0
  19. package/.claude/skills/components/display-tooltip.md +85 -0
  20. package/.claude/skills/components/marquee-scroller.md +130 -0
  21. package/.claude/skills/components/navigation-items.md +1 -0
  22. package/.claude/skills/components/responsive-header.md +3 -0
  23. package/.claude/skills/components/site-header.md +4 -0
  24. package/.claude/skills/components/site-navigation.md +1 -0
  25. package/.claude/skills/components/tab-navigation.md +1 -0
  26. package/.claude/skills/composable-tooltips-guide.md +10 -0
  27. package/.claude/skills/index.md +8 -1
  28. package/.claude/skills/storybook-add-story.md +30 -0
  29. package/.vscode/srcdev-component-animated-svg-text.code-snippets +15 -0
  30. package/.vscode/srcdev-component-breadcrumb.code-snippets +13 -0
  31. package/.vscode/srcdev-component-canvas-switcher.code-snippets +9 -0
  32. package/.vscode/srcdev-component-clip-element.code-snippets +20 -0
  33. package/.vscode/srcdev-component-clipped-panel.code-snippets +23 -0
  34. package/.vscode/srcdev-component-content-docs.code-snippets +18 -0
  35. package/.vscode/srcdev-component-deep-expanding-menu-classic.code-snippets +9 -0
  36. package/.vscode/srcdev-component-deep-expanding-menu.code-snippets +9 -0
  37. package/.vscode/srcdev-component-display-banner.code-snippets +30 -0
  38. package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +29 -0
  39. package/.vscode/srcdev-component-display-tooltip.code-snippets +38 -0
  40. package/.vscode/srcdev-component-marquee-scroller.code-snippets +93 -0
  41. package/.vscode/srcdev-component-responsive-header.code-snippets +12 -0
  42. package/.vscode/srcdev-component-site-header.code-snippets +17 -0
  43. package/.vscode/srcdev-component-site-navigation.code-snippets +30 -0
  44. package/.vscode/srcdev-component-slider-gallery.code-snippets +38 -0
  45. package/.vscode/srcdev-component-tab-navigation.code-snippets +30 -0
  46. package/app/components/01.atoms/animations/animated-svg-text/AnimatedSvgText.vue +58 -0
  47. package/app/components/01.atoms/animations/animated-svg-text/CONSUMER-STYLING.md +34 -0
  48. package/app/components/01.atoms/animations/animated-svg-text/stories/AnimatedSvgText.stories.ts +69 -0
  49. package/app/components/01.atoms/animations/animated-svg-text/tests/AnimatedSvgText.spec.ts +63 -0
  50. package/app/components/01.atoms/animations/animated-svg-text/tests/__snapshots__/AnimatedSvgText.spec.ts.snap +7 -0
  51. package/app/components/01.atoms/animations/clip-element/CONSUMER-STYLING.md +27 -0
  52. package/app/components/{clip-element → 01.atoms/animations/clip-element}/ClipElement.vue +16 -11
  53. package/app/components/01.atoms/animations/clip-element/stories/ClipElement.stories.ts +133 -0
  54. package/app/components/01.atoms/animations/clip-element/tests/ClipElement.spec.ts +70 -0
  55. package/app/components/01.atoms/animations/clip-element/tests/__snapshots__/ClipElement.spec.ts.snap +7 -0
  56. package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +94 -0
  57. package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +331 -0
  58. package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +151 -0
  59. package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +315 -0
  60. package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +28 -0
  61. package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +27 -0
  62. package/app/components/01.atoms/canvas-switcher/CanvasSwitcher.vue +111 -0
  63. package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +75 -0
  64. package/app/components/01.atoms/canvas-switcher/tests/CanvasSwitcher.spec.ts +67 -0
  65. package/app/components/01.atoms/clipped-panel/CONSUMER-STYLING.md +39 -0
  66. package/app/components/01.atoms/clipped-panel/ClippedPanel.vue +68 -0
  67. package/app/components/01.atoms/clipped-panel/stories/ClippedPanel.stories.ts +105 -0
  68. package/app/components/01.atoms/clipped-panel/tests/ClippedPanel.spec.ts +67 -0
  69. package/app/components/01.atoms/clipped-panel/tests/__snapshots__/ClippedPanel.spec.ts.snap +3 -0
  70. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +8 -2
  71. package/app/components/01.atoms/display-banner/CONSUMER-STYLING.md +35 -0
  72. package/app/components/{display-banner → 01.atoms/display-banner}/DisplayBanner.vue +13 -35
  73. package/app/components/01.atoms/display-banner/stories/DisplayBanner.stories.ts +100 -0
  74. package/app/components/01.atoms/display-banner/tests/DisplayBanner.spec.ts +78 -0
  75. package/app/components/01.atoms/display-banner/tests/__snapshots__/DisplayBanner.spec.ts.snap +8 -0
  76. package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +62 -0
  77. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +188 -0
  78. package/app/components/01.atoms/display-tooltip/stories/DisplayTooltip.stories.ts +94 -0
  79. package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +75 -0
  80. package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +12 -0
  81. package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +4 -1
  82. package/app/components/02.molecules/display-tooltip-defined/CONSUMER-STYLING.md +42 -0
  83. package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +116 -0
  84. package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +126 -0
  85. package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +66 -0
  86. package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +17 -0
  87. package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +44 -0
  88. package/app/components/{deep-expanding-menu → 02.molecules/navigation/deep-expanding-menu}/DeepExpandingMenu.vue +53 -64
  89. package/app/components/02.molecules/navigation/deep-expanding-menu/stories/DeepExpandingMenu.stories.ts +79 -0
  90. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +92 -0
  91. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +17 -0
  92. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +40 -0
  93. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +200 -0
  94. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/stories/DeepExpandingMenuClassic.stories.ts +79 -0
  95. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/DeepExpandingMenuClassic.spec.ts +82 -0
  96. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/__snapshots__/DeepExpandingMenuClassic.spec.ts.snap +18 -0
  97. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +4 -1
  98. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +4 -1
  99. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +16 -4
  100. package/app/components/03.organisms/responsive-header/NavigationItems.vue +4 -1
  101. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +16 -3
  102. package/app/components/03.organisms/site-header/SiteHeader.vue +16 -1
  103. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
  104. package/app/types/components/display-tooltip-defined.d.ts +10 -0
  105. package/app/types/components/index.ts +2 -0
  106. package/app/types/components/marquee-scroller.d.ts +10 -0
  107. package/package.json +1 -1
  108. package/app/components/animated-svg-text/AnimatedSvgText.vue +0 -89
  109. package/app/components/canvas-switcher/CanvasSwitcher.vue +0 -104
  110. package/app/components/clipped-panels/ClippedPanel.vue +0 -87
  111. package/app/components/deep-expanding-menu/DeepExpandingMenuOld.vue +0 -218
  112. package/app/components/display-details/DisplayDetailsCore.vue +0 -122
  113. package/app/components/display-tooltip/DisplayTooltip.vue +0 -170
  114. package/app/components/display-tooltip/DisplayTooltipDefined.vue +0 -103
  115. package/app/components/magnetic-navigation/MagneticNavigation.vue +0 -162
  116. package/app/components/marquee-scroller/MarqueeScroller.vue +0 -289
@@ -1,170 +0,0 @@
1
- <template>
2
- <div class="display-tooltip-core" :class="[elementClasses]" :style="`--anchor-name: ${tooltipAnchorName}`">
3
- <div class="display-tooltip-trigger-wrapper body-md">
4
- <slot v-if="slots.triggerContent" name="triggerContent"></slot>
5
- <button
6
- :popovertarget="tooltipId"
7
- popovertargetaction="toggle"
8
- class="display-tooltip-trigger-button"
9
- :class="{ hide: hideTooltipTrigger }"
10
- aria-label="Toggle the popover"
11
- >
12
- <Icon name="fa7-solid:circle-question" class="display-tooltip-trigger-icon" aria-hidden="true" />
13
- </button>
14
- </div>
15
- <div :id="tooltipId" ref="popover1" popover class="display-tooltip-popover">
16
- <div class="display-tooltip-popover-content">
17
- <slot name="tooltipContent"></slot>
18
- </div>
19
- </div>
20
- </div>
21
- </template>
22
-
23
- <script setup lang="ts">
24
- const props = defineProps({
25
- tooltipId: {
26
- type: String,
27
- default: "",
28
- },
29
- styleClassPassthrough: {
30
- type: [String, Array] as PropType<string | string[]>,
31
- default: () => [],
32
- },
33
- });
34
-
35
- const slots = useSlots();
36
- const tooltipId = computed(() => {
37
- return props.tooltipId.length ? props.tooltipId : `nuxt-tooltip-${useId()}`;
38
- });
39
- const tooltipAnchorName = `tooltip-anchor-${useId()}`;
40
- const hideTooltipTrigger = ref(false);
41
-
42
- const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
43
- </script>
44
-
45
- <style lang="css">
46
- @layer components {
47
- .display-tooltip-core {
48
- padding-block: 8px;
49
- position: relative;
50
-
51
- .display-tooltip-trigger-wrapper {
52
- display: inline-flex;
53
- align-items: flex-start;
54
-
55
- .display-tooltip-trigger-button {
56
- margin-left: 8px;
57
-
58
- &.hide {
59
- width: 0;
60
- overflow: hidden;
61
- opacity: 0;
62
- }
63
-
64
- .display-tooltip-trigger-icon {
65
- display: inline-flex;
66
- align-items: center;
67
- justify-content: center;
68
- width: 20px;
69
- height: 20px;
70
- color: var(--nuxt-text-header);
71
- font-size: 18px;
72
- line-height: 20px;
73
- }
74
- }
75
- }
76
-
77
- .display-tooltip-trigger-button {
78
- all: unset;
79
- aspect-ratio: 1 / 1;
80
- width: 24px;
81
- display: grid;
82
- place-items: center;
83
-
84
- outline: 1px solid transparent;
85
- anchor-name: var(--anchor-name);
86
-
87
- @supports (corner-shape: squircle) {
88
- corner-shape: squircle;
89
- border-radius: 50%;
90
- }
91
-
92
- &:hover {
93
- cursor: pointer;
94
- text-decoration: underline;
95
- }
96
-
97
- &:hover,
98
- &:focus-visible {
99
- outline-offset: 2px;
100
- }
101
- }
102
-
103
- .display-tooltip-popover {
104
- display: none;
105
- position: absolute;
106
- border: none;
107
- width: 300px;
108
-
109
- outline: 1px solid light-dark(black, white);
110
- color: light-dark(black, white);
111
- background-color: light-dark(white, black);
112
- border-radius: 8px;
113
-
114
- position-anchor: var(--anchor-name);
115
- margin: 0;
116
- inset: auto;
117
- top: calc(anchor(top) + 0px);
118
- left: calc(anchor(right) + 1px);
119
- opacity: 0;
120
- transition:
121
- opacity 200ms,
122
- display 200ms,
123
- overlay 200ms;
124
-
125
- transition-behavior: allow-discrete;
126
- position-try-fallbacks: flip-inline;
127
-
128
- /* .popover-action {
129
- display: flex;
130
- justify-content: flex-end;
131
- padding: 8px;
132
- border-bottom: 1px solid var(--nuxt-stroke-border-grey);
133
- } */
134
-
135
- .display-tooltip-popover-content {
136
- .display-tooltip-close-button {
137
- all: unset;
138
- cursor: pointer;
139
- color: var(--nuxt-text-accessibility-blue);
140
- border: 1px solid var(--theme-border);
141
- outline: 1px solid var(--theme-ring);
142
-
143
- font-weight: 600;
144
- padding: 8px 12px;
145
-
146
- &:hover,
147
- &:focus {
148
- text-decoration: underline;
149
- border: 1px solid var(--theme-surface);
150
- outline: 1px solid var(--theme-surface);
151
- outline-offset: 2px;
152
- }
153
- }
154
- }
155
-
156
- &:popover-open {
157
- display: flex;
158
- opacity: 1;
159
-
160
- flex-direction: column;
161
-
162
- @starting-style {
163
- display: flex;
164
- opacity: 0;
165
- }
166
- }
167
- }
168
- }
169
- }
170
- </style>
@@ -1,103 +0,0 @@
1
- <template>
2
- <DisplayTooltip :tooltip-id="tooltipId" :style-class-passthrough>
3
- <template v-if="slots.triggerContent" #triggerContent>
4
- <slot name="triggerContent"></slot>
5
- </template>
6
- <template #tooltipContent>
7
- <div class="popover-content-defined">
8
- <component
9
- :is="props.contentText.tooltipTitle?.tag"
10
- v-if="props.contentText.tooltipTitle"
11
- class="tooltip-title subtitle-sm"
12
- >
13
- {{ props.contentText.tooltipTitle.text }}
14
- </component>
15
- <component
16
- :is="props.contentText.tooltipContent?.tag"
17
- v-if="props.contentText.tooltipContent"
18
- class="tooltip-body body-sm"
19
- >
20
- {{ props.contentText.tooltipContent.text }}
21
- </component>
22
- <component
23
- :is="props.contentText.tooltipAction?.tag"
24
- v-if="props.contentText.tooltipAction"
25
- class="tooltip-action input-value"
26
- >
27
- {{ props.contentText.tooltipAction.text }}
28
- </component>
29
- <button
30
- :popovertarget="tooltipId"
31
- popovertargetaction="hide"
32
- class="display-tooltip-close-button"
33
- aria-label="Close tool tip"
34
- >
35
- Close
36
- </button>
37
- </div>
38
- </template>
39
- </DisplayTooltip>
40
- </template>
41
-
42
- <script lang="ts">
43
- export interface TooltipContentText {
44
- tooltipTitle?: {
45
- tag: string
46
- text: string
47
- }
48
- tooltipContent?: {
49
- tag: string
50
- text: string
51
- }
52
- tooltipAction?: {
53
- tag: string
54
- text: string
55
- }
56
- }
57
- </script>
58
-
59
- <script setup lang="ts">
60
- const props = defineProps({
61
- tooltipId: {
62
- type: String,
63
- default: "",
64
- },
65
- contentText: {
66
- type: Object as PropType<TooltipContentText>,
67
- default: () => ({}),
68
- },
69
- styleClassPassthrough: {
70
- type: [String, Array] as PropType<string | string[]>,
71
- default: () => [],
72
- },
73
- })
74
-
75
- const slots = useSlots()
76
- const tooltipId = computed(() => {
77
- return props.tooltipId.length ? `nuxt-tooltip-${props.tooltipId}` : `nuxt-tooltip-${useId()}`
78
- })
79
- </script>
80
-
81
- <style lang="css">
82
- @layer components {
83
- .display-tooltip-core {
84
- .popover {
85
- .popover-content {
86
- .popover-content-defined {
87
- .tooltip-title {
88
- color: var(--nuxt-text-white-header);
89
- }
90
-
91
- .tooltip-body {
92
- color: var(--nuxt-text-white-body);
93
- }
94
-
95
- .tooltip-action {
96
- color: var(--nuxt-text-white-body);
97
- }
98
- }
99
- }
100
- }
101
- }
102
- }
103
- </style>
@@ -1,162 +0,0 @@
1
- <template>
2
- <component :is="tag" class="magnetic-navigation" :class="[elementClasses]">
3
- <nav>
4
- <ul>
5
- <li><a href="#">Home</a></li>
6
- <li><a href="#">About</a></li>
7
- <li><a href="#">Blog</a></li>
8
- <li><a href="#">Contact</a></li>
9
- </ul>
10
- </nav>
11
- </component>
12
- </template>
13
-
14
- <script setup lang="ts">
15
- const props = defineProps({
16
- tag: {
17
- type: String,
18
- default: "div",
19
- validator(value: string) {
20
- return ["div", "header", "footer", "nav"].includes(value)
21
- },
22
- },
23
- styleClassPassthrough: {
24
- type: [String, Array] as PropType<string | string[]>,
25
- default: () => [],
26
- },
27
- })
28
-
29
- const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough)
30
-
31
- watch(
32
- () => props.styleClassPassthrough,
33
- () => {
34
- resetElementClasses(props.styleClassPassthrough)
35
- }
36
- )
37
- </script>
38
-
39
- <style lang="css">
40
- @layer components {
41
- .magnetic-navigation {
42
- /*
43
- --_background-image: url("/images/rotating-carousel/image-3.webp");
44
-
45
- background-image: var(--_background-image);
46
- background-size: cover;
47
- background-position: 0 -400px;
48
- background-attachment: fixed;
49
- */
50
- /* padding-block: 200px 2000px; */
51
-
52
- nav {
53
- width: fit-content;
54
- margin: 3rem auto;
55
- background: hsl(0 0% 0% / 0.8);
56
-
57
- padding: 8px;
58
- border-radius: 8px;
59
-
60
- isolation: isolate;
61
-
62
- anchor-name: --hovered-link;
63
-
64
- li:hover {
65
- anchor-name: --hovered-link;
66
- }
67
-
68
- &::before,
69
- &::after {
70
- content: "";
71
- position: absolute;
72
- top: calc(anchor(bottom) - 10px);
73
- left: calc(anchor(left) + 1rem);
74
- right: calc(anchor(right) + 1rem);
75
- bottom: calc(anchor(bottom) + 5px);
76
- border-radius: 10px;
77
-
78
- position-anchor: --hovered-link;
79
-
80
- transition: 500ms
81
- linear(
82
- 0,
83
- 0.029 1.6%,
84
- 0.123 3.5%,
85
- 0.651 10.6%,
86
- 0.862 14.1%,
87
- 1.002 17.7%,
88
- 1.046 19.6%,
89
- 1.074 21.6%,
90
- 1.087 23.9%,
91
- 1.086 26.6%,
92
- 1.014 38.5%,
93
- 0.994 46.3%,
94
- 1
95
- );
96
- }
97
-
98
- &::before {
99
- z-index: -1;
100
- background: rgb(0 0 0 / 0.2);
101
- backdrop-filter: blur(2px);
102
- }
103
-
104
- &::after {
105
- z-index: -2;
106
- background-image: var(--_background-image);
107
- background-size: cover;
108
- background-position: var(--_background-position);
109
- background-attachment: fixed;
110
- }
111
-
112
- &:has(a:hover)::before,
113
- &:has(a:hover)::after {
114
- top: anchor(top);
115
- left: anchor(left);
116
- right: anchor(right);
117
- bottom: anchor(bottom);
118
-
119
- @supports (corner-shape: squircle) {
120
- corner-shape: squircle;
121
- border-radius: 50%;
122
- }
123
- }
124
-
125
- &:has(li:first-of-type a:hover)::before,
126
- &:has(li:first-of-type a:hover)::after {
127
- @supports (corner-shape: squircle) {
128
- border-radius: 32px 50% 50% 32px;
129
- }
130
- }
131
-
132
- &:has(li:last-of-type a:hover)::before,
133
- &:has(li:last-of-type a:hover)::after {
134
- @supports (corner-shape: squircle) {
135
- border-radius: 50% 32px 32px 50%;
136
- }
137
- }
138
-
139
- @supports (corner-shape: squircle) {
140
- border-radius: 24px;
141
- corner-shape: squircle;
142
- }
143
-
144
- > ul {
145
- padding: 0;
146
- margin: 0;
147
- list-style: none;
148
- display: flex;
149
- gap: 24px;
150
-
151
- a {
152
- display: block;
153
- padding: 1rem;
154
- text-decoration: none;
155
- color: white;
156
- font-size: var(--step-7);
157
- }
158
- }
159
- }
160
- }
161
- }
162
- </style>
@@ -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>