@anubis609/astroanimate-core 0.1.2
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.
- package/LICENSE +21 -0
- package/README.md +212 -0
- package/dist/components/AnimatedBorderButton/AnimatedBorderButton.astro +129 -0
- package/dist/components/AnimatedBorderButton/index.js +3 -0
- package/dist/components/AnimatedBorderButton/index.js.map +1 -0
- package/dist/components/AnimatedButton/AnimatedButton.astro +299 -0
- package/dist/components/AnimatedButton/index.js +3 -0
- package/dist/components/AnimatedButton/index.js.map +1 -0
- package/dist/components/AnimatedCard/AnimatedCard.astro +832 -0
- package/dist/components/AnimatedCard/index.js +3 -0
- package/dist/components/AnimatedCard/index.js.map +1 -0
- package/dist/components/AnimatedTabs/AnimatedTabs.astro +348 -0
- package/dist/components/AnimatedTabs/index.js +3 -0
- package/dist/components/AnimatedTabs/index.js.map +1 -0
- package/dist/components/ArrowCTAButton/ArrowCTAButton.astro +159 -0
- package/dist/components/ArticleCard/ArticleCard.astro +208 -0
- package/dist/components/CardStack/CardStack.astro +444 -0
- package/dist/components/CardStack/index.js +3 -0
- package/dist/components/CardStack/index.js.map +1 -0
- package/dist/components/CountUp/CountUp.astro +89 -0
- package/dist/components/CountUp/index.js +3 -0
- package/dist/components/CountUp/index.js.map +1 -0
- package/dist/components/Dock/Dock.astro +567 -0
- package/dist/components/Dock/DockItem.astro +135 -0
- package/dist/components/Dropdown/Dropdown.astro +264 -0
- package/dist/components/ExpandableCard/ExpandableCard.astro +402 -0
- package/dist/components/ExpandableCard/index.js +3 -0
- package/dist/components/ExpandableCard/index.js.map +1 -0
- package/dist/components/FadeInText/FadeInText.astro +314 -0
- package/dist/components/FadeInText/index.js +3 -0
- package/dist/components/FadeInText/index.js.map +1 -0
- package/dist/components/FillHoverButton/FillHoverButton.astro +125 -0
- package/dist/components/GitHubShineButton/GitHubShineButton.astro +208 -0
- package/dist/components/GlassCard/GlassCard.astro +245 -0
- package/dist/components/GlassCard/index.js +3 -0
- package/dist/components/GlassCard/index.js.map +1 -0
- package/dist/components/GridDotsBackground/GridDotsBackground.astro +144 -0
- package/dist/components/HighlightText/HighlightText.astro +106 -0
- package/dist/components/InfiniteMarquee/InfiniteMarquee.astro +339 -0
- package/dist/components/JobCard/JobCard.astro +230 -0
- package/dist/components/LiquidGlassCard/LiquidGlassCard.astro +569 -0
- package/dist/components/Loader/Loader.astro +156 -0
- package/dist/components/Loader/index.js +3 -0
- package/dist/components/Loader/index.js.map +1 -0
- package/dist/components/NewsletterPopupCard/NewsletterPopupCard.astro +331 -0
- package/dist/components/ProductReviewCard/ProductReviewCard.astro +188 -0
- package/dist/components/ProgressBar/ProgressBar.astro +137 -0
- package/dist/components/ProgressBar/index.js +3 -0
- package/dist/components/ProgressBar/index.js.map +1 -0
- package/dist/components/RevealImage/RevealImage.astro +160 -0
- package/dist/components/RevealImage/index.js +3 -0
- package/dist/components/RevealImage/index.js.map +1 -0
- package/dist/components/ScaleIn/ScaleIn.astro +231 -0
- package/dist/components/ScaleIn/index.js +3 -0
- package/dist/components/ScaleIn/index.js.map +1 -0
- package/dist/components/SlidingOverlayButton/SlidingOverlayButton.astro +126 -0
- package/dist/components/StaggerTextButton/StaggerTextButton.astro +132 -0
- package/dist/components/Tooltip/Tooltip.astro +255 -0
- package/dist/components/Tooltip/index.js +3 -0
- package/dist/components/Tooltip/index.js.map +1 -0
- package/dist/components/TypewriterText/TypewriterText.astro +380 -0
- package/dist/components/TypewriterText/index.js +3 -0
- package/dist/components/TypewriterText/index.js.map +1 -0
- package/dist/components/index.js +33 -0
- package/dist/components/index.js.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/countup.js +90 -0
- package/dist/internal/countup.js.map +1 -0
- package/dist/internal/dropdown.js +166 -0
- package/dist/internal/dropdown.js.map +1 -0
- package/dist/internal/fadein.js +116 -0
- package/dist/internal/fadein.js.map +1 -0
- package/dist/internal/guards.js +12 -0
- package/dist/internal/guards.js.map +1 -0
- package/dist/internal/tabs.js +140 -0
- package/dist/internal/tabs.js.map +1 -0
- package/package.json +229 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* RevealImage — Displays text with two images that reveal on hover.
|
|
4
|
+
*
|
|
5
|
+
* Pure CSS component with no JavaScript. Images are initially small
|
|
6
|
+
* and expand to full size on hover, with one image rotated for visual effect.
|
|
7
|
+
* Respects prefers-reduced-motion to disable animations.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
/**
|
|
12
|
+
* Text to display.
|
|
13
|
+
* @required
|
|
14
|
+
*/
|
|
15
|
+
text: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* First image source URL.
|
|
19
|
+
* @required
|
|
20
|
+
*/
|
|
21
|
+
image1: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Second image source URL.
|
|
25
|
+
* @required
|
|
26
|
+
*/
|
|
27
|
+
image2: string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Alt text for images.
|
|
31
|
+
* @default ""
|
|
32
|
+
*/
|
|
33
|
+
alt?: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Optional class applied to the root element.
|
|
37
|
+
*/
|
|
38
|
+
class?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
text,
|
|
43
|
+
image1,
|
|
44
|
+
image2,
|
|
45
|
+
alt = "",
|
|
46
|
+
class: className = "",
|
|
47
|
+
} = Astro.props;
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
<style>
|
|
51
|
+
.reveal-image-item {
|
|
52
|
+
position: relative;
|
|
53
|
+
padding: 32px 0;
|
|
54
|
+
overflow: visible;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.reveal-image-item-text {
|
|
58
|
+
font-size: 72px;
|
|
59
|
+
font-weight: 900;
|
|
60
|
+
color: #000;
|
|
61
|
+
transition: opacity 0.5s ease;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@media (prefers-color-scheme: dark) {
|
|
65
|
+
.reveal-image-item-text {
|
|
66
|
+
color: #000;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.reveal-image-item:hover .reveal-image-item-text {
|
|
71
|
+
opacity: 0.4;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.reveal-image-container {
|
|
75
|
+
position: absolute;
|
|
76
|
+
right: 32px;
|
|
77
|
+
top: -4px;
|
|
78
|
+
z-index: 40;
|
|
79
|
+
height: 80px;
|
|
80
|
+
width: 64px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.reveal-image-effect {
|
|
84
|
+
position: relative;
|
|
85
|
+
duration: 500ms;
|
|
86
|
+
delay: 100ms;
|
|
87
|
+
scale: 0;
|
|
88
|
+
opacity: 0;
|
|
89
|
+
width: 64px;
|
|
90
|
+
height: 64px;
|
|
91
|
+
overflow: hidden;
|
|
92
|
+
border-radius: 6px;
|
|
93
|
+
transition: all 0.5s ease 0.1s;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.reveal-image-item:hover .reveal-image-effect {
|
|
97
|
+
scale: 1;
|
|
98
|
+
opacity: 1;
|
|
99
|
+
width: 100%;
|
|
100
|
+
height: 100%;
|
|
101
|
+
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.reveal-image-effect img {
|
|
105
|
+
width: 300px;
|
|
106
|
+
height: 300px;
|
|
107
|
+
object-fit: cover;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.reveal-image-container-rotated {
|
|
111
|
+
position: absolute;
|
|
112
|
+
right: 32px;
|
|
113
|
+
top: -4px;
|
|
114
|
+
z-index: 40;
|
|
115
|
+
height: 80px;
|
|
116
|
+
width: 64px;
|
|
117
|
+
transition: all 0.5s ease 0.15s;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.reveal-image-item:hover .reveal-image-container-rotated {
|
|
121
|
+
transform: translateX(24px) translateY(24px) rotate(12deg);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@media (prefers-reduced-motion: reduce) {
|
|
125
|
+
.reveal-image-item-text,
|
|
126
|
+
.reveal-image-effect,
|
|
127
|
+
.reveal-image-container-rotated {
|
|
128
|
+
transition: none;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.reveal-image-item:hover .reveal-image-item-text {
|
|
132
|
+
opacity: 1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.reveal-image-item:hover .reveal-image-effect {
|
|
136
|
+
scale: 1;
|
|
137
|
+
opacity: 1;
|
|
138
|
+
width: 64px;
|
|
139
|
+
height: 64px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.reveal-image-item:hover .reveal-image-container-rotated {
|
|
143
|
+
transform: none;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
</style>
|
|
147
|
+
|
|
148
|
+
<div class:list={["reveal-image-item", className]}>
|
|
149
|
+
<h1 class="reveal-image-item-text">{text}</h1>
|
|
150
|
+
<div class="reveal-image-container">
|
|
151
|
+
<div class="reveal-image-effect">
|
|
152
|
+
<img alt={alt} src={image2} loading="lazy" />
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="reveal-image-container-rotated">
|
|
156
|
+
<div class="reveal-image-effect" style="transition-duration: 0.2s;">
|
|
157
|
+
<img alt={alt} src={image1} loading="lazy" />
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js","sourcesContent":[]}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
|
|
4
|
+
type ScaleInTag =
|
|
5
|
+
| "div"
|
|
6
|
+
| "section"
|
|
7
|
+
| "article"
|
|
8
|
+
| "aside"
|
|
9
|
+
| "main"
|
|
10
|
+
| "header"
|
|
11
|
+
| "footer"
|
|
12
|
+
| "nav"
|
|
13
|
+
| "span"
|
|
14
|
+
| "p"
|
|
15
|
+
| "h1"
|
|
16
|
+
| "h2"
|
|
17
|
+
| "h3"
|
|
18
|
+
| "h4"
|
|
19
|
+
| "h5"
|
|
20
|
+
| "h6"
|
|
21
|
+
| "li"
|
|
22
|
+
| "ul"
|
|
23
|
+
| "ol"
|
|
24
|
+
| "a"
|
|
25
|
+
| "button";
|
|
26
|
+
|
|
27
|
+
interface Props extends HTMLAttributes<"div"> {
|
|
28
|
+
/**
|
|
29
|
+
* Explicit opt-in to JS enhancement (Scroll Reveal).
|
|
30
|
+
* If false, component renders visible immediately with no animation.
|
|
31
|
+
* @default false
|
|
32
|
+
*/
|
|
33
|
+
enhance?: boolean;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initial scale value to start from.
|
|
37
|
+
* @default 0.9
|
|
38
|
+
*/
|
|
39
|
+
initialScale?: number;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Duration of animation in milliseconds.
|
|
43
|
+
* @default 600
|
|
44
|
+
*/
|
|
45
|
+
duration?: number;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Delay before animation starts in milliseconds.
|
|
49
|
+
* @default 0
|
|
50
|
+
*/
|
|
51
|
+
delay?: number;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* CSS easing function.
|
|
55
|
+
* @default "cubic-bezier(0.34, 1.56, 0.64, 1)"
|
|
56
|
+
*/
|
|
57
|
+
easing?: string;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether animation should only play once.
|
|
61
|
+
* If false, it reverses when scrolling out.
|
|
62
|
+
* @default true
|
|
63
|
+
*/
|
|
64
|
+
once?: boolean;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* IntersectionObserver margin.
|
|
68
|
+
* @default "0px 0px -10% 0px"
|
|
69
|
+
*/
|
|
70
|
+
margin?: string;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Optional CSS class for the root element.
|
|
74
|
+
*/
|
|
75
|
+
class?: string;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* HTML tag to use for the root element.
|
|
79
|
+
* @default "div"
|
|
80
|
+
*/
|
|
81
|
+
as?: ScaleInTag;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const {
|
|
85
|
+
enhance = false,
|
|
86
|
+
initialScale = 0.9,
|
|
87
|
+
duration = 600,
|
|
88
|
+
delay = 0,
|
|
89
|
+
easing = "cubic-bezier(0.34, 1.56, 0.64, 1)",
|
|
90
|
+
once = true,
|
|
91
|
+
margin = "0px 0px -10% 0px",
|
|
92
|
+
class: className = "",
|
|
93
|
+
as: Tag = "div",
|
|
94
|
+
...rest
|
|
95
|
+
} = Astro.props as Props;
|
|
96
|
+
|
|
97
|
+
const mergedStyles = `
|
|
98
|
+
--duration: ${duration}ms;
|
|
99
|
+
--delay: ${delay}ms;
|
|
100
|
+
--easing: ${easing};
|
|
101
|
+
--initial-scale: ${initialScale};
|
|
102
|
+
`;
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
<style>
|
|
106
|
+
/**
|
|
107
|
+
* ✅ CRITERION 1: Visible Without JS
|
|
108
|
+
* The default state is fully visible with no transform.
|
|
109
|
+
*/
|
|
110
|
+
[data-astro-scale-in] {
|
|
111
|
+
opacity: 1;
|
|
112
|
+
transform: scale(1);
|
|
113
|
+
will-change: transform, opacity;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* ✅ CRITERION 7: CSS-First (States)
|
|
118
|
+
* Hidden state only applies if JS has marked the component as 'ready'.
|
|
119
|
+
*/
|
|
120
|
+
[data-astro-scale-in][data-ready="true"][data-state="hidden"] {
|
|
121
|
+
opacity: 0;
|
|
122
|
+
transform: scale(var(--initial-scale));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* ✅ CRITERION 7: Visible state handles the transition.
|
|
127
|
+
*/
|
|
128
|
+
[data-astro-scale-in][data-ready="true"][data-state="visible"] {
|
|
129
|
+
opacity: 1;
|
|
130
|
+
transform: scale(1);
|
|
131
|
+
transition:
|
|
132
|
+
opacity var(--duration) var(--easing) var(--delay),
|
|
133
|
+
transform var(--duration) var(--easing) var(--delay);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* ✅ CRITERION 6: Reduced Motion Respected
|
|
138
|
+
* All animations disabled if the user prefers reduced motion.
|
|
139
|
+
*/
|
|
140
|
+
@media (prefers-reduced-motion: reduce) {
|
|
141
|
+
[data-astro-scale-in] {
|
|
142
|
+
opacity: 1 !important;
|
|
143
|
+
transform: scale(1) !important;
|
|
144
|
+
transition: none !important;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
</style>
|
|
148
|
+
|
|
149
|
+
<!-- ✅ CRITERION 1: Meaningful HTML content exists -->
|
|
150
|
+
<Tag
|
|
151
|
+
class:list={["astro-scale-in", className]}
|
|
152
|
+
data-astro-scale-in
|
|
153
|
+
data-enhance={enhance ? "true" : "false"}
|
|
154
|
+
data-once={once ? "true" : "false"}
|
|
155
|
+
data-margin={margin}
|
|
156
|
+
style={mergedStyles}
|
|
157
|
+
{...rest}
|
|
158
|
+
>
|
|
159
|
+
<slot />
|
|
160
|
+
</Tag>
|
|
161
|
+
|
|
162
|
+
<!-- ✅ CRITERION 2: Zero Hydration (Vanilla script) -->
|
|
163
|
+
{enhance && (
|
|
164
|
+
<script is:inline>
|
|
165
|
+
(function () {
|
|
166
|
+
if (typeof window === "undefined") return;
|
|
167
|
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
168
|
+
|
|
169
|
+
var initialized = new WeakMap();
|
|
170
|
+
|
|
171
|
+
function enhanceScaleIn(root) {
|
|
172
|
+
if (!(root instanceof HTMLElement)) return;
|
|
173
|
+
if (initialized.has(root)) return;
|
|
174
|
+
|
|
175
|
+
root.dataset.ready = "true";
|
|
176
|
+
root.dataset.state = "hidden";
|
|
177
|
+
|
|
178
|
+
var isOnce = root.dataset.once === "true";
|
|
179
|
+
var margin = root.dataset.margin || "0px 0px -10% 0px";
|
|
180
|
+
|
|
181
|
+
var cleanupObserver;
|
|
182
|
+
|
|
183
|
+
var observer = new IntersectionObserver(
|
|
184
|
+
function (entries) {
|
|
185
|
+
entries.forEach(function (entry) {
|
|
186
|
+
if (entry.target !== root) return;
|
|
187
|
+
|
|
188
|
+
if (entry.isIntersecting) {
|
|
189
|
+
root.dataset.state = "visible";
|
|
190
|
+
|
|
191
|
+
if (isOnce) {
|
|
192
|
+
observer.unobserve(root);
|
|
193
|
+
observer.disconnect();
|
|
194
|
+
cleanupObserver.disconnect();
|
|
195
|
+
initialized.delete(root);
|
|
196
|
+
}
|
|
197
|
+
} else if (!isOnce) {
|
|
198
|
+
root.dataset.state = "hidden";
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
rootMargin: margin,
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
observer.observe(root);
|
|
208
|
+
initialized.set(root, observer);
|
|
209
|
+
|
|
210
|
+
cleanupObserver = new MutationObserver(function () {
|
|
211
|
+
if (!document.contains(root)) {
|
|
212
|
+
observer.disconnect();
|
|
213
|
+
cleanupObserver.disconnect();
|
|
214
|
+
initialized.delete(root);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
cleanupObserver.observe(document.body, {
|
|
219
|
+
childList: true,
|
|
220
|
+
subtree: true,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
document
|
|
225
|
+
.querySelectorAll('[data-astro-scale-in][data-enhance="true"]:not([data-ready])')
|
|
226
|
+
.forEach(function (element) {
|
|
227
|
+
enhanceScaleIn(element);
|
|
228
|
+
});
|
|
229
|
+
})();
|
|
230
|
+
</script>
|
|
231
|
+
)}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js","sourcesContent":[]}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
as?: "button" | "a";
|
|
4
|
+
href?: string;
|
|
5
|
+
type?: "button" | "submit" | "reset";
|
|
6
|
+
class?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
as = "button",
|
|
11
|
+
href,
|
|
12
|
+
type = "button",
|
|
13
|
+
class: className = "",
|
|
14
|
+
} = Astro.props;
|
|
15
|
+
|
|
16
|
+
const Tag = as === "a" ? "a" : "button";
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<style>
|
|
20
|
+
/* ✅ CRITERION 7: CSS-FIRST (all visuals handled in CSS) */
|
|
21
|
+
[data-sob] {
|
|
22
|
+
position: relative;
|
|
23
|
+
font-family: inherit;
|
|
24
|
+
font-weight: 500;
|
|
25
|
+
font-size: 18px;
|
|
26
|
+
letter-spacing: 0.05em;
|
|
27
|
+
border-radius: 0.8em;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
border: none;
|
|
30
|
+
background: linear-gradient(to right, #8e2de2, #4a00e0);
|
|
31
|
+
color: ghostwhite;
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
display: inline-block;
|
|
34
|
+
text-decoration: none;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Inner content */
|
|
38
|
+
[data-sob] > span {
|
|
39
|
+
position: relative;
|
|
40
|
+
z-index: 1;
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
padding: 0.8em 1.2em 0.8em 1.05em;
|
|
44
|
+
|
|
45
|
+
/* Animation */
|
|
46
|
+
transition: color 0.4s;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Icon sizing (optional content) */
|
|
50
|
+
[data-sob] svg {
|
|
51
|
+
width: 1.2em;
|
|
52
|
+
height: 1.2em;
|
|
53
|
+
margin-right: 0.5em;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Overlay layers */
|
|
57
|
+
[data-sob]::before,
|
|
58
|
+
[data-sob]::after {
|
|
59
|
+
content: "";
|
|
60
|
+
position: absolute;
|
|
61
|
+
inset: 0;
|
|
62
|
+
z-index: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Sliding overlay */
|
|
66
|
+
[data-sob]::before {
|
|
67
|
+
background: #000;
|
|
68
|
+
width: 120%;
|
|
69
|
+
left: -10%;
|
|
70
|
+
transform: skew(30deg);
|
|
71
|
+
|
|
72
|
+
/* Animation */
|
|
73
|
+
transition: transform 0.4s cubic-bezier(0.3, 1, 0.8, 1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Hover effect */
|
|
77
|
+
[data-sob]:hover::before {
|
|
78
|
+
transform: translate3d(100%, 0, 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Active press */
|
|
82
|
+
[data-sob]:active {
|
|
83
|
+
transform: scale(0.95);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Focus accessibility */
|
|
87
|
+
[data-sob]:focus-visible {
|
|
88
|
+
outline: 2px solid #fff;
|
|
89
|
+
outline-offset: 4px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Box sizing */
|
|
93
|
+
[data-sob],
|
|
94
|
+
[data-sob]::before,
|
|
95
|
+
[data-sob]::after {
|
|
96
|
+
box-sizing: border-box;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* ✅ CRITERION 6: REDUCED MOTION (disable ALL motion) */
|
|
100
|
+
@media (prefers-reduced-motion: reduce) {
|
|
101
|
+
[data-sob],
|
|
102
|
+
[data-sob] * {
|
|
103
|
+
transition: none !important;
|
|
104
|
+
transform: none !important;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
[data-sob]::before {
|
|
108
|
+
transform: none !important;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
</style>
|
|
112
|
+
|
|
113
|
+
<!-- ✅ CRITERION 1: Meaningful HTML without JS -->
|
|
114
|
+
{as === "a" ? (
|
|
115
|
+
<a data-sob href={href} class={className}>
|
|
116
|
+
<span>
|
|
117
|
+
<slot />
|
|
118
|
+
</span>
|
|
119
|
+
</a>
|
|
120
|
+
) : (
|
|
121
|
+
<button data-sob type={type} class={className}>
|
|
122
|
+
<span>
|
|
123
|
+
<slot />
|
|
124
|
+
</span>
|
|
125
|
+
</button>
|
|
126
|
+
)}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
as?: "button" | "a";
|
|
4
|
+
href?: string;
|
|
5
|
+
label: string;
|
|
6
|
+
type?: "button" | "submit" | "reset";
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
as = "button",
|
|
12
|
+
href,
|
|
13
|
+
label,
|
|
14
|
+
type = "button",
|
|
15
|
+
class: className = "",
|
|
16
|
+
} = Astro.props;
|
|
17
|
+
|
|
18
|
+
const chars = [...label];
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<style>
|
|
22
|
+
/* ✅ CRITERION 7: CSS-FIRST */
|
|
23
|
+
|
|
24
|
+
[data-stb] {
|
|
25
|
+
position: relative;
|
|
26
|
+
|
|
27
|
+
display: inline-flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: center;
|
|
30
|
+
|
|
31
|
+
padding-inline: 1.5rem;
|
|
32
|
+
min-height: 2.75rem;
|
|
33
|
+
|
|
34
|
+
border: none;
|
|
35
|
+
border-radius: 999px;
|
|
36
|
+
|
|
37
|
+
background: #3653f8;
|
|
38
|
+
color: white;
|
|
39
|
+
|
|
40
|
+
font-weight: 700;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
|
|
44
|
+
text-decoration: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.layer {
|
|
48
|
+
display: flex;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.layer.secondary {
|
|
53
|
+
position: absolute;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.layer.secondary span {
|
|
57
|
+
transform: translateY(-1.2em);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
[data-stb]:hover .layer.primary span {
|
|
61
|
+
transform: translateY(1.2em);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
[data-stb]:hover .layer.secondary span {
|
|
65
|
+
transform: translateY(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.layer span {
|
|
69
|
+
display: inline-block;
|
|
70
|
+
transition: transform 300ms ease;
|
|
71
|
+
white-space: pre;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Stagger system */
|
|
75
|
+
.layer span:nth-child(1) { transition-delay: 0ms; }
|
|
76
|
+
.layer span:nth-child(2) { transition-delay: 50ms; }
|
|
77
|
+
.layer span:nth-child(3) { transition-delay: 100ms; }
|
|
78
|
+
.layer span:nth-child(4) { transition-delay: 150ms; }
|
|
79
|
+
.layer span:nth-child(5) { transition-delay: 200ms; }
|
|
80
|
+
.layer span:nth-child(6) { transition-delay: 250ms; }
|
|
81
|
+
.layer span:nth-child(7) { transition-delay: 300ms; }
|
|
82
|
+
.layer span:nth-child(8) { transition-delay: 350ms; }
|
|
83
|
+
.layer span:nth-child(9) { transition-delay: 400ms; }
|
|
84
|
+
.layer span:nth-child(10) { transition-delay: 450ms; }
|
|
85
|
+
|
|
86
|
+
/* Accessibility */
|
|
87
|
+
[data-stb]:focus-visible {
|
|
88
|
+
outline: 2px solid white;
|
|
89
|
+
outline-offset: 4px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ✅ CRITERION 6: REDUCED MOTION */
|
|
93
|
+
@media (prefers-reduced-motion: reduce) {
|
|
94
|
+
[data-stb],
|
|
95
|
+
[data-stb] * {
|
|
96
|
+
transition: none !important;
|
|
97
|
+
transform: none !important;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
101
|
+
|
|
102
|
+
{as === "a" ? (
|
|
103
|
+
<a
|
|
104
|
+
data-stb
|
|
105
|
+
href={href}
|
|
106
|
+
class={className}
|
|
107
|
+
aria-label={label}
|
|
108
|
+
>
|
|
109
|
+
<span class="layer primary" aria-hidden="true">
|
|
110
|
+
{chars.map((char) => <span>{char}</span>)}
|
|
111
|
+
</span>
|
|
112
|
+
|
|
113
|
+
<span class="layer secondary" aria-hidden="true">
|
|
114
|
+
{chars.map((char) => <span>{char}</span>)}
|
|
115
|
+
</span>
|
|
116
|
+
</a>
|
|
117
|
+
) : (
|
|
118
|
+
<button
|
|
119
|
+
data-stb
|
|
120
|
+
type={type}
|
|
121
|
+
class={className}
|
|
122
|
+
aria-label={label}
|
|
123
|
+
>
|
|
124
|
+
<span class="layer primary" aria-hidden="true">
|
|
125
|
+
{chars.map((char) => <span>{char}</span>)}
|
|
126
|
+
</span>
|
|
127
|
+
|
|
128
|
+
<span class="layer secondary" aria-hidden="true">
|
|
129
|
+
{chars.map((char) => <span>{char}</span>)}
|
|
130
|
+
</span>
|
|
131
|
+
</button>
|
|
132
|
+
)}
|