docs: add content carousel (#5086)

This commit is contained in:
dianed-square
2025-10-09 08:36:04 -07:00
committed by GitHub
parent 1396d315e5
commit 4f4c2c4a66
3 changed files with 356 additions and 0 deletions
@@ -278,3 +278,50 @@ The following operations are blocked to ensure subagents remain focused on their
:::info
Subagents can browse extensions for suggestions but cannot enable them to avoid modifying the parent session.
:::
## Additional Resources
import ContentCardCarousel from '@site/src/components/ContentCardCarousel';
import subagentsVsSubrecipes from '@site/blog/2025-09-26-subagents-vs-subrecipes/subrecipes-vs-subagents.png';
import agentCoordination from '@site/blog/2025-08-14-agent-coordination-patterns/agent-coordination.png';
<ContentCardCarousel
items={[
{
type: 'video',
title: 'How I Built an App with 6 Subagents',
description: 'Deep dive into goose subagents. Walk through building an app using 6 specialized AI agents for advanced workflow automation and development.',
thumbnailUrl: 'https://img.youtube.com/vi/yIBrD5AxtTc/maxresdefault.jpg',
linkUrl: 'https://www.youtube.com/watch?v=yIBrD5AxtTc',
date: '2025-10-01',
duration: '6:53'
},
{
type: 'video',
title: 'Flight School - Choosing the Right Tools for AI Work',
description: 'Discover the differences between subagents and subrecipes for efficient task execution in goose. Learn which approach is best for your workflow.',
thumbnailUrl: 'https://img.youtube.com/vi/joePzlkARjs/maxresdefault.jpg',
linkUrl: 'https://www.youtube.com/watch?v=joePzlkARjs',
date: '2025-09-29',
duration: '6:13'
},
{
type: 'blog',
title: 'How to Choose Between Subagents and Subrecipes in goose',
description: 'Detailed guide to subagents and subrecipes in goose. Compare reusability, setup complexity, and get practical advice for choosing the right approach.',
thumbnailUrl: subagentsVsSubrecipes,
linkUrl: '/goose/blog/2025/09/26/subagents-vs-subrecipes',
date: '2025-09-26',
duration: '6 min read'
},
{
type: 'blog',
title: 'Agents, Subagents, and Multi Agents: What They Are and When to Use Them',
description: 'Compare agents, subagents, and multi agents in AI workflows. Learn how they work together and practical scenarios for each approach.',
thumbnailUrl: agentCoordination,
linkUrl: '/goose/blog/2025/08/14/agent-coordination-patterns',
date: '2025-08-14',
duration: '4 min read'
}
]}
/>
@@ -0,0 +1,232 @@
import React from 'react';
type ContentType = 'video' | 'blog';
interface ContentCardProps {
type: ContentType;
title: string;
description: string;
thumbnailUrl?: string; // meta url or ES6 import for blogs
linkUrl: string;
date?: string;
duration?: string; // e.g. '6:04' for videos and '5 min read' for blogs
size?: 'large' | 'compact';
}
const styles = {
cardContainer: {
display: 'flex',
flexDirection: 'row' as const,
width: '100%',
border: '1px solid var(--ifm-color-emphasis-200)',
borderRadius: '12px',
textDecoration: 'none',
color: 'inherit',
overflow: 'hidden',
background: 'var(--ifm-background-color)',
transition: 'box-shadow 0.2s ease, transform 0.2s ease',
marginBottom: '1rem',
},
cardContainerLarge: {
width: '100%',
maxWidth: '500px',
aspectRatio: '16/9',
},
cardContainerCompact: {
width: '100%',
maxWidth: '350px',
aspectRatio: '16/9',
},
cardHover: {
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
transform: 'translateY(-2px)',
},
mainArea: {
flex: 1,
display: 'flex',
flexDirection: 'column' as const,
},
thumbnailWrapper: {
position: 'relative' as const,
width: '100%',
height: '100%',
paddingBottom: 0,
overflow: 'hidden' as const,
background: 'var(--ifm-color-emphasis-100)',
},
thumbnail: {
position: 'absolute' as const,
top: 0,
left: 0,
width: '100%',
height: '100%',
objectFit: 'cover' as const,
},
placeholderLogo: {
position: 'absolute' as const,
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '64px',
height: '64px',
opacity: 0.6,
},
hoverOverlay: {
position: 'absolute' as const,
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.9)',
color: 'white',
padding: '1.25rem',
display: 'flex',
flexDirection: 'column' as const,
justifyContent: 'center',
opacity: 0,
transition: 'opacity 0.3s ease',
zIndex: 10,
borderRadius: '12px',
},
hoverOverlayVisible: {
opacity: 1,
},
hoverTitle: {
fontSize: '1.1rem',
fontWeight: '600' as const,
marginBottom: '0.5rem',
color: 'white',
},
hoverDescription: {
fontSize: '0.875rem',
lineHeight: '1.4',
marginBottom: '0.75rem',
color: 'rgba(255, 255, 255, 0.9)',
},
hoverMetadata: {
fontSize: '0.75rem',
color: 'rgba(255, 255, 255, 0.8)',
fontWeight: '600' as const,
display: 'flex',
justifyContent: 'space-between',
marginTop: 'auto',
},
};
export default function ContentCard({
type,
title,
description,
thumbnailUrl,
linkUrl,
date,
duration,
size = 'compact',
}: ContentCardProps) {
const [isHovering, setIsHovering] = React.useState(false);
const [isTouchDevice, setIsTouchDevice] = React.useState(false);
const isCompact = size === 'compact';
const showHoverOverlay = true;
// Detect touch device on mount
React.useEffect(() => {
setIsTouchDevice('ontouchstart' in window || navigator.maxTouchPoints > 0);
}, []);
const containerStyle = {
...styles.cardContainer,
...(isCompact ? styles.cardContainerCompact : styles.cardContainerLarge),
...(isHovering ? styles.cardHover : {}),
position: 'relative' as const,
};
const thumbnailWrapperStyle = styles.thumbnailWrapper;
const hoverOverlayStyle = {
...styles.hoverOverlay,
...(isHovering && showHoverOverlay && !isTouchDevice ? styles.hoverOverlayVisible : {}),
...(size === 'large' ? {
padding: '2.00rem',
} : {}),
};
const hoverTitleStyle = {
...styles.hoverTitle,
...(size === 'large' ? {
fontSize: '1.4rem',
} : {}),
};
const hoverDescriptionStyle = {
...styles.hoverDescription,
...(size === 'large' ? {
fontSize: '1.1rem',
} : {}),
};
const hoverMetadataStyle = {
...styles.hoverMetadata,
...(size === 'large' ? {
fontSize: '0.9rem',
} : {}),
};
const formatDate = (dateString: string) => {
const [year, month, day] = dateString.split('-').map(Number);
const date = new Date(year, month - 1, day);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};
return (
<a
href={linkUrl}
style={containerStyle}
onMouseEnter={() => !isTouchDevice && setIsHovering(true)}
onMouseLeave={() => !isTouchDevice && setIsHovering(false)}
>
<div style={styles.mainArea}>
<div style={thumbnailWrapperStyle}>
{thumbnailUrl ? (
<img
style={styles.thumbnail}
src={thumbnailUrl}
alt={`Thumbnail for ${title}`}
/>
) : (
<img
style={styles.placeholderLogo}
src="/goose/img/goose.svg"
alt="Goose logo placeholder"
/>
)}
</div>
</div>
{showHoverOverlay && !isTouchDevice && (
<div style={hoverOverlayStyle}>
<h3 style={hoverTitleStyle}>{title}</h3>
<p style={hoverDescriptionStyle}>{description}</p>
<div style={hoverMetadataStyle}>
<div>
<span>{type.toUpperCase()}</span>
</div>
<div>
{date && <span>{formatDate(date)}</span>}
</div>
<div>
{duration && <span>{duration}</span>}
{type === 'blog' && !duration && <span>5 min read</span>}
</div>
</div>
</div>
)}
</a>
);
}
@@ -0,0 +1,77 @@
import React from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Navigation, Pagination, FreeMode } from 'swiper/modules';
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import 'swiper/css/free-mode';
import ContentCard from './ContentCard';
type ContentType = 'video' | 'blog' ;
interface ContentItem {
type: ContentType;
title: string;
description: string;
thumbnailUrl?: string;
linkUrl: string;
date?: string;
duration?: string;
}
interface ContentCardCarouselProps {
items: ContentItem[];
size?: 'large' | 'compact';
showNavigation?: boolean;
showPagination?: boolean;
}
const carouselStyles = {
container: {
margin: '2rem 0',
},
swiperContainer: {
paddingBottom: '2rem', // Space for pagination dots
},
};
export default function ContentCardCarousel({
items,
size,
showNavigation = true,
showPagination = true,
}: ContentCardCarouselProps) {
return (
<div style={carouselStyles.container}>
<Swiper
slidesPerView="auto"
spaceBetween={16}
freeMode={false}
navigation={showNavigation}
pagination={showPagination ? {
clickable: true
} : false}
modules={[Navigation, Pagination, FreeMode]}
style={carouselStyles.swiperContainer}
>
{items.map((item, index) => (
<SwiperSlide key={index} style={{
width: size === 'large' ? 'min(500px, 90vw)' : 'min(350px, 85vw)',
minWidth: size === 'large' ? '300px' : '250px'
}}>
<ContentCard
type={item.type}
title={item.title}
description={item.description}
thumbnailUrl={item.thumbnailUrl}
linkUrl={item.linkUrl}
date={item.date}
duration={item.duration}
size={size}
/>
</SwiperSlide>
))}
</Swiper>
</div>
);
}