Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f1240f9
feat(shared): add a Polymarket-style feed hero section
claude Aug 23, 2026
e7c6ba5
feat(shared): rebuild the hero ad slot from the feed ad card's elements
claude Aug 23, 2026
2777645
feat(shared): flatten the hero rail and use the feed's list card on m…
claude Aug 23, 2026
7949d5b
refactor(shared): drop the carousel position counter
claude Aug 23, 2026
4074b6f
feat(shared): render the feed hero on the feed behind a flag
claude Aug 24, 2026
8dad3d3
chore(shared): add a temporary URL switch for the feed hero
claude Aug 24, 2026
f4890bb
feat(shared): cap the hero width and resize the ad slot and highlights
claude Aug 24, 2026
1aec4c6
feat(shared): reorder the hero ad and let its card summary breathe
claude Aug 24, 2026
eb46163
feat(shared): autoplay the feed hero carousel and align its rail
claude Aug 24, 2026
107164f
feat(shared): crossfade the hero carousel and fold its ad into Happen…
claude Aug 24, 2026
7ed7a41
Merge remote-tracking branch 'origin/main' into claude/hero-carousel-…
claude Aug 24, 2026
462f110
feat(shared): give the hero a 40/60 split and make its ad read as a h…
claude Aug 24, 2026
58bdb0a
fix(shared): stop a long hero headline pushing the action row off the…
claude Aug 24, 2026
d4781d9
feat(shared): add a second, flat ad slot under the hero rail
claude Aug 25, 2026
a8424fa
feat(shared): move the hero ad beside the headlines instead of under …
claude Aug 25, 2026
cb12036
Merge remote-tracking branch 'origin/main' into claude/hero-carousel-…
claude Aug 25, 2026
60db0ad
feat(shared): let the hero span the card grid and pad its hover surfaces
claude Aug 25, 2026
70ce843
Merge branch 'main' into claude/hero-carousel-layout-e5e06u
rebelchris Aug 26, 2026
6e270a6
Merge remote-tracking branch 'origin/main' into claude/hero-carousel-…
claude Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions packages/shared/src/components/Feed.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,64 @@ describe('Feed logged in', () => {
).toEqual(['postItem', 'postItem', 'highlightItem', 'postItem']);
});

it('should drop feedV2 highlights when the surface shows them itself', async () => {
renderComponent(
[
{
request: {
query: FEED_V2_QUERY,
variables,
},
result: {
data: {
page: {
pageInfo: defaultFeedPage.pageInfo,
edges: [
{
node: {
__typename: 'FeedPostItem',
post: defaultFeedPage.edges[0].node,
feedMeta: defaultFeedPage.edges[0].node.feedMeta ?? null,
},
},
{
node: {
__typename: 'FeedHighlightsItem',
feedMeta: null,
highlights: [
{
id: 'highlight-1',
channel: 'agents',
headline: 'The first highlight',
highlightedAt: '2026-04-05T09:00:00.000Z',
post: {
id: defaultFeedPage.edges[0].node.id,
commentsPermalink:
defaultFeedPage.edges[0].node.commentsPermalink,
},
},
],
},
},
],
},
},
},
},
],
defaultUser,
SharedFeedPage.MyFeed,
FEED_V2_QUERY,
{ disableHighlightCards: true },
);

await waitForNock();

expect(await screen.findByTestId('postItem')).toBeInTheDocument();
expect(screen.queryByTestId('highlightItem')).not.toBeInTheDocument();
expect(screen.queryByText('Happening Now')).not.toBeInTheDocument();
});

