feat(onboarding): redesign the post-signup funnel steps - #6391
feat(onboarding): redesign the post-signup funnel steps#6391tsahimatsliah wants to merge 23 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
One design language across the /onboarding funnel: shared headline scale, 440px rail, top chrome strip (logo + skip), docked glass CTA, and the giveback brand-gradient canvas. The onboarding_chrome experiment adds an edge-aura frame + progress dots as the variant arm. The paid /helloworld funnel is untouched — shared surfaces gate on isOnboarding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
51fe24b to
8c71702
Compare
The tag step's feed preview took its column count from the viewport, so on a wide screen it asked for six columns and divided its own narrower box by six — cards landed far under the 340px they are designed at. FeedLayoutProvider now accepts a maxNumCards cap, and the preview pairs a 3-column cap with the width those columns need. A Storybook story renders the real preview at 3 and 4 so the count can be chosen visually. The top bar's logo and skip also step up a size from laptop, and the row is now sized off the logo rather than the taller button, so the space above the wordmark matches the 24px gutter beside it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
h-logo-big (33px) overpowered the corner strip. The desktop size is now a single named constant so it is one line to retune. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The glass CTA carries the funnel's own logo and skip, so six shared steps passing a bare isGlass gave the paid funnel a second logo and a second Skip over its stepper header. The gate now lives in FunnelStepCtaWrapper, which reads useIsOnboardingFunnel() directly. Also drops FunnelVerifyEmail: nothing ever emits FunnelStepType.VerifyEmail, so the step never rendered — the live screen is AuthOptionsInner's EmailVerification tab, which already shows a large mail icon. Two bugs died with it (the auto-submit dropped the last digit, and Resend reset the cooldown without asking the backend for a new code). The signup and verify-email screens now take the funnel's canvas, top bar and headline scale via an onboarding-only prop, so the two screens in front of the funnel stop looking like a different product. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up gating from the review pass. The paid funnel now keeps main's
behaviour on every step the redesign touched:
- The content-types, install-PWA and edit-tags steps keep main's container,
padding and "Next" label; only the onboarding funnel gets the rail and
"Continue".
- The browser-extension step keeps "Get it for {browser}"; the shorter
"Add to {browser}" is onboarding-only, and a Freyja-provided cta still wins.
- Restored bg-gradient-funnel-onboarding-tall and the edit-tags branch that
selects it; only the onboarding funnel swaps in OnboardingBackground.
- ProfileForm moved out of stepsFullWidth into an onboarding-only list, so
/helloworld keeps its tablet:max-w-md column.
Also: the funnel's brand mark is now drawn directly instead of through Logo.
linkDisabled only adds pointer-events-none -- it keeps the real href and stays
in the tab order -- so one Tab + Enter walked the user out of the funnel.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…board Closes out the review pass. Every shared surface the redesign touched now keeps main's behaviour for the paid funnel: - ReadingReminder is restored byte-identical to main and the new screen lives in OnboardingReadingReminder. A separate component, not a flag: the two differ in where the actions live, so the paid screen renders its own Submit and skip while the funnel's sit in the docked CTA rail. - The new screen implements the APG radio pattern -- one tab stop with roving tabindex, arrows move selection. Replacing native radios with role="radio" buttons had dropped arrow-key navigation entirely. - ContentTypes takes isOnboarding: retired types, the two-column grid and the shared headline are onboarding-only. The retired-title filter moved into helpers and is now applied to getContentTypeNotEmpty too, so the "pick at least one" gate can no longer count a card the step never rendered. - OnboardingPWA, UploadCv, EditTag and RegistrationFieldsForm keep main's type scale, width caps, padding and field icons off the onboarding path. - FunnelProfileForm only docks its submit in the CTA rail when onboarding. Also fixes a pre-existing strict-mode violation in ContentTypes/helpers.ts, surfaced because the file is now in the changed-file guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ton-alignment-6e8571-eng # Conflicts: # packages/shared/src/components/auth/OnboardingRegistrationForm.tsx
The account-details screen on /onboarding is not the page's isAuthenticating branch -- the funnel is configured to reach it through the organicRegistration step, so the URL is /onboarding?stepId=... and FunnelOrganicSignup owns the AuthOptions. It builds its own staticAuthProps and renders its own FooterLinks, so nothing set on the page reached it: the headline stayed at typo-title2 and both the footer strip and the terms strip stayed on screen. It now passes hideSignupDisclaimer and onboardingHeadline, and drops the footer strip -- all gated on useIsOnboardingFunnel(), since the paid funnel shares this step and keeps its own chrome. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The live /onboarding signup flow runs through the heroLanding step, not the organicRegistration one -- FunnelHeroLanding renders AuthOptions inside OnboardingSignupHero, with its own staticAuthProps. So the account-details screen kept AuthHeader's typo-title2 and its terms strip. It now passes hideSignupDisclaimer and onboardingHeadline, gated on useIsOnboardingFunnel() since the paid funnel shares the step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The live funnel has no profileForm step -- its steps are heroLanding, editTags, contentTypes, uploadCv, plusCards, readingReminder, installPwa, browserExtension -- so the account-details screen is the heroLanding step's expanded auth form inside OnboardingSignupHero, not FunnelProfileForm. That is why styling the funnel step never showed up in the flow. OnboardingSignupHero's isFormExpanded branch now renders the funnel's canvas (OnboardingBackground) and FunnelStepTopBar in place of the marketing shell's own logo, and drops the footer links and the disclaimer row. Gated on useIsOnboardingFunnel(), and scoped to the expanded form so the signup wall and the paid funnel's landing are untouched. FunnelProfileForm is left in place: it predates this work and Freyja can still emit a profileForm step for campaign cohorts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6121db7 to
1ae65b5
Compare
tsahimatsliah
left a comment
There was a problem hiding this comment.
Summary
A genuinely large redesign of the post-signup funnel, and the craft in it is high — the shared type scale, the funnelStepRail, moving the isGlass gate inside FunnelStepCtaWrapper so no caller can leak it, the FeedContext maxNumCards cap (a real bug: the embedded preview was dividing its own narrow box by a viewport-derived column count), the getContentTypeNotEmpty fix so the gate can't count a card the step never drew, the strict-mode fix in filters/helpers.ts, and removing the focusable-but-pointer-events-none logo link that let Tab + Enter walk out of the funnel. AuthHeader.spec.tsx pinning both directions of onboardingHeadline is the right instinct given simplified has eleven consumers.
Three blocking items, all inline, plus notes. None of them are about the design.
Blocking
- New third-party dependency
edge-aura@^0.6.0— first published two and a half weeks ago, one maintainer, ~478 downloads/month, statically imported so it ships to the 100% control cohort and into the extension bundle. Needs an owner's sign-off, an exact pin, and a dynamic import gated onhasAura. - Terms/Privacy notice disappears from the phone-width signup flow —
hideSignupDisclaimerplus theFooterLinksremoval leaves no consent line on any screen belowtablet. Compliance call, not a visual one. /helloworldnow double-logs the reading-reminder impression —useReadingReminderis called above the!isOnboardingearly return, alongside main'sReadingReminderwhich logs the same event. The paid funnel is the one thing this PR promises not to touch.
Non-blocking
Inline: the two different CTA fallback labels (Continue vs Next), the unmentioned global tooltip alignment change in PlusListItem, retiring content types by display-title string, and aria-pressed vs role="checkbox" on CardCheckbox.
Two description issues, since the description is what a reviewer navigates by:
- The "Removed
FunnelVerifyEmail" bullet describes intra-branch churn, not a change tomain. That file never existed onmain— it was added in this branch's first commit and deleted in a later one, so the net diff contains nothing. The two bugs it claims to have fixed (stale-closure auto-submit, Resend cooldown) were bugs in code that never shipped. There's nothing for a reviewer to verify and it shouldn't reach a changelog. - The
PlusListItemnote covers the width override but not the alignment change that ships with it.
Reviewability
55 files, +3,222/−406, and it bundles a redesign, a new experiment, a new runtime dependency, cross-cutting edits to shared primitives used well outside onboarding (SearchField, CardCheckbox, FeedContext, FeedPreviewControls, PlusListItem) and ~1,190 lines of stories. The description itself says an earlier revision leaked into /helloworld in twelve places and that only ReadingReminder is provably byte-identical to main — and finding 3 above is a thirteenth leak that survived that pass, found by reading one early return. That pattern is the argument for splitting: the dependency plus the aura/dots experiment arm is a self-contained slice that would take the riskiest part out of this diff, and whatever ships should get a human click-through of /helloworld on the preview rather than a read-through of restored markup.
Verification
- Read the review guide, code-style and privacy rules
- Verified the
edge-auraregistry metadata and unpacked the published tarball (reduced-motion handling, no network/eval, static engine import) - Traced the
isOnboardingthreading: page →FunnelStepper→FunnelProgressContext→useIsOnboardingFunnel, and every consumer of the new props - Traced the mobile signup path screen by screen for the disclaimer regression
- Checked the mount effects in
useReadingReminderagainst main'sReadingReminder - Checked shared-primitive blast radius and
FunnelVerifyEmailagainstmain's history - Not verified by me:
/helloworldend to end, the aura arm rendering, light theme on the new cards, and any visual comparison against design - Tests not run locally (no node toolchain in my environment); CI was green/pending at review time
Verdict
Changes requested (posted as a comment — GitHub blocks a formal request-changes on the caller's own PR) on the three blocking items.
Reviewed by AI.
| import type { CSSProperties, ReactElement, ComponentProps } from 'react'; | ||
| import React, { useEffect, useMemo, useState } from 'react'; | ||
| import classNames from 'classnames'; | ||
| import { EdgeAura } from 'edge-aura/react'; |
There was a problem hiding this comment.
Blocking (dependency risk): this static import puts a brand-new third-party package into the shared bundle. From the npm registry today: edge-aura was first published 2026-07-12 (about two and a half weeks ago), latest 0.6.0, one maintainer, ~478 downloads in the last month, 122 kB unpacked / ~64 kB of dist JS. I unpacked the tarball — the code itself looks clean (no network calls, no eval, MIT, zero runtime deps, and it does honour prefers-reduced-motion), so this is about exposure rather than anything malicious.
Three concrete problems:
- It ships to everyone, for a feature almost nobody sees.
edge-aura/reactstatically imports the engine chunk, and this module is imported at the top ofFunnelStepBackground, which renders on every funnel step including/helloworld. The experiment defaults togradient, so 100% of users download the engine and the aura arm is the only cohort that uses it.next/dynamic(orReact.lazy) gated onhasAurakeeps it out of the control path entirely. - It lands in the extension bundle, which is why this PR needs
edge-auramappings in the extension's tsconfig and jest config. Extension releases go through store review, so a bad 0.6.x from a single-maintainer package is expensive to undo. ^0.6.0on a pre-1.0 package means any0.6.xpublish is pulled in unreviewed. Repo convention is to pin tightly enough that an unreviewed release can't disrupt builds — for a decorative dependency this new, I'd pin the exact version.
Independent of the above: adding a dependency that needs paths overrides in three tsconfigs plus a jest moduleNameMapper in three packages is a lot of permanent build coupling for a background animation. Worth an explicit "yes we want this dependency" from someone who owns the bundle before it merges — has that happened?
Reviewed by AI.
There was a problem hiding this comment.
Two of the three are fixed in 1b264b0:
- Pinned to an exact
0.6.0, no caret. next/dynamicgated onhasAura, so the control cohort and the extension bundle no longer download the engine. Theimport type { EdgeAuraOptions }is erased at compile time, so it costs nothing.
The third one is yours, not mine to answer: nobody who owns the bundle has said yes to the dependency. I have written the trade-off into the description under "The new dependency" so it is a decision on the PR rather than a line in package.json. If the answer is no, the aura arm is separable — useOnboardingChrome is the only thing reading the flag, and the control path is the gradient canvas that ships regardless.
Blocking: - /helloworld logged the reading-reminder impression twice per view. useReadingReminder ran above the !isOnboarding early return, alongside main's ReadingReminder which runs the same mount effects. The onboarding branch is now its own component so the hook only runs where it renders. - The Terms/Privacy notice had disappeared from the phone-width signup flow: the wall shows its own only from tablet up, and the account-details screen no longer rendered one. It is docked once at the bottom of that screen, at every width, on both entry paths. - edge-aura is pinned exactly (0.6.0) and loaded through next/dynamic gated on hasAura, so the control cohort and the extension bundle no longer carry the canvas engine. Whether we want the dependency at all is still an open question for whoever owns the bundle. Non-blocking: - One CTA fallback per branch, named, instead of two bare literals; the three steps that restated the same ternary now pass their label through. - CardCheckbox takes role="checkbox" + aria-checked, drops the unreachable hidden input, and darkens the unselected ring, which is the only thing marking a card as selectable. - PlusListItem keeps main's centered tooltip; only the width un-capping, which was a real bug on every Plus surface, ships globally. - Documented why the retired content types join on display copy: every other join into that data (TOGGLEABLE_TYPES, getAdvancedContentTypes, contentTypeIcon) already keys on title, and the id is a row id. Mockup-to-eng-pass: 1 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings in #6398 (feed card glass action bar contrast), merged into the PR branch on GitHub while these fixes were in progress. No conflicts. Mockup-to-eng-pass: 1
… the auth screens - The brand sweep drops from a 42rem band to 21rem, so the colour occupies roughly the top fifth of the screen instead of half of it. - The top bar's sticky wrapper had no width. On the sign-back screen, whose shell centres its children, it shrink-wrapped to 80px and the logo drifted to the middle of the screen, landing on the "Welcome back!" headline. - "Welcome back!" rendered at typo-title2 because AuthSignBack never received the funnel headline flag. It does now, and the shell gained the same pt-3 clearance the steps use under the top bar. - Account details and verify-email ended on a bare full-width button while every funnel step docks its CTA in the bordered glass bar. The bar is now its own FunnelGlassBar component, used by FunnelStepCtaWrapper, RegistrationForm and EmailCodeVerification. `onboardingHeadline` is renamed `isOnboardingFunnel` on the props that thread through AuthOptions, since it now carries the CTA treatment as well. AuthHeader keeps the old name — it only ever renders a headline. Mockup-to-eng-pass: 1 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The disclaimer was docked on the auth shell, which also serves sign-back, login and verify-email — screens where the account already exists and consent has already been given. It now lives on RegistrationForm, under the button that creates the account, so it follows the signup screen on both entry paths and at every width without the shell having to guess which tab is active. RegistrationForm.spec.tsx pins both directions; the hero spec now asserts the shell renders no footer chrome and no disclaimer of its own. Mockup-to-eng-pass: 1 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Status — ready for internal review Everything from the review round is fixed and those threads are resolved. One thread is deliberately left open: the Since the review, three things changed beyond the review items:
Two things to know while reviewing:
Still worth a human pass: a click through |
Flips the brand sweep to the bottom edge and softens it: the band is bottom-anchored with the mask reversed, the tints drop from 34/30% to 24/21%, and the horizon bloom widens to carry the diffuse falloff. The vignette centre moves to 76% so the darkening lands on the top corners the colour has left. Bottom-anchored puts the colour behind the docked CTA and leaves the top of every step flat, so the logo and headline sit on clean ground. Mockup-to-eng-pass: 1 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the pink → purple → blue linear sweep with two stacked ellipses in `onion` alone, which is the closest the palette gets to the reference art — raw onion.10 (#887BF8) sits on its bright violet, onion.90 (#401F9E) on its deep purple. Hard-coding the reference's own hexes would trip no-custom-color and break the light theme, so the hue comes from tokens and the saturation from opacity. Both ellipses are centred ON the bottom edge rather than below it: pushing the centre off-screen shows only the top arc, which reads as a horizontal streak instead of a dome. The wide haze carries the deep purple to the corners and the tighter core supplies the luminous middle. Mockup-to-eng-pass: 1 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drops the bloom from the lower third to a 16rem box at the bottom edge and narrows the core, so the oval reads as a glow beneath the primary button rather than a wash behind the content. The CTA scrim has to give way for it: it ran opaque from the screen edge up, which painted the page colour straight over the bloom's brightest band. It now fades at the bottom as well as the top, staying opaque only across the band the bar occupies, which is the part that actually has to hide scrolling content. Mockup-to-eng-pass: 1 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Leaves a single ellipse at the bottom edge. The wide outer haze was the layer reaching up past the button and washing the content area; without it the top of every step is flat and the only colour is the oval behind the primary button. Mockup-to-eng-pass: 1 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deletes OnboardingBackground and its three call sites, so the onboarding steps, the account-details screen and the auth shell all render on the flat page surface. The paid funnel's per-step gradients are untouched, and the aura experiment arm still draws its own frame. Reverts the CTA scrim to opaque as well: it was softened only so the oval bloom could show through it, and with no bloom the original is the better scrim. Mockup-to-eng-pass: 1 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Redesigns the post-signup onboarding funnel (
/onboarding) into one visual language, from the signup wall through to the Plus step.The paid
/helloworldfunnel is unchanged. Every shared surface here gates its onboarding treatment onisOnboarding, threaded from the page throughFunnelStepperintoFunnelProgressContext(useIsOnboardingFunnel).Status: all review findings are addressed. One item still needs a human decision — see The new dependency. Preview: https://claude-onboarding-button-alignme.preview.app.daily.dev
The design language
FunnelStepTopBar— logo top-left (icon only below laptop, 24px on desktop), Skip top-right, an even 24px on all three sides. The row is sized off the logo, so the space above the wordmark matches the gutter beside it and the y position is identical on every step.OnboardingHeadline/OnboardingSubheadline: one size, weight and 440px measure. Six steps previously used three different sizes.FunnelGlassBar— a blurred, bordered bar the CTA sits inside, on a 440px rail (funnelStepRail), with a scrim so content dissolves under it. Used by the steps, by account details and by verify-email, so the primary action looks the same on all of them.OnboardingBackground), ported with theme tokens viacolor-mix. The band is 21rem, so the colour occupies roughly the top fifth of the screen.onboarding_chrome, default = control): the variant swaps the gradient for an animatededge-auraframe plus progress dots under the CTA. Read throughuseOnboardingChromeso the two pieces can't disagree.Screens
isAuthenticatingbranch beforeFunnelSteppermounts, so they take the funnel's chrome through an explicitisOnboardingFunnelprop rather than context — there is no context to read yet. It carries the headline scale and the glass CTA. Deliberately not folded intosimplified, which eleven other surfaces set, including/helloworldand the recruiter modals.Community picks,Standups) dropped — and the same filter feedsgetContentTypeNotEmpty, so the "pick at least one" gate can't count a card the step never drew.OnboardingReadingReminder; state extracted touseReadingReminderso Submit and skip sit in the docked bar. Implements the APG radio pattern (one tab stop, arrows move selection).Where the Terms/Privacy notice appears
It lives on
RegistrationForm, under the button that creates the account — not on the funnel shell, which also serves screens where the account already exists.tablet+ (unchanged from main)Account details previously had none below
tablet, so a phone could reach "Sign up" without ever being shown the terms.Keeping it out of the paid funnel
Review found the redesign reaching
/helloworldin thirteen places. All fixed:isGlasswas hardcoded on six steps, so the paid funnel got the glass bar plus a second logo and Skip over its own stepper header. The gate now lives insideFunnelStepCtaWrapper, which readsuseIsOnboardingFunnel()itself — no caller can get it wrong.useReadingReminderran above the!isOnboardingearly return, so/helloworldmounted it alongside main'sReadingReminder— which runs the same effects internally — and logged the step's impression twice per view. The onboarding branch is its own component now.FunnelReadingReminder.spec.tsxpins both directions.ReadingReminderis restored byte-identical to main; the new screen is a separate component, because the two differ in where the actions live.ContentTypes,OnboardingPWA,UploadCv,EditTag,RegistrationFieldsFormandFunnelProfileFormkeep main's type scale, width caps, padding, field icons and containers off the onboarding path.bg-gradient-funnel-onboarding-talland the edit-tags branch that selects it.ProfileFormmoved out ofstepsFullWidthinto an onboarding-only list.Get it for {browser}(notAdd to {browser}).The new dependency
edge-auradraws the experiment arm's animated frame. MIT, zero runtime deps, no network calls oreval, and it honoursprefers-reduced-motion. It is also two weeks old with a single maintainer, so:0.6.0, no caret) — a pre-1.0 package should not pull in an unreviewed0.6.x.next/dynamic, gated onhasAura. The engine is ~64 kB of canvas code andFunnelStepBackgroundrenders on every funnel step, so a static import shipped it to the whole control cohort and into the extension bundle for a background they never see. Only thetypeimport stays static, and that is erased at compile time.It still needs an explicit yes from whoever owns the bundle: permanent build coupling (
pathsin three tsconfigs, a jestmoduleNameMapperin three packages) for a decorative background, and extension releases go through store review. If the answer is no, the arm detaches cleanly —useOnboardingChromeis the only reader of the flag, and the gradient canvas is the control path either way.Also worth a look
linkDisabledonly addspointer-events-none, keeping the realhrefand the tab stop, so Tab + Enter walked the user out of the funnel mid-flow. The mark is drawn directly as decoration now.role="checkbox"+aria-checkedrather thanaria-pressed. The hidden<input>behind them was unreachable and nothing read itsname, so it is gone. The unselected ring moved toborder-subtlest-primary— it is the only affordance marking a card as selectable.PlusListItem's tooltip dropped its width override on every Plus surface. The old value fought itself:!tablet:max-w-72was dead (Tailwind's important prefix goes after the variant), leaving!max-w-fullto un-cap the tooltip to the full viewport. That was wrong everywhere, so the fix is global. Nothing else about those tooltips changes; the centering is untouched.ContentTypes/helpers.ts, surfaced once the file entered the changed-file guard.Reviewing
/helloworldon the preview. The paid-funnel branches were restored by reading main's markup; onlyReadingReminderis provably byte-identical, and the reading-reminder double-log above is the kind of leak that survives a read-through.🤖 Generated with Claude Code
Preview domain
https://claude-onboarding-button-alignme.preview.app.daily.dev