@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,314 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* FadeInText — Fades in with blur + vertical translate.
|
|
4
|
+
*
|
|
5
|
+
* enhance=false (default): Pure CSS @keyframes plays on every page load / refresh.
|
|
6
|
+
* No JavaScript involved. Works with JS disabled.
|
|
7
|
+
*
|
|
8
|
+
* enhance=true: IntersectionObserver delays the animation until the element
|
|
9
|
+
* scrolls into view. Animation fires once (or repeatedly if once=false).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
/**
|
|
14
|
+
* Opt-in to JS-powered scroll-triggered animation (IntersectionObserver).
|
|
15
|
+
* When false (default), CSS @keyframes plays on every page load automatically.
|
|
16
|
+
* @default false
|
|
17
|
+
*/
|
|
18
|
+
enhance?: boolean;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Animation duration in seconds.
|
|
22
|
+
* @default 0.6
|
|
23
|
+
*/
|
|
24
|
+
duration?: number;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Delay before animation starts in seconds.
|
|
28
|
+
* CSS animation: applied as animation-delay.
|
|
29
|
+
* JS-enhanced: applied as transition-delay once element is in view.
|
|
30
|
+
* @default 0
|
|
31
|
+
*/
|
|
32
|
+
delay?: number;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* CSS easing / timing function.
|
|
36
|
+
* @default "cubic-bezier(0.4, 0, 0.2, 1)"
|
|
37
|
+
*/
|
|
38
|
+
easing?: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initial blur amount. Must include a CSS unit (e.g. "6px", "4px").
|
|
42
|
+
* @default "6px"
|
|
43
|
+
*/
|
|
44
|
+
blur?: string;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Vertical distance (px) the element travels upward during the animation.
|
|
48
|
+
* @default 20
|
|
49
|
+
*/
|
|
50
|
+
yOffset?: number;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* [enhance=true only] Play the animation only once (true) or reset and
|
|
54
|
+
* re-animate every time the element re-enters the viewport (false).
|
|
55
|
+
* Has no effect when enhance=false.
|
|
56
|
+
* @default true
|
|
57
|
+
*/
|
|
58
|
+
once?: boolean;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* [enhance=true only] IntersectionObserver bottom rootMargin.
|
|
62
|
+
* A negative value (e.g. "-50px") means the element must be this far
|
|
63
|
+
* inside the viewport before the animation fires.
|
|
64
|
+
* Has no effect when enhance=false.
|
|
65
|
+
* @default "-50px"
|
|
66
|
+
*/
|
|
67
|
+
inViewMargin?: string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* CSS class name applied to the root element.
|
|
71
|
+
*/
|
|
72
|
+
class?: string;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* HTML element rendered as the root wrapper.
|
|
76
|
+
* @default "div"
|
|
77
|
+
*/
|
|
78
|
+
as?:
|
|
79
|
+
| "div"
|
|
80
|
+
| "section"
|
|
81
|
+
| "article"
|
|
82
|
+
| "aside"
|
|
83
|
+
| "main"
|
|
84
|
+
| "header"
|
|
85
|
+
| "footer"
|
|
86
|
+
| "nav"
|
|
87
|
+
| "span"
|
|
88
|
+
| "p"
|
|
89
|
+
| "h1"
|
|
90
|
+
| "h2"
|
|
91
|
+
| "h3"
|
|
92
|
+
| "h4"
|
|
93
|
+
| "h5"
|
|
94
|
+
| "h6"
|
|
95
|
+
| "li"
|
|
96
|
+
| "ul"
|
|
97
|
+
| "ol"
|
|
98
|
+
| "a"
|
|
99
|
+
| "button";
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Any additional HTML attributes (id, aria-label, role, tabindex, data-*, …)
|
|
103
|
+
* are forwarded directly to the root element.
|
|
104
|
+
*/
|
|
105
|
+
[key: string]: unknown;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const {
|
|
109
|
+
enhance = false,
|
|
110
|
+
duration = 0.6,
|
|
111
|
+
delay = 0,
|
|
112
|
+
easing = "cubic-bezier(0.4, 0, 0.2, 1)",
|
|
113
|
+
blur = "6px",
|
|
114
|
+
yOffset = 20,
|
|
115
|
+
once = true,
|
|
116
|
+
inViewMargin = "-50px",
|
|
117
|
+
class: className = "",
|
|
118
|
+
as: Tag = "div",
|
|
119
|
+
...rest
|
|
120
|
+
} = Astro.props as Props;
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
<style>
|
|
124
|
+
/*
|
|
125
|
+
* Separate CSS paths for enhance=false vs enhance=true.
|
|
126
|
+
*
|
|
127
|
+
* CSS animation ONLY fires on data-enhance="false" elements.
|
|
128
|
+
* enhance=true elements start statically hidden — JS owns their animation.
|
|
129
|
+
* A 2s CSS fallback animation on enhance=true ensures content is never
|
|
130
|
+
* permanently invisible if JS fails to load.
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
/* ✅ C1 + C7: CSS-only path for enhance=false.
|
|
134
|
+
Plays automatically on every page load and page refresh.
|
|
135
|
+
fill-mode "both": element starts in "from" state during delay,
|
|
136
|
+
and stays in "to" state permanently after the animation ends. */
|
|
137
|
+
[data-astro-fade-in-text][data-enhance="false"]:not([data-ready]) {
|
|
138
|
+
animation: fadeInText var(--fit-duration) var(--fit-easing) var(--fit-delay) both;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* ✅ C1: Static hidden state for enhance=true, before JS runs.
|
|
142
|
+
CSS animation is intentionally suppressed here — JS will control it.
|
|
143
|
+
The 2s fallback animation is a safety net: if JS fails or is blocked,
|
|
144
|
+
content becomes visible after 2s instead of staying hidden forever. */
|
|
145
|
+
[data-astro-fade-in-text][data-enhance="true"]:not([data-ready]) {
|
|
146
|
+
opacity: 0;
|
|
147
|
+
filter: blur(var(--fit-blur));
|
|
148
|
+
transform: translateY(var(--fit-y-offset));
|
|
149
|
+
animation: fadeInText var(--fit-duration) var(--fit-easing) 2s both;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@keyframes fadeInText {
|
|
153
|
+
from {
|
|
154
|
+
opacity: 0;
|
|
155
|
+
filter: blur(var(--fit-blur));
|
|
156
|
+
transform: translateY(var(--fit-y-offset));
|
|
157
|
+
}
|
|
158
|
+
to {
|
|
159
|
+
opacity: 1;
|
|
160
|
+
filter: blur(0px);
|
|
161
|
+
transform: translateY(0);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* ── JS-enhanced states ─────────────────────────────────────────────────
|
|
166
|
+
Activated once JS sets data-ready="true". All visual changes are
|
|
167
|
+
driven purely by data-state attribute — JS never writes element.style.
|
|
168
|
+
──────────────────────────────────────────────────────────────────────── */
|
|
169
|
+
|
|
170
|
+
/* ✅ C7: Waiting for viewport entry */
|
|
171
|
+
[data-astro-fade-in-text][data-ready="true"][data-state="hidden"] {
|
|
172
|
+
opacity: 0;
|
|
173
|
+
filter: blur(var(--fit-blur));
|
|
174
|
+
transform: translateY(var(--fit-y-offset));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* ✅ C7: Element is in viewport — CSS transition to fully visible */
|
|
178
|
+
[data-astro-fade-in-text][data-ready="true"][data-state="animate"] {
|
|
179
|
+
opacity: 1;
|
|
180
|
+
filter: blur(0px);
|
|
181
|
+
transform: translateY(0);
|
|
182
|
+
transition:
|
|
183
|
+
opacity var(--fit-duration) var(--fit-easing) var(--fit-delay),
|
|
184
|
+
filter var(--fit-duration) var(--fit-easing) var(--fit-delay),
|
|
185
|
+
transform var(--fit-duration) var(--fit-easing) var(--fit-delay);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ✅ C6: ALL CSS motion neutralised for prefers-reduced-motion.
|
|
189
|
+
Covers both the CSS-only path and the JS-enhanced path.
|
|
190
|
+
Content is shown immediately regardless of animation state. */
|
|
191
|
+
@media (prefers-reduced-motion: reduce) {
|
|
192
|
+
[data-astro-fade-in-text]:not([data-ready]) {
|
|
193
|
+
animation: none !important;
|
|
194
|
+
opacity: 1 !important;
|
|
195
|
+
filter: none !important;
|
|
196
|
+
transform: none !important;
|
|
197
|
+
}
|
|
198
|
+
[data-astro-fade-in-text][data-ready="true"] {
|
|
199
|
+
opacity: 1 !important;
|
|
200
|
+
filter: none !important;
|
|
201
|
+
transform: none !important;
|
|
202
|
+
transition: none !important;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
</style>
|
|
206
|
+
|
|
207
|
+
<!-- ✅ C1: Slot content always present in HTML — meaningful without JS.
|
|
208
|
+
✅ C4: No import.meta.env.DEV — identical in dev, build, and preview.
|
|
209
|
+
✅ C5: No astro:* events. Standard data attributes only.
|
|
210
|
+
{...rest} forwards id, aria-label, role, tabindex, data-*, etc.
|
|
211
|
+
Style is a single inline string — no stray newlines in HTML output. -->
|
|
212
|
+
<Tag
|
|
213
|
+
{...rest}
|
|
214
|
+
class:list={["astro-fade-in-text", className]}
|
|
215
|
+
data-astro-fade-in-text
|
|
216
|
+
data-enhance={enhance ? "true" : "false"}
|
|
217
|
+
data-once={once ? "true" : "false"}
|
|
218
|
+
data-margin={inViewMargin}
|
|
219
|
+
style={`--fit-duration:${duration}s;--fit-delay:${delay}s;--fit-easing:${easing};--fit-blur:${blur};--fit-y-offset:${yOffset}px`}
|
|
220
|
+
>
|
|
221
|
+
<!-- ✅ C8: Slot always rendered. JS only adds timing/trigger control. -->
|
|
222
|
+
<slot />
|
|
223
|
+
</Tag>
|
|
224
|
+
|
|
225
|
+
<!-- ✅ C2: Script emitted ONLY when enhance=true. No unconditional scripts.
|
|
226
|
+
is:inline is required — Astro does NOT bundle <script> tags sourced
|
|
227
|
+
from node_modules. is:inline embeds the script verbatim in HTML output,
|
|
228
|
+
guaranteeing it ships regardless of the consumer's build configuration. -->
|
|
229
|
+
{enhance && (
|
|
230
|
+
<script is:inline>
|
|
231
|
+
(function () {
|
|
232
|
+
// ✅ C6: SSR guard — bail immediately if not in a browser context
|
|
233
|
+
if (typeof window === "undefined") return;
|
|
234
|
+
|
|
235
|
+
// ✅ C6: Reduced motion check BEFORE any initialisation
|
|
236
|
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
237
|
+
|
|
238
|
+
// ✅ C3: WeakMap stores IntersectionObserver per element for cleanup.
|
|
239
|
+
var initialized = new WeakMap();
|
|
240
|
+
|
|
241
|
+
function enhanceFadeInText(root) {
|
|
242
|
+
/*
|
|
243
|
+
* Cross-IIFE initialisation guard via DOM attribute.
|
|
244
|
+
*
|
|
245
|
+
* With N enhance=true FadeInText components on the page,
|
|
246
|
+
* N IIFEs execute. Each IIFE creates its own fresh WeakMap. Each IIFE's
|
|
247
|
+
* querySelectorAll finds ALL N elements. Since each WeakMap is always
|
|
248
|
+
* empty at that moment, initialized.has(root) is always false, so every
|
|
249
|
+
* IIFE enhances every element. N elements × N IIFEs = N² observers.
|
|
250
|
+
* Animations race; cleanup fires multiple times per element.
|
|
251
|
+
*
|
|
252
|
+
* AFTER (fix): data-ready is set directly on the DOM element, which is
|
|
253
|
+
* shared state across all IIFEs. The first IIFE to run sets
|
|
254
|
+
* data-ready="true"; every subsequent IIFE exits immediately on this
|
|
255
|
+
* check. Exactly one IntersectionObserver + one MutationObserver per
|
|
256
|
+
* element, regardless of how many components are on the page.
|
|
257
|
+
*/
|
|
258
|
+
if (root.dataset.ready === "true") return;
|
|
259
|
+
|
|
260
|
+
var isOnce = root.dataset.once === "true";
|
|
261
|
+
var margin = root.dataset.margin || "-50px";
|
|
262
|
+
|
|
263
|
+
// ✅ C7: JS sets data attributes only — never element.style.
|
|
264
|
+
//
|
|
265
|
+
// Setting data-ready="true" does three things simultaneously:
|
|
266
|
+
// 1. Acts as the cross-IFFE initialisation guard (see above).
|
|
267
|
+
// 2. Switches CSS from the static-hidden path to the data-state path.
|
|
268
|
+
// 3. Cancels the 2s CSS fallback animation (selector no longer matches).
|
|
269
|
+
root.dataset.ready = "true";
|
|
270
|
+
root.dataset.state = "hidden";
|
|
271
|
+
|
|
272
|
+
// ✅ C3: IntersectionObserver — tracked in WeakMap for cleanup
|
|
273
|
+
var observer = new IntersectionObserver(
|
|
274
|
+
function (entries) {
|
|
275
|
+
entries.forEach(function (entry) {
|
|
276
|
+
if (entry.isIntersecting) {
|
|
277
|
+
root.dataset.state = "animate"; // ✅ C7: attribute-only transition
|
|
278
|
+
if (isOnce) {
|
|
279
|
+
// once=true: stop observing after first intersection —
|
|
280
|
+
// no further callbacks, no unnecessary CPU work.
|
|
281
|
+
observer.unobserve(root);
|
|
282
|
+
}
|
|
283
|
+
} else if (!isOnce) {
|
|
284
|
+
// once=false: element scrolled back out — reset for re-animation.
|
|
285
|
+
root.dataset.state = "hidden";
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
{ threshold: 0.1, rootMargin: "0px 0px " + margin + " 0px" }
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
observer.observe(root);
|
|
293
|
+
initialized.set(root, observer); // ✅ C3: store ref for guaranteed cleanup
|
|
294
|
+
|
|
295
|
+
// ✅ C3: MutationObserver guarantees both observers are disconnected
|
|
296
|
+
// when the root element is removed from the DOM (SPA navigation,
|
|
297
|
+
// conditional rendering, island teardown, etc.).
|
|
298
|
+
var cleanupObserver = new MutationObserver(function () {
|
|
299
|
+
if (!document.contains(root)) {
|
|
300
|
+
observer.disconnect(); // ✅ C3: IntersectionObserver disconnected
|
|
301
|
+
cleanupObserver.disconnect(); // ✅ C3: MutationObserver disconnected
|
|
302
|
+
initialized.delete(root);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
cleanupObserver.observe(document.body, { childList: true, subtree: true });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
document
|
|
310
|
+
.querySelectorAll('[data-astro-fade-in-text][data-enhance="true"]')
|
|
311
|
+
.forEach(function (el) { enhanceFadeInText(el); });
|
|
312
|
+
})();
|
|
313
|
+
</script>
|
|
314
|
+
)}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js","sourcesContent":[]}
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
|
|
17
|
+
<style>
|
|
18
|
+
/* ✅ CRITERION 7: CSS-FIRST */
|
|
19
|
+
|
|
20
|
+
[data-fhb] {
|
|
21
|
+
position: relative;
|
|
22
|
+
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: center;
|
|
26
|
+
|
|
27
|
+
padding: 1em 1.5em;
|
|
28
|
+
|
|
29
|
+
border: none;
|
|
30
|
+
background: transparent;
|
|
31
|
+
|
|
32
|
+
color: #ffc506;
|
|
33
|
+
|
|
34
|
+
font-size: 1.0625rem;
|
|
35
|
+
text-transform: uppercase;
|
|
36
|
+
text-decoration: none;
|
|
37
|
+
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
|
|
40
|
+
overflow: hidden;
|
|
41
|
+
isolation: isolate;
|
|
42
|
+
|
|
43
|
+
transition: color 500ms ease;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Underline */
|
|
47
|
+
[data-fhb]::before {
|
|
48
|
+
content: "";
|
|
49
|
+
|
|
50
|
+
position: absolute;
|
|
51
|
+
left: 0;
|
|
52
|
+
bottom: 0;
|
|
53
|
+
|
|
54
|
+
width: 0;
|
|
55
|
+
height: 2px;
|
|
56
|
+
|
|
57
|
+
background: #ffc506;
|
|
58
|
+
|
|
59
|
+
transition: width 500ms ease;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Fill layer */
|
|
63
|
+
[data-fhb]::after {
|
|
64
|
+
content: "";
|
|
65
|
+
|
|
66
|
+
position: absolute;
|
|
67
|
+
inset: auto 0 0 0;
|
|
68
|
+
|
|
69
|
+
width: 100%;
|
|
70
|
+
height: 0;
|
|
71
|
+
|
|
72
|
+
background: #ffc506;
|
|
73
|
+
|
|
74
|
+
z-index: -1;
|
|
75
|
+
|
|
76
|
+
transition: height 400ms ease;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
[data-fhb]:hover {
|
|
80
|
+
color: #1e1e2b;
|
|
81
|
+
transition-delay: 500ms;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
[data-fhb]:hover::before {
|
|
85
|
+
width: 100%;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
[data-fhb]:hover::after {
|
|
89
|
+
height: 100%;
|
|
90
|
+
transition-delay: 400ms;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Accessibility */
|
|
94
|
+
[data-fhb]:focus-visible {
|
|
95
|
+
outline: 2px solid #ffc506;
|
|
96
|
+
outline-offset: 4px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* ✅ CRITERION 6: REDUCED MOTION */
|
|
100
|
+
@media (prefers-reduced-motion: reduce) {
|
|
101
|
+
[data-fhb],
|
|
102
|
+
[data-fhb]::before,
|
|
103
|
+
[data-fhb]::after {
|
|
104
|
+
transition: none !important;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
</style>
|
|
108
|
+
|
|
109
|
+
{as === "a" ? (
|
|
110
|
+
<a
|
|
111
|
+
data-fhb
|
|
112
|
+
href={href}
|
|
113
|
+
class={className}
|
|
114
|
+
>
|
|
115
|
+
<slot />
|
|
116
|
+
</a>
|
|
117
|
+
) : (
|
|
118
|
+
<button
|
|
119
|
+
data-fhb
|
|
120
|
+
type={type}
|
|
121
|
+
class={className}
|
|
122
|
+
>
|
|
123
|
+
<slot />
|
|
124
|
+
</button>
|
|
125
|
+
)}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
as?: "button" | "a";
|
|
4
|
+
href?: string;
|
|
5
|
+
stars?: number | string;
|
|
6
|
+
showStars?: boolean;
|
|
7
|
+
type?: "button" | "submit" | "reset";
|
|
8
|
+
class?: string;
|
|
9
|
+
fontFamily?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
as = "button",
|
|
14
|
+
href,
|
|
15
|
+
stars = 0,
|
|
16
|
+
showStars = true,
|
|
17
|
+
type = "button",
|
|
18
|
+
class: className = "",
|
|
19
|
+
fontFamily = "Inter, sans-serif",
|
|
20
|
+
} = Astro.props;
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<style>
|
|
24
|
+
/* ✅ CRITERION 7: CSS-FIRST */
|
|
25
|
+
|
|
26
|
+
[data-ghsb] {
|
|
27
|
+
position: relative;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
gap: 0.75rem;
|
|
33
|
+
|
|
34
|
+
height: 2.6rem;
|
|
35
|
+
padding-inline: 1rem;
|
|
36
|
+
|
|
37
|
+
border-radius: 0.5rem;
|
|
38
|
+
background: black;
|
|
39
|
+
color: white;
|
|
40
|
+
|
|
41
|
+
font-size: 0.875rem;
|
|
42
|
+
font-weight: 500;
|
|
43
|
+
text-decoration: none;
|
|
44
|
+
white-space: nowrap;
|
|
45
|
+
|
|
46
|
+
transition:
|
|
47
|
+
background-color 300ms ease,
|
|
48
|
+
box-shadow 300ms ease,
|
|
49
|
+
transform 300ms ease;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Shine */
|
|
53
|
+
[data-ghsb]::before {
|
|
54
|
+
content: "";
|
|
55
|
+
position: absolute;
|
|
56
|
+
top: -4rem;
|
|
57
|
+
right: 0;
|
|
58
|
+
|
|
59
|
+
width: 2rem;
|
|
60
|
+
height: 8rem;
|
|
61
|
+
|
|
62
|
+
transform: translateX(3rem) rotate(12deg);
|
|
63
|
+
|
|
64
|
+
background: rgba(255,255,255,0.1);
|
|
65
|
+
|
|
66
|
+
transition: transform 1000ms ease;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
[data-ghsb]:hover::before {
|
|
70
|
+
transform: translateX(-10rem) rotate(12deg);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
[data-ghsb]:hover {
|
|
74
|
+
background: rgba(0,0,0,0.9);
|
|
75
|
+
box-shadow: 0 0 0 2px white;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.main,
|
|
79
|
+
.stats {
|
|
80
|
+
position: relative;
|
|
81
|
+
z-index: 1;
|
|
82
|
+
|
|
83
|
+
display: inline-flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
gap: 0.35rem;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.github-icon,
|
|
89
|
+
.star-icon {
|
|
90
|
+
width: 1rem;
|
|
91
|
+
height: 1rem;
|
|
92
|
+
fill: currentColor;
|
|
93
|
+
flex-shrink: 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.star-icon {
|
|
97
|
+
color: #a3a3a3;
|
|
98
|
+
transition: color 300ms ease;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
[data-ghsb]:hover .star-icon {
|
|
102
|
+
color: #fde047;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.count {
|
|
106
|
+
font-variant-numeric: tabular-nums;
|
|
107
|
+
letter-spacing: 0.04em;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Accessibility */
|
|
111
|
+
[data-ghsb]:focus-visible {
|
|
112
|
+
outline: 2px solid white;
|
|
113
|
+
outline-offset: 4px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* ✅ CRITERION 6: REDUCED MOTION */
|
|
117
|
+
@media (prefers-reduced-motion: reduce) {
|
|
118
|
+
[data-ghsb],
|
|
119
|
+
[data-ghsb] * {
|
|
120
|
+
transition: none !important;
|
|
121
|
+
transform: none !important;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
</style>
|
|
125
|
+
|
|
126
|
+
{as === "a" ? (
|
|
127
|
+
<a
|
|
128
|
+
data-ghsb
|
|
129
|
+
href={href}
|
|
130
|
+
class={className}
|
|
131
|
+
style={`font-family: ${fontFamily};`}
|
|
132
|
+
>
|
|
133
|
+
<span class="main">
|
|
134
|
+
<svg
|
|
135
|
+
class="github-icon"
|
|
136
|
+
aria-hidden="true"
|
|
137
|
+
viewBox="0 0 24 24"
|
|
138
|
+
>
|
|
139
|
+
<path
|
|
140
|
+
fill="currentColor"
|
|
141
|
+
d="M12 .5C5.648.5.5 5.648.5 12a11.5 11.5 0 0 0 7.863 10.923c.575.106.787-.25.787-.556 0-.275-.012-1.188-.018-2.156-3.2.694-3.877-1.544-3.877-1.544-.525-1.337-1.282-1.694-1.282-1.694-1.05-.719.081-.706.081-.706 1.162.081 1.775 1.194 1.775 1.194 1.031 1.769 2.706 1.258 3.369.962.106-.75.406-1.256.737-1.544-2.556-.288-5.244-1.281-5.244-5.706 0-1.262.45-2.294 1.188-3.106-.119-.288-.519-1.45.112-3.019 0 0 .969-.31 3.175 1.188A11.06 11.06 0 0 1 12 6.094c.975.006 1.956.131 2.875.381 2.206-1.5 3.175-1.188 3.175-1.188.631 1.569.231 2.731.112 3.019.738.812 1.188 1.844 1.188 3.106 0 4.438-2.694 5.413-5.256 5.694.419.362.794 1.075.794 2.169 0 1.569-.013 2.831-.013 3.219 0 .306.206.669.794.556A11.503 11.503 0 0 0 23.5 12C23.5 5.648 18.352.5 12 .5Z"
|
|
142
|
+
/>
|
|
143
|
+
</svg>
|
|
144
|
+
|
|
145
|
+
<span>
|
|
146
|
+
<slot />
|
|
147
|
+
</span>
|
|
148
|
+
</span>
|
|
149
|
+
|
|
150
|
+
{showStars && (
|
|
151
|
+
<span class="stats">
|
|
152
|
+
<svg
|
|
153
|
+
class="star-icon"
|
|
154
|
+
aria-hidden="true"
|
|
155
|
+
viewBox="0 0 24 24"
|
|
156
|
+
>
|
|
157
|
+
<path
|
|
158
|
+
fill="currentColor"
|
|
159
|
+
d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"
|
|
160
|
+
/>
|
|
161
|
+
</svg>
|
|
162
|
+
|
|
163
|
+
<span class="count">{stars}</span>
|
|
164
|
+
</span>
|
|
165
|
+
)}
|
|
166
|
+
</a>
|
|
167
|
+
) : (
|
|
168
|
+
<button
|
|
169
|
+
data-ghsb
|
|
170
|
+
type={type}
|
|
171
|
+
class={className}
|
|
172
|
+
style={`font-family: ${fontFamily};`}
|
|
173
|
+
>
|
|
174
|
+
<span class="main">
|
|
175
|
+
<svg
|
|
176
|
+
class="github-icon"
|
|
177
|
+
aria-hidden="true"
|
|
178
|
+
viewBox="0 0 24 24"
|
|
179
|
+
>
|
|
180
|
+
<path
|
|
181
|
+
fill="currentColor"
|
|
182
|
+
d="M12 .5C5.648.5.5 5.648.5 12a11.5 11.5 0 0 0 7.863 10.923c.575.106.787-.25.787-.556 0-.275-.012-1.188-.018-2.156-3.2.694-3.877-1.544-3.877-1.544-.525-1.337-1.282-1.694-1.282-1.694-1.05-.719.081-.706.081-.706 1.162.081 1.775 1.194 1.775 1.194 1.031 1.769 2.706 1.258 3.369.962.106-.75.406-1.256.737-1.544-2.556-.288-5.244-1.281-5.244-5.706 0-1.262.45-2.294 1.188-3.106-.119-.288-.519-1.45.112-3.019 0 0 .969-.31 3.175 1.188A11.06 11.06 0 0 1 12 6.094c.975.006 1.956.131 2.875.381 2.206-1.5 3.175-1.188 3.175-1.188.631 1.569.231 2.731.112 3.019.738.812 1.188 1.844 1.188 3.106 0 4.438-2.694 5.413-5.256 5.694.419.362.794 1.075.794 2.169 0 1.569-.013 2.831-.013 3.219 0 .306.206.669.794.556A11.503 11.503 0 0 0 23.5 12C23.5 5.648 18.352.5 12 .5Z"
|
|
183
|
+
/>
|
|
184
|
+
</svg>
|
|
185
|
+
|
|
186
|
+
<span>
|
|
187
|
+
<slot />
|
|
188
|
+
</span>
|
|
189
|
+
</span>
|
|
190
|
+
|
|
191
|
+
{showStars && (
|
|
192
|
+
<span class="stats">
|
|
193
|
+
<svg
|
|
194
|
+
class="star-icon"
|
|
195
|
+
aria-hidden="true"
|
|
196
|
+
viewBox="0 0 24 24"
|
|
197
|
+
>
|
|
198
|
+
<path
|
|
199
|
+
fill="currentColor"
|
|
200
|
+
d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"
|
|
201
|
+
/>
|
|
202
|
+
</svg>
|
|
203
|
+
|
|
204
|
+
<span class="count">{stars}</span>
|
|
205
|
+
</span>
|
|
206
|
+
)}
|
|
207
|
+
</button>
|
|
208
|
+
)}
|