diff --git a/documentation/docs/guides/subagents.md b/documentation/docs/guides/subagents.mdx similarity index 81% rename from documentation/docs/guides/subagents.md rename to documentation/docs/guides/subagents.mdx index 068884c3e2db..be056c5a7b37 100644 --- a/documentation/docs/guides/subagents.md +++ b/documentation/docs/guides/subagents.mdx @@ -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'; + + diff --git a/documentation/src/components/ContentCard.tsx b/documentation/src/components/ContentCard.tsx new file mode 100644 index 000000000000..c8e89226078f --- /dev/null +++ b/documentation/src/components/ContentCard.tsx @@ -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 ( + !isTouchDevice && setIsHovering(true)} + onMouseLeave={() => !isTouchDevice && setIsHovering(false)} + > + +
+
+ {thumbnailUrl ? ( + {`Thumbnail + ) : ( + Goose logo placeholder + )} +
+
+ + {showHoverOverlay && !isTouchDevice && ( +
+

{title}

+

{description}

+
+
+ {type.toUpperCase()} +
+
+ {date && {formatDate(date)}} +
+
+ {duration && {duration}} + {type === 'blog' && !duration && 5 min read} +
+
+
+ )} +
+ ); +} diff --git a/documentation/src/components/ContentCardCarousel.tsx b/documentation/src/components/ContentCardCarousel.tsx new file mode 100644 index 000000000000..8fd9ab0fe823 --- /dev/null +++ b/documentation/src/components/ContentCardCarousel.tsx @@ -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 ( +
+ + {items.map((item, index) => ( + + + + ))} + +
+ ); +}