it('should send upvote mutation', async () => {
let mutationCalled = false;
renderComponent([
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/components/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export interface FeedProps<T>
showSearch?: boolean;
actionButtons?: ReactNode;
disableAds?: boolean;
/** The surface shows the highlights itself, so keep them out of the grid. */
disableHighlightCards?: boolean;
staticAd?: { ad: Ad; index: number };
disableAdRefresh?: boolean;
allowFetchMore?: boolean;
Expand Down Expand Up @@ -203,6 +205,7 @@ export default function Feed<T>({
shortcuts,
actionButtons,
disableAds,
disableHighlightCards,
staticAd,
disableAdRefresh = false,
allowFetchMore,
Expand Down Expand Up @@ -366,6 +369,7 @@ export default function Feed<T>({
excludePinnedPosts,
settings: {
disableAds,
disableHighlightCards,
staticAd,
adPostLength: isSquadFeed ? 2 : undefined,
feedName,
Expand Down
24 changes: 2 additions & 22 deletions packages/shared/src/components/FeedItemComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,19 @@ import type { FeedPostClick } from '../hooks/feed/useFeedOnPostClick';
import { LogEvent, Origin, TargetType } from '../lib/log';
import type { UseVotePost } from '../hooks';
import { useFeedLayout } from '../hooks';
import { CollectionList } from './cards/collection/CollectionList';
import { FeedItemType } from './cards/common/common';
import { AdGrid } from './cards/ad/AdGrid';
import { AdList } from './cards/ad/AdList';
import { SignalAdList } from './cards/ad/SignalAdList';
import type { AdCardProps } from './cards/ad/common/common';
import { FreeformGrid } from './cards/Freeform/FreeformGrid';
import { FreeformList } from './cards/Freeform/FreeformList';
import type { PostClick } from '../lib/click';
import { ArticleList } from './cards/article/ArticleList';
import { ArticleGrid } from './cards/article/ArticleGrid';
import type { FeaturedWideColSpan } from './cards/common/featuredWide';
import { PostTypeToWideCard } from './cards/common/wideCards';
import { PostTypeToListCard } from './cards/common/listCards';
import { ShareGrid } from './cards/share/ShareGrid';
import { ShareList } from './cards/share/ShareList';
import { CollectionGrid } from './cards/collection';
import type { UseBookmarkPost } from '../hooks/useBookmarkPost';
import { AdActions } from '../lib/ads';
Expand All @@ -50,11 +48,8 @@ import {
import { useEngagementAdsContext } from '../contexts/EngagementAdsContext';
import { useLogContext } from '../contexts/LogContext';
import PollGrid from './cards/poll/PollGrid';
import { PollList } from './cards/poll/PollList';
import { SocialTwitterGrid } from './cards/socialTwitter/SocialTwitterGrid';
import { SocialTwitterList } from './cards/socialTwitter/SocialTwitterList';
import { LiveRoomPostGrid } from './cards/liveRoom/LiveRoomPostGrid';
import { LiveRoomPostList } from './cards/liveRoom/LiveRoomPostList';
import { SignalList } from './cards/common/list/SignalList';
import { OtherFeedPage } from '../lib/query';
import { isSourceSquadOrMachine } from '../graphql/sources';
Expand Down Expand Up @@ -135,21 +130,6 @@ const PostTypeToTagCard: Record<PostType, React.ComponentType<any>> = {
[PostType.LiveRoom]: LiveRoomPostGrid,
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const PostTypeToTagList: Record<PostType, React.ComponentType<any>> = {
[PostType.Article]: ArticleList,
[PostType.Share]: ShareList,
[PostType.Welcome]: FreeformList,
[PostType.Freeform]: FreeformList,
[PostType.VideoYouTube]: ArticleList,
[PostType.Collection]: CollectionList,
[PostType.Brief]: BriefCard,
[PostType.Poll]: PollList,
[PostType.SocialTwitter]: SocialTwitterList,
[PostType.Digest]: ArticleList,
[PostType.LiveRoom]: LiveRoomPostList,
};

const getPostTypeForCard = (post?: Post): PostType => {
if (!post) {
return PostType.Article;
Expand Down Expand Up @@ -177,7 +157,7 @@ const getTags = ({
}: GetTagsProps) => {
const useListCards = isListFeedLayout || shouldUseListMode;
const isSignalFeed = feedName === OtherFeedPage.AgentsVibes;
const listPostTag = isSignalFeed ? SignalList : PostTypeToTagList[postType];
const listPostTag = isSignalFeed ? SignalList : PostTypeToListCard[postType];
const listPlaceholderTag = isSignalFeed
? SignalPlaceholderList
: PlaceholderList;
Expand Down
46 changes: 34 additions & 12 deletions packages/shared/src/components/MainFeedLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,16 @@ import {
discussedFeedVersion,
feature,
featureFeedChips,
featureFeedHero,
FeedChipsVariant,
followingFeedVersion,
latestFeedVersion,
popularFeedVersion,
upvotedFeedVersion,
} from '../lib/featureManagement';
import type { FeedContainerProps } from './feeds';
import { FeedHero } from './feeds/hero/FeedHero';
import { useFeedHeroPreview } from './feeds/hero/useFeedHeroPreview';
import { getFeedName } from '../lib/feed';
import CommentFeed from './CommentFeed';
import { COMMENT_FEED_QUERY } from '../graphql/comments';
Expand Down Expand Up @@ -357,6 +360,16 @@ export default function MainFeedLayout({
[showExploreChips, exploreCategories, feeds, isV2],
);

const isMainFeedPage =
feedName === SharedFeedPage.MyFeed || feedName === SharedFeedPage.Popular;
const { value: isFeedHeroFlagOn } = useConditionalFeature({
feature: featureFeedHero,
shouldEvaluate: isMainFeedPage,
});
const isFeedHeroPreview = useFeedHeroPreview();
const isFeedHeroEnabled =
isMainFeedPage && (isFeedHeroFlagOn || isFeedHeroPreview);

const { isSearchPageLaptop } = useSearchResultsLayout();

const config = useMemo(() => {
Expand Down Expand Up @@ -746,6 +759,25 @@ export default function MainFeedLayout({
}
return '';
}, [customFeedsData, feedName, router.query.slugOrId]);
const chipsTopContent =
(isExploreTag || shouldUseListFeedLayout) && chipsNode ? (
<div
className={classNames('mb-8 w-full', shouldUseListFeedLayout && 'mt-8')}
>
{chipsNode}
</div>
) : undefined;
// Left undefined when the hero is off so `Feed` keeps its own top slot for
// the reading reminder.
const topContent = isFeedHeroEnabled ? (
<>
<FeedHero className="mb-8 w-full" />
{chipsTopContent}
</>
) : (
chipsTopContent
);

const v2ActionButtons = feedProps?.actionButtons;
const showFeedV2PageHeader =
isV2 &&
Expand Down Expand Up @@ -823,18 +855,8 @@ export default function MainFeedLayout({
<Feed
{...feedProps}
shortcuts={shortcuts}
topContent={
(isExploreTag || shouldUseListFeedLayout) && chipsNode ? (
<div
className={classNames(
'mb-8 w-full',
shouldUseListFeedLayout && 'mt-8',
)}
>
{chipsNode}
</div>
) : undefined
}
topContent={topContent}
disableHighlightCards={isFeedHeroEnabled}
className={classNames(
shouldUseListFeedLayout && !isFinder && 'laptop:px-6',
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ import { useCardCover } from '../../../hooks/feed/useCardCover';
import { stripHtmlTags } from '../../../lib/strings';
import { HighlightChip } from '../common/HighlightChip';
import type { FeaturedWideCardProps } from '../common/featuredWide';
import { INNER_GRID_COLS } from '../common/featuredWide';
import {
DESCRIPTION_CLASS_NAME,
HERO_DESCRIPTION_CLASS_NAME,
HERO_TEXT_FIT_CLASS_NAME,
HERO_TITLE_CLASS_NAME,
INNER_GRID_COLS,
TEXT_COL_SPAN,
TITLE_CLASS_NAME,
} from '../common/featuredWide';
import { FeaturedWideCardShell } from '../common/FeaturedWideCardShell';
import { FeaturedWideImageColumn } from '../common/FeaturedWideImageColumn';
import { FeaturedWideActions } from '../common/FeaturedWideActions';
Expand All @@ -34,6 +42,7 @@ export const FreeformFeaturedWideGridCard = forwardRef(
eagerLoadImage = false,
enableSourceHeader = false,
wideColSpan = 2,
hero,
}: FeaturedWideCardProps,
ref: Ref<HTMLElement>,
): ReactElement {
Expand Down Expand Up @@ -64,13 +73,25 @@ export const FreeformFeaturedWideGridCard = forwardRef(
image || overlay ? INNER_GRID_COLS[wideColSpan] : 'grid-cols-1',
)}
>
<div className="relative flex min-h-0 min-w-0 flex-col overflow-hidden">
<FeaturedWideTextContainer>
<div
className={classNames(
'relative flex min-h-0 min-w-0 flex-col overflow-hidden',
image || overlay ? TEXT_COL_SPAN[wideColSpan] : undefined,
)}
>
<FeaturedWideTextContainer
className={hero ? HERO_TEXT_FIT_CLASS_NAME : undefined}
>
<SquadPostCardHeader
post={post}
enableSourceHeader={enableSourceHeader}
/>
<h3 className="mt-2 line-clamp-4 break-words font-bold text-text-primary typo-title1">
<h3
className={classNames(
'mt-2 break-words font-bold text-text-primary',
hero ? HERO_TITLE_CLASS_NAME : TITLE_CLASS_NAME,
)}
>
{title}
</h3>
<div className="mt-2 flex min-w-0 items-center gap-2">
Expand All @@ -87,7 +108,12 @@ export const FreeformFeaturedWideGridCard = forwardRef(
className="mt-1"
/>
{!!description && (
<p className="mt-2 line-clamp-3 text-text-secondary typo-callout">
<p
className={classNames(
'mt-2 text-text-secondary typo-callout',
hero ? HERO_DESCRIPTION_CLASS_NAME : DESCRIPTION_CLASS_NAME,
)}
>
{description}
</p>
)}
Expand All @@ -106,6 +132,7 @@ export const FreeformFeaturedWideGridCard = forwardRef(
image={image}
alt={post.title ?? ''}
wideColSpan={wideColSpan}
coverImage={hero}
overlay={overlay}
eagerLoadImage={eagerLoadImage}
/>
Expand Down
Loading
Loading