Skip to content

Commit 98dbe2a

Browse files
committed
Fix CI build errors and add PhotoGallery component
Root causes fixed: 1. headroom.js type resolution: Created src/types/headroom.js.d.ts to bridge package name mismatch 2. Container style prop: Extended Props with HTMLAttributes<'div'> for passthrough support 3. Accordion Motion One types: Fixed DOMKeyframesDefinition usage and 'ease' vs 'easing' property 4. Mermaid import: Removed unused @ts-expect-error directive PhotoGallery component: - Restored from stash with type-safe props and gallery utilities - Uncommented imports in statsbomb.mdx
1 parent 50ace32 commit 98dbe2a

File tree

8 files changed

+525
-24
lines changed

8 files changed

+525
-24
lines changed

src/components/Accordion.astro

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ const {
8181

8282
<script>
8383
import { animate } from "motion";
84-
import type { AnimationControls } from "motion";
84+
import type { DOMKeyframesDefinition, AnimationOptions } from "motion";
8585
import { goldenTiming, egyptianEasing, prefersReducedMotion } from "../utils/animations";
8686

8787
function initAccordions() {
@@ -107,14 +107,13 @@ const {
107107
content.style.height = '0px';
108108
content.style.overflow = 'hidden';
109109

110-
animate(
111-
content,
112-
{ height: [`0px`, `${fullHeight}px`] },
113-
{
114-
duration: goldenTiming.fast,
115-
easing: egyptianEasing.water
116-
}
117-
).finished.then(() => {
110+
const keyframes: DOMKeyframesDefinition = { height: [`0px`, `${fullHeight}px`] };
111+
const options: AnimationOptions = {
112+
duration: goldenTiming.fast,
113+
ease: egyptianEasing.water
114+
};
115+
116+
animate(content, keyframes, options).finished.then(() => {
118117
// Reset to auto for dynamic content
119118
content.style.height = 'auto';
120119
content.style.overflow = 'visible';
@@ -128,14 +127,13 @@ const {
128127
// Force reflow to ensure starting height is applied
129128
void content.offsetHeight;
130129

131-
animate(
132-
content,
133-
{ height: [`${currentHeight}px`, `0px`] },
134-
{
135-
duration: goldenTiming.fast,
136-
easing: egyptianEasing.water
137-
}
138-
);
130+
const keyframes: DOMKeyframesDefinition = { height: [`${currentHeight}px`, `0px`] };
131+
const options: AnimationOptions = {
132+
duration: goldenTiming.fast,
133+
ease: egyptianEasing.water
134+
};
135+
136+
animate(content, keyframes, options);
139137
}
140138
});
141139
});

src/components/Container.astro

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
import { cva, type VariantProps } from 'class-variance-authority';
3+
import type { HTMLAttributes } from 'astro/types';
34
45
const container = cva(
56
'mx-auto px-4 md:px-6',
@@ -23,14 +24,13 @@ const container = cva(
2324
}
2425
);
2526
26-
type Props = VariantProps<typeof container> & {
27-
class?: string;
27+
interface Props extends HTMLAttributes<'div'>, VariantProps<typeof container> {
2828
as?: 'div' | 'section' | 'article' | 'main';
29-
};
29+
}
3030
31-
const { width, spacing, class: className, as: Tag = 'div' } = Astro.props;
31+
const { width, spacing, class: className, as: Tag = 'div', ...htmlAttrs } = Astro.props;
3232
---
3333

34-
<Tag class:list={[container({ width, spacing }), className]}>
34+
<Tag class:list={[container({ width, spacing }), className]} {...htmlAttrs}>
3535
<slot />
3636
</Tag>

src/components/PhotoGallery.astro

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
---
2+
/**
3+
* Photo Gallery Component
4+
*
5+
* Reusable gallery with PhotoSwipe lightbox integration.
6+
* Features:
7+
* - Responsive grid (1 col mobile, 2-3 col desktop)
8+
* - Egyptian design system styling
9+
* - WCAG AA accessibility (keyboard nav, screen reader)
10+
* - Respects prefers-reduced-motion
11+
* - Lazy loading below fold
12+
*/
13+
14+
import { validatePhotos } from "../utils/gallery";
15+
import { cva, type VariantProps } from "class-variance-authority";
16+
import type { HTMLAttributes } from "astro/types";
17+
18+
// CVA Variants
19+
const galleryVariants = cva("gallery-grid", {
20+
variants: {
21+
columns: {
22+
2: "md:grid-cols-2",
23+
3: "md:grid-cols-3",
24+
},
25+
variant: {
26+
default: "gap-6 md:gap-10",
27+
compact: "gap-4 md:gap-6",
28+
},
29+
},
30+
defaultVariants: {
31+
columns: 3,
32+
variant: "default",
33+
},
34+
});
35+
36+
interface Props
37+
extends HTMLAttributes<"div">,
38+
VariantProps<typeof galleryVariants> {
39+
photos: unknown; // Accept unknown, validate at runtime
40+
id?: string; // Gallery ID for PhotoSwipe targeting
41+
}
42+
43+
const {
44+
photos: rawPhotos,
45+
columns = 3,
46+
variant = "default",
47+
id = "photo-gallery",
48+
class: className,
49+
...rest
50+
} = Astro.props;
51+
52+
// Validate photos at build time (fail fast if invalid)
53+
let photos;
54+
try {
55+
photos = validatePhotos(rawPhotos);
56+
} catch (error) {
57+
throw new Error(
58+
`PhotoGallery: Invalid photo data\n${error.message}\n` +
59+
`Check your JSON file for schema violations (src, alt, caption, width, height)`,
60+
);
61+
}
62+
---
63+
64+
<div class="photo-gallery-container" {...rest}>
65+
<div
66+
class={galleryVariants({ columns, variant, className })}
67+
id={id}
68+
role="list"
69+
>
70+
{
71+
photos.map((photo, index) => (
72+
<figure class="photo-card" role="listitem">
73+
<a
74+
href={photo.src}
75+
data-pswp-width={photo.width}
76+
data-pswp-height={photo.height}
77+
data-pswp-caption={photo.caption}
78+
aria-label={`View full size: ${photo.alt}`}
79+
class="photo-link"
80+
>
81+
<picture>
82+
<source
83+
srcset={photo.src.replace(/\.(jpg|jpeg|png)$/, ".webp")}
84+
type="image/webp"
85+
/>
86+
<img
87+
src={photo.src}
88+
alt={photo.alt}
89+
loading={index < 3 ? "eager" : "lazy"}
90+
decoding="async"
91+
width={photo.width}
92+
height={photo.height}
93+
class="photo-image"
94+
/>
95+
</picture>
96+
</a>
97+
</figure>
98+
))
99+
}
100+
</div>
101+
</div>
102+
103+
<style>
104+
/* Masonry Gallery (CSS Columns) */
105+
.gallery-grid {
106+
column-count: 1;
107+
column-gap: var(--spacing-sm); /* 8px */
108+
}
109+
110+
/* 2 columns on tablet */
111+
@media (min-width: 640px) {
112+
.gallery-grid {
113+
column-count: 2;
114+
}
115+
}
116+
117+
/* 3 columns on desktop */
118+
@media (min-width: 768px) {
119+
.gallery-grid {
120+
column-count: 3;
121+
column-gap: var(--spacing-md); /* 16px */
122+
}
123+
}
124+
125+
/* Photo Card */
126+
.photo-card {
127+
display: flex;
128+
flex-direction: column;
129+
margin: 0 0 var(--spacing-sm) 0; /* Minimal vertical spacing (8px) */
130+
break-inside: avoid; /* Prevent splitting across columns */
131+
}
132+
133+
/* Photo Link */
134+
.photo-link {
135+
display: block;
136+
position: relative;
137+
overflow: hidden;
138+
border-radius: var(--radius-lg);
139+
background: var(--color-surface);
140+
border: 1px solid var(--color-neutral-light);
141+
transition: all 0.2s cubic-bezier(0.65, 0, 0.35, 1); /* Egyptian water */
142+
}
143+
144+
.photo-link:hover {
145+
transform: scale(1.02);
146+
box-shadow:
147+
0 4px 6px -1px rgb(0 0 0 / 0.1),
148+
0 2px 4px -2px rgb(0 0 0 / 0.1);
149+
border-color: var(--color-primary);
150+
}
151+
152+
.photo-link:focus {
153+
outline: 3px solid var(--color-primary);
154+
outline-offset: 3px;
155+
}
156+
157+
/* Photo Image */
158+
.photo-image {
159+
display: block;
160+
width: 100%;
161+
height: auto;
162+
border-radius: var(--radius-md);
163+
}
164+
165+
/* Respect reduced motion */
166+
@media (prefers-reduced-motion: reduce) {
167+
.photo-link {
168+
transition: none;
169+
}
170+
171+
.photo-link:hover {
172+
transform: none;
173+
}
174+
}
175+
</style>
176+
177+
<script>
178+
import PhotoSwipeLightbox from "photoswipe/lightbox";
179+
import PhotoSwipe from "photoswipe";
180+
import "photoswipe/style.css";
181+
182+
// Check for reduced motion preference
183+
function prefersReducedMotion(): boolean {
184+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
185+
}
186+
187+
// Initialize PhotoSwipe on all galleries
188+
function initializeGalleries() {
189+
const galleries = document.querySelectorAll(".photo-gallery-container");
190+
191+
galleries.forEach((container) => {
192+
const gallery = container.querySelector(".gallery-grid");
193+
if (!gallery) return;
194+
195+
const galleryId = gallery.getAttribute("id") || "photo-gallery";
196+
197+
const lightbox = new PhotoSwipeLightbox({
198+
gallery: `#${galleryId}`,
199+
children: "a",
200+
pswpModule: PhotoSwipe,
201+
202+
// Animation durations (will be overridden by CSS if reduced motion)
203+
showAnimationDuration: prefersReducedMotion() ? 0 : 400,
204+
hideAnimationDuration: prefersReducedMotion() ? 0 : 400,
205+
206+
// Accessibility
207+
closeOnVerticalDrag: true,
208+
ariaLabel: "Photo gallery lightbox",
209+
210+
// Caption parser - extract from data attribute
211+
captionContent: (slide: { data: { element?: HTMLElement } }) => {
212+
const caption = slide.data.element?.dataset.pswpCaption;
213+
return caption || "";
214+
},
215+
});
216+
217+
lightbox.init();
218+
});
219+
}
220+
221+
// Initialize on page load
222+
initializeGalleries();
223+
224+
// Re-initialize after Astro View Transitions
225+
document.addEventListener("astro:after-swap", () => {
226+
initializeGalleries();
227+
});
228+
</script>
229+
230+
<!-- PhotoSwipe Egyptian Easing Overrides -->
231+
<style is:global>
232+
/* Egyptian easing for PhotoSwipe animations */
233+
.pswp__img {
234+
transition-timing-function: cubic-bezier(
235+
0.65,
236+
0,
237+
0.35,
238+
1
239+
) !important; /* water */
240+
}
241+
242+
.pswp--open {
243+
animation-timing-function: cubic-bezier(
244+
0.76,
245+
0,
246+
0.24,
247+
1
248+
) !important; /* monument */
249+
}
250+
251+
/* Respect reduced motion */
252+
@media (prefers-reduced-motion: reduce) {
253+
.pswp__img,
254+
.pswp--open,
255+
.pswp__bg {
256+
animation: none !important;
257+
transition: none !important;
258+
}
259+
}
260+
261+
/* PhotoSwipe UI customization (Egyptian colors) */
262+
.pswp__button {
263+
background-color: var(--color-surface) !important;
264+
border: 1px solid var(--color-neutral-light) !important;
265+
}
266+
267+
.pswp__button:hover {
268+
background-color: var(--color-primary) !important;
269+
border-color: var(--color-primary) !important;
270+
}
271+
272+
.pswp__bg {
273+
background: rgba(0, 0, 0, 0.9) !important;
274+
}
275+
</style>

0 commit comments

Comments
 (0)