diff --git a/packages/shared/src/components/MainFeedLayout.tsx b/packages/shared/src/components/MainFeedLayout.tsx
index 6b863d8fc04..f1132426f89 100644
--- a/packages/shared/src/components/MainFeedLayout.tsx
+++ b/packages/shared/src/components/MainFeedLayout.tsx
@@ -17,7 +17,7 @@ import classNames from 'classnames';
import { useRouter } from 'next/router';
import type { FeedProps } from './Feed';
import Feed from './Feed';
-import { FeedPageLayoutMobile } from './utilities/common';
+import { FeedPageLayoutMobile, feedGutter } from './utilities/common';
import { ExploreChipsBar } from './feeds/ExploreChipsBar';
import { buildPersonalizedCategories } from './feeds/exploreCategories';
import { useFeeds } from '../hooks/feed/useFeeds';
@@ -671,7 +671,14 @@ export default function MainFeedLayout({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sortingEnabled, selectedAlgo, loadedSettings, loadedAlgo]);
- const disableTopPadding = isFinder || shouldUseListFeedLayout;
+ // Explore keeps the page's top padding in both layouts. It renders a
+ // breadcrumb and tab header above the feed, and zeroing the padding
+ // leaves that header jammed under the site header — while
+ // `shouldUseListFeedLayout` flips between first paint and mount
+ // (see `enableSsrSafeLayout`), so keying the spacing to it made the
+ // gap change size on navigation and settle differently on reload.
+ const disableTopPadding =
+ isFinder || (shouldUseListFeedLayout && !isAnyExplore);
const onTabChange = useCallback(
(clickedTab: ExploreTabs) => {
if (clickedTab === ExploreTabs.BestOf && isExtension) {
@@ -694,7 +701,13 @@ export default function MainFeedLayout({
+ All of them are one row, always on, and flush to the bottom. What + differs is how each one earns the right to stay there. Most are + behavioural — they respond to scrolling or to stillness — so this + page is an index, not a preview. Open a story and scroll it. +
++ {children} +
+); + +const Bench = ({ + children, + linkStyle, +}: { + children: ReactElement; + linkStyle?: 'long' | 'short' | 'tracking'; +}): ReactElement => ( +| + {v.name} + | ++ {v.how} + | ++ {v.cost} + | ++ + { + { + width: 'holds · any URL', + slug: 'holds · post links only', + short: 'needs shorter URLs', + }[v.holds] + } + + | +
+ {children} +
+); + +// --------------------------------------------------------------- +// Why the dock exists +// --------------------------------------------------------------- +const BubbleGhost = ({ label }: { label: string }): ReactElement => ( + + {label} + +); + +const LONG_URL = + 'https://app.daily.dev/posts/github-copilot-app-is-actually-good-just-not-with-copilot-nrl7lxzbn'; + +export const BubbleProblem: Story = { + name: 'Bubble problem', + render: () => ( ++ {children} +
+); + +// --------------------------------------------------------------- +// A. Pinned rail +// --------------------------------------------------------------- +export const PinnedRail: Story = { + name: 'A · Pinned rail', + render: (args) => ( +.invert class, so it is the real
+ theme, not a mock-up of it, and the pair swaps when you flip the
+ toolbar. Of the library's vector wordmarks only a handful
+ pass; the bottom two are why Sentry was dropped as the primary.
+ + One lead sponsor plus twelve partner logos, in five placements. + The columns below are geometry, measured off these very stories at + 375px and 1440px. They are not performance data — nothing here has + been A/B tested. +
++ A broadcast lower third is free real estate: the video is already + letterboxed, and a viewer who is watching is not reading. A pinned + rail on the new tab spends feed on every session instead, and a + moving ticker spends attention on every session — which is why + every concept here is static, and why only A and E persist. +
++ The rails no longer clip. The row measures itself and renders only + the marks that fit whole, so an advertiser is either shown or not + — never a half logo fading out at the edge. Twelve marks at a 16px + cap measure an 889px run, and the “Made possible by” lockup takes + another 196px, so all twelve need about 1250px. Below that the + wall steps down: 11 at 1200px, 9 at 1000px, 7 at 800px, 1 at + 375px, measured off these stories. +
++ Those are story widths. On the feed the sidebar and the column + padding take their cut first, so the same viewport yields a + narrower row and one fewer mark — 8 rather than 9 at 1000px, + measured on the preview. Read the numbers as the shape of the + curve, not as a rate card. +
++ That step-down is the real inventory question, and it is a + commercial one rather than a layout one: twelve slots are only + twelve slots on a large desktop, and a laptop buyer is sharing a + shorter wall. The dials are slot count, the 16px cap (13px would + buy back about 120px, at marks small enough to be decoration), the + lead mark's size, and the label. The wrapping concepts (C, D, E) + hold all twelve at every width instead, which is the argument for + pairing the rail with one of them rather than relying on it alone. +
++ Browsers draw their link-status bubble over the bottom corners, so + anything flush to the edge of a feed — which is nearly all links — + is covered most of the time. Floating the bar clear of the edge + fixes it and reads badly: it detaches from the product. The answer + is to stack instead, putting a value rail underneath to take the + hit. See the Sponsor Dock story; concept A on its own still has + the problem. +
++ Partner order is reshuffled on every page load, so no advertiser + is permanently first and — since the row trims from the end — none + is permanently the one that gets dropped on a narrow window. Over + a run of sessions the exposure evens out on its own. The lead slot + never moves: it is the one that was paid for by position. The + shuffle waits for mount rather than running during render, so the + server and client agree on the first paint. +
++ The lead sponsor's mark is a link to their site and the only + thing in the strip that answers a cursor — it lifts 5% on hover. + The partner wall is inert: no links, no hover, no focus stops. + That keeps the one paid click target unambiguous, and it keeps a + row of twelve hover states from competing with the posts they sit + between. It also means the wall costs nothing in keyboard + navigation, which matters most for the pinned rail, where it would + otherwise sit in the tab order of every session. +
++ Every concept renders logos as silhouettes so one asset works in + both themes. That only holds if the file is a clean vector: in the + current advertiser library 31 of 66 wordmarks are raster images + wrapped in an .svg and mask to a solid block, and several more + carry knockouts that fill in. See the LogoTreatment story. Whoever + sells this slot needs a single-colour vector in the spec. +
++ C or D for the partner wall, since they hold all twelve and give + the space back on scroll, with the lead sponsor also carried in E + where the rail exists. A is the only option that guarantees a + session-long impression, and the only one that never returns the + pixels — worth testing against the header-ad experiment rather + than shipping alongside it. +
+