Skip to content

fix(website): readable code panes, self-healing hero handshake, focus ring, mobile hero - #998

Merged
blove merged 3 commits into
mainfrom
blove/homepage-polish
Sep 4, 2026
Merged

fix(website): readable code panes, self-healing hero handshake, focus ring, mobile hero#998
blove merged 3 commits into
mainfrom
blove/homepage-polish

Conversation

@blove

@blove blove commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #997, from verifying the deployed homepage in a real browser. Everything here is a defect found on the live site, not a refinement.

The hero bridge could stall, and did

The frame does not start its walkthrough until the parent posts visible: true, and both sides sent that handshake once with no retry. The parent's first post always fired while contentWindow was still about:blank and was dropped, which is the postMessage ... does not match the recipient window's origin warning on every production load. The only surviving post came from the iframe's load event, and the frame registers its listener after its lazy chunk boots, so if load won that race the message was lost and nothing retried. Live, this stalled once in nine loads: the pill said ready and the walkthrough never started.

The handshake is now self-healing from either side. The parent gates its first post on the frame having navigated and answers every ready; the frame re-announces ready every 500ms, capped at 10s, until an embedder answers.

Measured against a real iframe, website production build embedding the local demo, where the dev server's chunk latency makes the frame lose the race every time:

reaches ready walkthrough starts
before 12/12 0/12
after 12/12 12/12

The permanent console warning is gone. hero-bridge.ts is untouched: the origin allowlist and the e.source === parent check are unchanged.

Code panes hid the argument they were making

The runtime-parity pane cut off at assistantId: 'agent, and the install dialog showed npm install @threadplane/chat @threadplane/langgraph @langcha…. Both were horizontal scrollers, and macOS overlay scrollbars leave no affordance, so the one line carrying each section's point was invisible.

A scrollbar affordance was tried first and measured dead: with scrollbar-width: thin the gutter was 0px, because Chromium keeps the overlay either way. So the panes wrap instead. Across all 17 code panes at 1440, 768 and 390, hidden content is now zero. Copy still copies the full unwrapped command.

Also

  • Focus ring. The primary button fell back to the UA default, invisible on a dark blue fill. Buttons now carry a 2px accent ring with a surface halo, and the accent token is re-scoped on dark sections so it survives there.
  • Mobile hero. "Play walkthrough" was bare white text on a near-black poster; it is now a light pill with a 44px target. The poster crops rather than shrinking below 768px, so it scales 0.60 instead of 0.29. A phone-width poster asset is a follow-up; the CSS comment says so.
  • Take-control pill moved into normal flow. It covered the transcript and half the send button at 768 and below, confirmed by sampling elementsFromPoint through a full walkthrough at both widths.

Verification

1119 website tests, 179 demo tests, 0 lint errors, clean type-check, green production builds. Four style contracts pin the load-bearing declarations, including a note recording the measured 0px scrollbar gutter so nobody restores the scrollbar as the fix. The handshake specs are mutation-checked against the old code.

🤖 Generated with Claude Code

blove and others added 3 commits September 4, 2026 12:29
The embedded walkthrough could reach `ready` and then sit on the empty
welcome state forever, because the whole handshake rode on a single
message that either side could drop.

Parent (HeroDemo): the first visibility post fired while the iframe's
contentWindow was still `about:blank`, so it was dropped with the
"target origin does not match the recipient window's origin" console
error that appeared on every production load. It now posts only after
the frame's window has navigated — proven by the iframe `load` event or
by the frame having spoken to us — and it answers EVERY `ready`, not
just the first, with the current visibility.

Frame (HeroMode): the `message` listener is registered inside `boot()`,
after the lazy route chunk has loaded, so the parent's `load`-time post
could land before anyone was listening and nothing retried. An embedded
frame now re-announces `ready` every 500ms (capped at 10s) until any
visibility message arrives, then stops; it also stops on destroy.

HeroBridge's origin allowlist and `e.source === parent` check are
untouched.

Verified locally against the real iframe with the demo served from
localhost: 0/12 loads started the walkthrough before, 12/12 after, and
the postMessage console error is gone.

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

`.hero__take` was `position: absolute; bottom: 72px`, so it floated over
the chat surface: at 768px it sat on the streaming answer and half-covered
the composer's send button, and at 390px it landed on "How can I help?".

Moved into normal flow as the last row of the `.hero` column, so it can
never overlap anything at any width. Sampled at six points across a full
walkthrough at 768px and 390px: the pill's box previously hit
chat-welcome / chat-typing-indicator / chat-scroll at every sample, and
now hits nothing inside the surface.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…g, fix the mobile hero

Three defects found on the deployed homepage.

Code panes hid their payload. `white-space: pre` plus `overflow-x: auto` is
invisible clipping on macOS, where the overlay scrollbar draws nothing until a
scroll is already under way: the runtime-parity pane ended mid-string at
`assistantId: 'agent` (scrollWidth 609 / clientWidth 550 at 1440px) and the
install dialog showed `... @langchain/langgraph @langcha`. Every shiki pane and
every install-dialog pane now soft-wraps (`pre-wrap`), with `break-word` as the
backstop for a run that carries no whitespace at all. A scrollbar was measured
and rejected as the affordance: `scrollbar-width: thin` and `::-webkit-scrollbar`
both leave 0px of layout gutter on macOS. The install command wraps outright —
it is one shell line with no meaningful breaks, and copy still takes the full
string from state.

