Skip to content

feat(feed): pinned sponsored strip on the main feed [mock-up, do not merge] - #6490

Draft
tsahimatsliah wants to merge 21 commits into
mainfrom
feat/feed-sponsored-strip
Draft

feat(feed): pinned sponsored strip on the main feed [mock-up, do not merge]#6490
tsahimatsliah wants to merge 21 commits into
mainfrom
feat/feed-sponsored-strip

Conversation

@tsahimatsliah

@tsahimatsliah tsahimatsliah commented Aug 18, 2026

Copy link
Copy Markdown
Member

⚠️ Mock-up — please do not merge

A design mock-up of a TBPN-style sponsored strip, put on the feed so it can be reviewed on a real page instead of only in Storybook. Sponsors are hard-coded placeholders. Nothing here has been sold.

Where to look

  • Preview deployment → the strip is pinned to the bottom of the main feed.
  • StorybookExtension / Sponsored Strip. Start with the Evaluation story; Gallery compares all five concepts, LogoTreatment shows the theme and asset constraints.

What it is

One lead sponsor plus a twelve-logo partner wall, in a 48px rail fixed to the bottom edge.

  • The lead mark keeps brand colour, sits ~20% larger than the wall, and is the only link and the only hover in the strip (5% lift). It is rendered inline so its wordmark rides currentColor — black on the light feed, white on the dark one — while the symbol holds #76B900. NVIDIA ships a black wordmark and a white one; a single flat file is invisible on one theme or the other.
  • The partner wall is silhouetted to currentColor, spread with justify-between, and completely inert — no links, no hover, no focus stops, so twelve logos cost nothing in the tab order of every session.
  • Logos are sized by area, not cap height, so a 2:1 mark and a 6.4:1 lockup carry the same visual weight.

Four other placements were evaluated

Concept A (this one) is the only option that guarantees a session-long impression, and the only one that never gives the pixels back. The full comparison is in the Evaluation story. The one number worth carrying into review:

Twelve marks need an 889px run, so the rail needs ~1220px of viewport before the last logo clears the fade — wider than most laptops. Below that it shows fewer sponsors than were sold, down to none at 375px. At twelve slots this is a large-desktop format. The dials are slot count, the 16px wall cap, and label length; the wrapping concepts (feed band, card slot) hold all twelve at every width instead.

Before this could ship

  • Sponsors come from a fixture, not the ad service
  • No feature flag — it currently renders for everyone on the feed
  • No impression logging; click logging is an unused optional callback
  • Partner logos are hot-linked from business.daily.dev
  • Advertiser logo assets need a spec: silhouettes need clean vectors, and 31 of the 66 wordmarks in the current library are raster images wrapped in an .svg, which mask to a solid block

Verification

Storybook is verified directly — both themes, 375 / 768 / 1440, no overlap or spill, and the story renders the same fixture objects the feed wiring uses. The webapp wiring itself is unverified locally: the app holds paint until the API boot resolves, which needs a real session, so the preview deployment is the first place this is actually seen on the feed.

🤖 Generated with Claude Code

Preview domain

https://feat-feed-sponsored-strip.preview.app.daily.dev

tsahimatsliah and others added 8 commits August 18, 2026 09:56
Five placements for a "presented by" slot plus a ten-logo partner
wall, modelled on a live show's lower third but static: a broadcast
bar covers nothing, a feed bar costs posts, and a marquee in the
periphery of a reading surface is the disruption we are avoiding.

Concepts (pinned rail, inline rail, feed band, card slot, side rail)
are rendered over real ArticleGrid cards so they can be judged
against actual card weight, and the Evaluation story records the
geometry measured off those stories at 375px and 1440px.

Logos are painted as currentColor silhouettes through a CSS mask
rather than filtered images, so one asset works in both themes, and
are sized by area rather than cap height so stubby marks stay
legible next to 6:1 lockups. Two asset constraints this surfaced,
shown in the LogoTreatment story: knockouts fill in under an alpha
mask, and 31 of the 66 wordmarks in the advertiser library are
raster images wrapped in an .svg.

Nothing is wired into the new tab — this is a design evaluation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three changes to the sponsored strip concepts:

