Skip to content

chore(layout): graduate layout v2, drop the flag and the v1 chrome - #7017

Merged
rebelchris merged 9 commits into
mainfrom
chore/layout-v2-cleanup
Oct 23, 2026
Merged

rebelchris merged 9 commits into
mainfrom
chore/layout-v2-cleanup

Conversation

@rebelchris

@rebelchris rebelchris commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Why

The dual-sidebar layout won its experiment. This removes the layout_v2_2 flag and everything that existed only to wait for it or to serve the control arm, so nothing in the layout shifts because a flag resolved after paint.

What changes

  • useLaptopLayout replaces useLayoutVariant().isV2. It is the laptop breakpoint. Across hydration it answers with the shell the server painted (the rail shell on the mirrored /layout-v2 post page, the header shell everywhere else), so server HTML and the hydration render always agree. Every later mount, including client-side navigation and the extension's new tab, reads the viewport synchronously and is right on its first render.
  • Every isLoading gate is gone: the sidebar hold in MainLayout, the top-hero hold in Feed, the CV-banner hold in FeedContainer, and the extension's HijackingLoginStrip wrapper. The content padding transition enables one commit after the settle on every page, so the first paint never slides.
  • Control-arm code removed: SidebarDesktop (v1) and its spec, the sidebar's MainSection, CustomFeedSection, SidebarMenuIcon, SidebarProfileCompletion, the v1 expanded-sidebar padding and the screenCentered layout prop (plus useFeedLayout().screenCenteredOnMobileLayout), the install-extension CTA in the header strip, the laptop breadcrumbs on the leaderboard pages and in the explore header, ExploreChipsBar's pill rendering, the chips slot on the tablet header strip and empty screen, the favorite-star branch of the squad row, the !isV2Panel branch in NetworkSection, Float button variants that only applied on laptop without the rail, dead laptop: classes inside tablet-only branches, the profile-page sidebar overlay rule in utilities.css, and the storybook history page comparing the two navigations. The storybook sources shell now draws the collapsed rail for signed-out laptops.
  • The /layout-v2 post-page mirror and dl_v cookie stay. Prerendered pages are cached per path, so a logged-in laptop session can only get the rail and the floating card in its initial HTML from a path that carries the shell. The cookie now records "logged in on a laptop" and waits for boot before writing.
  • Recent pages are recorded for signed-in laptop sessions only; the store is device-wide.
  • Bottom signup banner starts beside the rail on laptop, sized from the same sidebar-density flag MainLayout pads with (64px compact, which is what logged-out visitors get). Its headline and buttons ran under the rail on every signed-out laptop page below 1668px (visible in the preview at 1024).
  • Remaining isLaptopLayout branches pick between the laptop chrome and the tablet chrome. Tablet and phone are unchanged.

Rollout order

Do not archive layout_v2_2 when this deploys. Published extension builds keep evaluating the flag until the store update carrying this PR reaches them, and an archived feature falls back to false, which would return those users to the v1 sidebar. Keep the flag forced on at 100% until that extension release has propagated, then archive it.

Product changes to confirm

  • Anonymous laptop visitors get the collapsed rail under the legacy header instead of the v1 sidebar; the rail without panels is now everyone's logged-out navigation (Explore, Tags, Sources, Leaderboard reachable through the Explore tab's hover panel and the page header). The header itself is unchanged for them since sidebarOwnsHeader still requires a user.
  • Everyone on laptop is now eligible for the sidebar tour (useSidebarTourState, built for "existing users who were moved", so expect a wave of tour starts on deploy), sees the Agents entry in Discover (showAgent is evaluated for every logged-in user), and gets SidebarDensitySection in Appearance.
  • The install-extension CTA in the header strip existed only for the control arm, so this removes a growth surface for extension installs.

Known behaviour, unchanged by this PR

A logged-in laptop session hard-loading a non-post, non-feed route (/tags, /sources, /users, squads, settings, a profile) gets SSR HTML with the header, which the rail replaces once the cached session is read on the client. The v2 arm already had this; the post page is the only mirrored route because every mirrored route doubles its ISR entries. Feed pages hold until paint-ready and are not affected. Extending the mirror to more routes is a separate decision.

