Repository navigation
feat(mobile): the phone shell - #6767
Conversation
Step 0 of the mobile shell work (Storybook, Mobile UX / 10. The build): the fixes that need no design decision, shipped together. TabContainer's swipe decided from the total movement at touch end with a 40px threshold, so a thumb scrolling at a slight angle switched the Highlights channel. The swipe now locks its axis on the first 10px, commits past 56px inside a 27 degree cone or on a fast flick past 32px, and the surface takes touch-action: pan-y. The numbers live in shell/constants.ts. FooterNavBarLayout waited for the window load event before rendering the bar; it renders once hydrated. The footer tabs fire a click with the footer as target and the tab in extra, and Activity fires click notification icon with NotificationTarget.Footer, which existed and was never used. HighlightCardOptions and SquadOptionsButton were hidden until hover on every device, so their actions did not exist on phones; they use the laptop-and-mouse-only hover class the post cards use. The Explore period drawer gets a Close, the notification menu icon stops being rotated, Unfollow uses the remove-user icon, "Manade Ad" is "Manage ad" and the settings menu labels match their page titles. shell.css carries the shell's two curves and four durations and the press class (scale 0.96, 150ms) applied to the footer tabs, the plus button, the header controls, the feed chips and the post bar; the post titles balance. Two pre-existing strict type errors in the touched files are fixed so the strict guard passes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Step 1 of the mobile shell work (Storybook, Mobile UX / 10. The build, PRs 1.1 to 1.8 together): the shell under 656px. Tablet, laptop, layout v2 and the extension keep the code they run today. The floating cluster replaces the docked footer: Home, Explore, Squads and Activity on the flat blur material, the Create square beside it opening the composer, the brand bubble on Activity, the shrink on scroll, the lit tab from the root that owns the URL, visitors sent to sign up from Home, Activity and Create. It is in the server HTML so it paints with the page. The top block replaces the phone header: the brand row on Home (logo, streak, the Plus door for free members, the square avatar opening the new You page), a name row on Explore, Squads and Activity, and back, title and actions on every leaf, fed by GoBackHeaderMobile and the page headers that drew their own bars. It is a fixed overlay that hides as one piece while reading down and returns on any short scroll up; content keeps a constant top padding from --shell-top. One scroll reader drives block and cluster. Drawer becomes the sheet (travel curve in, faster out, a grabber, drag to dismiss), DropdownMenu renders as a modal bottom sheet on phones, the gated sign-up closes with an X and names what the action gets you, and people's pictures are rounded squares where they were circles. Deleted: MobileFooterNavbar, FooterNavBarTabs, FooterNavBarItem, FooterPlusButton, MobileFeedActions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts: # packages/shared/src/components/MainFeedLayout.tsx # packages/shared/src/components/feeds/FeedNav.spec.tsx # packages/shared/src/components/feeds/FeedNav.tsx # packages/shared/src/components/feeds/MobileFeedActions.tsx # packages/shared/src/components/post/GoBackHeaderMobile.tsx # packages/shared/src/components/profile/Header.tsx
On review Tsahi asked for the header's rows in the same PR so no page shows a second header. ShellRow carries the three weights of the 28px chip (segments, chips, a menu label) and a sheet of choices. Home's row is For you, Happening now with its channel sheet, Following, the member's custom feeds and the plus; the Highlights page shows the channel on its own line instead of its title and tab strip. Explore's row is the search field and the places, and its sort is a text menu on the feed line instead of the sticky tab strip. Squads root puts its categories in the block, Activity its types, Bookmarks its lists as segments under a title; Tags, Sources, Leaderboard and History take their names in the block and lose their in-page navbars. Tablet keeps every old row. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On review every phone page still kept a button row of its own under the block: New Squad under the Squads title, the channel line and copy link under Happening now, Search and the menu in the squad hero, Follow, Award and the menu twice on a profile, Sort and Share in the Bookmarks header, the full-width Filters on search results. They move into the block. ShellPage now portals its actions into a slot the block exposes instead of passing the nodes through context, because the block renders outside the page's providers and the squad menu reads SquadPageContext. The hero copies are gated by viewport in JS rather than CSS so each label exists once in the DOM, which is also what the page tests query. The streak was never hidden: the local proxy's fake member had no streak response. The proxy stubs one now so the shots show it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A route-by-route pass at 393px over the member app found the pages that still drew a header row of their own under the block: Sources (a full-width Suggest new source button), Jobs, the Core wallet, the briefings, squad moderation, post analytics, the gear rankings, the best-of archives (breadcrumbs and a copy link row), the experience lists (a second back arrow), the squad form (Cancel and Create under a back square), the leaderboard detail (no title at all), the tag explore page (no title), and the composer page (the block over the composer's own chrome). Each now registers its title and actions with the block on a phone and keeps its row for tablet and up; the composer hides the block. FormWrapper gets an inBlock mode for page forms so the squad form's submit rides in the block and the back square replaces Cancel. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts: # packages/webapp/pages/sources/index.tsx # packages/webapp/pages/users.tsx
# Conflicts: # packages/shared/src/components/MainLayout.tsx
The 72% rest opacity sat on the whole tab, so the unread bubble dimmed with the glyph and read as disabled next to the lit tab. The dim moves onto the glyph alone; the bubble is the same colour in both states. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
BootProvider.spec "refetch boot on focus within the interval when another tab changed the Plus status" counted three boot requests instead of two on CI; it passes alone and this branch does not touch contexts/. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…finger The lit tab now has a soft pill behind it (the Instagram reference Tsahi gave), sliding on the travel curve when the root changes. Press and hold the bar, move along it, and the pill follows the finger with the glyph under it lit; lifting selects that tab. A 6px travel threshold keeps a tap a tap, and the click the browser fires for the release is swallowed so the drag's choice is not navigated twice. No glass: the flat material only. The Squads root also loses most of its top padding on phones, since the chips moved into the block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Suggest new source, New Squad and Job preferences need an account, so their squares in the block render for members only; visitors keep Log in and Open app in the row and nothing they cannot use. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A finger on the bar scales the whole bar to 1.04 from its bottom edge for as long as it stays down (150ms ease-out in, 220ms on the no-bounce curve out), the lift Instagram's and iOS 26's bars give; the per-tab 0.96 press goes, since the bar now answers as one piece. The pill slides on the same no-bounce curve, per chapter 9l. Reduced motion drops the lift. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Hover on touch - Four button rules still took :hover on a touch screen (the quaternary button's label, the tag buttons, the sign-back button), so an upvote count stayed lit after its button had cleared. Insets in the wrappers - The h-screen and min-h-screen overrides applied on the web too and displaced the iOS 100vh fix that follows Tailwind's own rules, so full-height pages ran under Safari's toolbar. They apply only where there is an inset. - The modal footer's bottom inset was done by swapping its fixed height for a minimum and adding bottom padding, which changed footers whose callers set their own height or padding, on desktop too. The footer keeps main's classes; on a phone its last instance takes the home indicator as a margin. Phone shell - Home's block rests at two rows but its segments wait for settings, so the block measured one row first and the page jumped up and back. The row holds its height while it waits. - A page with no title rendered an empty h1 in the block, now in the server HTML of every leaf page. - Bookmarks' block has a row of its own; the layout holds its height. - Set a reminder lost its accessible name with the duplicate title. - The search filters sheet mounts on the root, out of the block's transform. Post links - A spec for the slug form and the id fallback of the post path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
tsahimatsliah
left a comment
There was a problem hiding this comment.
Review of ccd627eb8
Read against the four earlier rounds (lit-tab re-tap, quest button, banners under the block, canGoBackInApp, feed_chips, useIsPhone, sheet drag, named group-hover, route-stack keys, refresh cost, transitionend, inert toast, aria-hidden peer, hover media split). Those threads look resolved in the code; I did not re-raise them. CI is green, no committed binaries under public/, no TODO … before merge left in the diff. Below are the new points; none of them is a correctness blocker on its own.
Reviewability and scope
227 files. By the PR's own account, #6775 (hover is pressed on touch, 5 files), #6776 (posts open in-app, 32 files) and #6777 (wrapper insets, 14 files) are still inside this diff while they wait as drafts. I did not review those three areas here; please do take them in their own PRs. Recommendation: land the three split PRs first, merge main back in, and then do the final pass on the shell-only diff. A diff this size tends to get approved unread, and the ~50 files that leave make the remaining shell reviewable as one piece.
No runtime kill switch
The plan audit says "none added" for flags, and that is accurate: the new shell (ShellBlock in MainLayoutHeader, ShellCluster via ClusterSlot, the sheet behaviour in Drawer/DropdownMenu) ships to every visitor under 656px the moment this merges, with no rollback short of a revert deploy. The repo's convention (AGENTS.md, and how layout v2 landed) is a GrowthBook flag with the control as default. Even if the product decision is "everyone", a single kill-switch flag read at the two mount points (MainLayoutHeader's block and FooterNavBarLayout's ClusterSlot, falling back to the old FeedNav phone branch and the deleted footer) would give a same-day rollback for a change this wide. The one piece that cannot be flagged is the hover: plugin, which is another reason to land #6775 separately so it can be reverted alone. Question for the team rather than a code finding.
Inline
6 non-blocking comments on the diff: a second copy of the root-ownership predicate (useShellBlockPlan vs shellNav) that already disagrees on /posts/best-of; ShellPage re-publishing config on every parent render; the off-screen level of PostOptionsSheet staying in the menu's focus order; the extension at phone width inheriting the block; the new color-scheme meta applying to desktop native controls; hardcoded scrim/shadow colours in shell.css.
Verification
- Root and package
AGENTS.mdread - Diff, commits, all prior review threads and CI inspected
- Shared consumers traced:
MainLayout,MainLayoutHeader,FooterNavBarLayout,Drawer,DropdownMenu,Modal,FormWrapper,GoBackHeaderMobile, profile/squad headers, 35DropdownMenuconsumers - Committed-asset and
_app/_documentgates run - Flag and experiment hygiene checked (
feed_chipsno longer evaluated in the block; no truthy defaults) - Not run locally: Jest suites (CI ran them); the phone walkthroughs are from the PR's own evidence
- Not verified: the extension's new tab at a window under 656px; desktop
<select>/scrollbar rendering after thecolor-schememeta
Verdict
Comment. No blocking code finding. I would hold approval until #6775/#6776/#6777 land and main is merged back, so the shell diff can be read on its own, and until the kill-switch question has an answer.
Reviewed by AI.
Shell - One predicate for which root shows its row: the block's plan reads shellNav.isRootView instead of its own copy. The best-of archive is Explore's page, not one of its views, in both places now, so the lit tab returns from it to Explore as its back row already did. - The block's measurement effect keeps one ResizeObserver for the life of the header instead of a new one on every page render. - The post menu's off-screen level is inert rather than aria-hidden with focusable rows. - The extension's new tab keeps its own chrome at every width: no block, no held padding, useIsPhone is false there. - The colour scheme is set in CSS for phones only; the document-wide meta also restyled native controls and scrollbars on desktop. - The menu scrim and the shell's shadows read theme tokens. Hover on touch - Checkbox, field, help card and mention hovers follow the same split. - The variant's doc says why Tailwind's own flag is not used and that group-hover and peer-hover stay as they are. Post links - The helper falls back to the id for an empty slug and accepts a null one; five more in-app links use it; the boost campaign list opens its post in-app; the reposts row no longer gates on a field it does not read. Insets - The repo's safe-area utilities replace three hand-written values; a modal thread keeps its own scroll margin; the stylesheet comment states the cascade as it is. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Kill switch: product decision. Tsahi's call is that the shell launches to everyone without a flag. A runtime switch would mean keeping the old phone footer and the Scope. Agreed on the order: the three small PRs merge first, The six inline points are answered on their threads (334e4ab). Replied by AI (Claude). |
Posts opened from a widget row now stay inside the app, so post to post reuses the /posts/[id] route and the page instance. An ad slot runs its auction once per mount and is keyed by surface, slot, format and sizes only, so the next post kept the previous post's creatives, and the top leaderboard kept its spent pin timer. Before, each of those taps was a document load with fresh auctions. The page's layout now keys the page and the pinned phone strip on the post id, so every unit and all per-post state start again on each post, as a full load did. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
#6776 is on main now; the source page keeps the shell's phone check beside it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
#6777 is on main now; the shell keeps its scroll-lock inset rule beside it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The profile page renders one header and flips its sticky prop once the page scrolls. On a phone the header returned nothing while sticky, on the belief that the page rendered two of them, so the block lost Profile and the menu as soon as the page moved. The header registers with the block either way now. Its visitor-header branches go too: they sat on the tablet path, where that header never shows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The shell block carries Log in and Open app on phones, and it is in the server HTML, so MobileAppHeader had nothing left to show. It still mounted on 16 page types and, because the phone check is false on a component's first render, it painted for one frame on every in-app navigation to them before removing itself. The component, its mounts and the offset variable it published are gone; the three sticky rows that read the variable pin at their fixed offset. Its spec stays as the spec of the actions and of the rule that shows them. Two tablet-only spots that branched on the visitor header lose the branch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… tab The extension does not get the phone shell, but the shell had deleted MobileFeedActions, the row that carries the logo, the streak and the way to the profile under 656px. A narrow extension window was left without them. The row is back as it is on main and renders in the extension only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An independent pass over the diff after the three split PRs merged. Unused shell constants, the always-true snapping field, the sheet drag's expandable option, a prefix for a route that no longer exists, props no caller passes, default exports nothing imports, the footer's tab indicator (its only user was the deleted footer nav) and a registration with no content. The shell provider wraps the layout from outside, so MainLayout's body keeps main's indentation. Two barrel imports and two import orders fixed; hover already covers the pressed state the rows repeated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…de/mobile-shell-1
rebelchris
left a comment
There was a problem hiding this comment.
Final pass at 99b0285cb, blockers only. Every earlier thread is resolved and CI is green. The commits since 334e4ab5d look good to me, including the post-to-post ad-auction remount and the 19 removed MobileAppHeader mounts. Each of those pages now gets Log in / Open app from the block, and the merge resolutions are clean.
Blocking
- Merge conflicts with
main.MainLayout.tsxis trivial: keepSquadJoinSuggestionsPopupfrom #6807.SquadProfileHeader.tsxconflicts with the verified-squad branding from #6795 (useSquadBranding, the brand wash). After the rebase, please check a verified squad page on a phone. The brand wash and the badge need to sit correctly under the fixed block, and the squad's block actions need to sit beside the new branded header. - Extension: phone sheets via
useViewSize(ViewSize.MobileL). See the inline comment onDropdownMenu.tsx. - Extension: logged-out Explore/search under 656px has no Log in. See the inline comment on
MainLayoutHeader.tsx. - Storybook import broken by the
MobileAppHeaderremoval.packages/storybook/stories/mobile-header/MobileAppHeader.stories.tsx:15still imports the deletedMobileAppHeaderand renders it on line 152, so the storybook bundle no longer resolves. CI skips storybook, so nothing caught it. Drop that story, or point it atMobileAppActions, which line 14 already imports.
No non-blocking items this round.
Reviewed by AI.
MainLayout keeps the toast in its portal and takes SquadJoinSuggestionsPopup; SquadProfileHeader keeps the block's actions and takes the verified squad's brand wash.
…s its logo row Chris's final pass on #6767. The menu sheet, the two-level post options sheet and the audience chip's sheet read useViewSize(MobileL), which is true in a narrow extension window too; they read useIsPhone now, which the extension never is. A logged-out extension window under 656px on search lost its logo and Log in when the old header left; the search header renders MobileFeedActions there, as FeedNav does. The mobile header story renders MobileAppActions instead of the removed MobileAppHeader. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
rebelchris
left a comment
There was a problem hiding this comment.
Re-check at dcc6fc26f. All four blockers from the last round are fixed:
- The #6795 and #6807 conflicts are resolved cleanly.
SquadProfileHeaderkeeps the block actions and takes the brand wash, andMainLayoutkeeps the toast portal and takes the join popup. DropdownMenu,PostOptionButtonandAudienceChipuseuseIsPhone().- The extension's search view has its logo and Log in row again.
- The Storybook story no longer imports
MobileAppHeader.
One blocker remains: a new conflict with main. #6802 ("show the mobile app footer on page load") landed after your merge. It rewrites MobileAppFooterContext: isRevealed and moment are gone in favour of title, and MobileAppFooterAnchor is deleted. That gives textual conflicts in FooterWrapper.tsx, FooterNavBarLayout.tsx and pages/users.tsx. It also gives one conflict git will not flag, on ClusterSlot (see the inline comment). When you resolve it, drop the MobileAppFooterAnchor mounts in users.tsx, as main does. HighlightsPage auto-merges correctly. Then check on a phone that the app footer and the cluster swap on page load.
Reviewed by AI.
The app footer shows whenever the context has a title (#6802): FooterWrapper, FooterSpacer and ClusterSlot read title, the footer keeps its pointer-events wrapper over the shell's pass-through container, and the leaderboard drops the removed MobileAppFooterAnchor.
Under 656px the webapp gets the phone shell: a floating bottom bar and one top block, in place of the docked footer and the header each page drew for itself. Tablet, laptop, layout v2 and the extension keep what they run today.
The parts that did not depend on the shell were reviewed and merged on their own (#6765, #6775, #6776, #6777), so this diff is the shell alone.
Changes
What a phone reader gets
/you, behind the avatar: profile, Plus, feed settings, bookmarks, history, analytics, game center, DevCard and settings in one list. The gear leaves the Home row.DropdownMenuopens as a bottom sheet on a phone.ProfilePicture's radius.Where to start reading (
packages/shared/src/components/shell/)ShellCluster.tsx: the bottom bar. Publishes--shell-bottom.ShellBlock.tsx: the top block. It is in the server HTML and publishes--shell-top.ShellPageContext.tsx: a page renders<ShellPage title actions row />to say what the block shows. Most of the modified files are this one small change per page: the page drops its own phone header and registers instead.shellNav.ts: which root owns a route, and where back goes.useShellBlockPlan.tsandconstants.ts: the height the layout reserves for the block before it has measured itself, so nothing moves on hydration.shell.css: phone-only styles, including the menu-as-sheet.MainLayoutHeader.tsxandMainLayout.tsx: where the block mounts and the column is padded.Removed
MobileFooterNavbar,FooterNavBarTabs,FooterNavBarItem,FooterPlusButton) and its test; its events are asserted inShellCluster.spec.MobileAppHeader) and its 19 mounts. The block carries Log in and Open app.SquadHeaderMenu(one squad menu now,SquadOptionsMenu).Decisions a reviewer should know
color-schemefromshell.css, so native controls and the overscroll area follow the theme./youadds one query,USER_FOLLOW_STATS_QUERY, for the follow counts.Events
No new event names.
ClickwithTargetId.MobileFooterNavand{tab, logged_in}.ClickNotificationIconwithNotificationTarget.Footer.OpenSmartComposerfrom the composer. Visitors get the sign-up with one new trigger value,AuthTriggers.CreatePost.ClickFeedTagChipsends{origin}withoutvarianton phones, because the block's row has one layout for every arm.Experiment
None.
Manual Testing
At phone width, logged in and logged out:
Verified
Review history and plan audit
Step 1 of the mobile shell work (Storybook
Mobile UX / 10. The build, PRs 1.1 to 1.8 delivered together as agreed): the phone shell under 656px. Tablet, laptop, layout v2 and the extension keep the code they run today.#6765 (the four fixes) merged to main on 2 Oct and is merged in here, so this diff is the shell alone.
What changed
The bottom cluster (1.1)
ShellClusterreplaces the docked footer: Home · Explore (compass) · Squads · Activity as a floating bar on the flat blur material, the Create square beside it opening the composer directly, the brand bubble on Activity only, Tint selection (filled glyph, the rest at 72%), the shrink on scroll (56→44px, inset 20→40, radius 22→18 over 96px), re-tap the lit tab to scroll to top, the lit tab from the root that owns the URL (Squads lights on a squad page, Home on a post). Visitors: Explore and Squads open; Home, Activity and Create open the sign-up with a context line. The cluster is in the server HTML (tablet:hidden), so it paints with the page. The Charm footer still replaces it at its trigger; the post action bar sits above it as before. Deleted:MobileFooterNavbar,FooterNavBarTabs,FooterNavBarItem,FooterPlusButton,MobileFeedActions.The You page (1.2)
/you, behind the avatar, with the block's back button; Invite (pill) and Help (icon square) are its actions. Name and handle, Following and Followers (opening the follow lists), Reputation and Cores as two small pills, then one 44px list: Profile, daily.dev Plus (Upgrade / Manage / Renew), Feed settings, Bookmarks, History, Analytics, Game center, DevCard, Settings (opens the settings menu). Settings is on screen without a scroll on a 393×660 viewport. Help opens a sheet with the desktop Support menu: Send feedback (the in-app form), Report a bug, Docs, Changelog, Get the mobile app (phone browsers only), Privacy policy, Terms of service. A slide-aside menu was tried and reverted the same day. The settings gear leaves the Home row.The top block on roots (1.3)
ShellBlockreplaces FeedNav's phone branch: Home = logo (Plus mark for members), streak, the Plus square for free members (opens Plus, firesUpgradeSubscription), the 38px square avatar; Explore, Squads and Activity get the name row they never had (Activity with the notification-settings square). Today's rows ride under it (the chip strip on Home, the Spotlight trigger on Explore). The block is a fixed overlay; content takes--shell-toppadding; it hides as one solid piece while reading down (64px travel, 96px dead zone, 24/8px tolerances, 300ms stop, 220ms snap on the interaction curve) and returns on any short scroll up, never on arrival, and reveals when a field takes focus.useShellScrollis the one scroll reader for block and cluster.The top block on pages (1.4) The page row: back (38px square), the title (20px), the page's actions.
GoBackHeaderMobilefeeds it under 656px (title prop for the profile sub-pages), as do the profile header, the no-sidebar layout, the squad sub-page header and the settings container. Every leaf has a back now; back is history when the previous entry is ours, otherwise the owning root. Inside settings, back is hierarchical as in iOS Settings and X: a section's back opens the sections menu, the menu's back leaves settings as a whole in one history move (to the page the member came from, You when there is none), You's back goes home; feed settings goes one level up the same way, to the menu when it was opened there, otherwise to the feed. A small ordered route stack in sessionStorage (shellNav.ts, mounted once in_app) gives every back its target, since Next no longer numbers history entries. Feed settings on a phone is one header row the width of the page (back, the feed's name or the section's, Save) and its sections are the settings menu's rows. The old sticky bars stay for tablet.Sign-up, two touches (1.5) On a phone the gated sign-up closes with an X top left (not a chevron) and shows one line under the title keyed by the trigger (Follow, Bookmark, Upvote, Comment, Join squad, Create…). Providers, order, email, terms, flow and analytics untouched.
The sheet (1.6) Drawer: 300ms in on the travel curve, 200ms out, scrim 150ms, a grabber on bottom sheets, drag to dismiss past a third of the height or on a fast flick (up barely moves, back springs in 220ms), inner lists keep scrolling. Every Drawer consumer gets it.
Menus as sheets (1.7)
DropdownMenuon a phone is modal and renders its Radix content as a bottom sheet (48px rows, grabber, the sheet's timings) throughshell-menu-sheet; it no longer closes on the scroll the collapsing address bar fires. Items, handlers and events are untouched, which keepsPostOptionButtonand the other 35 consumers as they are.People are rounded squares (1.8) Referral slots, embedded tweets, profile metadata and the feedback widget drop their circles; squads and sources keep theirs.
The rows (added on review, from 2.1, 2.2, 2.4, 2.5 and 2.8)
ShellRowwith the three weights of the 28px chip: Segments (which list, replacing history), Chips (narrow this list), MenuLabel (a sheet). Home's row is For you · Happening now ▾ · Following · your custom feeds · +; the chevron on Happening now opens the channel sheet (Headlines, All, every channel), and the Highlights page shows the channel on its own line instead of its old title and tab strip. Explore's row is the search field and the places (Tags, Sources, Leaderboard, Discussions); its sort is a text menu on the feed line (Popular ▾, with the period inside for the two sorts that have one) instead of the sticky tab strip. Squads root puts its categories in the block as chips; Activity its types; Bookmarks its lists as segments under a Bookmarks title; Tags, Sources, Leaderboard and History get their names in the block and lose their in-page navbars and breadcrumbs. The old strip's other chips are reachable from Explore, You or the bar. Tablet keeps every old row.The buttons (second review) A page keeps no button row of its own above the content once the block exists. The Squads root carries New Squad as a plus square (the Primary button under the title is gone on phones). Home shows the streak and the quest count beside the Plus square. Happening now loses the channel line and the copy link under the segments; the chevron sheet is the one channel control and marks the channel you are on. Search results are a page: back, the query as the title, a Filters square (the same drawer), the field in the row. The squad page block carries Search and the menu; the hero keeps the link and Join. The profile block carries the menu only; the hero keeps Follow and Award. Bookmarks carries Sort, Share and the folder menu in the block; the search field stays in the content. Explore loses the 40px band the old sticky tabs reserved.
ShellPagenow portalsactionsinto the block's slot from the page's own tree, so an action keeps its page's providers (the squad menu needsSquadPageContext); the hero copies are gated by viewport in JS so a label exists once in the DOM.Every page (third review) A route-by-route pass over 110 member routes at 393px. The pages that still drew a header row of their own under the block now register with it: Sources (Suggest new source as a plus square), Jobs (title, How it works, preferences), the Core wallet (title, Buy Cores), the briefings (title, Generate, settings), squad moderation, post analytics (title, Boost), the gear rankings, the best-of archives (title and copy link in the block, breadcrumbs gone on phones), the experience lists (the second back arrow gone), the squad form (title and Create Squad in the block, the back square replaces Cancel), the leaderboard detail and the tag explore page (titles). The composer page hides the block over its own chrome.
FormWrappergained aninBlockmode for page forms. Pages with their own standalone layout (Plus, Cores) are unchanged: they never render the block.Step 1 complete (fourth review) Everything the plan lists for 1.1 to 1.8 is in this PR now. The post menu on a phone is one sheet with two levels: Share · Read it later · Follow the source · Not interested · Report, then the owner's rows (Delete in the error colour), then More; Not interested gathers hide, block source, block author, block tags and content type, More the rest; the sub-level slides in and its title row carries the back chevron (
PostOptionsSheet, grouping by option id inpostOptionGroups). Every option keeps its handler and event; desktop keeps the flat menu. One squad menu serves the page and the card (SquadOptionsMenugained Share and Notifications rows and a styled trigger;SquadHeaderMenuis deleted). Copy link lives inside Share on tag and source pages. Report post, user and comment, Add to custom feed, Move bookmark, Bookmark folder and Bookmarks sharing open as sheets on phones; sheets no longer carry a bottom Close button. The block's last Float buttons are 38px squares. The bar publishes--shell-bottomfor the footer spacer.Plus:
shell/constants.ts(the numbers),shell.css(material, hit areas, sheet timings, menu sheet),ShellSquare,ShellPageContext,ShellRow,HomeSegments,ExploreSortMenu.Fifth review (sheets, glass, back to top, the plan audit) A sheet is a header (grabber, title), a scroller and a foot, so a row pinned inside it reaches the panel's edges: nothing shows through above the grabber or below a form's Cancel/Submit bar once a sheet has grown to the top (the panel's own padding, the status-bar inset on iOS, used to leave a strip). Titles sit at the left in every sheet; list sheets and menu sheets run their rows edge to edge like the settings lists.
.shell-materialis one recipe for the bottom bar, its squares, the block's squares and pills and the post page's floating bar (which now uses the class), so the Create square no longer reads darker than the bars.ShellTopButton: a square of that material above the Create square, at the bar's inset, on the main feed only, once a screen of feed has gone by. Tab switches no longer jump: the active feed name derives from the route in the same render, the Explore search slot reserves its height, and the block publishes--shell-topin a layout effect.owningRootasked the/posts/prefix before the Explore sorts, so/posts/upvoted,/posts/discussed,/posts/latestand/posts/best-oflit Home; it asksisRootViewfirst now. The channel chevron on Happening now is one button rather than a button inside a link. New specs:shellNav.spec.ts,YouPage.spec.tsx.react-flip-toolkitleft with the old footer and is removed from the webapp package.Plan audit (Storybook chapter 10, step 1)
Every technical claim in the build plan for 1.1 to 1.8 was checked against this branch. Legend: Done, Diverged (the build moved on; plan text updated), Open (a decision for review).
/youpages/you.tsxrendersYouPage,noindex, visitorsreplace('/')/squads/create/squads/new"?channel=/highlights/[channel]path routes, unchanged from productionExploreSortMenumaps all 8;isRootView(Explore)covers them;owningRootfixeduseActiveNavshellNav.owningRoot(+spec)/squads/discover/squads/discover/mywhen the member has squadshidesClusterfor settings and forms (+spec)Click,TargetId.MobileFooterTargetId.MobileFooterNav(PR 0's id), extra{tab, logged_in}ClickNotificationIcon,NotificationTarget.Footer, countUpgradeSubscription, mobile header targetTargetId.MobileHeaderOpenSmartComposerunchangedAuthTriggers.CreatePostfor visitorsClickFeedTagChip{variant, origin}{origin}only, the block row has one layout for every armfeatureManagement.tsuntouched;feed_chipsnot evaluated in the block;mobile_app_footerreplaces the clusterYouPage.spec.tsx,shellNav.spec.ts,sheetDrag.spec.ts,ModalSidebar.spec.tsxadded; MainLayoutHeader block cases and the circles grep still not written__tests__/MobileFooterNavbar.tsx(events now inShellCluster.spec),MobileFeedActions.spec.tsx(component deleted, rows live inHomeSegments),SquadHeaderMenu.spec.tsx(one squad menu now,SquadOptionsMenu)constants.ts,shell.css,useShellScroll+ provider, ShellBlock rows,--shell-top, ShellRowuseActiveFromRouteRootPortal; sheets follow the finger between rest and full and size to the visual viewportshellNav.ts, spec)ClickFeedTagChipdropsvarianton phones on purpose;AuthTriggers.CreatePostis one new trigger value/youaddsUSER_FOLLOW_STATS_QUERYfor the follow countslib/ios.tsandsafeArea.cssuntouched;_appstatus-bar meta changedstories/mobile-shell/Verified
Review rounds (4 Oct): an independent review of the four PRs, then a second reviewer's 8 points here. Fixed in ccd627e and 334e4ab: Home's block held one row then two while settings loaded; an empty
h1in the block on pages with no title; Bookmarks' resting height; the reminder sheet's accessible name; the search filters sheet mounts on the root; one predicate for which root shows its row (useShellBlockPlanreadsshellNav.isRootView, and the best-of archive is Explore's page, not one of its views); the block keeps oneResizeObserver; the post menu's off-screen level isinert; the extension keeps its own chrome at every width;color-schemeis set in CSS for phones only instead of a document-wide meta; the menu scrim and the shell's shadows read theme tokens. No runtime kill switch, by product decision: the shell launches to everyone; rollback is a revert of this PR, which is why the three app-wide pieces land separately first.Sheet title rows (4 Oct): every modal and drawer that opens as a sheet on a phone was opened at 393px and its top row measured. Seven did not use the sheet's title row (20px bold at left 16, the close square at right 8) and do now: Set a reminder (its title showed twice), Squad notifications (28px title, X beside it), the achievement picker, showcase and comparison and Intro quests (heading in the body, no X on a phone;
ModalSheetTitleadds the row on a phone only), search Filters (own row with a plain icon button), and the form sheets (report, New Folder) whose title was 17px. Wider screens are unchanged. Hero and celebration sheets, confirmations and action menus keep no title by design.The composer's Post to sheet (4 Oct): the audience picker's list is capped at 240px and scrolls under its header in the desktop popover; on a phone the capped list stayed a second scroller inside the sheet, so a swipe up grew the sheet instead of scrolling the list and the list was cut mid-row at full height. The cap and the list's own scroll apply from tablet up; on a phone the sheet scrolls as one piece with the sheet's 48px rows and 16px inset. Checked with 14 squads at 393x760 and 375x568 (every squad reachable) and at desktop width (unchanged). A menu sheet's list also kept its own height cap until the sheet was marked expanded, so a pull up showed empty space until release: the sheet owns the height now (up to 70% of the screen at rest) and the list fills it, at rest and under the finger. With 18 squads: 10 rows at rest, 12 and 14 as the finger pulls 100 and 200px, the end reachable by scroll; short menus measure as before. On a phone the picker also takes a sheet's title row: a 20px bold title and Reset as a pill matching the close square (dimmed and inert with nothing to reset); the desktop popover's header is unchanged.
Posts from widgets and lists (4 Oct): Similar posts, a collection's related posts and the squad post list were plain anchors (a full document load on production); a highlight's Read more, the reposts list, the topic sign-up previews and the two creator analytics links were router links to the absolute permalink. All use
getPostPathnow, with viewport prefetch off on the widgets. Six queries returnslug(FurtherReading, RelatedPost, the highlight feed card, creator achievements, creator analytics, the topic sign-up previews) and their hand-written types carry it. New-tab links, share and copy-link values and external article links keep the absolute permalink. On the preview at phone and desktop size a tap on a Similar posts row navigates in-app. The phone ad strip is unchanged by decision: holding its 66px from the server paint would spare logged-out visitors the push but make logged-in members' pages slide up once on a cold load; Tsahi chose to keep members' pages still, so the strip behaves as on production.The top block in the server HTML (4 Oct): a hard-loaded phone page used to paint without its top block, which mounted once hydrated and settings had loaded. The server and the first client render now emit both the block and the wider header and CSS shows one of them; from the second render on only this screen's stays mounted. This is the rule layout v2 follows for its own chrome (#6453, #6587, #6726): the first render is the same on the server and the client, client-only answers apply after it, and the room for the frame is held up front. The column holds the height the block rests at on the route (
useShellBlockPlan: page row, root row, the rows Home, Squads and Explore keep, the field on search). Rows and spacing that were switched by the phone flag in JavaScript and showed on a phone in the server paint are CSS (Squads directory padding; the sources, search, Game center and Analytics rows wider screens use). On the preview build at 393px: the block is in the JavaScript-off paint andmainhas the same top before and after hydration on ten routes; layout shift on a hard load is at or below production on all nine routes measured (Explore 0.071 to 0, tags 0.173 to 0, squad 0.098 to 0.025, Squads directory 0.009 to 0); no hydration failure at phone or tablet width on ten routes, and at desktop width only the search page fails, as it does on production (production also fails on the profile page). A new spec hydrates the header's server HTML at phone width with zero recoverable errors. Page titles, page actions and the session's controls still fill in after hydration, inside the row.Post open and the server paint (3 Oct): opening a post from the feed on a phone, and what a hard load paints before React runs. The list cards linked to the absolute permalink; Next prefetched the post's data as
posts/slug.jsonand navigated withposts/slug.json?id=slug, so the prefetch was never reused: four requests per tap and a round trip before the post showed (150 to 290ms on production), and on a preview or a wrapper test build every tap was a full document load. Cards, their comment link, the grid overlay and the reading history rows now use the same-origin path (getPostPath): about 15ms from touch to post on the preview build, on a normal and a throttled network. Comparing each page with JavaScript off against the hydrated page found five things that moved on a hard load, all fixed: the toast's inert-exempt wrapper (from the second review) rendered inside a body portal and failed hydration on every page (React 418/423), so the attribute is on the toast itself; the post action bar took its clearance above the bottom bar after hydration and painted behind it first (on production too), now CSS; the column held no room for the top block, now one row from the server paint with hidden blocks publishing zero; the desktop header painted as an empty bar on phones and the post page held a 48px placeholder for the old back header, both hidden below tablet. The action bar ships with the footer chunk instead of a nested lazy chunk (late on the first post of a session, and a hydration error under Turbopack). Measured on the preview at 393px with 4x CPU: zero console errors on a hard-loaded post, the action bar 72px above the bottom from the first frame, layout shift down against production on tag, source, squad, profile, Explore and search pages (source 0.094 to 0.022, Explore 0.066 to 0), unchanged on the post page where all of it is the ad strip on both (0.1 to 0.6 depending on the post), and up on the Squads directory (0.009 to 0.035, a two-row block with one row held), padding equal to the block on 27 routes; in the iOS Simulator a tap on an Explore card opens the post in-app. The phone ad strip's push on a cold load stays (production behaviour, see above).Touch feedback (3 Oct): a touch screen keeps
:hoveron whatever lands under the last tap, so a row looked selected on the screen that opened under the finger. Thehover:variant is redefined for the whole app (packages/shared/tailwind/hover.ts): hover where a pointer can hover, the pressed state on touch, in its original place in the cascade (hover.spec.tsholds it there). Buttons take their CSS hover only where a pointer can hover.group-hover:is unchanged on purpose, since about a dozen controls are revealed by it. On a phone the settings menu marks no current row; desktop keeps the marker (9b24f08, fe5b3f8). Reviewers: this one changes everyhover:utility on touch devices, desktop is unaffected.Inset sweep (3 Oct): after the device pass, a code sweep plus an on-device scan (25 routes at three scroll positions) for anything pinned to an edge, sized to the viewport or offset from the bars by a fixed number. Fixed: the post action bar behind the bottom bar (
--shell-bottomnow includes the home-indicator inset), the Spotlight sheet leaving the screen with the keyboard up, the phone ad strip covering the block on visitor post pages, pages without the bar ending flush, the composer's link and poll footer and the GIF picker, the job page's footer over the bar, quest particles, the agent workspace height (61efdf6, 84f402d). Older wrapper inset bugs outside the shell were fixed in the same PR at Tsahi's request: the shared safe-area rules, full-height modal footers, scroll targets, the DevCard and welcome headers, the World chips, and the bars keeping their state while a sheet pins the page (f3d9f50). The scan is clean after the fixes.Real iPhone, inside the iOS app (3 Oct): the wrapper built at this preview on an iPhone 16 Pro (iOS 27), its web view driven and measured over USB, then used by hand. 17 routes clean. It found five bugs that only the app's 62px status-bar inset produces (Safari has none): sheets 62px too low, a doubled inset on a full-height sheet, the hidden block's last row under the clock, the page behind a menu jumping by the inset, and the menu sheet's rows jumping at full height. All fixed (fb831e4, 533107a, 8e80d28) and re-measured on the phone. The real feed settings page, the Bookmarks sharing sheet and the settings back flow were verified there too.
Device pass (3 Oct): a Playwright sweep in Chromium with ten device profiles (Samsung Galaxy S8 and S9+, Pixel 7, iPhone SE, 13 mini, 14, 15 Pro Max, iPhone 14 with the Chrome-on-iOS UA, and two landscapes), 22 member routes and 9 visitor routes each, eight geometry checks (content under the block, overflow, bar inside the viewport, spacer above the bar, fixed elements wider than the screen) and five interactions per device (You fit, Help sheet, post menu and Report sheet, composer header and formatting sheet, settings menu); Safari on the iPhone 17 Pro simulator on 40 pages. Portrait is clean on every size; the You list scrolls only on a 568px-tall screen. One fix came out of it (08a6389): the old phone visitor header no longer doubles the block on tag, highlights, best-of and source pages. Landscape above 656px is the tablet layout by the breakpoint, as on main.
useShellScroll(dead zone, tolerance, hide, snap, reveal),ShellCluster(lit root, badge, both events, composer, visitor sign-up),ShellBlock(brand row, root names, page title and actions, hidden pages, the slide). Shared and webapp suites green, strict typecheck and lint clean, extension builds.Known and deferred (by the plan)
ShellPage, and what needs the session, fill in after hydration inside the row.safeArea.css); the no-frame look there is step 4.🤖 Generated with Claude Code
Preview domain
https://claude-mobile-shell-1.preview.app.daily.dev