Buttons had no visible focus ring. The UA default `outline: auto 1px
rgb(0, 95, 204)` is invisible on the primary button's own #004090 fill, which is
exactly where focus lands when the install dialog closes.

The mobile hero read as bare text on a smudge. The play control's `#111` fill
was camouflaged against the near-black poster, and its `left: 50%` inset let it
shrink-to-fit into half the stage. The 1200x720 desktop poster is now cropped
rather than shrunk below 768px, so the chat is legible at ~2x.

Verified against a production build at 1440/768/390: all 17 code panes report
scrollWidth == clientWidth.

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

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
threadplane Ready Ready Preview Sep 4, 2026 7:35pm UTC

Request Review

@blove
blove enabled auto-merge (squash) September 4, 2026 19:33
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@blove
blove merged commit ad7803a into main Sep 4, 2026
39 checks passed
blove added a commit that referenced this pull request Sep 4, 2026
…edesigned OG card (#1002)

* feat(website): redesign the Open Graph card for feed legibility

The old card put the product name in an 18px eyebrow, the runtimes and
the capability list inside a three-line 26px paragraph, and three 15px
mono pills along the bottom. At the ~500px a timeline actually renders a
share card, all of that is grey texture: the only legible element was the
headline, and the brand name was the smallest type on the card.

Rebuilt around what survives a 0.42x downscale. Four elements, none below
30px source: the wordmark at 100px, the category line (HERO_H1, now on one
line) at 52px, HERO_SUBHEAD at 36px for what you get, and a LangGraph +
AG-UI pill at 34px for what it plugs into. Centred rather than left-aligned
so a square thumbnail crop keeps the product name.

Moved onto the production dark-surface tokens from ui.css
([data-ui="section"][data-surface="dark"]) resolved to literals, since
Satori cannot read CSS variables, plus the radial accent glow from
landing.css .proof-strip::before and --color-angular-red for the seam.
Text contrast is 16.8:1 (primary), 11:1 (secondary) and 8.6:1 (accent).

Copy stays single-sourced: the wordmark is derived from PRIMARY_TAGLINE
rather than retyped, and the runtime claim is POSITIONING_PROOF_POINTS[0].
`alt` now describes what the card says instead of naming the page.

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

* fix(website): ship the OG card's dimensions and alt text

Next's file-convention metadata (the alt and size exports in
app/opengraph-image.tsx) is overridden the moment openGraph.images is set
explicitly. Both layout.tsx and createPageMetadata passed a bare URL string, so
production emitted og:image alone — no width or height for a platform to lay
the card out before fetching it, and no alt text at all.

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

* chore(examples): record the hero poster at phone width

A parallel to record-hero-poster.record.ts that drives /hero at 390x650 and
writes hero-walkthrough-poster-mobile.webp. Same beat as the desktop script —
the first streamed reply — because 650 CSS px is the shortest frame that holds
the whole answer from its first line, and because matching beats mean crossing
the breakpoint swaps the source without changing the story.

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

* feat(website): ship a phone-width hero poster

The hero poster is the server-rendered LCP and the only thing a phone sees
until it taps Play. It was a 1200x720 desktop capture; at 390px it landed in a
348px stage, so the last change narrowed the stage to 4/5 and pushed
object-position to 40% so cover would crop rather than shrink. That bought
legibility and paid for it in credibility: every line of prose lost its right
edge, and the frame read as a broken screenshot.

Replaced with a second render of the same moment at phone dimensions
(585x975, 33,576 bytes against the desktop poster's 37,718), chosen by a
<picture> media source on the same 768px boundary the stage ratio and
MIN_AUTOPLAY_WIDTH already use. The stage below the breakpoint now holds 3/5 —
the capture's own ratio — so object-fit: cover crops nothing and the
object-position nudge is gone. currentSrc resolves to the phone poster at 390
and to the desktop one at 768 and 1440; the stage box measures the same with
the image aborted as with it loaded at all three widths, so the swap costs no
layout shift.

Two follow-on fixes the new asset exposed: the phone frame ends on the demo's
own "Take control" button, which read straight through the play control's
97% fill and sat half-uncovered beneath it. The fill is now opaque and the
control drops to 12px on phones so it lands squarely on the dead one.

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

* fix(website): re-record both hero posters on the shipping layout

The desktop poster was stale. It showed "Take control" floating over the
composer — the absolutely-positioned layout replaced in #998 — and a beat
where the walkthrough's second prompt was already typed into the composer.
It is the LCP element for every desktop visitor, so the largest image on the
homepage was showing a UI that no longer exists.

Both recorders now hold 2500ms after the interrupt detaches instead of 1500ms.
That is about the scripted cursor, not the text: HOLD_AFTER_ANSWER_MS (2000)
plus CURSOR_MOVE_MS (650) means at 1500ms the arrow is still parked where it
pressed Accept, which at phone width dropped it onto the word `retain` in step
3 — an artifact rather than a hint that the demo is live. At 2500ms it has
reached the composer, which is the beat the desktop script's docstring has
always claimed. The beat intent is otherwise unchanged: the first streamed
reply, with the user turn, the tool call and the whole answer on screen.

Both scripts now assert what they captured — `.hero__take` visible in normal
flow, composer empty — so a drifted wait or another layout change fails the
recording instead of silently shipping a poster of a UI we no longer have.

Desktop 37,718 -> 38,114 bytes; phone 33,576 -> 33,624.

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

---------

Co-authored-by: Claude Fable 5.1 <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