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 :::info
Subagents can browse extensions for suggestions but cannot enable them to avoid modifying the parent session. 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>
);
}