From e959114899a7f4a5e215b1c7dbe5d0db641bec92 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 1 Jun 2026 17:42:51 +0300 Subject: [PATCH 01/17] feat(extension): welcome-back cover on hijacking new-tab Redesign the hijacking new-tab cover into a centered welcome-back card: returning users with a remembered account get an avatar, provider badge, and a one-tap "Continue as {name}"; logged-out new users get a clear "sign you in" message with explicit Sign up / Log in actions. The feed (promo ad + first stories) still renders below. Co-authored-by: Cursor --- .../src/newtab/HijackingLoginStrip.spec.tsx | 116 ++++++-- .../src/newtab/HijackingLoginStrip.tsx | 260 +++++++++++++----- 2 files changed, 296 insertions(+), 80 deletions(-) diff --git a/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx b/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx index 087c8a528b7..9cfa529062b 100644 --- a/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx +++ b/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx @@ -1,8 +1,11 @@ import React from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { AuthContextData } from '@dailydotdev/shared/src/contexts/AuthContext'; import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext'; import { getLogContextStatic } from '@dailydotdev/shared/src/contexts/LogContext'; +import { useSignBack } from '@dailydotdev/shared/src/hooks/auth/useSignBack'; +import { SocialProvider } from '@dailydotdev/shared/src/components/auth/common'; import { AuthTriggers } from '@dailydotdev/shared/src/lib/auth'; import { onboardingUrl } from '@dailydotdev/shared/src/lib/constants'; import { LogEvent, TargetType } from '@dailydotdev/shared/src/lib/log'; @@ -14,10 +17,15 @@ jest.mock('@dailydotdev/shared/src/contexts/AuthContext', () => ({ useAuthContext: jest.fn(), })); +jest.mock('@dailydotdev/shared/src/hooks/auth/useSignBack', () => ({ + useSignBack: jest.fn(), +})); + const LogContext = getLogContextStatic(); const mockUseAuthContext = useAuthContext as jest.MockedFunction< typeof useAuthContext >; +const mockUseSignBack = useSignBack as jest.MockedFunction; const logEvent = jest.fn(); const showLogin = jest.fn(); @@ -61,40 +69,92 @@ const renderComponent = ( ...authContext, }); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + return render( - - - , + + + + + , ); }; beforeEach(() => { jest.clearAllMocks(); + mockUseSignBack.mockReturnValue({ + isLoaded: true, + signBack: undefined, + provider: undefined, + onUpdateSignBack: jest.fn(), + }); }); describe('HijackingLoginStrip', () => { - it('shows a login CTA for logged out users', () => { + it('encourages signup for logged out users without a remembered account', () => { renderComponent(); expect( - screen.getByText('Unlock the full daily.dev experience'), + screen.getByRole('heading', { name: "Let's sign you in" }), ).toBeVisible(); + + const signup = screen.getByRole('button', { name: 'Sign up' }); + fireEvent.click(signup); + + expect(logEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Click, + target_type: TargetType.SignupButton, + target_id: 'hijacking', + }); + expect(showLogin).toHaveBeenCalledWith({ + trigger: AuthTriggers.Onboarding, + options: { isLogin: false }, + }); + }); + + it('logs a signup impression for new visitors', () => { + renderComponent(); + + expect(logEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Impression, + target_type: TargetType.SignupButton, + target_id: 'hijacking', + }); + }); + + it('offers a welcome-back "Continue as" for users with a remembered account', () => { + mockUseSignBack.mockReturnValue({ + isLoaded: true, + signBack: { + name: 'Tsahi Matsliah', + email: 'tsahi@daily.dev', + image: 'https://daily.dev/tsahi.png', + }, + provider: SocialProvider.Google, + onUpdateSignBack: jest.fn(), + }); + + renderComponent(); + expect( - screen.getByText('Log in to pick up where you left off.'), + screen.getByRole('heading', { name: 'Welcome back!' }), ).toBeVisible(); + expect(screen.getByText('tsahi@daily.dev')).toBeVisible(); - const cta = screen.getByRole('button', { name: 'Log in to continue' }); + const cta = screen.getByRole('button', { name: 'Continue as Tsahi' }); fireEvent.click(cta); expect(logEvent).toHaveBeenCalledWith({ - event_name: LogEvent.Click, + event_name: LogEvent.Impression, target_type: TargetType.LoginButton, target_id: 'hijacking', }); @@ -104,13 +164,33 @@ describe('HijackingLoginStrip', () => { }); }); + it('lets remembered users create a different account', () => { + mockUseSignBack.mockReturnValue({ + isLoaded: true, + signBack: { + name: 'Tsahi Matsliah', + email: 'tsahi@daily.dev', + image: 'https://daily.dev/tsahi.png', + }, + provider: SocialProvider.Google, + onUpdateSignBack: jest.fn(), + }); + + renderComponent(); + + fireEvent.click(screen.getByRole('button', { name: 'Create an account' })); + + expect(showLogin).toHaveBeenCalledWith({ + trigger: AuthTriggers.Onboarding, + options: { isLogin: false }, + }); + }); + it('shows an onboarding CTA for logged in users who still need onboarding', () => { renderComponent({ user: loggedUser, isLoggedIn: true }); expect( - screen.getByText( - 'You still have a few onboarding steps left. Finish them to unlock the full experience.', - ), + screen.getByText('Finish onboarding to unlock your personalized feed.'), ).toBeVisible(); const cta = screen.getByRole('link', { name: 'Continue onboarding' }); diff --git a/packages/extension/src/newtab/HijackingLoginStrip.tsx b/packages/extension/src/newtab/HijackingLoginStrip.tsx index 7185dbc58ac..d53f351bc37 100644 --- a/packages/extension/src/newtab/HijackingLoginStrip.tsx +++ b/packages/extension/src/newtab/HijackingLoginStrip.tsx @@ -1,22 +1,74 @@ -import type { ReactElement } from 'react'; -import React from 'react'; +import type { ReactElement, ReactNode } from 'react'; +import React, { useEffect, useRef } from 'react'; import classNames from 'classnames'; import { Button, + ButtonSize, ButtonVariant, } from '@dailydotdev/shared/src/components/buttons/Button'; +import { ClickableText } from '@dailydotdev/shared/src/components/buttons/ClickableText'; +import { + ProfileImageSize, + ProfilePicture, +} from '@dailydotdev/shared/src/components/ProfilePicture'; +import { + providerMap, + type SocialProvider, +} from '@dailydotdev/shared/src/components/auth/common'; import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext'; import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext'; +import { useSignBack } from '@dailydotdev/shared/src/hooks/auth/useSignBack'; import { AuthTriggers } from '@dailydotdev/shared/src/lib/auth'; import { onboardingUrl } from '@dailydotdev/shared/src/lib/constants'; import { LogEvent, TargetType } from '@dailydotdev/shared/src/lib/log'; import feedStyles from '@dailydotdev/shared/src/components/Feed.module.css'; -import { cloudinaryReadingReminderCat } from '@dailydotdev/shared/src/lib/image'; +import LogoIcon from '@dailydotdev/shared/src/svg/LogoIcon'; + +type CoverVariant = 'continue' | 'signin' | 'onboarding'; + +function HeroChrome({ children }: { children: ReactNode }): ReactElement { + return ( +
+
+
+
+
+
+
+
+ {children} +
+
+
+
+ ); +} export default function HijackingLoginStrip(): ReactElement { const { showLogin, user } = useAuthContext(); const { logEvent } = useLogContext(); + const { signBack, provider, isLoaded: isSignBackLoaded } = useSignBack(); + const hasLoggedImpression = useRef(false); + const isLoggedOut = !user; + const hasContinueAs = isLoggedOut && isSignBackLoaded && !!signBack?.name; + const firstName = signBack?.name?.split(' ')[0] ?? signBack?.name; + const socialProvider = + provider && provider !== 'password' + ? (provider as SocialProvider) + : undefined; + const providerIcon = socialProvider + ? providerMap[socialProvider]?.icon + : undefined; + + const variant: CoverVariant = (() => { + if (!isLoggedOut) { + return 'onboarding'; + } + + return hasContinueAs ? 'continue' : 'signin'; + })(); + const onboardingHref = (() => { const base = new URL(onboardingUrl); base.searchParams.append('r', 'extension'); @@ -24,72 +76,156 @@ export default function HijackingLoginStrip(): ReactElement { return base.toString(); })(); - const logHijackingClick = (): void => { + const logClick = (targetType: TargetType): void => { logEvent({ event_name: LogEvent.Click, - target_type: TargetType.LoginButton, + target_type: targetType, target_id: 'hijacking', }); }; - return ( -
-
-
-
-
-
-
-
-
-
-

- Unlock the full daily.dev experience -

-

- {isLoggedOut - ? 'Log in to pick up where you left off.' - : 'You still have a few onboarding steps left. Finish them to unlock the full experience.'} -

- {isLoggedOut ? ( - - ) : ( - - )} -
-
-
- Sleeping cat on laptop -
-
+ useEffect(() => { + if (hasLoggedImpression.current) { + return; + } + hasLoggedImpression.current = true; + + logEvent({ + event_name: LogEvent.Impression, + target_type: + variant === 'signin' ? TargetType.SignupButton : TargetType.LoginButton, + target_id: 'hijacking', + }); + }, [variant, logEvent]); + + const onSignupClick = (): void => { + logClick(TargetType.SignupButton); + showLogin({ + trigger: AuthTriggers.Onboarding, + options: { isLogin: false }, + }); + }; + + const onLoginClick = (): void => { + logClick(TargetType.LoginButton); + showLogin({ + trigger: AuthTriggers.Onboarding, + options: { isLogin: true }, + }); + }; + + if (variant === 'onboarding') { + return ( + +

+ You're almost set +

+

+ Finish onboarding to unlock your personalized feed. +

+ +
+ ); + } + + if (variant === 'continue' && signBack) { + return ( + +

+ Welcome back! +

+

+ Let's pick up right where you left off. +

+
+ + {!!providerIcon && ( + + {providerIcon} + + )} +
+ {!!signBack?.email && ( + + {signBack.email} + + )} + +
+ Not you? + + Use another account + +
+
+ New here? + + Create an account +
+
+ ); + } + + return ( + + + + +

+ Let's sign you in +

+

+ Sign in to start using daily.dev — keep your personalized feed, streak, + and reputation in sync wherever you open a new tab. +

+
+ +
-
+ ); } From beaa87961010f7e13aad1ead4e03e25af4251e3d Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 1 Jun 2026 22:00:42 +0300 Subject: [PATCH 02/17] feat(extension): richer welcome-back cover UI Layer depth onto the hijacking new-tab cover: cabbage/onion brand wash, accent glows, dotted texture, top sheen, and an inset edge over the existing gradient frame. Elevate content with a brand eyebrow, gradient accent headlines, a glowing avatar/logo, and lifting cabbage-glow CTAs. Force a dark token context so buttons stay legible on the dark panel. Co-authored-by: Cursor --- .../src/newtab/HijackingLoginStrip.tsx | 108 ++++++++++++------ packages/shared/src/styles/base.css | 9 ++ 2 files changed, 83 insertions(+), 34 deletions(-) diff --git a/packages/extension/src/newtab/HijackingLoginStrip.tsx b/packages/extension/src/newtab/HijackingLoginStrip.tsx index d53f351bc37..edc834d3062 100644 --- a/packages/extension/src/newtab/HijackingLoginStrip.tsx +++ b/packages/extension/src/newtab/HijackingLoginStrip.tsx @@ -26,16 +26,41 @@ import LogoIcon from '@dailydotdev/shared/src/svg/LogoIcon'; type CoverVariant = 'continue' | 'signin' | 'onboarding'; +const accentText = + 'bg-gradient-to-r from-accent-cabbage-bolder to-accent-onion-default bg-clip-text text-transparent'; + +const primaryCta = + 'shadow-2-cabbage transition-transform duration-200 ease-out hover:-translate-y-0.5'; + +const glassCta = + '!border-white/20 !bg-white/[0.06] !text-white backdrop-blur-sm transition-colors duration-200 hover:!bg-white/[0.12]'; + +function BrandEyebrow(): ReactElement { + return ( + + + + + + daily.dev + + ); +} + function HeroChrome({ children }: { children: ReactNode }): ReactElement { return (
-
-
-
-
-
+
+
+
+
+
+
+
+
+
{children}
@@ -117,10 +142,10 @@ export default function HijackingLoginStrip(): ReactElement { if (variant === 'onboarding') { return ( -

- You're almost set +

+ You're almost set

-

+

Finish onboarding to unlock your personalized feed.

); @@ -165,45 +169,38 @@ export default function HijackingLoginStrip(): ReactElement { if (variant === 'continue' && signBack) { return ( - -

- Welcome back! -

-

- Let's pick up right where you left off. -

-
-
+ -
- - {!!providerIcon && ( - - {providerIcon} - - )} -
+ {!!providerIcon && ( + + {providerIcon} + + )}
+

+ Welcome back, {firstName}! +

{!!signBack?.email && ( - - {signBack.email} - +

{signBack.email}

)}
Not you? @@ -229,22 +226,18 @@ export default function HijackingLoginStrip(): ReactElement { return ( - - - - - - - -

- Let's sign you in + +

+ Your feed is one tap away

-

- Keep your personalized feed, streak, and reputation in sync wherever you - open a new tab. +

+ Sign in to keep the dev news, tools, and discussions that matter synced + across every new tab.

-

+

Finish onboarding to unlock the full daily.dev experience.

+ ); +} + +function FeedPeekRail({ posts }: { posts: PeekPost[] }): ReactElement { + return ( +
+
+ {posts.slice(0, 4).map((post) => ( + + ))} +
+
); } @@ -81,6 +173,9 @@ export default function HijackingLoginStrip(): ReactElement { return hasContinueAs ? 'continue' : 'signin'; })(); + const feedPeek = useFeedPeek(variant === 'signin'); + const showPeek = variant === 'signin' && feedPeek.length >= 4; + const onboardingHref = (() => { const base = new URL(onboardingUrl); base.searchParams.append('r', 'extension'); @@ -126,9 +221,18 @@ export default function HijackingLoginStrip(): ReactElement { }); }; + const chrome = (children: ReactNode): ReactElement => ( +
+
+
+
{children}
+
+
+ ); + if (variant === 'onboarding') { - return ( - + return chrome( +

Continue ➔ - +

, ); } if (variant === 'continue' && signBack) { - return ( - + return chrome( +
- +
, ); } - return ( - - -

- Your feed is one tap away -

-

- All the dev news, tools, and discussions that matter — in every new tab. -

-
- - + + +
-
+ {showPeek && } +
, ); } From e8eb1ebc50fc8dd80fa17712d964ce6a7319ec8e Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 1 Jun 2026 23:18:06 +0300 Subject: [PATCH 06/17] feat(extension): animate feed peek as a live, endless feed Add a slow, seamless auto-scroll to the hero feed-peek rail so the preview feels alive and continuously updating. Respects prefers-reduced-motion. Co-authored-by: Cursor --- .../src/newtab/HijackingLoginStrip.tsx | 16 ++++++++++----- packages/shared/src/styles/base.css | 20 +++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/extension/src/newtab/HijackingLoginStrip.tsx b/packages/extension/src/newtab/HijackingLoginStrip.tsx index 50dccd72089..e2a75af0146 100644 --- a/packages/extension/src/newtab/HijackingLoginStrip.tsx +++ b/packages/extension/src/newtab/HijackingLoginStrip.tsx @@ -134,15 +134,21 @@ function FeedPeekCard({ post }: { post: PeekPost }): ReactElement { } function FeedPeekRail({ posts }: { posts: PeekPost[] }): ReactElement { + const visible = posts.slice(0, 4); return (
-
- {posts.slice(0, 4).map((post) => ( - - ))} +
+ {/* Render the set twice so a -50% translate loops seamlessly. */} + {['a', 'b'].map((set) => + visible.map((post) => ( +
+ +
+ )), + )}
); diff --git a/packages/shared/src/styles/base.css b/packages/shared/src/styles/base.css index 1ab480d4ce5..ca4c5523393 100644 --- a/packages/shared/src/styles/base.css +++ b/packages/shared/src/styles/base.css @@ -1100,6 +1100,26 @@ meter::-webkit-meter-bar { ); } + .top-hero-feed-scroll { + animation: top-hero-feed-scroll 32s linear infinite; + } + + @keyframes top-hero-feed-scroll { + from { + transform: rotate(2deg) translateY(0); + } + to { + transform: rotate(2deg) translateY(-50%); + } + } + + @media (prefers-reduced-motion: reduce) { + .top-hero-feed-scroll { + animation: none; + transform: rotate(2deg); + } + } + @keyframes enable-notification-bell-ring { 0%, 100% { From 292b382c2d2415a87ac0c60bdefea30435b5a710 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 1 Jun 2026 23:33:18 +0300 Subject: [PATCH 07/17] feat(extension): full-bleed living feed wall hero Replace the single side-rail peek with a full-width wall of the real feed: multiple columns of live posts scrolling at staggered speeds in alternating directions, dimmed behind a center spotlight so the sign-in is the focal "unlock" point. Falls back to the centered layout when feed data is sparse. Co-authored-by: Cursor --- .../src/newtab/HijackingLoginStrip.tsx | 159 +++++++++++------- packages/shared/src/styles/base.css | 19 ++- 2 files changed, 111 insertions(+), 67 deletions(-) diff --git a/packages/extension/src/newtab/HijackingLoginStrip.tsx b/packages/extension/src/newtab/HijackingLoginStrip.tsx index e2a75af0146..6b84f1f8b05 100644 --- a/packages/extension/src/newtab/HijackingLoginStrip.tsx +++ b/packages/extension/src/newtab/HijackingLoginStrip.tsx @@ -1,5 +1,5 @@ import type { ReactElement, ReactNode } from 'react'; -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import classNames from 'classnames'; import { useQuery } from '@tanstack/react-query'; import { gql } from 'graphql-request'; @@ -36,6 +36,9 @@ import LogoText from '@dailydotdev/shared/src/svg/LogoText'; type CoverVariant = 'continue' | 'signin' | 'onboarding'; +const WALL_COLUMNS = 5; +const WALL_MIN_POSTS = 9; + const primaryCta = 'shadow-2-cabbage transition-transform duration-200 ease-out hover:-translate-y-0.5'; @@ -78,7 +81,7 @@ function useFeedPeek(enabled: boolean): PeekPost[] { queryKey: ['hijacking', 'feed-peek'], queryFn: () => gqlClient.request(FEED_PEEK_QUERY, { - first: 8, + first: 18, supportedTypes: ['article'], }), enabled, @@ -101,7 +104,7 @@ function BrandLockup(): ReactElement { ); } -function FeedPeekCard({ post }: { post: PeekPost }): ReactElement { +function FeedWallCard({ post }: { post: PeekPost }): ReactElement { return (
@@ -133,19 +136,29 @@ function FeedPeekCard({ post }: { post: PeekPost }): ReactElement { ); } -function FeedPeekRail({ posts }: { posts: PeekPost[] }): ReactElement { - const visible = posts.slice(0, 4); +function WallColumn({ + posts, + reverse, + durationSec, +}: { + posts: PeekPost[]; + reverse: boolean; + durationSec: number; +}): ReactElement { return ( -
-
+
+
{/* Render the set twice so a -50% translate loops seamlessly. */} {['a', 'b'].map((set) => - visible.map((post) => ( + posts.map((post) => (
- +
)), )} @@ -154,6 +167,32 @@ function FeedPeekRail({ posts }: { posts: PeekPost[] }): ReactElement { ); } +function FeedWall({ posts }: { posts: PeekPost[] }): ReactElement { + const columns = useMemo( + () => + Array.from({ length: WALL_COLUMNS }, (__, col) => + posts.filter((_post, index) => index % WALL_COLUMNS === col), + ).filter((column) => column.length > 0), + [posts], + ); + + return ( +
+ {columns.map((column, index) => ( + post.id).join('-')} + posts={column} + reverse={index % 2 === 1} + durationSec={42 + index * 6} + /> + ))} +
+ ); +} + export default function HijackingLoginStrip(): ReactElement { const { showLogin, user } = useAuthContext(); const { logEvent } = useLogContext(); @@ -180,7 +219,7 @@ export default function HijackingLoginStrip(): ReactElement { })(); const feedPeek = useFeedPeek(variant === 'signin'); - const showPeek = variant === 'signin' && feedPeek.length >= 4; + const showWall = variant === 'signin' && feedPeek.length >= WALL_MIN_POSTS; const onboardingHref = (() => { const base = new URL(onboardingUrl); @@ -323,55 +362,55 @@ export default function HijackingLoginStrip(): ReactElement { ); } - return chrome( -
-
- -

- Make this feed yours -

-

- The dev news, tools, and discussions that matter — in every new tab. -

-
- - + Make this feed yours + +

+ The dev news, tools, and discussions that matter — in every new tab. +

+
+ + +
+

+ Join millions of developers staying ahead. +

- {showPeek && } -
, +
); } diff --git a/packages/shared/src/styles/base.css b/packages/shared/src/styles/base.css index ca4c5523393..45193dbc818 100644 --- a/packages/shared/src/styles/base.css +++ b/packages/shared/src/styles/base.css @@ -1100,23 +1100,28 @@ meter::-webkit-meter-bar { ); } - .top-hero-feed-scroll { - animation: top-hero-feed-scroll 32s linear infinite; + .top-hero-col { + animation-name: top-hero-col-scroll; + animation-timing-function: linear; + animation-iteration-count: infinite; } - @keyframes top-hero-feed-scroll { + .top-hero-col-reverse { + animation-direction: reverse; + } + + @keyframes top-hero-col-scroll { from { - transform: rotate(2deg) translateY(0); + transform: translateY(0); } to { - transform: rotate(2deg) translateY(-50%); + transform: translateY(-50%); } } @media (prefers-reduced-motion: reduce) { - .top-hero-feed-scroll { + .top-hero-col { animation: none; - transform: rotate(2deg); } } From 75779ea1887d735a61bec0572fff0704ff0d62c5 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 1 Jun 2026 23:45:27 +0300 Subject: [PATCH 08/17] feat(extension): sharpen hijacking hero voice Remove the noisy animated feed wall and replace it with a cleaner developer-focused hero, using live feed signals as supporting context instead of background decoration. Co-authored-by: Cursor --- .../src/newtab/HijackingLoginStrip.spec.tsx | 2 +- .../src/newtab/HijackingLoginStrip.tsx | 159 +++++++----------- packages/shared/src/styles/base.css | 25 --- 3 files changed, 59 insertions(+), 127 deletions(-) diff --git a/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx b/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx index 5b85793b140..3e278cc3281 100644 --- a/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx +++ b/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx @@ -109,7 +109,7 @@ describe('HijackingLoginStrip', () => { renderComponent(); expect( - screen.getByRole('heading', { name: 'Make this feed yours' }), + screen.getByRole('heading', { name: 'Your dev feed should know you.' }), ).toBeVisible(); const signup = screen.getByRole('button', { name: 'Sign up' }); diff --git a/packages/extension/src/newtab/HijackingLoginStrip.tsx b/packages/extension/src/newtab/HijackingLoginStrip.tsx index 6b84f1f8b05..4b98df97aea 100644 --- a/packages/extension/src/newtab/HijackingLoginStrip.tsx +++ b/packages/extension/src/newtab/HijackingLoginStrip.tsx @@ -1,5 +1,5 @@ import type { ReactElement, ReactNode } from 'react'; -import React, { useEffect, useMemo, useRef } from 'react'; +import React, { useEffect, useRef } from 'react'; import classNames from 'classnames'; import { useQuery } from '@tanstack/react-query'; import { gql } from 'graphql-request'; @@ -18,11 +18,6 @@ import { type SocialProvider, } from '@dailydotdev/shared/src/components/auth/common'; import { onboardingGradientClasses } from '@dailydotdev/shared/src/components/onboarding/common'; -import { - UpvoteIcon, - DiscussIcon, -} from '@dailydotdev/shared/src/components/icons'; -import { IconSize } from '@dailydotdev/shared/src/components/Icon'; import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext'; import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext'; import { useSignBack } from '@dailydotdev/shared/src/hooks/auth/useSignBack'; @@ -36,9 +31,6 @@ import LogoText from '@dailydotdev/shared/src/svg/LogoText'; type CoverVariant = 'continue' | 'signin' | 'onboarding'; -const WALL_COLUMNS = 5; -const WALL_MIN_POSTS = 9; - const primaryCta = 'shadow-2-cabbage transition-transform duration-200 ease-out hover:-translate-y-0.5'; @@ -104,91 +96,57 @@ function BrandLockup(): ReactElement { ); } -function FeedWallCard({ post }: { post: PeekPost }): ReactElement { - return ( -
-
- {!!post.source?.image && ( - - )} - - {post.source?.name} - -
-

- {post.title} -

-
- - - {post.numUpvotes ?? 0} - - - - {post.numComments ?? 0} - -
-
- ); -} +function formatCompactCount(value: number): string { + if (value < 1000) { + return `${value}`; + } -function WallColumn({ - posts, - reverse, - durationSec, -}: { - posts: PeekPost[]; - reverse: boolean; - durationSec: number; -}): ReactElement { - return ( -
-
- {/* Render the set twice so a -50% translate loops seamlessly. */} - {['a', 'b'].map((set) => - posts.map((post) => ( -
- -
- )), - )} -
-
- ); + return `${Math.floor(value / 100) / 10}k`; } -function FeedWall({ posts }: { posts: PeekPost[] }): ReactElement { - const columns = useMemo( - () => - Array.from({ length: WALL_COLUMNS }, (__, col) => - posts.filter((_post, index) => index % WALL_COLUMNS === col), - ).filter((column) => column.length > 0), - [posts], +function LiveSignal({ posts }: { posts: PeekPost[] }): ReactElement { + const discussions = posts.reduce( + (total, post) => total + (post.numComments ?? 0), + 0, ); + const sources = posts + .map((post) => post.source?.name) + .filter((name): name is string => !!name); + const topSource = sources[0]; + + if (!posts.length) { + return ( +
+ + Personalized topics + + + Saved reads + + + Real reputation + +
+ ); + } return ( -
- {columns.map((column, index) => ( - post.id).join('-')} - posts={column} - reverse={index % 2 === 1} - durationSec={42 + index * 6} - /> - ))} +
+ + + Live dev feed + + + {posts.length} fresh reads + + + {formatCompactCount(discussions)} comments + + {!!topSource && ( + + Trending from {topSource} + + )}
); } @@ -219,7 +177,6 @@ export default function HijackingLoginStrip(): ReactElement { })(); const feedPeek = useFeedPeek(variant === 'signin'); - const showWall = variant === 'signin' && feedPeek.length >= WALL_MIN_POSTS; const onboardingHref = (() => { const base = new URL(onboardingUrl); @@ -365,27 +322,27 @@ export default function HijackingLoginStrip(): ReactElement { return (
- {showWall && } - {showWall && ( -
- )} - {showWall && ( -
- )}
-
- +
+
+
+
+
+ +

- Make this feed yours + Your dev feed should know you.

- The dev news, tools, and discussions that matter — in every new tab. + Sign in so every new tab remembers your topics, saves, upvotes, and + reputation.

+
- + Make every new tab feel like yours. + +

+ Sign in to turn daily.dev into a calm briefing built around what + you read, save, upvote, and discuss. +

+ +
+ + +
+
+
+
-

- Join millions of developers staying ahead. -

From beb7df36d7c1da7f12b587df5cd0627822a729c0 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 1 Jun 2026 23:57:40 +0300 Subject: [PATCH 10/17] feat(extension): simplify hijacking hero with cat artwork Remove the live feed stats from the logged-out hero, use the existing cat artwork as the supporting visual, remove the logo capsule, and place signup/login actions side by side. Co-authored-by: Cursor --- .../src/newtab/HijackingLoginStrip.spec.tsx | 5 - .../src/newtab/HijackingLoginStrip.tsx | 208 ++---------------- 2 files changed, 23 insertions(+), 190 deletions(-) diff --git a/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx b/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx index 2ca71bbcb51..c87ff6c9e7f 100644 --- a/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx +++ b/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx @@ -21,11 +21,6 @@ jest.mock('@dailydotdev/shared/src/hooks/auth/useSignBack', () => ({ useSignBack: jest.fn(), })); -jest.mock('@dailydotdev/shared/src/graphql/common', () => ({ - ...jest.requireActual('@dailydotdev/shared/src/graphql/common'), - gqlClient: { request: jest.fn().mockResolvedValue({ page: { edges: [] } }) }, -})); - const LogContext = getLogContextStatic(); const mockUseAuthContext = useAuthContext as jest.MockedFunction< typeof useAuthContext diff --git a/packages/extension/src/newtab/HijackingLoginStrip.tsx b/packages/extension/src/newtab/HijackingLoginStrip.tsx index d1e035490f5..2b7225ea774 100644 --- a/packages/extension/src/newtab/HijackingLoginStrip.tsx +++ b/packages/extension/src/newtab/HijackingLoginStrip.tsx @@ -1,8 +1,6 @@ import type { ReactElement, ReactNode } from 'react'; import React, { useEffect, useRef } from 'react'; import classNames from 'classnames'; -import { useQuery } from '@tanstack/react-query'; -import { gql } from 'graphql-request'; import { Button, ButtonSize, @@ -23,8 +21,8 @@ import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext'; import { useSignBack } from '@dailydotdev/shared/src/hooks/auth/useSignBack'; import { AuthTriggers } from '@dailydotdev/shared/src/lib/auth'; import { onboardingUrl } from '@dailydotdev/shared/src/lib/constants'; +import { cloudinaryReadingReminderCat } from '@dailydotdev/shared/src/lib/image'; import { LogEvent, TargetType } from '@dailydotdev/shared/src/lib/log'; -import { gqlClient } from '@dailydotdev/shared/src/graphql/common'; import feedStyles from '@dailydotdev/shared/src/components/Feed.module.css'; import LogoIcon from '@dailydotdev/shared/src/svg/LogoIcon'; import LogoText from '@dailydotdev/shared/src/svg/LogoText'; @@ -37,56 +35,6 @@ const primaryCta = const glassCta = '!border-white/20 !bg-white/[0.06] !text-white backdrop-blur-sm transition-colors duration-200 hover:!bg-white/[0.12]'; -interface PeekPost { - id: string; - title: string; - numUpvotes?: number; - numComments?: number; - source?: { name?: string; image?: string }; -} - -const FEED_PEEK_QUERY = gql` - query HijackingFeedPeek($first: Int, $supportedTypes: [String!]) { - page: mostUpvotedFeed(first: $first, supportedTypes: $supportedTypes) { - edges { - node { - id - title - numUpvotes - numComments - source { - name - image - } - } - } - } - } -`; - -interface FeedPeekData { - page?: { edges?: { node: PeekPost }[] }; -} - -function useFeedPeek(enabled: boolean): PeekPost[] { - const { data } = useQuery({ - queryKey: ['hijacking', 'feed-peek'], - queryFn: () => - gqlClient.request(FEED_PEEK_QUERY, { - first: 18, - supportedTypes: ['article'], - }), - enabled, - staleTime: 60 * 60 * 1000, - gcTime: 60 * 60 * 1000, - retry: false, - }); - - return (data?.page?.edges ?? []) - .map((edge) => edge.node) - .filter((node): node is PeekPost => !!node?.title); -} - function BrandLockup(): ReactElement { return ( @@ -96,137 +44,31 @@ function BrandLockup(): ReactElement { ); } -function formatCompactCount(value: number): string { - if (value < 1000) { - return `${value}`; - } - - return `${Math.floor(value / 100) / 10}k`; -} - -function BriefingCard({ posts }: { posts: PeekPost[] }): ReactElement { - const discussions = posts.reduce( - (total, post) => total + (post.numComments ?? 0), - 0, - ); - const sources = Array.from( - new Set( - posts - .map((post) => post.source?.name) - .filter((name): name is string => !!name), - ), - ); - const topSource = sources[0]; - - if (!posts.length) { - return ( -
-
- - Your briefing is waiting -
-

- Sign in once and daily.dev can remember what you read, save, and care - about. -

-
- {['Personalized topics', 'Saved reads', 'Real reputation'].map( - (item) => ( -
- {item} -
- ), - )} -
-
- ); - } - - const topPost = posts[0]; - const sourceLabel = - sources.length > 1 ? `${sources.length} sources` : topSource; - +function CatHeroImage(): ReactElement { return ( -
-
-
- - Live briefing -
- - now - -
-

- {topPost.title} -

-
-
-

{posts.length}

-

fresh reads

-
-
-

- {formatCompactCount(discussions)} -

-

comments

-
-
- {!!sourceLabel && ( -

- Trending across {sourceLabel} -

- )} +
+
+ Sleeping cat on laptop
); } -function HeroBenefits({ posts }: { posts: PeekPost[] }): ReactElement { - const sources = Array.from( - new Set( - posts - .map((post) => post.source?.name) - .filter((name): name is string => !!name), - ), - ); - - if (!posts.length) { - return ( -
- - remembers your topics - - - keeps your saves - - - carries your reputation - -
- ); - } - - const comments = posts.reduce( - (total, post) => total + (post.numComments ?? 0), - 0, - ); - +function HeroBenefits(): ReactElement { return (
- {posts.length} fresh reads waiting + remembers your topics - - {formatCompactCount(comments)} comments + + keeps your saves + + + carries your reputation - {!!sources.length && ( - - across {sources.length} sources - - )}
); } @@ -256,8 +98,6 @@ export default function HijackingLoginStrip(): ReactElement { return hasContinueAs ? 'continue' : 'signin'; })(); - const feedPeek = useFeedPeek(variant === 'signin'); - const onboardingHref = (() => { const base = new URL(onboardingUrl); base.searchParams.append('r', 'extension'); @@ -407,11 +247,9 @@ export default function HijackingLoginStrip(): ReactElement {
-
+
-
- -
+

- -
+ +
- +

From 1417774184f5c4538cda7fcb11735512faa6de70 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 1 Jun 2026 23:59:33 +0300 Subject: [PATCH 11/17] feat(extension): add polished stage background to hijacking hero Give the logged-out hero a stronger daily.dev stage treatment with layered brand gradients, a brighter horizon glow, a larger cat visual, and a more prominent gradient CTA while keeping the feed grid removed. Co-authored-by: Cursor --- .../src/newtab/HijackingLoginStrip.tsx | 22 ++++++++----- packages/shared/src/styles/base.css | 31 +++++++++++++++++++ 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/packages/extension/src/newtab/HijackingLoginStrip.tsx b/packages/extension/src/newtab/HijackingLoginStrip.tsx index 2b7225ea774..827a6dae566 100644 --- a/packages/extension/src/newtab/HijackingLoginStrip.tsx +++ b/packages/extension/src/newtab/HijackingLoginStrip.tsx @@ -30,7 +30,7 @@ import LogoText from '@dailydotdev/shared/src/svg/LogoText'; type CoverVariant = 'continue' | 'signin' | 'onboarding'; const primaryCta = - 'shadow-2-cabbage transition-transform duration-200 ease-out hover:-translate-y-0.5'; + '!bg-gradient-to-r !from-accent-cabbage-default !to-accent-onion-default shadow-2-cabbage transition-transform duration-200 ease-out hover:-translate-y-0.5'; const glassCta = '!border-white/20 !bg-white/[0.06] !text-white backdrop-blur-sm transition-colors duration-200 hover:!bg-white/[0.12]'; @@ -46,8 +46,9 @@ function BrandLockup(): ReactElement { function CatHeroImage(): ReactElement { return ( -
-
+
+
+
Sleeping cat on laptop
-
+
+
-
-
+
+
+
+
+

+ Your developer home, remembered +

Make every new tab feel like yours.

+

Sign in to turn daily.dev into a calm briefing built around what you read, save, upvote, and discuss. diff --git a/packages/shared/src/styles/base.css b/packages/shared/src/styles/base.css index 1ab480d4ce5..37eb23d2944 100644 --- a/packages/shared/src/styles/base.css +++ b/packages/shared/src/styles/base.css @@ -1100,6 +1100,37 @@ meter::-webkit-meter-bar { ); } + .top-hero-stage { + background: + linear-gradient( + 118deg, + transparent 0%, + rgb(255 255 255 / 5%) 42%, + transparent 58% + ), + radial-gradient( + 78% 88% at 76% 112%, + color-mix(in srgb, var(--theme-accent-cabbage-default), transparent 6%) + 0%, + transparent 58% + ), + radial-gradient( + 70% 84% at 16% 112%, + color-mix(in srgb, var(--theme-accent-onion-default), transparent 20%) 0%, + transparent 62% + ), + radial-gradient( + 62% 72% at 92% 4%, + color-mix(in srgb, var(--theme-accent-bacon-default), transparent 54%) 0%, + transparent 58% + ), + radial-gradient( + 42% 48% at 26% 16%, + rgb(255 255 255 / 8%) 0%, + transparent 64% + ); + } + @keyframes enable-notification-bell-ring { 0%, 100% { From 22c4f3247aab03b1eed493443c29f05b1c555710 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 2 Jun 2026 00:02:06 +0300 Subject: [PATCH 12/17] feat(extension): sharpen hijacking hero headline Remove the cat shadow treatment and benefit chips, then update the logged-out hero headline and supporting copy to make the developer briefing value more explicit. Co-authored-by: Cursor --- .../src/newtab/HijackingLoginStrip.spec.tsx | 2 +- .../src/newtab/HijackingLoginStrip.tsx | 27 +++---------------- 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx b/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx index c87ff6c9e7f..afa4d831975 100644 --- a/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx +++ b/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx @@ -105,7 +105,7 @@ describe('HijackingLoginStrip', () => { expect( screen.getByRole('heading', { - name: 'Make every new tab feel like yours.', + name: 'Own your new tab. Make it your dev briefing.', }), ).toBeVisible(); diff --git a/packages/extension/src/newtab/HijackingLoginStrip.tsx b/packages/extension/src/newtab/HijackingLoginStrip.tsx index 827a6dae566..d11f2894083 100644 --- a/packages/extension/src/newtab/HijackingLoginStrip.tsx +++ b/packages/extension/src/newtab/HijackingLoginStrip.tsx @@ -47,33 +47,15 @@ function BrandLockup(): ReactElement { function CatHeroImage(): ReactElement { return (

-
-
Sleeping cat on laptop
); } -function HeroBenefits(): ReactElement { - return ( -
- - remembers your topics - - - keeps your saves - - - carries your reputation - -
- ); -} - export default function HijackingLoginStrip(): ReactElement { const { showLogin, user } = useAuthContext(); const { logEvent } = useLogContext(); @@ -263,14 +245,13 @@ export default function HijackingLoginStrip(): ReactElement { onboardingGradientClasses, )} > - Make every new tab feel like yours. + Own your new tab. Make it your dev briefing.

- Sign in to turn daily.dev into a calm briefing built around what - you read, save, upvote, and discuss. + Sign in and daily.dev will remember the topics, saves, upvotes, + and discussions that matter to you.

-
+ +
+ ); +} + +function CatStageHero({ + onSignupClick, + onLoginClick, +}: SigninHeroProps): ReactElement { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+ +

+ Own your new tab. Make it your dev briefing. +

+
+

+ Sign in and daily.dev will remember the topics, saves, upvotes, + and discussions that matter to you. +

+ +
+
+ +
+
+
+
+ ); +} + +function OnboardingSignupHero({ + onSignupClick, + onLoginClick, +}: SigninHeroProps): ReactElement { + return ( +
+
+ + + + + +
+
+
+
+
+
+
+ +

+ Where developers make every tab count. +

+

+ Sign in to turn daily.dev into your personalized feed, reputation, + saves, and community in every new tab. +

+
+
+

+ Set up your developer feed +

+

+ The same onboarding energy, compressed into one calm hero. +

+ +
+
+
+
+ ); +} + +const SigninHeroVariationMap = { + catStage: CatStageHero, + onboardingSignup: OnboardingSignupHero, +} satisfies Record< + SigninHeroVariation, + (props: SigninHeroProps) => ReactElement +>; + export default function HijackingLoginStrip(): ReactElement { const { showLogin, user } = useAuthContext(); const { logEvent } = useLogContext(); @@ -130,6 +276,7 @@ export default function HijackingLoginStrip(): ReactElement { options: { isLogin: true }, }); }; + const SigninHero = SigninHeroVariationMap[signinHeroVariation]; const chrome = (children: ReactNode): ReactElement => (
@@ -228,53 +375,6 @@ export default function HijackingLoginStrip(): ReactElement { } return ( -
-
-
-
-
-
-
-
-
-
-
-
- -

- Own your new tab. Make it your dev briefing. -

-
-

- Sign in and daily.dev will remember the topics, saves, upvotes, - and discussions that matter to you. -

-
- - -
-
-
- -
-
-
-
+ ); } From 38b6d751a063084808b6d28a8cbdfb02b1b45e5e Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 2 Jun 2026 01:00:18 +0300 Subject: [PATCH 16/17] feat(extension): center hijacking onboarding auth hero Co-authored-by: Cursor --- .../src/newtab/HijackingLoginStrip.spec.tsx | 73 ++++++++++++++++--- .../src/newtab/HijackingLoginStrip.tsx | 68 +++++++++++++---- 2 files changed, 119 insertions(+), 22 deletions(-) diff --git a/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx b/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx index f3d1b1880e6..0db8b2d6664 100644 --- a/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx +++ b/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx @@ -5,7 +5,10 @@ import type { AuthContextData } from '@dailydotdev/shared/src/contexts/AuthConte import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext'; import { getLogContextStatic } from '@dailydotdev/shared/src/contexts/LogContext'; import { useSignBack } from '@dailydotdev/shared/src/hooks/auth/useSignBack'; -import { SocialProvider } from '@dailydotdev/shared/src/components/auth/common'; +import { + AuthDisplay, + SocialProvider, +} from '@dailydotdev/shared/src/components/auth/common'; import { AuthTriggers } from '@dailydotdev/shared/src/lib/auth'; import { onboardingUrl } from '@dailydotdev/shared/src/lib/constants'; import { LogEvent, TargetType } from '@dailydotdev/shared/src/lib/log'; @@ -21,6 +24,45 @@ jest.mock('@dailydotdev/shared/src/hooks/auth/useSignBack', () => ({ useSignBack: jest.fn(), })); +jest.mock('@dailydotdev/shared/src/components/auth/AuthOptions', () => { + const { AuthDisplay: MockAuthDisplay } = jest.requireActual( + '@dailydotdev/shared/src/components/auth/common', + ); + + return { + __esModule: true, + default: ({ + onAuthStateUpdate, + }: { + onAuthStateUpdate?: (props: { defaultDisplay?: string }) => void; + }) => ( +
+ + + + +

By continuing, you agree to the Terms of Service

+
+ ), + }; +}); + const LogContext = getLogContextStatic(); const mockUseAuthContext = useAuthContext as jest.MockedFunction< typeof useAuthContext @@ -115,17 +157,30 @@ describe('HijackingLoginStrip', () => { }), ).toBeVisible(); - const signup = screen.getByRole('button', { name: 'Sign up' }); - fireEvent.click(signup); + expect( + screen.getByRole('button', { name: 'Continue with Google' }), + ).toBeVisible(); + expect( + screen.getByRole('button', { name: 'Continue with GitHub' }), + ).toBeVisible(); + expect( + screen.getByRole('button', { name: 'Continue with email' }), + ).toBeVisible(); + expect( + screen.getByText(/By continuing, you agree to the Terms of Service/), + ).toBeVisible(); + + fireEvent.click( + screen.getByRole('button', { name: 'Continue with email' }), + ); - expect(logEvent).toHaveBeenCalledWith({ - event_name: LogEvent.Click, - target_type: TargetType.SignupButton, - target_id: 'hijacking', - }); expect(showLogin).toHaveBeenCalledWith({ trigger: AuthTriggers.Onboarding, - options: { isLogin: false }, + options: { + isLogin: false, + defaultDisplay: AuthDisplay.Registration, + formValues: undefined, + }, }); }); diff --git a/packages/extension/src/newtab/HijackingLoginStrip.tsx b/packages/extension/src/newtab/HijackingLoginStrip.tsx index 07c54eb5c98..c1affcf5217 100644 --- a/packages/extension/src/newtab/HijackingLoginStrip.tsx +++ b/packages/extension/src/newtab/HijackingLoginStrip.tsx @@ -7,11 +7,14 @@ import { ButtonVariant, } from '@dailydotdev/shared/src/components/buttons/Button'; import { ClickableText } from '@dailydotdev/shared/src/components/buttons/ClickableText'; +import AuthOptions from '@dailydotdev/shared/src/components/auth/AuthOptions'; import { ProfileImageSize, ProfilePicture, } from '@dailydotdev/shared/src/components/ProfilePicture'; import { + AuthDisplay, + type AuthOptionsProps, providerMap, type SocialProvider, } from '@dailydotdev/shared/src/components/auth/common'; @@ -66,12 +69,19 @@ function CatHeroImage(): ReactElement { interface SigninHeroProps { onSignupClick: () => void; onLoginClick: () => void; + formRef: AuthOptionsProps['formRef']; + onAuthStateUpdate: AuthOptionsProps['onAuthStateUpdate']; } +type HeroActionButtonsProps = Pick< + SigninHeroProps, + 'onSignupClick' | 'onLoginClick' +>; + function HeroActionButtons({ onSignupClick, onLoginClick, -}: SigninHeroProps): ReactElement { +}: HeroActionButtonsProps): ReactElement { return (