The presenting slot now keeps its brand colour while the partner wall
stays silhouetted — one coloured mark against a neutral row is the
hierarchy, and colour is what the paid slot is buying. That only works
with a logo whose inks clear both grounds, so the primary moves from
Sentry (#362D59, 0.20 luminance, sinks into the dark theme) to Google,
the most colourful of the few library wordmarks with no near-black or
near-white ink. LogoTreatment now proves this rather than asserting it:
each candidate renders on both grounds at once via the app's own
.invert class, with Sentry and Notion kept as the failing cases.

The partner wall is spread across the full run instead of packed left,
matching the reference bar. The rails use justify-between over a gap-4
floor with the fade reserved by padding, so the last mark only meets
the fade on genuine overflow; the band moves to an even ten-column
grid that breaks to five by two on mobile. Spreading also lowered the
all-ten threshold from ~980px to ~920px — measurements in the
Evaluation story updated throughout.

Storybook's Tailwind never scanned packages/extension, so utilities
used only in the concept file — anything with a breakpoint variant
especially — generated no CSS and the band silently rendered five
columns at every width. Added the glob.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pinned rail's translucent, blurred ground read as an overlay
floating over the feed, which is more presence than a sponsor row
should take. It is now an opaque background-default with the same
hairline top border.

Logo cap heights go 13 -> 16 for the wall and 18 -> 21 for the
presenting mark, and the wall moves from tertiary to secondary text
contrast. Both rails keep their fixed h-12 / h-11: the tallest optical
result at the new cap is 22px, so the marks grow inside the bar rather
than growing the bar.

The wider marks broke the band's equal-fraction columns — LaunchDarkly
at 83px overflowed a 78px cell and collided with its neighbour — so
its grid moves to `auto` columns with the grid's own justify-between
handing out the slack, stepping 10 / 5 / 3 columns as it narrows.
Verified free of overlap and spill at 375, 768 and 1440.

Bigger logos cost reach: the minimum run goes 625px -> 746px and the
all-ten threshold ~920px -> ~1040px, and the band is taller on mobile
(123px -> 179px). Evaluation numbers updated, with the trade recorded
so the cap can be argued back down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Label copy goes from "Presented by" to "Made possible by" (the lockup
component follows the copy: PresentedBy -> PrimaryLockup).

NVIDIA is not in the advertiser logo library, so the mark is a local
story fixture under storybook/public/mock-logos: the Wikimedia
wordmark recoloured to the brand green. The recolour is the point —
the stock lockup is black, which fails the both-grounds test the
coloured slot has to pass, while #76B900 sits at 0.62 luminance and
clears light and dark alike. It leads the LogoTreatment candidate
list; Google stays as the alternative.

Logos are no longer lazy-loaded. Eleven inline SVGs weigh nothing,
the masked partner marks are CSS and never defer, and deferring the
one <img> let the paid slot be the last thing on the page to appear —
which is exactly how it surfaced, as a lead mark that never painted.

The longer label plus NVIDIA's wordmark widen the lockup 164px ->
210px, pushing the all-ten threshold ~1040px -> ~1090px and leaving
no partner clear of the fade at 375px. Evaluation updated, and the
copy is now named as one of the dials that buys that room back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The strip fixture is now NVIDIA's complete logo — eye mark, wordmark
and (R) — rather than the wordmark alone, taken from the 656x120
Wikimedia lockup and recoloured to the brand green on the root so all
three paths inherit it.

The recolour still carries its weight: NVIDIA sets the wordmark in
black or white to suit the background, which is two assets, and the
black one vanishes on the dark feed. The all-green lockup is a
standard NVIDIA presentation and holds at both grounds from one file.

Labels drop out of all caps. "Made possible by" and "Also backing
daily.dev" now read in sentence case, and the letter-spacing that
existed to make caps legible goes with it.

Both changes narrow the lockup 210px -> 190px, which pulls the all-ten
threshold ~1090px -> ~1070px and puts a partner back on screen at
375px, where the caps label had left none. Evaluation updated and the
label casing named as the dial that did it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A flat file cannot satisfy this slot. NVIDIA sets its symbol in green
and its wordmark in black or white to suit the background — two
assets, and whichever one ships is invisible on the other theme. The
all-green compromise shipped previously was legible but not the
brand's lockup.

Sponsor gains an optional `Artwork`: inline artwork rendered in place
of the logo image, for a lockup whose parts need different colour
rules. The NVIDIA fixture keeps #76B900 on the symbol and puts the
wordmark and (R) on `currentColor`, so it is black on the light feed
and white on the dark one from one source. Paths are NVIDIA's own
artwork; only the fills are ours, and the three path roles were
identified by bounding box rather than by eye. The flat file stays as
the silhouette source.

The lead mark is also ranked above the wall: PRIMARY_CAP 21 -> 23
lands it at 18px against a 15px median partner, a 20% step rather
than a tier. Threshold moves ~1070px -> ~1070px (the wider lockup is
offset by the earlier sentence-case saving); measurements refreshed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The lead sponsor's mark is now a link to their site — target=_blank
with rel=noopener noreferrer — and the only thing in the strip that
answers a cursor, lifting 5% from its left edge on hover. The partner
wall is inert: no buttons, no links, no hover, no focus stops. One
unambiguous paid click target, no row of hover states competing with
the posts around it, and nothing extra in the tab order of every
session — which matters most for the pinned rail.

Two more partners (dbt, Sonar), both clean vectors, bringing the wall
to twelve. Twelve divides evenly into the band's columns, which move
to 3 / 4 / 6 so every row stays full; ten would have left a ragged
last row at every breakpoint.

The wall's minimum run goes 746px -> 889px and the all-ten threshold
becomes an all-twelve threshold at ~1220px, which is wider than most
laptops — at twelve slots the rails are a large-desktop format, and
the evaluation now says so. The band grows 56px -> 89px at 1440px;
the card slot still fits one post card exactly. Verified free of
overlap and spill at 375, 768 and 1440.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MOCK-UP FOR REVIEW — not for merge. Concept A of the sponsored strip
put on the webapp feed so it can be seen on a preview deployment
rather than only in Storybook.

The component moves from the extension's newtab folder to
shared/components/sponsors, since the webapp renders it now, and the
NVIDIA lockup moves with it. Sponsor fixtures live in mockSponsors.ts
and are imported by both the story and the feed wiring, so what is
reviewed in Storybook is the same data the preview renders. The flat
nvidia.svg is deleted: the lead mark is inline artwork and nothing
referenced the file any more.

SponsorRailPinned is now `fixed` rather than `sticky`. It has to pin
regardless of what it is nested in, and a consumer cannot override
`sticky` with a position utility of equal specificity.

What is deliberately missing before this could ship: sponsors come
from a hard-coded fixture rather than the ad service, there is no
feature flag, no impression or click logging beyond the optional
callback, and the partner logos are hot-linked from
business.daily.dev.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
daily-webapp Ready Ready Preview Aug 20, 2026 11:02am
storybook Building Building Preview Aug 20, 2026 11:02am

Request Review

…debar

The rail was `fixed`, so it was positioned against the viewport: full
window width, running underneath the left sidebar and clipping its own
"Made possible by" label behind it.

It is `sticky` again, in flow inside the layout's padded main — which
is where the sidebar offset already lives. The bar now spans the feed
column and nothing else, and picks up that offset across layout
variants and sidebar states without having to know what either is,
which is what makes the old and new layouts both work from one rule.
`laptop:px-10` matches the feed column's own padding so the marks line
up with the card grid rather than the gutter.

Height drops 48px -> 40px. The tallest optical mark is 22px, so the
bar still has 9px of air above and below.

Also fixes a stale comment: the blur was removed two commits ago but
the note claiming the bar is translucent survived, because that edit
targeted the wrong comment style and silently matched nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Concept A's cost is now 40px of the feed column rather than 48px of
the viewport, and the padding that aligns the bar with the card grid
pushes the all-twelve threshold ~1220px -> ~1250px. Measured, not
derived.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two changes to how the wall behaves.

It reshuffles on every page load, so no advertiser is permanently
first and — since the row trims from the end — none is permanently
the one dropped on a narrow window. The lead slot never moves; it was
paid for by position. The shuffle runs on mount rather than during
render, so the server and client agree on the first paint instead of
tripping a hydration mismatch.

The rails now render only the marks that fit whole, instead of
overflowing behind a fade. Mark widths are known from their ratios, so
this needs no measurement beyond the row itself. Measured: 12 marks at
1440px, 11 at 1200, 9 at 1000, 7 at 800, 1 at 375 — an advertiser is
either shown or not, never a half logo under a gradient. The fade and
its padding are gone with the overflow they existed to hide.

The row measures directly on mount rather than waiting for
ResizeObserver's first callback, which is both more deterministic for
the initial paint and necessary where that callback never arrives; RO
still drives resizes, with a window-resize fallback where it is
missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On the deployed feed the wall rendered empty: the row was measured
before layout settled, came back 0, and 0 was taken to mean no mark
fits. With no resize afterwards it stayed empty — a strip with a lead
sponsor and no wall at all.

A zero width now means "not measured yet" and renders the full wall,
same as before the first measurement. The row is also re-measured once
after the layout's 300ms padding transition, since the first reading
can be of a width that is still on its way somewhere else when the
sidebar opens or closes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The step-down counts are story geometry. On the feed the sidebar and
column padding take their cut first, so the same viewport fits one
fewer mark — 8 rather than 9 at 1000px, measured on the preview.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Browsers draw their URL preview in the bottom corners, over anything
the page renders. A feed is almost entirely links, so a bar flush to
the bottom edge spends most of its life with a tooltip across it — on
a long URL, across the whole strip, including the paid slot.

The bar now holds 28px clear of the edge as a rounded island with a
border and a shadow, and the bubble lands in the gap. Moving it
sideways would not have worked: Chrome relocates the bubble to the
opposite corner when the cursor nears it, so there is no safe side,
only a safe height. The offset is deliberately generous rather than
exact — the bubble is browser chrome, invisible to the DOM, and it
scales with page zoom.

The ground stays opaque. `surface-float` is a translucent token, and
cards scrolling through the marks costs more legibility than the
float is worth.

The gallery's concept-A override drops the border and radius it used
to add by hand, which the component now owns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The floating island is reverted. It cleared the browser's URL tooltip
but read as a widget bolted onto the product rather than part of it,
which is most of what made the flush version work.

The bar is flush again, and the tooltip is handled by what sits under
it. A value rail stacks beneath the sponsor row, so the thing the
bubble covers is ambient data rather than the row someone paid for —
and each rail opens with its label on the left, which is exactly where
the bubble lands, so what it eats is a word rather than a number. The
reference format stacks the same way: sponsor lockup above, tickers
below.

Stacking also answers a question the strip could not answer before.
A permanent bar carrying only advertising is rent; one carrying
something the reader came for is a feature that happens to be
sponsored.

Ten rails to choose from, ordered ambient to personal — tag momentum,
model rankings, hot posts, streak, leaderboard, squad pulse, live
rooms, role pulse, release radar, poll. Each is mocked, and none needs
new plumbing: trendingTags, userStreak, the leaderboard queries, live
rooms, opportunities and poll posts are already in the app.

The feed wiring now renders the dock with tag momentum, that being the
closest thing daily.dev has to the reference's stock ticker.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A permanent bar has to justify being permanent every second it is on
screen. These are ten answers, grouped by the argument they make.

Get out of the way — Retract on read (leaves on scroll down, returns
on scroll up, the pattern browsers use for their own chrome), Condense
on read (the wall collapses to a count while moving; the paid mark
never leaves), Hairline peek (a 6px seam that opens on approach), Idle
reveal (minimal while anything moves, full when the reader stops).

Become chrome — Status bar (dressed as an editor status bar, a shape
developers read as tooling), Shortcut bar (carries the app's keyboard
hints, so the row is useful before it is sold), Browser seam (reads as
the window's bottom edge rather than the page's).

Do a job — Progress rail (its top edge is the feed's scroll progress).

Earn it by restraint — Broadcast credits (one partner at a time,
crossfading, a mention rather than a wall), Colophon (no panel at all,
low-contrast marks on the page that brighten on hover).

No second row and no float: every one is a single flush bar. The
strip's primitives are exported so the variants share optical sizing,
rotation and fitting rather than reimplementing them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bar is not restyled in any of these. It stays flush, full width,
sticky, one row, opaque, no float — the version that already read as
part of the product. What changes is geometry, ordering, or what the
page hands the browser.

Give it nothing to cover — Left gutter (content starts past the
tooltip's reach), Right-anchored (lead mark at the far end, the wall's
tail takes the corner, and the wall already reshuffles so it is a
different mark each session), Sacrificial band (taller bar, content
top-aligned, the bottom 26px are padding), Centred (both corners
empty, which is the only static layout safe from the tooltip in either
one).

Make it smaller — Short href (the tooltip is exactly as wide as the
URL inside it, so linking cards to /p/<id> shrinks it from most of the
bar to a stub; a routing change with no layout cost at all), Narrow
anchor (only the title is a link, so most of the pointer's time over a
card draws no tooltip).

Move only when it matters — Lift on hover, Slide on hover (contents
move, bar does not), Adaptive (estimates the tooltip's width from the
hovered href and moves only for long ones), Swap ends (lead mark and
wall trade places, geometry untouched).

The story ships a tooltip simulator: hover any card and a stand-in
appears bottom-left with the real href at roughly the real size, and
it jumps corners the way Chrome does. Each fix can be judged by
hovering rather than by argument.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured on the live feed, 28 links on one screen, at the 12px UI
font the tooltip uses:

  nav and tag links     33 chars   189px   13% of a 1440 screen
  post slugs            68 chars   393px   27%
  ad click-throughs    742 chars   clipped 50% (Chrome's cap)

The promoted cards are the worst offender by an order of magnitude:
they link through a signed token, so the URL carries a whole JWT and
the tooltip runs to the truncation limit every time.

That re-ranks the fixes, and not in favour of the ones that looked
neatest. A horizontal clearance has to be 400px to survive an
ordinary post link and half the bar to survive a promoted one, at
which point there is no bar left to protect. A vertical clearance is
26px whatever the URL says. Each variant now carries a `holds` verdict
— any URL, post links only, or needs shorter URLs — and the overview
leads with the measurement rather than the taxonomy.

The testbed was also lying: it drove `commentsPermalink`, but cards
build their href from the slug (see CardOverlay), so every bench ran
against a 33-character URL. It now drives the slug, with a tracking
length that reproduces the real worst case — verified at 670 chars,
a 720px tooltip, 5 of 12 marks buried unfixed and 0 of 12 with the
vertical fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dock ships on the feed with Hot right now as the default rail,
and the rail's label is now a dropdown that swaps it in place.

Putting the control in the label is what makes this cost nothing.
That word was already the throwaway element — it sits where the
browser's URL tooltip lands, which is why the rail opens with it —
so turning it into a trigger spends no space the row was not already
spending, and moves nothing else on the strip.

Eleven channels, including a new Your agents rail: what the reader's
own agents are doing, which is closer to a build indicator than a
ticker and the most personal of the set.

The choice is kept in localStorage rather than on the account — it is
a preference about one strip, it should survive a reload, and it is
not worth a round trip or a migration. It is read after mount, not in
a state initialiser, so the server and client agree on first paint.

Verified: defaults to Hot right now, the menu lists all eleven,
selecting Release radar swaps the row and stores `releases`, and the
choice survives a reload.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Several rails carried three items and left most of the row empty on a
wide feed, which read as a strip with nothing to say. Every channel is
now stocked well past what fits: 16 tags, 10 models, 8 headlines, 12
releases, 8 squads, and six or seven lines each for streak,
leaderboard, live rooms, roles, agents and the poll.

Carrying more than fits is the point — the row should read as a ticker
continuing past the edge rather than a list that happens to stop. That
only works if the edge is deliberate, so the row now fades out over
its last 40px. A hard clip cuts a word in half and reads as a bug.

Measured at 1440: Release radar fills 958px of a 1252px row with all
twelve visible; Hot right now, whose items are sentences, shows five
and fades the rest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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