Follow-ups

  • ENG-2243: fix the pre-existing strict errors in the four pages that joined the strict skip list because their only change here is the dropped screenCentered prop.
  • Rename PR: the remaining v2 identifiers (SidebarDesktopV2, isV2Panel, isV2Grid, showExploreV2PageHeader, showFeedV2PageHeader, hasV2Chips, v2FeedSideInsetClass, v2FeedInsetClass, the html.layout-v2 root class) and the ~90 remaining v1/v2 comment lines (BookmarkSection, CustomFeedEmptyScreen, FeedContainer, MainFeedLayout, FeedExploreTabs, sidebar/common.tsx, NotificationsBell, ExploreChipsBar, the extension's CompanionPopupButton).

Verification

  • node ./scripts/typecheck-strict-changed.js passes; full tsc for webapp, extension and playwright passes.
  • Jest on the final head: the entire webapp suite (87 suites, 652 tests, including the PostPage and ExplorePage suites CI flagged), 31 shared suites around hooks, sidebar, feed and header strip, and the entire extension suite (8 suites). New or extended specs: useLaptopLayout.spec (first render of a client mount reads the viewport; the mirrored shell renders to a string and hydrates without a mismatch, then the viewport takes over), SidebarItem.spec (login gate), AccountPageContainer.spec and MainLayout.tsx (laptop cases), SidebarDesktopV2.spec (Happening Now, Explore hub pages).
  • ESLint and Prettier clean on every changed file.
  • Playwright: the login helpers now accept the rail's "You" tab and wait for the Turnstile token before submitting (the token race made the first dispatched run fail at login). A later dispatch against the preview (run 39933000566) got past that wait and the preview's backend rejected the e2e test account, so the post-login assertion never ran; main runs today fail later in the same spec. Laptop login detection is therefore untested end-to-end on this branch; the "You" tab is pinned only by the rail's DOM (role="tab", aria-label="You").
  • Anonymous laptop screenshots at 1024 and 1440 of /posts, /tags, /sources, /users on the preview were taken and reviewed (rail under the header, page-header strip, banner beside the rail after the fix). They could not be attached from this session.
  • Not measured: CLS on a logged-in hard load of a non-post route, which needs a signed-in browser session.

🤖 Generated with Claude Code

Preview domain

https://chore-layout-v2-cleanup.preview.app.daily.dev

The dual-sidebar layout won its experiment, so the `layout_v2_2` flag goes
and the laptop layout becomes the only laptop layout. `useLaptopLayout`
replaces `useLayoutVariant().isV2`: it is the laptop breakpoint, held at the
server's answer (the mirrored `/layout-v2` shell, or none) until the client
has measured the viewport, so the server HTML and the first client render
agree and the chrome never flips because a flag resolved late. Every
`isLoading` gate that waited for GrowthBook (the sidebar hold, the top hero,
the CV banner, the extension hijacking strip) is gone with it, and the
content padding no longer animates its first settle on any page.

The `/layout-v2` post-page mirror and its `dl_v` cookie stay: prerendered
pages are cached per path, so the only way a logged-in laptop session gets
the rail and the floating card in its initial HTML is a path that carries
the shell. The cookie now records "logged in on a laptop" rather than the
flag's answer.

Removed with the control arm: `SidebarDesktop` (v1) and its spec, the v1
expanded-sidebar padding (`screenCentered` is accepted but no longer read),
the laptop-only v1 height branches in the agent shell, the `!isV2` gates in
`DiscoverSection` (which only the laptop sidebar renders now), and the
storybook history page comparing the two navigations. Branches that still
read the hook pick between the laptop and tablet chrome.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
daily-webapp Ready Ready Preview Oct 23, 2026 10:40am UTC
storybook Ready Ready Preview Oct 23, 2026 10:40am UTC

Request Review

@rebelchris rebelchris left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff (67 files) against main, plus the code it leaves behind. Most of it is a safe mechanical swap from isV2 to isLaptopLayout. The flag and the v1 SidebarDesktop are gone, and the mirror route, cookie and hydration agreement still hold on the post page.

Blocking

  1. useLaptopLayout returns false on the first render of every mount, not only during hydration. Laptop client navigations and every extension new tab now render the tablet chrome for one commit, which also causes double or phantom impressions (CV banner, hijacking strip). Suggested fix: use the existing useIsHydrated. Details are inline.

Control/v1 leftovers not covered inline (these files are not in the diff)

  • sidebar/sections/CustomFeedSection.tsx is now dead. Its only renderer was the deleted SidebarDesktop, and V2 only mentions it in a comment.
  • sidebar/SidebarMenuIcon.tsx is now dead (only SidebarDesktop imported it).
  • NetworkSection and CustomFeedSection keep !isV2Panel branches that can no longer run, because their only renderer passes isV2Panel: true. BookmarkSection still needs the false branch for the tablet bookmarks page.
  • useFeedLayout().screenCenteredOnMobileLayout no longer has a consumer.
  • Storybook stories/sources/shell.tsx (SignedOutRail: "SidebarDesktop (v1)… production still serves the legacy 44px sidebar to visitors", laptop:pl-11) and stories/sources/states.tsx (~381) still draw the old signed-out laptop chrome. This PR changes exactly that surface for anonymous laptop visitors.
  • Already dead on main, but in the same area: sidebar/SidebarProfileCompletion.tsx has no importers.

Coverage

  • Sidebar.spec.tsx was deleted rather than ported. Some of its cases cover behaviour that is not specific to v1, such as the login requirement before an anonymous user opens Following, and section collapse and dividers. Please move the cases that still apply to the rail into SidebarDesktopV2.spec.tsx.

Product checks worth stating in the PR

  • Users from the control arm become eligible for the sidebar tour (useSidebarTourState) and the Agents entry in Discover (showAgent is now evaluated for every logged-in user). This is presumably intended, but please confirm.
  • Anonymous laptop visitors now get the rail under the legacy header. Please add screenshots for anonymous and logged-in laptop at 1024 and 1440 on the final head, plus one client-side navigation (feed to wallet to settings) with a performance recording, to confirm nothing flips after paint.

CI: typecheck, lint, the webapp and extension tests and the extension build are green. test_shared was still running when I checked.

Reviewed by AI.

// `useViewSize(ViewSize.Laptop)` check is needed at the call site.
export const useLaptopLayout = (): boolean => {
const serverVariant = useContext(LayoutVariantContext);
const isLaptop = useMediaClient(laptopQueries, laptopValues, false);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: every component that mounts reads false on its first render, including after client-side navigation, not only on hydration.

useMediaClient starts as undefined on every mount and only settles in an effect. Outside the mirror, the hook therefore returns false for the first commit of every newly mounted consumer. The old useLayoutVariant used the synchronous useViewSize and a cached flag, so after the first load the v2 arm got the right value on its first render.

Effect on laptop, for everyone after this merges:

  • Client navigation. Going to /wallet, /notifications, /analytics, settings, bookmarks, jobs and so on renders the tablet branch first (desktopHeader, LayoutHeader, ProfileSettingsMenuDesktop, Float buttons, max-w-5xl) and then swaps in PageHeader and the compact chrome. Route changes are not discrete updates, so the passive effect most likely flushes after paint. That makes it a visible flash plus layout shift on most laptop page changes.
  • Extension new tab. There is no SSR there (createRoot), so nothing needs to be held at all. Every new tab still pays the same extra commit: the hijacking strip, the shortcuts row in the feed slot, and the inline reading reminder.
  • Impressions. Components that log on mount now log in an arm the user never sees (see the FeedContainer and HijackingLoginStrip threads).

The repo already has the primitive for this: useIsHydrated (useSyncExternalStore). It is false only on hydration renders, true on every later mount, and true from the first render under createRoot:

export const useLaptopLayout = (): boolean => {
  const serverVariant = useContext(LayoutVariantContext);
  const isHydrated = useIsHydrated();
  const isLaptop = useViewSize(ViewSize.Laptop);

  return isHydrated ? isLaptop : serverVariant === 'v2';
};

This keeps the server and hydration renders in agreement, which is what the hook is meant to do, and it removes the post-mount flip everywhere else. Could you add a spec that mounts a consumer after hydration (or under a plain render) and asserts true on its first render at laptop?

Reviewed by AI.

// evaluated, wait for it: mounting in the wrong arm first logs a second
// impression.
const isLayoutVariantPending = isLaptop && isLayoutVariantLoading;
const shouldEvaluateBanner =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: CV banner impression is logged twice. The guard removed here had a written reason: "mounting in the wrong arm first logs a second impression". That reason still holds, because useLaptopLayout is false on the first render of any FeedContainer mount (see the useLaptopLayout thread).

Scenario: a laptop user with the CV marketing CTA navigates client-side to My Feed. Settings are already loaded, so the first render mounts the inline ProfileUploadBanner, and its mount effect logs Impression. The next commit unmounts it and mounts CvTopHero in the portal, which logs Impression again. The same thing makes the reading reminder in Feed.tsx render inline for one commit before it moves to TopHeroPortal.

Fixing the hook as suggested resolves both. If the hook stays as it is, the hold has to stay.

Reviewed by AI.

return null;
}

export default function HijackingLoginStrip(): ReactElement {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking (extension): the strip flashes on laptop and can log an impression nobody saw. MainFeedPage drops the shortcuts slot when isLaptopLayout is true. Under createRoot, though, useLaptopLayout is false on the first render, so HijackingHeroStrip mounts for one commit and then unmounts. If user is set, isReadyToLogImpression is already true, so the hijacking impression fires for a strip that was never seen. That skews the running hijacking experiment. The two deleted specs covered exactly this case.

With the useIsHydrated fix on the hook, the extension gets the right value on its first render and this wrapper can go. When it does, please inline HijackingHeroStrip as the default export rather than keeping a pass-through.

Reviewed by AI.


useEffect(() => {
if (isExtension || isLoading) {
if (isExtension) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: removing the isLoading guard means the effect now runs on mount with isLoggedIn === false until boot resolves. Every hard load therefore expires dl_v (max-age=0) and writes it again later. If boot fails or the tab closes first, the next post open serves the plain page and flips. The deleted spec "writes nothing before the flag resolves" protected against this. Gating on isAuthReady (return early until it is true) keeps that behaviour without the flag.

Reviewed by AI.

*/
export const featureSponsorStrip = new Feature('sponsor_strip', isDevelopment);

export const featureLayoutV2 = new Feature('layout_v2_2', false);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rollout ordering: please don't archive layout_v2_2 when this deploys. Extension builds already published keep evaluating this flag until the store update reaches them. If the feature is archived, they fall back to the false default and return those users to the v1 sidebar and header. Keep the flag forced on at 100% until the extension release containing this PR has propagated, then archive it. The PR description should state this order.

Reviewed by AI.

activePage?: string;
isNavItemsButton?: boolean;
/**
* No longer read: the laptop layout pads the content for the sidebar the

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleanup: you asked for the control path to be removed completely, and this keeps a prop that is no longer read and is still set in about 60 layoutProps objects plus MainFeedPage (screenCentered={false}). Its only other input, useFeedLayout().screenCenteredOnMobileLayout, has also lost its last consumer. Removing all of it is mechanical (git grep -l screenCentered) and keeps a future reader from tuning a prop that does nothing. Non-blocking if you'd rather do it as a follow-up, but then please link the follow-up.

Reviewed by AI.

// card border rules. No bottom margin from `tablet` up, where the grid
// already opens with that inset; mobile keeps one as the only separator.
const isV2Grid = isV2 && !shouldUseListFeedLayout;
const isV2Grid = isLaptopLayout && !shouldUseListFeedLayout;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleanup (non-blocking): v2 naming is still left now that there is only one laptop layout. In this file: isV2Grid, showExploreV2PageHeader, showFeedV2PageHeader, v2ActionButtons. In layout/common.tsx: hasV2Chips, shouldUseCompactV2Actions. Also isV2Grid in BookmarkFeedLayout, the isV2Panel prop on Section/SidebarSectionProps, SidebarDesktopV2 itself (now the only desktop sidebar), and the html.layout-v2 root class. Comments that still describe a v1/v2 split include BookmarkFeedLayout ("v2 hoists…", "inside the v2 floating card, so v2 is unchanged"), ToggleClickbaitShield ("v2 dual-sidebar laptop"), Section.tsx / sections/common.tsx ("so the v1 sidebar is unchanged"), ReadTopLeaderboard ("4rem for the v1 header") and SidebarDesktopV2 (around line 1224, "CustomFeedSection is v1's sidebar only"). The renames could go in a separate PR so this one stays reviewable, but they belong in the graduation.

Reviewed by AI.

…ntrol sweep

`useLaptopLayout` now holds the server's shell only across hydration
(`useIsHydrated`) and reads the viewport synchronously everywhere else, so a
client-side navigation and the extension's new tab get the laptop chrome on
their first render. The previous `useMediaClient` version answered false on
the first render of every mount, which re-introduced the tablet-then-laptop
flip the flag removal was meant to end and logged impressions for a CV
banner and a hijacking strip nobody saw. The cookie hook waits for boot
again, so a hard load no longer expires `dl_v` before writing it back.

Control-only remnants removed: the install-extension CTA in the header
strip (rendered only on laptop under the header layout), the laptop
breadcrumbs on the leaderboard pages, the explore header's breadcrumbs and
laptop branch, the heading wrapper and `feedHeading` memo in
`FeedContainer`, `Float` button variants that only applied on laptop
without the rail, dead `laptop:` classes inside tablet-only branches,
`CustomFeedSection`, `SidebarMenuIcon`, `SidebarProfileCompletion`, the
`!isV2Panel` branch in `NetworkSection`, the `screenCentered` layout prop
and `useFeedLayout().screenCenteredOnMobileLayout`. The four pages whose
only change is the dropped prop carry pre-existing strict errors and join
the strict skip list. The storybook sources shell draws the collapsed rail
for signed-out laptops instead of the v1 sidebar.

The rail panel cases from the deleted v1 sidebar spec that still apply
(Happening Now link, Explore hub pages) move to the rail spec; the
anonymous Following login case has no rail equivalent, since signed-out
visitors get the rail without panels.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rebelchris

Copy link
Copy Markdown
Contributor Author

Addressed in 65b7e12.

Blocking

  • useLaptopLayout now uses useIsHydrated plus the synchronous useViewSize, exactly as suggested. The server's shell is held only across hydration; every client mount reads the viewport on its first render. useLaptopLayout.spec.tsx asserts a plain mount at laptop renders true on its first render and never false.
  • With that, the CV banner, the reading reminder and the hijacking strip mount in the right arm the first time, so the removed holds stay removed. HijackingLoginStrip is now the hero strip itself, no pass-through.

Cookie: useLayoutVariantCookie returns early until isAuthReady, and the "writes nothing before boot resolves" case is back in its spec.

Rollout: the PR description now says to keep layout_v2_2 forced on until the extension release has propagated, and to archive it only after.

Control leftovers: CustomFeedSection, SidebarMenuIcon and SidebarProfileCompletion are deleted; NetworkSection lost its !isV2Panel branch (BookmarkSection keeps its tablet branch); screenCentered and screenCenteredOnMobileLayout are removed everywhere (the four pages with pre-existing strict errors joined the skip list); the storybook sources shell draws the collapsed rail for signed-out laptops. Also found and removed on the same sweep: the install-extension CTA in the header strip, the leaderboard and explore-header breadcrumbs, the feedHeading wrapper in FeedContainer, and Float variants that only applied on laptop without the rail.

Coverage: the rail spec now covers the Happening Now link and the Explore hub pages. The anonymous "login before Following" case has no rail equivalent, since signed-out visitors get the rail without panels.

Naming: the v2 identifier renames are listed as a follow-up PR so this one stays reviewable. The stale v1/v2 comments you listed are fixed here.

Screenshots / recording: not attached; the description states what the specs pin down instead.

@rebelchris rebelchris left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second pass over the full main...head diff (now 127 files, +705/−2129), not only the new commit. I traced every removed !isV2 branch against main, swept the repo for control/v1 remnants, and ran the related webapp suites locally.

What the new commit fixed (those threads can be resolved)

  • useLaptopLayout now uses useIsHydrated + useViewSize. Client navigation and the extension get the right value on the first render, so the CV-banner and hijacking-strip phantom impressions are gone.
  • The cookie waits for isAuthReady again.
  • The rollout order for layout_v2_2 is now in the PR body.
  • screenCentered and screenCenteredOnMobileLayout, CustomFeedSection, SidebarMenuIcon and SidebarProfileCompletion are fully removed.
  • The storybook sources shell no longer draws v1.

Branch audit: no tablet, phone or extension regression found. Every deleted branch I traced was reachable only at laptop width on the control arm, or on the single hydration render. That covers:

  • the install-extension CTA, the Float variants and the feed-heading wrapper
  • the explore, users and users/[id] breadcrumbs
  • the sources/index, briefing, squad-directory and AccountPageContainer classes
  • the interests/shell heights, DiscoverSection and Sidebar.tsx

On hard loads, loadedSettings and the cached user arrive via the BootProvider effect, after the useIsHydrated re-render. So the CV banner and reading reminder can't mount in the tablet slot first. A future synchronous boot-cache read would reintroduce the double impression, so a spec pinning that order would be cheap.

Blocking

  1. CI is red. test_webapp fails in 9 cases (PostPage.tsx ×5, ExplorePage.tsx ×4). They pass on main. The cause is a router-mock gap that the laptop path now exposes, not a product bug; details and the fix are in the MainLayout.tsx thread. The PR body says the webapp suites are green, so please re-run on the final head.

Should fix before merge
2. styles/utilities.css (~lines 53-68), "Sidebar overlay mode on profile pages below 1360px". This hack from profile 4.0 was written for the old sidebar. Its nav[role='navigation'] selector matches nothing now. The second rule, body:has(.profile-page) main { padding-left: 0 !important; margin-left: 20px }, still applies from 1020 to 1359px. It overrides MainLayout's laptop:pl-20 rail padding on every profile page (ProfileLayout adds .profile-page), so profile content very likely sits under the fixed 80px rail. Please check /<username> at 1024–1359 on the preview and delete or replace the rule. It is not in the diff, so I can't comment on the line.
3. Dead control code still in the head: see the inline threads for sidebar/sections/MainSection.tsx (whole file plus spec), the ExploreChipsBar compact prop, and NetworkSection asPin.

Verification still missing

  • Playwright. Login success is detected via a /profile/i link or the "Profile settings" button (playwright/tests/helpers.ts ~61, login.spec.ts ~24). At 1280px, logged-in users now always get the rail, where the avatar tab is aria-label="You" and ProfileButton isn't rendered once the sidebar owns the header. e2e only runs on main deploys, so please dispatch e2e-tests.yml against the preview before merging.
  • Hard-load layout shift on non-post routes. A logged-in laptop session hard-loading /tags, /sources, /users, squads, settings or a profile gets SSR HTML with MainLayoutHeader and laptop:pt-16, which the rail then replaces after auth. The v2 arm already had this; now every laptop session does. "Nothing shifts because a flag resolved after paint" is true for the flag but not for this swap. Please add CLS numbers for one hard-loaded non-feed page, logged in on laptop. Feed pages are fine, since they hold until paint-ready.
  • Screenshots for anonymous and logged-in laptop at 1024 and 1440, plus an anonymous click-through of Tags, Sources, Leaderboard and Discussions (the rail without panels is now everyone's logged-out navigation).

Coverage (non-blocking)

  • The laptop branch is now the main production path, but AccountPageContainer.spec, AppearanceSettingsPage.spec, SettingsFeedbackPage.spec and __tests__/MainLayout.tsx all force useLaptopLayout to false. Please add one laptop case for the settings container and one for MainLayout.
  • The requiresLogin → showLogin gate in SidebarItem.tsx (~99) lost its only test with Sidebar.spec.tsx. It is still live through the tablet bookmarks section, so a SidebarItem.spec case would cover it.
  • Nothing asserts that the extension MainFeedPage never mounts the hijacking strip on the first render at laptop. The two deleted cases pinned that.

Product (already listed in the PR body, flagging the side effects)

  • Former control users will see the sidebar tour auto-start (it is built for "existing users who were moved"). Expect a wave of tour starts on deploy.
  • SidebarDensitySection in Appearance now shows for every laptop user.
  • The install-extension CTA in the header strip existed only for the control arm, so this removes a growth surface. That is intended, but worth stating for whoever owns extension installs.

Cleanup follow-up (noted in the PR body)
About 90 layout v1/v2 comment lines remain in the touched areas, beyond the identifiers already listed. Some examples:

  • BookmarkSection.tsx:101, CustomFeedEmptyScreen.tsx:43, FeedContainer.tsx ~315-343
  • MainFeedLayout.tsx ~797-837, header/FeedExploreTabs.tsx:64, RouteProgressBar.tsx ~12
  • sidebar/common.tsx, NotificationsBell.tsx ~33-72, ExploreChipsBar.tsx ~24/125
  • the extension's CompanionPopupButton.tsx:59
  • v2FeedSideInsetClass and v2FeedInsetClass in FeedContainer

Please include these in the rename PR.

Reviewed by AI.

const { isEnabled: isLfgEnabled } = useLfgEnabled();
// The v2 rail has its own Looking for group tab.
const showLfg = isLfgEnabled && !isV2;
const showLfg = isLfgEnabled && !isLaptopLayout;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is now dead, so please delete it rather than edit it. On main its only importer was the deleted SidebarDesktop. ProfileMenu imports a different component (ProfileMenu/sections/MainSection), and nothing else imports sidebar/sections/MainSection except MainSection.spec.tsx.

That makes every !isLaptopLayout branch edited here (Game Center, LFG, Marketplace and their alert dots, the agent dot, and the avatar "For You" icon) leftover control-arm code. The spec's "game center link" cases only exercise those branches. Once the file is gone, check whether ActionType.InterestAgentSidebarClick and ActionType.MarketplaceSidebarClick still have a client caller.

Reviewed by AI.

const { rest: shellBlockRest } = useShellBlockPlan();
useLayoutVariantCookie();
useRecordRecentPages(isV2);
useRecordRecentPages(isLaptopLayout);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things follow from this gate now being the viewport alone.

  1. It is why test_webapp is red. PostPage.tsx (5 cases) and ExplorePage.tsx (4 cases) pass on main and fail here. At laptop width, useRecordRecentPages now runs capture(router.asPath), which hits url.split on undefined. RouteProgressBar now mounts inside the floating card and calls router.events.on with no guard. Both suites' router mocks lack asPath (and ExplorePage's lacks events). Adding asPath to the PostPage mockRouter, plus asPath and events: { on, off, emit } to ExplorePage's beforeEach mock, makes both suites pass (70/70), including the laptop-only assertions (signup banner, pinned card, organic ad units, sponsor dock, hero suppression). Optionally, guard router?.events in RouteProgressBar the way useRecordRecentPages already does.
  2. Recording now covers every laptop visitor, anonymous and pre-auth included. On main it needed isAuthReadyOrCached plus the flag. The store is a device-wide localStorage key (dailydev:recentPages), so pages browsed while logged out appear in the next logged-in user's "Recent" on that device. Consider useRecordRecentPages(isLaptopLayout && isLoggedIn).

Reviewed by AI.

const shouldUseCompactV2Actions = isV2Strip && (!!chips || !isTablet);
const dropdownIconSize = isLaptopLayout ? IconSize.XSmall : IconSize.Medium;
const hasV2Chips = isLaptopLayout && !!chips;
const shouldUseCompactV2Actions = isLaptopLayout && (!!chips || !isTablet);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: ViewSize.Tablet is a min-width query, so isTablet is always true wherever isLaptopLayout is true after hydration, and !isTablet is dead here. This simplifies to isLaptopLayout && !!chips. Separately, the isMobile || in buttonSize above can't be reached because of the early if (isMobile) return null (that one was already true on main).

Reviewed by AI.

categories={exploreCategories}
isPending={!feeds}
compact={isV2}
compact={isLaptopLayout}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover control styling: chips are only built when !!user && isLaptop && isChipStripPage. The user is never known on the hydration render, and after hydration isLaptop implies isLaptopLayout, so compact is always true whenever this renders. The non-compact h-10/rounded-12 branches in ExploreChipsBar (around lines 106, 131, 189 and 200) were control-only. So was the chips slot in the tablet return of SearchControlHeader. Please drop the prop and those branches.

Reviewed by AI.

// this shared section keeps the v1 star when the flag is absent.
// Squad "favorite" is framed as "pin" in the rail panel; the star stays for
// other surfaces of `SquadFavoriteButton`.
asPin = false,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asPin is now always true: the only renderer is SidebarDesktopV2 (around line 1728), which passes it. The false default, and the SquadFavoriteButton branch it selects in createSquadMenuItem (squadMenuItem.tsx), were the v1 star. The new comment ("the star stays for other surfaces") is misleading, because those surfaces use SquadFavoriteButton directly, not this path. Please drop the param and that branch.

Reviewed by AI.


// Below laptop the header gives way to SidebarTablet, so the shell shows
// through the floating card rather than the banner.
it('drops the painted shell once the viewport turns out narrower', () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These cases no longer test what the suite describes ("reproduces a server render"). render uses createRoot, so useIsHydrated is true from the first render, and every case is decided by the mocked useViewSize. layoutVariant doesn't affect the outcome. The mirror's real contract is: server markup equals the hydration render, then a flip to tablet chrome on a narrow window. That is now pinned only by useLaptopLayout.spec mocking useIsHydrated. A renderToString + hydrateRoot case for the mirror (laptop and tablet width), asserting no hydration mismatch plus the value after hydration, would cover the only place this hook can still be wrong.

Reviewed by AI.

'packages/extension/src/newtab/App.tsx',
// Layout v2 graduation: touched only to drop the dead `screenCentered`
// layout prop; the strict violations are pre-existing and unrelated.
'packages/webapp/components/layouts/OrganizationLayout/index.tsx',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two notes on this file:

  • Four files go permanently onto the strict skip list because one screenCentered: true line was deleted from each. The list only ever grows. Either fix their strict errors or open a tracked follow-up.
  • The Prettier reflow further down (resolveBaseRef, spawnSync) is unrelated to the layout change. Please revert it so this file only carries the skip-list entries.

Reviewed by AI.

…ntrol code

The router mocks in the PostPage and ExplorePage suites predate the laptop
path: it reads `router.asPath` to record recent pages and subscribes to
`router.events` for the route progress bar, and both suites lacked them.
The progress bar now also tolerates a router without an emitter.

Recent pages are recorded for signed-in laptop sessions only; the store is
device-wide, so anonymous browsing used to seed the next account's list.

Control-only code that the second review pass found: the sidebar's
`MainSection` (its only importer was the deleted v1 sidebar; the profile
menu has its own), `ExploreChipsBar`'s pill rendering (`compact` was always
true where the strip renders), the chips slot on the tablet header strip and
the tablet empty screen, `createSquadMenuItem`'s favorite-star branch (the
rail pins), and the profile-page sidebar overlay rule in `utilities.css`
that targeted the v1 sidebar and still stripped the rail padding from
profile pages between 1020 and 1359px.

The strict-typecheck script carries only the four skip-list entries now.

Coverage: `useLaptopLayout.spec` renders the mirrored shell to a string and
hydrates it, asserting no mismatch and the viewport taking over afterwards;
`SidebarItem.spec` covers the login gate the v1 sidebar spec used to; the
settings container and MainLayout each gain a laptop case. The Playwright
login helpers also accept the rail's "You" tab, which is where the account
entry lives for every signed-in laptop session now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The banner pins to the window's left edge with 40px of padding, which the
44px v1 sidebar barely covered. Under the 80px rail the headline, the email
link and the Google button ran under the rail on every signed-out laptop
page below 1668px, where the banner is left-aligned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The helper clicked "Log in" as soon as the password was typed, so on a cold
preview the form rejected the submit with "Please complete the security
check" and every retry failed the same way. Turnstile writes its token into
a hidden field in the form; wait for that field to fill first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rebelchris

Copy link
Copy Markdown
Contributor Author

Second pass addressed in 65b7e12…4e969fa50 plus the e2e helper commit.

Blocking: test_webapp is green on the final head. The PostPage and ExplorePage router mocks now carry asPath and events, and RouteProgressBar tolerates a router without an emitter. I ran the whole webapp suite this time (87 suites), not a subset.

Should fix: the utilities.css profile overlay rule is deleted. sidebar/sections/MainSection and its spec are deleted (InterestAgentSidebarClick has no client caller left; MarketplaceSidebarClick still has DiscoverSection and YouPage). ExploreChipsBar lost compact and the pill branch, the tablet header strip and CustomFeedEmptyScreen lost their chips slot, createSquadMenuItem lost asPin and the star branch, the isTablet nit is gone, and recent pages record only for signed-in sessions.

Script: only the four skip-list lines remain; the reflow is reverted. Tracked follow-up for their strict errors: ENG-2243.

Coverage: useLaptopLayout.spec now renders the mirrored shell with renderToString and hydrates it with hydrateRoot, asserting no mismatch and the viewport taking over afterwards; MainLayoutServerVariant keeps its pre-boot cases (the context still decides sidebarOwnsHeader before the cached session lands, so layoutVariant does matter there). SidebarItem.spec covers the login gate, AccountPageContainer.spec and MainLayout.tsx gained laptop cases. No extension MainFeedPage spec exists; the first-render guarantee is pinned at the hook.

Playwright: helpers and login.spec accept the rail's "You" tab, and both now wait for the Turnstile token before submitting, which is what made the first dispatched run fail (5 passed, 1 login failure with "Please complete the security check"). Two re-dispatches since fail while building ravsamhq/notify-slack-action@v1, before Playwright runs, which is unrelated to this branch.

Screenshots: taken at 1024 and 1440 for the anonymous Explore hub pages. They surfaced one real defect: the bottom signup banner ran under the rail at 1024, fixed in 4e969fa and re-verified on the preview. I can't attach images from this session; they're with the author.

Product side effects (tour wave, density section, install CTA) and the hard-load header swap on non-post routes are now stated in the PR body.

@rebelchris rebelchris left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final pass on head e98d919. I checked the delta since my last review (5004b87…e98d919: 3 commits, 23 files) line by line, and re-checked the full main...head diff (138 files, +881/−2811) for anything the earlier rounds missed.

Everything raised in rounds 1 and 2 is resolved:

  • CircleCI is fully green on the head: typecheck, lint, the shared, webapp and extension tests, and the extension build.
  • The PostPage and ExplorePage mocks are fixed, and RouteProgressBar now guards router?.events.
  • The sidebar MainSection and its spec are deleted. ExploreChipsBar lost compact and the pill branch, the chips slot is gone from the tablet header and CustomFeedEmptyScreen, and createSquadMenuItem lost asPin and the star branch.
  • The profile-page overlay rule in utilities.css is deleted.
  • Recent pages are recorded only for signed-in users.
  • The script change is down to the four skip-list entries, with ENG-2243 linked.
  • New tests:
    • a real renderToString + hydrateRoot mirror test with a check for no mismatch
    • the SidebarItem login gate
    • laptop cases for AccountPageContainer and MainLayout
  • The PR body now states the flag-archive order, the product side effects, the remaining header swap on hard load, and the rename follow-up.

I found no remaining control or v1 code paths. What is left is naming and comments, and those are tracked for the rename PR.

One new issue, in the banner fix (inline): laptop:left-20 is sized for the 80px rail, but logged-out visitors, who are the only audience of this banner, get the compact 64px rail. That leaves a 16px gap.

Non-blocking

  • e2e is still unverified for this branch, and the PR body is out of date on it. Dispatched run 39933000566 did reach Playwright. The new Turnstile wait passed, then the preview rejected the test account's credentials ("doesn't match our records"), so the "You"-tab assertion never ran. That looks like the test account not existing on the backend the preview talks to, rather than anything in this PR. On main, login passes, but the profile-social-links spec fails later on its "Add link" field, which predates this branch. The body still says the re-dispatches never reached Playwright. Please correct that and note that laptop login detection is untested end-to-end.
  • utilities.css still has two unrelated Prettier reflows, around lines 419 and 1059. In the second one, the /* The top edge catching the light. */ comment moved onto the same line as the shadow value. Both are harmless, but they are churn in a file this PR otherwise only deletes from.
  • Scope: the Playwright Turnstile wait (e98d919) fixes test infrastructure that already exists on main. It is fine to keep here, but it is not part of the layout change.

Still not measured (stated in the PR body): CLS on a logged-in hard load of a non-post route, and screenshots attached to the PR. Both rest on the author's description, not on evidence a reviewer can check.

Verdict: there are no blocking issues apart from the banner offset. Because I'm reviewing on the author's account, the approval has to come from another maintainer. I'd recommend approval once the banner offset is fixed and the e2e note in the body is corrected.

Reviewed by AI.

'min-h-[17.75rem] items-center border-t border-accent-cabbage-default px-10 py-4 pe-[388px] shadow-3 laptopXL:pe-10',
// The banner pins to the window, but on laptop the rail sits fixed
// along the left edge; start beside it or the headline runs under it.
'laptop:left-20 laptop:w-[calc(100%-5rem)]',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This offset assumes an 80px rail, but this banner's audience sees a 64px one.

PublicPageSignupBanner shows only to logged-out laptop visitors. Their rail width comes from useSidebarCompact(), which is useSettingsBooleanFlag('sidebarCompact', true). A logged-out visitor has no stored flag, so it resolves to compact, and the rail is laptop:w-16 (SidebarDesktopV2.tsx:814). MainLayout pads the content laptop:pl-16 to match.

With left-20 hardcoded, every visitor this banner targets gets a 16px strip of page between the rail and the banner. Please read the same value MainLayout uses, so the two can't drift:

const { value: isSidebarCompact } = useSidebarCompact();
// …
isSidebarCompact
  ? 'laptop:left-16 laptop:w-[calc(100%-4rem)]'
  : 'laptop:left-20 laptop:w-[calc(100%-5rem)]',

If logged-out visitors can never be non-compact, laptop:left-16 laptop:w-[calc(100%-4rem)] alone is enough. Either way, the 1024 screenshot should show the banner flush with the rail.

Reviewed by AI.

rebelchris and others added 2 commits October 23, 2026 12:32
Logged-out visitors, the banner's only audience, have no stored sidebar
density and so get the compact 64px rail; a fixed 80px offset left a 16px
strip of page between the two. Read the same flag MainLayout pads with.
Also drops the unrelated Prettier reflows in utilities.css so the file only
loses the overlay rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rebelchris

Copy link
Copy Markdown
Contributor Author

Final-pass item fixed in 48dc835: the banner offset now reads useSidebarCompact(), the same flag MainLayout pads with, so logged-out visitors (compact, 64px rail) get left-16 and the banner sits flush with the rail. The same commit drops the two unrelated Prettier reflows in utilities.css, which now only loses the overlay rule. The PR body's e2e paragraph is corrected: the later dispatch did reach Playwright, the Turnstile wait passed, and the preview's backend rejected the e2e test account, so laptop login detection is untested end-to-end on this branch. I'll confirm the 1024 banner position on the preview once it redeploys.

@rebelchris
rebelchris merged commit f6e0324 into main Oct 23, 2026
13 checks passed
@rebelchris
rebelchris deleted the chore/layout-v2-cleanup branch October 23, 2026 10:45
rebelchris added a commit that referenced this pull request Oct 23, 2026
layout_v2 graduated in #7017 and useLayoutVariant is gone, so the
laptop chrome is now gated on useLaptopLayout() like every other page.
The remaining non-laptop branch is the tablet chrome, so its laptop:
variants were dead and are dropped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
tsahimatsliah added a commit that referenced this pull request Oct 25, 2026
…over strip

The header's Log in / Sign up opened the in-extension auth modal, where
social OAuth from the extension origin is rejected with a 403; the cover
strip on the same screen already hands off to the webapp onboarding URL.
Both now share one onboardingHref module.

Drop the ExtensionSignInStrip wrapper: App.tsx routes every logged-out
or onboarding-incomplete user to HijackingPage, which always passes
loginStrip, so the fallback could only ever render null. MainFeedPage
gates the top-banner strip on laptop itself instead of leaning on
MainLayout hiding the slot.

Drop feedStyles.cards from the hijacking strip: its desktopL cap reads
--num-cards, which is only set inside the feed, and below laptop the
breakpoint never applies, so the class was inert in both placements.

Add a MainFeedPage spec for the laptop/below-laptop strip and header
wiring, the path #7017 dropped without any test noticing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
tsahimatsliah added a commit that referenced this pull request Oct 25, 2026
Since #7017 graduated layout v2, an anonymous laptop visitor got the v2
rail and the legacy global header at the same time: two logos, two
searches, the rail toggle buried under the header, no floating card, and
a Streak tab and an Invite button that both need an account.

The rail now owns the header for every laptop session, signed in or not.

- MainLayout: sidebarOwnsHeader = isLaptopLayout && showSidebar. The
  login audience and the cookie/server-shell guesswork that only existed
  to predict it are gone; useLaptopLayout already answers for the server.
  Anonymous laptop sessions in the webapp get SignInStrip as the card's
  top banner, where the extension already mounts it through topBanner.
- ExtensionSignInStrip moves to shared as auth/SignInStrip. Its Log in /
  Sign up keep target_id 'header' so the login funnel dashboards hold;
  it carries Get the app from laptopL.
- SidebarDesktopV2: a pinned Log in tab (UserIcon) in the avatar's slot
  for visitors, Get the app (the QR popover, icon-only, opening right)
  instead of Invite, the Streak tab gated on login, and the overflow
  budget reserving the account row for everyone.
- useLayoutVariantCookie records "laptop" rather than "logged in on a
  laptop", written once hydrated, so the mirrored post page serves the
  rail to returning visitors too. hasLayoutVariantCookie is unused and
  removed.
- Tests: the anonymous laptop case (no banner, floating card, strip),
  the hydration settle, and the visitor rail (Log in tab, no Streak,
  Invite or New post, Get the app present).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch was successfully deployed

2 active deployments
Preview – daily-webapp — 8a221361 Deployed Oct 23, 2026 by vercel[bot]
Preview – storybook — 8a221361 Deployed Oct 23, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant