Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
197f6b2
fix(mobile): the four step-0 shell fixes
tsahimatsliah Oct 1, 2026
0321efa
Merge branch 'main' into claude/mobile-shell-0-fixes
tsahimatsliah Oct 1, 2026
78eed0d
feat(mobile): the phone shell
tsahimatsliah Oct 1, 2026
06a7127
Merge branch 'main' into claude/mobile-shell-0-fixes
tsahimatsliah Oct 1, 2026
8950438
Merge remote-tracking branch 'origin/main' into claude/mobile-shell-1
tsahimatsliah Oct 1, 2026
0651e88
feat(mobile): the rows of the shell
tsahimatsliah Oct 1, 2026
6e57455
feat(mobile): the buttons ride in the block
tsahimatsliah Oct 1, 2026
692b8f6
Merge remote-tracking branch 'origin/main' into claude/mobile-shell-1
tsahimatsliah Oct 1, 2026
66cd759
feat(mobile): every page's header lives in the block
tsahimatsliah Oct 1, 2026
ea43425
Merge branch 'main' into claude/mobile-shell-0-fixes
tsahimatsliah Oct 1, 2026
230dce5
Merge remote-tracking branch 'origin/main' into claude/mobile-shell-1
tsahimatsliah Oct 1, 2026
c8a707b
Merge remote-tracking branch 'origin/main' into claude/mobile-shell-1
tsahimatsliah Oct 1, 2026
104b787
fix(mobile): the Activity count keeps its colour on an unlit tab
tsahimatsliah Oct 1, 2026
5befec5
ci: rerun test_shared after the boot focus-refetch timing flake
tsahimatsliah Oct 1, 2026
b2d32a0
feat(mobile): the bar's selected tab gets a pill that follows a held …
tsahimatsliah Oct 1, 2026
44b029f
fix(mobile): no member-only squares for visitors
tsahimatsliah Oct 1, 2026
d4609de
feat(mobile): the bar lifts under a finger
tsahimatsliah Oct 1, 2026
a33808f
Merge remote-tracking branch 'origin/main' into claude/mobile-shell-1
tsahimatsliah Oct 1, 2026
448734f
fix(lint): import order on the Sources page
tsahimatsliah Oct 1, 2026
105840a
fix(mobile): the pill stops at the bar's right edge while lifted
tsahimatsliah Oct 1, 2026
ddf9241
fix(mobile): the swipe cone gates the fast path too, and review notes
tsahimatsliah Oct 1, 2026
77ab129
Merge branch 'claude/mobile-shell-0-fixes' into claude/mobile-shell-1
tsahimatsliah Oct 1, 2026
8e49efb
Merge remote-tracking branch 'origin/main' into claude/mobile-shell-1
tsahimatsliah Oct 1, 2026
d6656d2
Merge remote-tracking branch 'origin/main' into claude/mobile-shell-1
tsahimatsliah Oct 1, 2026
070289e
feat(mobile): step 1 complete, the menus as the plan drew them
tsahimatsliah Oct 2, 2026
fe42ecb
fix(mobile): a Button in the block keeps the square's material
tsahimatsliah Oct 2, 2026
751566a
style: format shell.css
tsahimatsliah Oct 2, 2026
1f141fd
Merge branch 'main' into claude/mobile-shell-1
tsahimatsliah Oct 2, 2026
fcbfbaf
fix(types): optional undo in the custom feed modal
tsahimatsliah Oct 2, 2026
b1474c8
test(feed): open Not interested before the hide and block rows on a p…
tsahimatsliah Oct 2, 2026
ad8cf56
feat(mobile): close step 1 of the shell against the plan
tsahimatsliah Oct 2, 2026
39cf708
fix(mobile): the bar owns its touches and the menu sheet gets a scrim
tsahimatsliah Oct 2, 2026
c038018
fix(mobile): no focus ring on the sheet panel
tsahimatsliah Oct 2, 2026
f63a6c3
Merge remote-tracking branch 'origin/main' into claude/mobile-shell-1
tsahimatsliah Oct 2, 2026
d94ec24
fix(marketplace): the plugin pages take the phone block
tsahimatsliah Oct 2, 2026
b739eb8
style: format the DropdownMenu spec
tsahimatsliah Oct 2, 2026
9821b30
Merge remote-tracking branch 'origin/main' into claude/mobile-shell-1
tsahimatsliah Oct 2, 2026
9057075
Merge remote-tracking branch 'origin/main' into claude/mobile-shell-1
tsahimatsliah Oct 2, 2026
60c0e46
fix(mobile): the phone gates agree with the server, and the block sit…
tsahimatsliah Oct 2, 2026
00c2ae6
fix(mobile): the Home row holds its sort controls, and You opens the …
tsahimatsliah Oct 2, 2026
b80cc5a
Merge remote-tracking branch 'origin/main' into claude/mobile-shell-1
tsahimatsliah Oct 2, 2026
0740364
feat(mobile): menu sheets drag to dismiss, tall sheets grow to a page
tsahimatsliah Oct 2, 2026
9817f7c
feat(mobile): the quest panel opens as a sheet on phones
tsahimatsliah Oct 2, 2026
9b060c5
fix(mobile): the form sheet scrolls as one piece, Cancel moves to the…
tsahimatsliah Oct 2, 2026
83e6361
feat(mobile): the bar leaves settings and forms, the lit tab returns,…
tsahimatsliah Oct 2, 2026
f88f287
feat(mobile): You as a side drawer, glass feel on the bar, and Chris'…
tsahimatsliah Oct 2, 2026
cbe6417
feat(mobile): the page slides aside to uncover the You menu
tsahimatsliah Oct 2, 2026
ea6508d
feat(mobile): the lens follows on a spring, releases settle, the lit …
tsahimatsliah Oct 2, 2026
90d2a85
test(shell): the refresh passes throwOnError to invalidateQueries
tsahimatsliah Oct 2, 2026
3adc909
fix(mobile): the page is an opaque card while it slides, and a finger…
tsahimatsliah Oct 2, 2026
c59445e
Merge remote-tracking branch 'origin/main' into claude/mobile-shell-1
tsahimatsliah Oct 2, 2026
c656aca
fix(mobile): a modal opened from the You menu first sends the page home
tsahimatsliah Oct 2, 2026
ca6ccb0
feat(mobile): You is a standalone page again, Help carries the suppor…
tsahimatsliah Oct 2, 2026
5fb8e0a
chore: keep the mobile-ux Storybook workspace out of the shell PR
tsahimatsliah Oct 2, 2026
853943a
feat(mobile): Settings stays on screen on the You page
tsahimatsliah Oct 2, 2026
4c36f46
ci: rerun after a flaky PostPage test
tsahimatsliah Oct 2, 2026
eb3daf5
feat(mobile): sheets reach their edges, one glass material, back to t…
tsahimatsliah Oct 2, 2026
79ebab0
fix(mobile): the floating material keeps production's opaque base
tsahimatsliah Oct 2, 2026
0b9bbd4
ci: rerun after a flaky further-reading test
tsahimatsliah Oct 2, 2026
6cd3c4c
fix(mobile): back to top uses the move-to glyph
tsahimatsliah Oct 2, 2026
e68d407
fix(mobile): the tinted glass is for the Create square and the action…
tsahimatsliah Oct 2, 2026
63fa425
fix(mobile): back to top stays put while the bar shrinks
tsahimatsliah Oct 2, 2026
9e92579
fix(mobile): a smaller outline glyph on back to top
tsahimatsliah Oct 2, 2026
b04a910
fix(mobile): the block and the bar hide and return as a whole, never …
tsahimatsliah Oct 2, 2026
891cc5d
ci: rerun after the flaky further-reading test
tsahimatsliah Oct 2, 2026
4934c8f
fix(mobile): menu sheets drag to dismiss on WebKit; titled sheets car…
tsahimatsliah Oct 2, 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
1 change: 1 addition & 0 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"peerDependencies": {
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-query-devtools": "^4.35.3",
"aria-hidden": "^1.2.6",
"classnames": "^2.3.1",
"date-fns": "^2.22.1",
"date-fns-tz": "1.2.2",
Expand Down
80 changes: 71 additions & 9 deletions packages/shared/src/components/BookmarkFeedLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import React, {
useState,
} from 'react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router';
import classNames from 'classnames';
import { webappUrl } from '../lib/constants';
import {
BookmarkSort,
BOOKMARKS_FEED_QUERY,
Expand All @@ -25,9 +27,13 @@ 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 { useIsPhone } from '../hooks/useViewSize';
import { useLayoutVariant } from '../hooks/layout/useLayoutVariant';
import { PageHeader } from './layout/PageHeader';
import { BookmarkSection } from './sidebar/sections/BookmarkSection';
import { ShellPage } from './shell/ShellPageContext';
import { Segments, ShellRow } from './shell/ShellRow';
import { useBookmarkFolderList } from '../hooks/bookmark';
import PlusMobileEntryBanner from './marketing/banners/PlusMobileEntryBanner';
import { DigestBookmarkBanner } from './marketing/banners/DigestBookmarkBanner';
import {
Expand Down Expand Up @@ -80,6 +86,9 @@ export default function BookmarkFeedLayout({
isReminderOnly,
}: BookmarkFeedLayoutProps): ReactElement | null {
const [isHydrated, setIsHydrated] = useState(false);
const router = useRouter();
const { folders: bookmarkFolders } = useBookmarkFolderList();
const bookmarkPath = (router.asPath ?? router.pathname ?? '').split('?')[0];
const {
shouldUseListFeedLayout,
FeedPageLayoutComponent,
Expand All @@ -94,6 +103,7 @@ export default function BookmarkFeedLayout({
DEFAULT_BOOKMARK_SORT_INDEX,
);
const isLaptop = useViewSize(ViewSize.Laptop);
const isPhone = useIsPhone();
const { isV2 } = useLayoutVariant();
const isV2Laptop = isV2;
const isSearchResults = !!searchQuery;
Expand Down Expand Up @@ -190,29 +200,36 @@ export default function BookmarkFeedLayout({
return null;
}

// In the block the controls are the shell's 38px squares.
const blockButtonClassName = isPhone
? 'shell-material !size-[2.375rem] !rounded-14 !p-0'
: undefined;

const sortDropdown = !isSearchResults && (
<Dropdown
className={{
label: 'hidden',
chevron: 'hidden',
button: isV2Laptop ? undefined : '!px-1',
container: isV2Laptop ? 'flex' : 'ml-4 flex',
button: blockButtonClassName ?? (isV2Laptop ? undefined : '!px-1'),
container: isV2Laptop || isPhone ? '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}
buttonVariant={
isV2Laptop || isPhone ? 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'}
className={blockButtonClassName ?? (isV2Laptop ? undefined : 'ml-4 flex')}
icon={
<ShareIcon
size={isV2Laptop ? IconSize.XSmall : IconSize.Medium}
Expand All @@ -222,7 +239,9 @@ export default function BookmarkFeedLayout({
}
onClick={() => setShowSharedBookmarks(true)}
size={isV2Laptop ? ButtonSize.Small : ButtonSize.Medium}
variant={isV2Laptop ? ButtonVariant.Tertiary : ButtonVariant.Secondary}
variant={
isV2Laptop || isPhone ? ButtonVariant.Tertiary : ButtonVariant.Secondary
}
>
{isLaptop ? <span>Share bookmarks</span> : null}
</Button>
Expand Down Expand Up @@ -292,9 +311,13 @@ export default function BookmarkFeedLayout({
)}
>
{searchChildren}
{sortDropdown}
{shareButton}
{folderMenu}
{!isPhone && (
<span className="hidden tablet:contents">
{sortDropdown}
{shareButton}
{folderMenu}
</span>
)}
</CustomFeedHeader>
</>
)}
Expand All @@ -305,7 +328,46 @@ export default function BookmarkFeedLayout({
onRequestClose={() => setShowSharedBookmarks(false)}
/>
)}
<div className="relative mb-4 laptop:hidden">
<ShellPage
title="Bookmarks"
actions={
<div className="flex items-center gap-2">
{sortDropdown}
{shareButton}
{folderMenu}
</div>
}
row={
<ShellRow>
<Segments
items={[
{
key: 'quick-saves',
label: 'Quick saves',
href: `${webappUrl}bookmarks`,
active: bookmarkPath === '/bookmarks',
replace: true,
},
{
key: 'later',
label: 'Read it later',
href: `${webappUrl}bookmarks/later`,
active: bookmarkPath === '/bookmarks/later',
replace: true,
},
...bookmarkFolders.map((list) => ({
key: list.id,
label: list.name,
href: `${webappUrl}bookmarks/${list.id}`,
active: bookmarkPath === `/bookmarks/${list.id}`,
replace: true,
})),
]}
/>
</ShellRow>
}
/>
<div className="relative mb-4 hidden tablet:block laptop:hidden">
<BookmarkSection
isItemsButton={false}
sidebarExpanded
Expand Down
19 changes: 19 additions & 0 deletions packages/shared/src/components/Feed.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,16 @@ const TopHeroSlotTarget = (): ReactElement => {
return <div data-testid="top-hero-slot" ref={setSlot} />;
};

// On a phone the post menu is a two-level sheet: hide and the block rows sit
// behind Not interested; desktop keeps them on the one level.
const openNotInterested = async () => {
await screen.findByText('Share via');
const row = screen.queryByText('Not interested');
if (row) {
fireEvent.click(row);
}
};

beforeEach(() => {
queryClient.clear();
jest.restoreAllMocks();
Expand Down Expand Up @@ -961,6 +971,7 @@ describe('Feed logged in', () => {
fireEvent.keyDown(menuBtn, {
key: ' ',
});
await openNotInterested();
const contextBtn = await screen.findByText('Hide');
contextBtn.click();
await waitFor(() => expect(hideCalled).toBeTruthy());
Expand Down Expand Up @@ -1000,6 +1011,7 @@ describe('Feed logged in', () => {

const [menuBtn] = await screen.findAllByLabelText('Options');
fireEvent.keyDown(menuBtn, { key: ' ' });
await openNotInterested();
(await screen.findByText('Hide')).click();
const undoBtn = await screen.findByRole('button', { name: 'Undo' });
fireEvent.click(undoBtn);
Expand Down Expand Up @@ -1034,6 +1046,7 @@ describe('Feed logged in', () => {

const [menuBtn] = await screen.findAllByLabelText('Options');
fireEvent.keyDown(menuBtn, { key: ' ' });
await openNotInterested();
(await screen.findByText('Hide')).click();

const closeBtn = await screen.findByTestId('postHiddenPanelClose');
Expand Down Expand Up @@ -1066,6 +1079,7 @@ describe('Feed logged in', () => {

const [menuBtn] = await screen.findAllByLabelText('Options');
fireEvent.keyDown(menuBtn, { key: ' ' });
await openNotInterested();
(await screen.findByText('Hide')).click();

const doneBtn = await screen.findByTestId('postHiddenPanelDone');
Expand Down Expand Up @@ -1121,6 +1135,7 @@ describe('Feed logged in', () => {
);
expect(data).toBeTruthy();
});
await openNotInterested();
(await screen.findByText('Hide')).click();

fireEvent.click(await screen.findByTestId('hideBlockSourceButton'));
Expand Down Expand Up @@ -1211,6 +1226,7 @@ describe('Feed logged in', () => {
);
expect(data).toBeTruthy();
});
await openNotInterested();
(await screen.findByText('Hide')).click();

fireEvent.click(await screen.findByTestId('hideBlockSourceButton'));
Expand Down Expand Up @@ -1264,6 +1280,7 @@ describe('Feed logged in', () => {
);
expect(data).toBeTruthy();
});
await openNotInterested();
const contextBtn = await screen.findByText('Block Echo JS');
fireEvent.click(contextBtn);
await waitForNock();
Expand Down Expand Up @@ -1315,6 +1332,7 @@ describe('Feed logged in', () => {
);
expect(data).toBeTruthy();
});
await openNotInterested();
const contextBtn = await screen.findByText('Unblock Echo JS');
fireEvent.click(contextBtn);

Expand Down Expand Up @@ -1359,6 +1377,7 @@ describe('Feed logged in', () => {
);
expect(data).toBeTruthy();
});
await openNotInterested();
const contextBtn = await screen.findByText('Block #javascript');
fireEvent.click(contextBtn);

Expand Down
39 changes: 35 additions & 4 deletions packages/shared/src/components/MainFeedLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import {
useViewSize,
ViewSize,
} from '../hooks';
import { useIsPhone } from '../hooks/useViewSize';
import { feedNameToHeading, v2FeedSideInsetClass } from './feeds/FeedContainer';
import { pageHeaderClassName } from './layout/PageHeader';
import {
Expand Down Expand Up @@ -105,6 +106,16 @@ import { useTrackQuestClientEvent } from '../hooks/useTrackQuestClientEvent';
import { useLayoutVariant } from '../hooks/layout/useLayoutVariant';
import SearchMobileFiltersButton from './search/SearchMobileFiltersButton';

import { ExploreSortMenu } from './shell/ExploreSortMenu';
import { ShellPage } from './shell/ShellPageContext';

const SpotlightTrigger = dynamic(
() =>
import(
/* webpackChunkName: "spotlightTrigger" */ './spotlight/SpotlightTrigger'
),
);

const FeedExploreHeader = dynamic(
() =>
import(/* webpackChunkName: "feedExploreHeader" */ './header').then(
Expand Down Expand Up @@ -264,6 +275,7 @@ export default function MainFeedLayout({
});
const { isCustomDefaultFeed, defaultFeedId } = useCustomDefaultFeed();
const isLaptop = useViewSize(ViewSize.Laptop);
const isPhone = useIsPhone();
const { isV2 } = useLayoutVariant();
const feedVersion = useFeature(feature.feedVersion);
const { time, contentCurationFilter, postTypesFilter } =
Expand Down Expand Up @@ -730,7 +742,8 @@ export default function MainFeedLayout({
// (see `enableSsrSafeLayout`), so keying the spacing to it made the
// gap change size on navigation and settle differently on reload.
const disableTopPadding =
isFinder || (shouldUseListFeedLayout && !isAnyExplore);
isFinder ||
(shouldUseListFeedLayout && (!isAnyExplore || (isPhone && hasMounted)));
const onTabChange = useCallback(
(clickedTab: ExploreTabs) => {
if (clickedTab === ExploreTabs.BestOf && isExtension) {
Expand Down Expand Up @@ -764,22 +777,26 @@ export default function MainFeedLayout({
);
}

if (isPhone) {
return <ExploreSortMenu />;
}

return (
<FeedExploreHeader
tab={tab}
setTab={onTabChange}
showBreadcrumbs={false}
className={{
container: classNames(
'sticky top-[calc(4.5rem+var(--mobile-app-header-offset,0px))] z-header w-full border-b border-border-subtlest-tertiary bg-background-default transition-[top] duration-200 ease-out',
'z-header w-full border-b border-border-subtlest-tertiary bg-background-default transition-[top] duration-200 ease-out tablet:sticky tablet:top-[calc(4.5rem+var(--mobile-app-header-offset,0px))]',
feedGutter,
),
tabBarHeader: 'no-scrollbar overflow-x-auto',
tabBarContainer: 'min-w-0 flex-1',
}}
/>
);
}, [isLaptop, onTabChange, tab]);
}, [isLaptop, isPhone, onTabChange, tab]);

// v2 reaches the Explore hub sections (Explore, Tags, Sources, Leaderboard,
// Discussions) from the sidebar's Explore panel, so the page header no longer
Expand Down Expand Up @@ -899,7 +916,21 @@ export default function MainFeedLayout({
>
{isAnyExplore && !showExploreV2PageHeader && <FeedExploreComponent />}
{isSearchOn && !isSearchPageLaptop && search}
{isSearchOn && !isSearchPageLaptop && (
{isExploreTag && isPhone && (
<ShellPage title={`#${router.query?.tag ?? ''}`} />
)}
{isSearchOn && !isSearchPageLaptop && isPhone && (
<ShellPage
title={searchQuery || 'Search'}
actions={<SearchMobileFiltersButton square />}
row={
<div className="px-2 pb-1">
<SpotlightTrigger />
</div>
}
/>
)}
{isSearchOn && !isSearchPageLaptop && !isPhone && (
<div
className={classNames(
'mb-3 flex justify-end px-4',
Expand Down
Loading
Loading