Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7757be9
feat: add layout_v2 feature flag and useLayoutVariant hook
rebelchris May 18, 2026
a777e31
refactor: simplify featureLayoutV2 to boolean flag
rebelchris May 18, 2026
0cf1f0e
feat: add v2 dual-sidebar layout behind featureLayoutV2 flag
rebelchris May 18, 2026
3fd9765
feat: pixel-match dual-sidebar v2 layout against designer mockup
rebelchris May 19, 2026
3187bc8
fix(v2): match designer panel — MagicIcon for For You, drop Game Center
rebelchris May 19, 2026
b15c4d4
fix(v2): widen sidebar item horizontal margin to match designer
rebelchris May 19, 2026
508a530
fix(v2): bordered page-header strip and grid inset for feed content
rebelchris May 19, 2026
bb6cf3a
fix(v2): compact ghost buttons + tighter feed/header insets
rebelchris May 19, 2026
c017a29
fix(v2): drop FeedPage 40px padding, restore grid inset, inline compa…
rebelchris May 19, 2026
d1dcb34
fix(v2): tighter header strip + smaller icons, snug card-to-strip gap
rebelchris May 19, 2026
4255f12
refactor(v2): align feed-container layout with designer's list-frame …
rebelchris May 19, 2026
19fefb6
fix(v2): drop feedHeading title in PageHeader to match mock
rebelchris May 19, 2026
8b6c09c
fix(v2): remove duplicate action strip — restyle isSearch branch instead
rebelchris May 19, 2026
8937a0a
fix(v2): tighter text buttons and snug grid top inset
rebelchris May 19, 2026
2f7f4f4
refactor(v2): drop button-style overrides, use Small + Tertiary natively
rebelchris May 19, 2026
7ed6d19
feat(v2): squads directory page-header strip
rebelchris May 19, 2026
ff70c54
fix(v2): NotificationsBell rail variant — match other rail icons
rebelchris May 19, 2026
470ad7e
feat(v2): port designer page-header pattern to remaining pages
rebelchris May 19, 2026
cdf36d9
fix(v2): full-width PageHeader on max-width pages + minimal game-cent…
rebelchris May 19, 2026
05cc552
feat(v2): port PageHeader pattern to bookmarks + settings AccountPage…
rebelchris May 19, 2026
c5b9aa3
fix(v2): unify bookmarks header — title + search + actions in one strip
rebelchris May 19, 2026
4ae42f8
fix(v2): show sidebar on /settings/* so the v2 rail appears
rebelchris May 19, 2026
4e20920
fix(v2): hide ProfileSettingsMenuDesktop — the v2 sidebar panel is th…
rebelchris May 19, 2026
cc79cfb
fix(v2): portal settings PageHeader to span full floating-card width
rebelchris May 19, 2026
831394b
feat(v2): settings/notifications — port FindSquad-style tabs into Pag…
rebelchris May 19, 2026
1b7f6e4
fix: some more changes to old pages
rebelchris May 19, 2026
f1a26b9
refactor(v2): isV2 is now laptop-gated — drop redundant isLaptop checks
rebelchris May 19, 2026
bad8b6a
feat(v2): route-loading progress bar at top of floating card
rebelchris May 19, 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
161 changes: 104 additions & 57 deletions packages/shared/src/components/BookmarkFeedLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PropsWithChildren, ReactElement, ReactNode } from 'react';
import type { ReactElement, ReactNode } from 'react';
import React, {
useCallback,
useContext,
Expand All @@ -21,11 +21,12 @@ import SearchEmptyScreen from './SearchEmptyScreen';
import type { FeedProps } from './Feed';
import Feed from './Feed';
import BookmarkEmptyScreen from './BookmarkEmptyScreen';
import type { ButtonProps } from './buttons/Button';
import { Button, ButtonSize, ButtonVariant } from './buttons/Button';
import { ShareIcon, SortIcon } from './icons';
import { generateQueryKey, OtherFeedPage, RequestKey } from '../lib/query';
import { useFeedLayout, useViewSize, ViewSize } from '../hooks';
import { useLayoutVariant } from '../hooks/layout/useLayoutVariant';
import { PageHeader } from './layout/PageHeader';
import { BookmarkSection } from './sidebar/sections/BookmarkSection';
import PlusMobileEntryBanner from './marketing/banners/PlusMobileEntryBanner';
import { DigestBookmarkBanner } from './marketing/banners/DigestBookmarkBanner';
Expand Down Expand Up @@ -60,17 +61,6 @@ const SharedBookmarksModal = dynamic(
),
);

const ShareBookmarksButton = ({
children,
...props
}: PropsWithChildren<
Pick<ButtonProps<'button'>, 'className' | 'onClick' | 'icon'>
>) => (
<Button variant={ButtonVariant.Secondary} {...props}>
{children}
</Button>
);

const bookmarkSortOptions = [
{ label: 'Newest first', value: BookmarkSort.TimeDesc },
{ label: 'Oldest first', value: BookmarkSort.TimeAsc },
Expand Down Expand Up @@ -103,6 +93,8 @@ export default function BookmarkFeedLayout({
DEFAULT_BOOKMARK_SORT_INDEX,
);
const isLaptop = useViewSize(ViewSize.Laptop);
const { isV2 } = useLayoutVariant();
const isV2Laptop = isV2;
const isSearchResults = !!searchQuery;
const isFolderPage = !!folder || isReminderOnly;
const listId = folder?.id;
Expand Down Expand Up @@ -190,54 +182,109 @@ export default function BookmarkFeedLayout({
return null;
}

const sortDropdown = !isSearchResults && (
<Dropdown
className={{
label: 'hidden',
chevron: 'hidden',
button: isV2Laptop ? undefined : '!px-1',
container: isV2Laptop ? 'flex' : 'ml-4 flex',
}}
shouldIndicateSelected
icon={<SortIcon size={isV2Laptop ? IconSize.XSmall : IconSize.Medium} />}
iconOnly
selectedIndex={selectedSort}
options={bookmarkSortOptionLabels}
onChange={(_, index) => setSelectedSort(index)}
buttonVariant={isV2Laptop ? ButtonVariant.Tertiary : ButtonVariant.Float}
buttonSize={isV2Laptop ? ButtonSize.Small : ButtonSize.Medium}
drawerProps={{ displayCloseButton: true }}
/>
);
const shareButton = !isFolderPage && (
<Button
aria-label="Share bookmarks"
className={isV2Laptop ? undefined : 'ml-4 flex'}
icon={
<ShareIcon
size={isV2Laptop ? IconSize.XSmall : IconSize.Medium}
secondary={showSharedBookmarks}
aria-hidden
/>
}
onClick={() => setShowSharedBookmarks(true)}
size={isV2Laptop ? ButtonSize.Small : ButtonSize.Medium}
variant={isV2Laptop ? ButtonVariant.Tertiary : ButtonVariant.Secondary}
>
{isLaptop ? <span>Share bookmarks</span> : null}
</Button>
);
const folderMenu = folder && !isReminderOnly && (
<BookmarkFolderContextMenu
folder={folder}
buttonProps={
isV2Laptop
? {
className: 'flex',
size: ButtonSize.Small,
variant: ButtonVariant.Tertiary,
}
: undefined
}
/>
);
const headerTitleSlot = isV2Laptop ? (
<div className="flex min-w-0 flex-1 items-center gap-3">
<Typography
bold
type={TypographyType.Callout}
tag={TypographyTag.H1}
truncate
className="min-w-0 shrink"
>
{title}
</Typography>
{searchChildren && (
<div className="min-w-0 max-w-[20rem] flex-1">{searchChildren}</div>
)}
</div>
) : (
title
);

return (
<FeedPageLayoutComponent>
{children}
<FeedPageHeader className="mb-5">
<Typography bold type={TypographyType.Title3} tag={TypographyTag.H1}>
{title}
</Typography>
</FeedPageHeader>
<CustomFeedHeader
className={classNames(
'mb-6',
shouldUseListFeedLayout && !shouldUseListMode && 'px-4',
)}
>
{searchChildren}
{!isSearchResults && (
<Dropdown
className={{
label: 'hidden',
chevron: 'hidden',
button: '!px-1',
container: 'ml-4 flex',
}}
shouldIndicateSelected
icon={<SortIcon size={IconSize.Medium} />}
iconOnly
selectedIndex={selectedSort}
options={bookmarkSortOptionLabels}
onChange={(_, index) => setSelectedSort(index)}
buttonVariant={ButtonVariant.Float}
buttonSize={ButtonSize.Medium}
drawerProps={{ displayCloseButton: true }}
/>
)}
{!isFolderPage && (
<ShareBookmarksButton
aria-label="Share bookmarks"
className="ml-4 flex"
icon={<ShareIcon secondary={showSharedBookmarks} aria-hidden />}
onClick={() => setShowSharedBookmarks(true)}
{isV2Laptop ? (
<PageHeader title={headerTitleSlot}>
{sortDropdown}
{shareButton}
{folderMenu}
</PageHeader>
) : (
<>
<FeedPageHeader className="mb-5">
<Typography
bold
type={TypographyType.Title3}
tag={TypographyTag.H1}
>
{title}
</Typography>
</FeedPageHeader>
<CustomFeedHeader
className={classNames(
'mb-6',
shouldUseListFeedLayout && !shouldUseListMode && 'px-4',
)}
>
{isLaptop ? <span>Share bookmarks</span> : null}
</ShareBookmarksButton>
)}
{folder && !isReminderOnly && (
<BookmarkFolderContextMenu folder={folder} />
)}
</CustomFeedHeader>
{searchChildren}
{sortDropdown}
{shareButton}
{folderMenu}
</CustomFeedHeader>
</>
)}

{showSharedBookmarks && (
<SharedBookmarksModal
Expand Down
70 changes: 55 additions & 15 deletions packages/shared/src/components/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import { SpotlightProvider } from './spotlight/SpotlightContext';
import { SpotlightHost } from './spotlight/SpotlightHost';
import { FeedbackWidget } from './feedback';
import { isExtension } from '../lib/func';
import { useLayoutVariant } from '../hooks/layout/useLayoutVariant';
import { RouteProgressBar } from './RouteProgressBar';

const GoBackHeaderMobile = dynamic(
() =>
Expand Down Expand Up @@ -84,7 +86,7 @@ function MainLayoutComponent({
}: MainLayoutProps): ReactElement | null {
const router = useRouter();
const { logEvent } = useLogContext();
const { user, isAuthReady, showLogin } = useAuthContext();
const { user, isAuthReady, isLoggedIn, showLogin } = useAuthContext();
const { growthbook } = useGrowthBookContext();
const { sidebarRendered } = useSidebarRendered();
const { isAvailable: isBannerAvailable } = useBanner();
Expand All @@ -99,8 +101,18 @@ function MainLayoutComponent({
const isLaptopXL = useViewSize(ViewSize.LaptopXL);
const { screenCenteredOnMobileLayout } = useFeedLayout();
const { isNotificationsReady, unreadCount } = useNotificationContext();
const { isV2 } = useLayoutVariant();
useNotificationParams();

// The dual-sidebar layout takes ownership of the global header chrome
// (logo + search + user actions) on laptop+ for authenticated users
// (and for extension new tab regardless of auth state). When that's
// the case the global header is hidden, the main content gets the
// floating-card treatment, and the global feedback widget is suppressed
// because the rail provides its own.
const sidebarOwnsHeader =
isV2 && (isLoggedIn || isExtension) && showSidebar && sidebarRendered;

useEffect(() => {
if (!isNotificationsReady || unreadCount === 0 || hasLoggedImpression) {
return;
Expand Down Expand Up @@ -179,7 +191,13 @@ function MainLayoutComponent({
isLaptopXL && screenCenteredOnMobileLayout ? true : screenCentered;

return (
<div className="antialiased">
<div
className={classNames(
'antialiased',
isV2 &&
'laptop:bg-[color-mix(in_srgb,var(--theme-surface-secondary)_3%,var(--theme-background-default))]',
)}
>
{canGoBack && <GoBackHeaderMobile />}
{customBanner}
{isBannerAvailable && <PromotionalBanner />}
Expand All @@ -197,35 +215,57 @@ function MainLayoutComponent({
/>
)}

<MainLayoutHeader
hasBanner={isBannerAvailable}
sidebarRendered={sidebarRendered}
additionalButtons={additionalButtons}
onLogoClick={onLogoClick}
/>
{!sidebarOwnsHeader && (
<MainLayoutHeader
hasBanner={isBannerAvailable}
sidebarRendered={sidebarRendered}
additionalButtons={additionalButtons}
onLogoClick={onLogoClick}
/>
)}
<main
className={classNames(
'flex flex-col transition-[padding] duration-300 ease-in-out laptop:pt-16',
showSidebar && 'tablet:pl-16 laptop:pl-11',
'flex flex-col transition-[padding] duration-300 ease-in-out',
!sidebarOwnsHeader && 'laptop:pt-16',
showSidebar && (isV2 ? 'tablet:pl-16 laptop:pl-16' : 'tablet:pl-16 laptop:pl-11'),
className,
isAuthReady &&
!isScreenCentered &&
showSidebar &&
sidebarExpanded &&
'laptop:!pl-60',
isBannerAvailable && 'laptop:pt-24',
(isV2
? 'laptop:!pl-[19rem]'
: !isScreenCentered && 'laptop:!pl-60'),
isBannerAvailable && !sidebarOwnsHeader && 'laptop:pt-24',
)}
>
{isAuthReady && showSidebar && (
<Sidebar
additionalButtons={additionalButtons}
isNavButtons={isNavItemsButton}
showFeedbackWidget={!hideFeedbackWidget}
onNavTabClick={onNavTabClick}
onLogoClick={onLogoClick}
activePage={activePage ?? router.asPath ?? router.pathname}
/>
)}
{children}
{sidebarOwnsHeader ? (
<div className="flex min-h-0 flex-1 flex-col laptop:my-3 laptop:ml-1 laptop:mr-3">
<div
className={classNames(
'relative flex min-h-0 flex-1 flex-col',
'laptop:overflow-hidden laptop:rounded-24 laptop:border laptop:border-border-subtlest-quaternary laptop:bg-background-default laptop:p-0.5 laptop:shadow-2',
'laptop:min-h-[calc(100vh-1.5rem)]',
)}
>
<RouteProgressBar />
{children}
</div>
</div>
) : (
children
)}
</main>
{!hideFeedbackWidget && <FeedbackWidget />}
{!hideFeedbackWidget && !sidebarOwnsHeader && <FeedbackWidget />}
</div>
);
}
Expand Down
12 changes: 12 additions & 0 deletions packages/shared/src/components/RouteProgressBar.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@keyframes route-progress-slide {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(400%);
}
}

.bar {
animation: route-progress-slide 1s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}
Loading