@specglass/theme-default 0.0.9 → 0.0.11
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/dist/__tests__/design-tokens.test.d.ts +1 -0
- package/dist/__tests__/design-tokens.test.js +107 -0
- package/dist/islands/CopyButton.js +2 -6
- package/dist/islands/LanguageToggle.d.ts +16 -0
- package/dist/islands/LanguageToggle.js +88 -0
- package/dist/islands/SearchPalette.js +57 -8
- package/dist/islands/ThemeToggle.js +1 -1
- package/dist/scripts/code-block-enhancer.js +6 -3
- package/dist/themes/notdiamond-dark.json +168 -0
- package/dist/themes/notdiamond-light.json +168 -0
- package/dist/ui/command.js +2 -2
- package/dist/ui/dialog.js +2 -2
- package/dist/utils/shiki.d.ts +1 -1
- package/dist/utils/shiki.js +5 -3
- package/package.json +5 -3
- package/src/components/ApiAuth.astro +31 -4
- package/src/components/ApiEndpoint.astro +67 -44
- package/src/components/ApiNavigation.astro +8 -11
- package/src/components/ApiParameters.astro +113 -162
- package/src/components/ApiResponse.astro +1 -1
- package/src/components/Callout.astro +59 -18
- package/src/components/Card.astro +4 -4
- package/src/components/CodeBlock.astro +7 -7
- package/src/components/CodeBlockGroup.astro +3 -3
- package/src/components/CodeExample.astro +183 -0
- package/src/components/EditLink.astro +53 -0
- package/src/components/Footer.astro +87 -25
- package/src/components/Header.astro +63 -7
- package/src/components/Sidebar.astro +43 -11
- package/src/components/TableOfContents.astro +5 -5
- package/src/components/Tabs.astro +51 -20
- package/src/islands/CopyButton.tsx +36 -34
- package/src/islands/LanguageToggle.tsx +214 -0
- package/src/islands/SearchPalette.tsx +121 -39
- package/src/islands/ThemeToggle.tsx +45 -48
- package/src/layouts/ApiReferencePage.astro +67 -56
- package/src/layouts/DocPage.astro +32 -27
- package/src/layouts/LandingPage.astro +348 -27
- package/src/scripts/code-block-enhancer.ts +8 -3
- package/src/styles/global.css +388 -59
- package/src/themes/notdiamond-dark.json +168 -0
- package/src/themes/notdiamond-light.json +168 -0
- package/src/ui/command.tsx +1 -2
- package/src/ui/dialog.tsx +8 -5
- package/src/utils/shiki.ts +5 -3
|
@@ -5,16 +5,29 @@
|
|
|
5
5
|
* Used for the site index page or any page with `layout: landing` in frontmatter.
|
|
6
6
|
* Architecture mandate: one of three MVP layouts (DocPage, ApiReferencePage, LandingPage).
|
|
7
7
|
* Typed props from core: { frontmatter, content, navigation }.
|
|
8
|
+
*
|
|
9
|
+
* Story 8.4: Premium redesign with animated gradient mesh, feature cards grid,
|
|
10
|
+
* gradient text hero, section dividers, and hero_image support.
|
|
8
11
|
*/
|
|
9
12
|
import Header from "../components/Header.astro";
|
|
10
13
|
import Footer from "../components/Footer.astro";
|
|
11
14
|
import { ThemeToggle } from "../islands/ThemeToggle";
|
|
12
15
|
import { SearchPalette } from "../islands/SearchPalette";
|
|
16
|
+
|
|
13
17
|
import "../styles/global.css";
|
|
14
18
|
import type { NavigationTree } from "@specglass/core";
|
|
15
19
|
import { config } from "virtual:specglass/config";
|
|
16
20
|
import { generateThemeCSS } from "../utils/theme-css";
|
|
17
21
|
|
|
22
|
+
export interface FeatureCard {
|
|
23
|
+
title: string;
|
|
24
|
+
description: string;
|
|
25
|
+
icon?: string;
|
|
26
|
+
href?: string;
|
|
27
|
+
/** Card size in the Bento grid: "large" spans 2 columns, "default" spans 1 */
|
|
28
|
+
size?: "large" | "default";
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
export interface Props {
|
|
19
32
|
/** Page title from frontmatter */
|
|
20
33
|
title: string;
|
|
@@ -36,6 +49,11 @@ const ctaLabel = (frontmatter.cta_label as string) ?? "Get Started";
|
|
|
36
49
|
const ctaHref = (frontmatter.cta_href as string) ?? "/getting-started";
|
|
37
50
|
const ctaSecondaryLabel = frontmatter.cta_secondary_label as string | undefined;
|
|
38
51
|
const ctaSecondaryHref = frontmatter.cta_secondary_href as string | undefined;
|
|
52
|
+
const heroImage = frontmatter.hero_image as string | undefined;
|
|
53
|
+
|
|
54
|
+
// Feature cards from frontmatter
|
|
55
|
+
const features = (frontmatter.features as FeatureCard[] | undefined) ?? [];
|
|
56
|
+
const hasFeatures = features.length > 0;
|
|
39
57
|
---
|
|
40
58
|
|
|
41
59
|
<html lang="en" class="dark">
|
|
@@ -50,28 +68,21 @@ const ctaSecondaryHref = frontmatter.cta_secondary_href as string | undefined;
|
|
|
50
68
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
51
69
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
52
70
|
<link
|
|
53
|
-
href="https://fonts.googleapis.com/css2?family=
|
|
71
|
+
href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
|
|
54
72
|
rel="stylesheet"
|
|
55
73
|
/>
|
|
56
|
-
|
|
74
|
+
|
|
57
75
|
<script is:inline>
|
|
58
76
|
(function () {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
document.documentElement.classList.remove("dark");
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
applyStoredTheme();
|
|
72
|
-
if (!window.__sgThemeInit) {
|
|
73
|
-
window.__sgThemeInit = true;
|
|
74
|
-
document.addEventListener("astro:after-swap", applyStoredTheme);
|
|
77
|
+
var stored = null;
|
|
78
|
+
try {
|
|
79
|
+
stored = localStorage.getItem("specglass-theme");
|
|
80
|
+
} catch (e) {}
|
|
81
|
+
var theme = stored === "dark" || stored === "light" ? stored : "dark";
|
|
82
|
+
if (theme === "dark") {
|
|
83
|
+
document.documentElement.classList.add("dark");
|
|
84
|
+
} else {
|
|
85
|
+
document.documentElement.classList.remove("dark");
|
|
75
86
|
}
|
|
76
87
|
})();
|
|
77
88
|
</script>
|
|
@@ -80,7 +91,7 @@ const ctaSecondaryHref = frontmatter.cta_secondary_href as string | undefined;
|
|
|
80
91
|
<!-- Skip to content link for accessibility -->
|
|
81
92
|
<a
|
|
82
93
|
href="#main-content"
|
|
83
|
-
class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:bg-
|
|
94
|
+
class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:bg-text focus:text-surface focus:px-4 focus:py-2 focus:rounded"
|
|
84
95
|
>
|
|
85
96
|
Skip to content
|
|
86
97
|
</a>
|
|
@@ -91,30 +102,45 @@ const ctaSecondaryHref = frontmatter.cta_secondary_href as string | undefined;
|
|
|
91
102
|
</Header>
|
|
92
103
|
|
|
93
104
|
<main id="main-content" class="pt-(--height-header)" data-pagefind-body>
|
|
94
|
-
<!-- Hero section -->
|
|
95
|
-
<section
|
|
105
|
+
<!-- Hero section with animated gradient mesh -->
|
|
106
|
+
<section
|
|
107
|
+
class="landing-hero relative overflow-hidden py-16 sm:py-24 lg:py-32 px-6 text-center"
|
|
108
|
+
aria-label="Hero"
|
|
109
|
+
>
|
|
110
|
+
<!-- Animated gradient mesh background (3 blobs with transform animation) -->
|
|
111
|
+
<div class="absolute inset-0 overflow-hidden" aria-hidden="true">
|
|
112
|
+
<div class="landing-blob landing-blob-1"></div>
|
|
113
|
+
<div class="landing-blob landing-blob-2"></div>
|
|
114
|
+
<div class="landing-blob landing-blob-3"></div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<!-- Static gradient fallback layer -->
|
|
96
118
|
<div
|
|
97
|
-
class="absolute inset-0 bg-linear-to-br from-
|
|
119
|
+
class="absolute inset-0 bg-linear-to-br from-surface-1/50 via-transparent to-surface-1/30"
|
|
98
120
|
aria-hidden="true"
|
|
99
121
|
>
|
|
100
122
|
</div>
|
|
123
|
+
|
|
101
124
|
<div class="relative max-w-3xl mx-auto">
|
|
125
|
+
<!-- Hero title with gradient text -->
|
|
102
126
|
<h1
|
|
103
|
-
class="text-5xl font-bold tracking-tight mb-6
|
|
127
|
+
class="landing-fade-in text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight mb-6 text-text"
|
|
104
128
|
>
|
|
105
129
|
{heroTitle}
|
|
106
130
|
</h1>
|
|
107
131
|
{
|
|
108
132
|
heroSubtitle && (
|
|
109
|
-
<p class="text-xl text-text-muted mb-10 max-w-2xl mx-auto leading-relaxed">
|
|
133
|
+
<p class="landing-fade-in landing-delay-1 text-lg sm:text-xl text-text-muted mb-10 max-w-2xl mx-auto leading-relaxed">
|
|
110
134
|
{heroSubtitle}
|
|
111
135
|
</p>
|
|
112
136
|
)
|
|
113
137
|
}
|
|
114
|
-
|
|
138
|
+
|
|
139
|
+
<!-- CTA buttons -->
|
|
140
|
+
<div class="landing-fade-in landing-delay-2 flex justify-center gap-4 flex-wrap">
|
|
115
141
|
<a
|
|
116
142
|
href={ctaHref}
|
|
117
|
-
class="inline-flex items-center px-6 py-3 bg-
|
|
143
|
+
class="inline-flex items-center px-6 py-3 bg-text text-surface font-semibold rounded-lg hover:opacity-90 transition-opacity duration-150"
|
|
118
144
|
>
|
|
119
145
|
{ctaLabel}
|
|
120
146
|
</a>
|
|
@@ -122,16 +148,76 @@ const ctaSecondaryHref = frontmatter.cta_secondary_href as string | undefined;
|
|
|
122
148
|
ctaSecondaryLabel && ctaSecondaryHref && (
|
|
123
149
|
<a
|
|
124
150
|
href={ctaSecondaryHref}
|
|
125
|
-
class="inline-flex items-center px-6 py-3 border border-border text-text font-medium rounded-lg hover:bg-hover-bg transition-
|
|
151
|
+
class="inline-flex items-center px-6 py-3 border border-border text-text font-medium rounded-lg hover:bg-hover-bg transition-all duration-200 hover:-translate-y-0.5"
|
|
126
152
|
>
|
|
127
153
|
{ctaSecondaryLabel}
|
|
128
154
|
</a>
|
|
129
155
|
)
|
|
130
156
|
}
|
|
131
157
|
</div>
|
|
158
|
+
|
|
159
|
+
<!-- Optional hero image -->
|
|
160
|
+
{
|
|
161
|
+
heroImage && (
|
|
162
|
+
<div class="landing-fade-in landing-delay-3 mt-12 max-w-4xl mx-auto">
|
|
163
|
+
<img
|
|
164
|
+
src={heroImage}
|
|
165
|
+
alt={`${heroTitle} screenshot`}
|
|
166
|
+
class="rounded-xl shadow-2xl border border-border/50 w-full"
|
|
167
|
+
loading="eager"
|
|
168
|
+
/>
|
|
169
|
+
</div>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
132
172
|
</div>
|
|
133
173
|
</section>
|
|
134
174
|
|
|
175
|
+
<!-- Section divider: hero → features/content -->
|
|
176
|
+
<div class="landing-divider mx-auto" aria-hidden="true"></div>
|
|
177
|
+
|
|
178
|
+
<!-- Feature cards grid (from frontmatter) -->
|
|
179
|
+
{
|
|
180
|
+
hasFeatures && (
|
|
181
|
+
<section class="max-w-(--width-content-max) mx-auto px-6 py-12" aria-label="Features">
|
|
182
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
183
|
+
{features.map((feature: FeatureCard) => {
|
|
184
|
+
const isLarge = feature.size === "large";
|
|
185
|
+
const inner = (
|
|
186
|
+
<div
|
|
187
|
+
class:list={[
|
|
188
|
+
"landing-glow-card group rounded-xl border border-border p-6 transition-all duration-200",
|
|
189
|
+
"bg-surface-1 hover:border-border hover:-translate-y-0.5",
|
|
190
|
+
isLarge && "md:col-span-2",
|
|
191
|
+
!isLarge && "col-span-1",
|
|
192
|
+
feature.href && "cursor-pointer",
|
|
193
|
+
]}
|
|
194
|
+
>
|
|
195
|
+
{feature.icon && (
|
|
196
|
+
<div class="mb-4 text-3xl" aria-hidden="true">
|
|
197
|
+
{feature.icon}
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
<h3 class="text-lg font-semibold text-text mb-2">{feature.title}</h3>
|
|
201
|
+
<p class="text-sm text-text-muted leading-relaxed">{feature.description}</p>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
return feature.href ? (
|
|
206
|
+
<a href={feature.href} class="no-underline">
|
|
207
|
+
{inner}
|
|
208
|
+
</a>
|
|
209
|
+
) : (
|
|
210
|
+
inner
|
|
211
|
+
);
|
|
212
|
+
})}
|
|
213
|
+
</div>
|
|
214
|
+
</section>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
<!-- Section divider: features → content (only if features present) -->
|
|
219
|
+
{hasFeatures && <div class="landing-divider mx-auto" aria-hidden="true" />}
|
|
220
|
+
|
|
135
221
|
<!-- Content area (renders MDX body) -->
|
|
136
222
|
<article class="sg-content max-w-(--width-content-max) mx-auto px-6 py-12">
|
|
137
223
|
<slot />
|
|
@@ -141,3 +227,238 @@ const ctaSecondaryHref = frontmatter.cta_secondary_href as string | undefined;
|
|
|
141
227
|
<Footer />
|
|
142
228
|
</body>
|
|
143
229
|
</html>
|
|
230
|
+
|
|
231
|
+
<style>
|
|
232
|
+
/* ─── Gradient Blob Base ──────────────────────────────────── */
|
|
233
|
+
.landing-blob {
|
|
234
|
+
position: absolute;
|
|
235
|
+
border-radius: 50%;
|
|
236
|
+
will-change: transform;
|
|
237
|
+
/* Prevent blobs from triggering scrollbar */
|
|
238
|
+
pointer-events: none;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.landing-blob-1 {
|
|
242
|
+
width: 60%;
|
|
243
|
+
height: 60%;
|
|
244
|
+
top: 10%;
|
|
245
|
+
left: 5%;
|
|
246
|
+
background: radial-gradient(ellipse at center, oklch(0.82 0.24 135 / 0.08), transparent 70%);
|
|
247
|
+
}
|
|
248
|
+
.landing-blob-2 {
|
|
249
|
+
width: 50%;
|
|
250
|
+
height: 70%;
|
|
251
|
+
top: -10%;
|
|
252
|
+
right: 0;
|
|
253
|
+
background: radial-gradient(ellipse at center, oklch(0.88 0.2 135 / 0.06), transparent 70%);
|
|
254
|
+
}
|
|
255
|
+
.landing-blob-3 {
|
|
256
|
+
width: 55%;
|
|
257
|
+
height: 50%;
|
|
258
|
+
bottom: 0;
|
|
259
|
+
left: 20%;
|
|
260
|
+
background: radial-gradient(ellipse at center, oklch(0.82 0.24 135 / 0.04), transparent 70%);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
264
|
+
.landing-blob-1 {
|
|
265
|
+
animation: blob-drift-1 20s ease-in-out infinite alternate;
|
|
266
|
+
}
|
|
267
|
+
.landing-blob-2 {
|
|
268
|
+
animation: blob-drift-2 24s ease-in-out infinite alternate;
|
|
269
|
+
}
|
|
270
|
+
.landing-blob-3 {
|
|
271
|
+
animation: blob-drift-3 18s ease-in-out infinite alternate;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
@keyframes blob-drift-1 {
|
|
276
|
+
0% {
|
|
277
|
+
transform: translate(0, 0) scale(1);
|
|
278
|
+
}
|
|
279
|
+
50% {
|
|
280
|
+
transform: translate(10%, 15%) scale(1.1);
|
|
281
|
+
}
|
|
282
|
+
100% {
|
|
283
|
+
transform: translate(5%, -5%) scale(0.95);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
@keyframes blob-drift-2 {
|
|
287
|
+
0% {
|
|
288
|
+
transform: translate(0, 0) scale(1);
|
|
289
|
+
}
|
|
290
|
+
50% {
|
|
291
|
+
transform: translate(-15%, 10%) scale(1.05);
|
|
292
|
+
}
|
|
293
|
+
100% {
|
|
294
|
+
transform: translate(-5%, 20%) scale(1.1);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
@keyframes blob-drift-3 {
|
|
298
|
+
0% {
|
|
299
|
+
transform: translate(0, 0) scale(1);
|
|
300
|
+
}
|
|
301
|
+
50% {
|
|
302
|
+
transform: translate(15%, -10%) scale(1.15);
|
|
303
|
+
}
|
|
304
|
+
100% {
|
|
305
|
+
transform: translate(-10%, 5%) scale(0.9);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* ─── Dark mode blob opacity boost ───────────────────────── */
|
|
310
|
+
:global(.dark) .landing-blob-1 {
|
|
311
|
+
background: radial-gradient(ellipse at center, oklch(0.82 0.24 135 / 0.15), transparent 70%);
|
|
312
|
+
}
|
|
313
|
+
:global(.dark) .landing-blob-2 {
|
|
314
|
+
background: radial-gradient(ellipse at center, oklch(0.88 0.2 135 / 0.1), transparent 70%);
|
|
315
|
+
}
|
|
316
|
+
:global(.dark) .landing-blob-3 {
|
|
317
|
+
background: radial-gradient(ellipse at center, oklch(0.82 0.24 135 / 0.08), transparent 70%);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* ─── Entrance Animations ────────────────────────────────── */
|
|
321
|
+
.landing-fade-in {
|
|
322
|
+
opacity: 0;
|
|
323
|
+
transform: translateY(1rem);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
327
|
+
.landing-fade-in {
|
|
328
|
+
animation: landing-enter 150ms ease-out forwards;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.landing-delay-1 {
|
|
332
|
+
animation-delay: 50ms;
|
|
333
|
+
}
|
|
334
|
+
.landing-delay-2 {
|
|
335
|
+
animation-delay: 100ms;
|
|
336
|
+
}
|
|
337
|
+
.landing-delay-3 {
|
|
338
|
+
animation-delay: 150ms;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
@media (prefers-reduced-motion: reduce) {
|
|
343
|
+
.landing-fade-in {
|
|
344
|
+
opacity: 1;
|
|
345
|
+
transform: none;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
@keyframes landing-enter {
|
|
350
|
+
to {
|
|
351
|
+
opacity: 1;
|
|
352
|
+
transform: translateY(0);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* ─── Section Divider ────────────────────────────────────── */
|
|
357
|
+
.landing-divider {
|
|
358
|
+
height: 1px;
|
|
359
|
+
max-width: var(--width-content-max);
|
|
360
|
+
background: linear-gradient(
|
|
361
|
+
to right,
|
|
362
|
+
transparent,
|
|
363
|
+
oklch(0.82 0.24 135 / 0.3),
|
|
364
|
+
oklch(0.88 0.2 135 / 0.2),
|
|
365
|
+
transparent
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* ─── Mouse-Tracking Glow on Feature Cards ──────────────── */
|
|
370
|
+
/* The signature "intelligence" interaction from the Brand Architecture
|
|
371
|
+
* Report §5.2/§6.2. A radial gradient follows the cursor via CSS
|
|
372
|
+
* custom properties set by JS. Creates an "active compute" metaphor. */
|
|
373
|
+
.landing-glow-card {
|
|
374
|
+
position: relative;
|
|
375
|
+
overflow: hidden;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.landing-glow-card::after {
|
|
379
|
+
content: "";
|
|
380
|
+
position: absolute;
|
|
381
|
+
inset: -1px;
|
|
382
|
+
z-index: 0;
|
|
383
|
+
border-radius: inherit;
|
|
384
|
+
background: radial-gradient(
|
|
385
|
+
300px circle at var(--mouse-x, 50%) var(--mouse-y, 50%),
|
|
386
|
+
oklch(0.82 0.24 135 / 0.15),
|
|
387
|
+
transparent 40%
|
|
388
|
+
);
|
|
389
|
+
opacity: 0;
|
|
390
|
+
transition: opacity 150ms ease;
|
|
391
|
+
pointer-events: none;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.landing-glow-card:hover::after {
|
|
395
|
+
opacity: 1;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/* Ensure card content sits above the glow layer */
|
|
399
|
+
.landing-glow-card > * {
|
|
400
|
+
position: relative;
|
|
401
|
+
z-index: 1;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
@media (prefers-reduced-motion: reduce) {
|
|
405
|
+
.landing-glow-card::after {
|
|
406
|
+
transition: none;
|
|
407
|
+
}
|
|
408
|
+
/* Fallback: static border color change instead of glow */
|
|
409
|
+
.landing-glow-card:hover {
|
|
410
|
+
border-color: oklch(0.82 0.24 135 / 0.5);
|
|
411
|
+
}
|
|
412
|
+
.landing-glow-card:hover::after {
|
|
413
|
+
opacity: 0;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
</style>
|
|
417
|
+
|
|
418
|
+
<script>
|
|
419
|
+
function initGlowCards() {
|
|
420
|
+
const cards = document.querySelectorAll<HTMLElement>(".landing-glow-card");
|
|
421
|
+
if (!cards.length) return;
|
|
422
|
+
|
|
423
|
+
// Check if user prefers reduced motion
|
|
424
|
+
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
425
|
+
if (prefersReducedMotion) return;
|
|
426
|
+
|
|
427
|
+
let rafId: number | null = null;
|
|
428
|
+
|
|
429
|
+
function handleMouseMove(e: MouseEvent) {
|
|
430
|
+
const card = e.currentTarget as HTMLElement;
|
|
431
|
+
const rect = card.getBoundingClientRect();
|
|
432
|
+
// Position relative to card, as CSS values
|
|
433
|
+
const x = e.clientX - rect.left;
|
|
434
|
+
const y = e.clientY - rect.top;
|
|
435
|
+
|
|
436
|
+
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
437
|
+
rafId = requestAnimationFrame(() => {
|
|
438
|
+
card.style.setProperty("--mouse-x", `${x}px`);
|
|
439
|
+
card.style.setProperty("--mouse-y", `${y}px`);
|
|
440
|
+
rafId = null;
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
cards.forEach((card) => {
|
|
445
|
+
card.addEventListener("mousemove", handleMouseMove, { passive: true });
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Cleanup on Astro page transition
|
|
449
|
+
document.addEventListener(
|
|
450
|
+
"astro:before-swap",
|
|
451
|
+
() => {
|
|
452
|
+
cards.forEach((card) => {
|
|
453
|
+
card.removeEventListener("mousemove", handleMouseMove);
|
|
454
|
+
});
|
|
455
|
+
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
456
|
+
},
|
|
457
|
+
{ once: true },
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Init on first load + on Astro page transitions
|
|
462
|
+
initGlowCards();
|
|
463
|
+
document.addEventListener("astro:after-swap", initGlowCards);
|
|
464
|
+
</script>
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
function initCopyButtons() {
|
|
17
|
+
const COPY_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="sg-copy-icon" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`;
|
|
18
|
+
const CHECK_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="sg-copy-check" style="color:#22c55e" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>`;
|
|
19
|
+
|
|
17
20
|
document
|
|
18
21
|
.querySelectorAll<HTMLDivElement>(".code-block--fenced")
|
|
19
22
|
.forEach((block) => {
|
|
@@ -27,15 +30,16 @@ function initCopyButtons() {
|
|
|
27
30
|
btn.className = "sg-copy-btn-native sg-copy-btn";
|
|
28
31
|
btn.setAttribute("aria-label", "Copy code");
|
|
29
32
|
btn.title = "Copy code";
|
|
30
|
-
|
|
33
|
+
// Render both icons overlaid — CSS controls visibility
|
|
34
|
+
btn.innerHTML = COPY_SVG + CHECK_SVG;
|
|
31
35
|
|
|
32
36
|
btn.addEventListener("click", () => {
|
|
33
37
|
navigator.clipboard.writeText(code).then(() => {
|
|
34
|
-
btn.
|
|
38
|
+
btn.classList.add("sg-copy-btn-copied");
|
|
35
39
|
btn.setAttribute("aria-label", "Copied!");
|
|
36
40
|
btn.title = "Copied!";
|
|
37
41
|
setTimeout(() => {
|
|
38
|
-
btn.
|
|
42
|
+
btn.classList.remove("sg-copy-btn-copied");
|
|
39
43
|
btn.setAttribute("aria-label", "Copy code");
|
|
40
44
|
btn.title = "Copy code";
|
|
41
45
|
}, 2000);
|
|
@@ -52,6 +56,7 @@ function initCopyButtons() {
|
|
|
52
56
|
});
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
|
|
55
60
|
// Run on initial load
|
|
56
61
|
document.addEventListener("DOMContentLoaded", initCopyButtons);
|
|
57
62
|
|