From a5501977af5c8e1bc35bf65f1810a8d56e351287 Mon Sep 17 00:00:00 2001 From: Ido Shamun <1993245+idoshamun@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:17:58 +0200 Subject: [PATCH 01/22] fix(org): reduce organization settings query payload (#5538) --- .../components/InviteMemberModal.tsx | 4 +- .../src/features/organizations/graphql.ts | 77 ++++++++++++++----- .../organizations/hooks/useOrganization.ts | 18 ++++- .../hooks/useOrganizationSubscription.ts | 9 ++- .../organizations/hooks/useOrganizations.ts | 4 +- .../settings/organization/[orgId]/members.tsx | 8 +- 6 files changed, 93 insertions(+), 27 deletions(-) diff --git a/packages/shared/src/features/organizations/components/InviteMemberModal.tsx b/packages/shared/src/features/organizations/components/InviteMemberModal.tsx index 851dfe85c9..573ccdfde2 100644 --- a/packages/shared/src/features/organizations/components/InviteMemberModal.tsx +++ b/packages/shared/src/features/organizations/components/InviteMemberModal.tsx @@ -33,7 +33,9 @@ export const InviteMemberModal = ({ const [isCopying, copyLink] = useCopyLink(); const { openModal } = useLazyModal(); - const { organization, referralUrl, seats } = useOrganization(organizationId); + const { organization, referralUrl, seats } = useOrganization(organizationId, { + includeMembers: true, + }); const isMobile = useViewSize(ViewSize.MobileL); diff --git a/packages/shared/src/features/organizations/graphql.ts b/packages/shared/src/features/organizations/graphql.ts index 05bb1a3abe..8567a5ba54 100644 --- a/packages/shared/src/features/organizations/graphql.ts +++ b/packages/shared/src/features/organizations/graphql.ts @@ -25,31 +25,72 @@ export const ORGANIZATION_SHORT_FRAGMENT = gql` } `; -export const ORGANIZATION_FRAGMENT = gql` - fragment OrganizationFragment on Organization { +export const ORGANIZATION_BASE_FRAGMENT = gql` + fragment OrganizationBaseFragment on Organization { ...OrganizationShortFragment seats activeSeats status + } + + ${ORGANIZATION_SHORT_FRAGMENT} +`; +export const ORGANIZATION_FRAGMENT = gql` + fragment OrganizationFragment on Organization { + ...OrganizationBaseFragment members { ...OrganizationMemberFragment } } - ${ORGANIZATION_SHORT_FRAGMENT} + ${ORGANIZATION_BASE_FRAGMENT} ${ORGANIZATION_MEMBER_FRAGMENT} `; -export const USER_ORGANIZATION_FRAGMENT = gql` - fragment UserOrganizationFragment on UserOrganization { +export const USER_ORGANIZATION_BASE_FRAGMENT = gql` + fragment UserOrganizationBaseFragment on UserOrganization { role referralToken - referralUrl seatType } `; +export const USER_ORGANIZATION_FRAGMENT = gql` + fragment UserOrganizationFragment on UserOrganization { + ...UserOrganizationBaseFragment + referralUrl + } + + ${USER_ORGANIZATION_BASE_FRAGMENT} +`; + +export const ORGANIZATIONS_BASE_QUERY = gql` + query OrganizationsBase { + organizations { + ...UserOrganizationBaseFragment + organization { + ...OrganizationShortFragment + } + } + } + ${USER_ORGANIZATION_BASE_FRAGMENT} + ${ORGANIZATION_SHORT_FRAGMENT} +`; + +export const ORGANIZATION_BASE_QUERY = gql` + query OrganizationBase($id: ID!) { + organization(id: $id) { + ...UserOrganizationBaseFragment + organization { + ...OrganizationBaseFragment + } + } + } + ${USER_ORGANIZATION_BASE_FRAGMENT} + ${ORGANIZATION_BASE_FRAGMENT} +`; + export const ORGANIZATIONS_QUERY = gql` query Organizations { organizations { @@ -98,27 +139,27 @@ export const GET_ORGANIZATION_BY_ID_AND_INVITE_TOKEN_QUERY = gql` export const UPDATE_ORGANIZATION_MUTATION = gql` mutation UpdateOrganization($id: ID!, $name: String, $image: Upload) { updateOrganization(id: $id, name: $name, image: $image) { - ...UserOrganizationFragment + ...UserOrganizationBaseFragment organization { - ...OrganizationFragment + ...OrganizationBaseFragment } } } - ${USER_ORGANIZATION_FRAGMENT} - ${ORGANIZATION_FRAGMENT} + ${USER_ORGANIZATION_BASE_FRAGMENT} + ${ORGANIZATION_BASE_FRAGMENT} `; export const JOIN_ORGANIZATION_MUTATION = gql` mutation JoinOrganization($id: ID!, $token: String!) { joinOrganization(id: $id, token: $token) { - ...UserOrganizationFragment + ...UserOrganizationBaseFragment organization { - ...OrganizationFragment + ...OrganizationBaseFragment } } } - ${USER_ORGANIZATION_FRAGMENT} - ${ORGANIZATION_FRAGMENT} + ${USER_ORGANIZATION_BASE_FRAGMENT} + ${ORGANIZATION_BASE_FRAGMENT} `; export const LEAVE_ORGANIZATION_MUTATION = gql` @@ -132,14 +173,14 @@ export const LEAVE_ORGANIZATION_MUTATION = gql` export const UPDATE_ORGANIZATION_SUBSCRIPTION_MUTATION = gql` mutation UpdateOrganizationSubscription($id: ID!, $quantity: Int!) { updateOrganizationSubscription(id: $id, quantity: $quantity) { - ...UserOrganizationFragment + ...UserOrganizationBaseFragment organization { - ...OrganizationFragment + ...OrganizationBaseFragment } } } - ${USER_ORGANIZATION_FRAGMENT} - ${ORGANIZATION_FRAGMENT} + ${USER_ORGANIZATION_BASE_FRAGMENT} + ${ORGANIZATION_BASE_FRAGMENT} `; export const PREVIEW_SUBSCRIPTION_UPDATE_QUERY = gql` diff --git a/packages/shared/src/features/organizations/hooks/useOrganization.ts b/packages/shared/src/features/organizations/hooks/useOrganization.ts index 2afff04600..9240a82186 100644 --- a/packages/shared/src/features/organizations/hooks/useOrganization.ts +++ b/packages/shared/src/features/organizations/hooks/useOrganization.ts @@ -6,6 +6,7 @@ import { DEFAULT_ERROR, gqlClient } from '../../../graphql/common'; import { DELETE_ORGANIZATION_MUTATION, JOIN_ORGANIZATION_MUTATION, + ORGANIZATION_BASE_QUERY, LEAVE_ORGANIZATION_MUTATION, ORGANIZATION_QUERY, REMOVE_ORGANIZATION_MEMBER_MUTATION, @@ -125,13 +126,22 @@ export const joinOrganizationHandler = async ({ export const useOrganization = ( organizationId: string, - queryOptions?: Partial>, + options?: Partial> & { + includeMembers?: boolean; + }, ) => { const router = useRouter(); const { displayToast } = useToastNotification(); const { user, isAuthReady, refetchBoot } = useAuthContext(); + const { includeMembers = false, ...queryOptions } = options || {}; + const queryMode = includeMembers ? 'members' : 'base'; + const query = includeMembers ? ORGANIZATION_QUERY : ORGANIZATION_BASE_QUERY; const enableQuery = !!organizationId && !!user && isAuthReady; - const queryKey = generateOrganizationQueryKey(user, organizationId); + const queryKey = generateOrganizationQueryKey( + user, + organizationId, + queryMode, + ); const queryClient = useQueryClient(); const { data, isFetching } = useQuery({ @@ -139,7 +149,9 @@ export const useOrganization = ( queryFn: async () => { const res = await gqlClient.request<{ organization: UserOrganization; - }>(ORGANIZATION_QUERY, { id: organizationId }); + }>(query, { + id: organizationId, + }); return res?.organization || null; }, diff --git a/packages/shared/src/features/organizations/hooks/useOrganizationSubscription.ts b/packages/shared/src/features/organizations/hooks/useOrganizationSubscription.ts index 5e1308c065..dd2383707f 100644 --- a/packages/shared/src/features/organizations/hooks/useOrganizationSubscription.ts +++ b/packages/shared/src/features/organizations/hooks/useOrganizationSubscription.ts @@ -97,9 +97,16 @@ export const useOrganizationSubscription = ( }, onSuccess: async (res) => { await queryClient.setQueryData( - generateOrganizationQueryKey(user, organizationId), + generateOrganizationQueryKey(user, organizationId, 'base'), () => res, ); + await queryClient.invalidateQueries({ + queryKey: generateOrganizationQueryKey( + user, + organizationId, + 'members', + ), + }); router.push(getOrganizationSettingsUrl(organizationId, 'members')); displayToast('The organization has been updated'); diff --git a/packages/shared/src/features/organizations/hooks/useOrganizations.ts b/packages/shared/src/features/organizations/hooks/useOrganizations.ts index 65b9f8e2d2..97cc184e26 100644 --- a/packages/shared/src/features/organizations/hooks/useOrganizations.ts +++ b/packages/shared/src/features/organizations/hooks/useOrganizations.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { gqlClient } from '../../../graphql/common'; -import { ORGANIZATIONS_QUERY } from '../graphql'; +import { ORGANIZATIONS_BASE_QUERY } from '../graphql'; import type { UserOrganization } from '../types'; import { generateQueryKey, RequestKey, StaleTime } from '../../../lib/query'; import { useAuthContext } from '../../../contexts/AuthContext'; @@ -13,7 +13,7 @@ export const useOrganizations = () => { queryFn: async () => { const data = await gqlClient.request<{ organizations: UserOrganization[]; - }>(ORGANIZATIONS_QUERY); + }>(ORGANIZATIONS_BASE_QUERY); if (!data || !data.organizations) { return []; diff --git a/packages/webapp/pages/settings/organization/[orgId]/members.tsx b/packages/webapp/pages/settings/organization/[orgId]/members.tsx index ac353886b6..39d24f04f3 100644 --- a/packages/webapp/pages/settings/organization/[orgId]/members.tsx +++ b/packages/webapp/pages/settings/organization/[orgId]/members.tsx @@ -80,7 +80,9 @@ const OrganizationOptionsMenu = ({ removeOrganizationMember, updateOrganizationMemberRole, toggleOrganizationMemberSeat, - } = useOrganization(router.query.orgId as string); + } = useOrganization(router.query.orgId as string, { + includeMembers: true, + }); const { user, role, seatType } = member || {}; @@ -324,7 +326,9 @@ const Page = (): ReactElement => { isOwner, leaveOrganization, isLeavingOrganization, - } = useOrganization(query.orgId as string); + } = useOrganization(query.orgId as string, { + includeMembers: true, + }); const onLeaveClick = async () => { const options: PromptOptions = { From 903246d325e66c12ba6011994d5c48ae937050c3 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 24 Feb 2026 10:29:52 +0200 Subject: [PATCH 02/22] feat(onboarding): add redesigned onboarding page and remove header search field Introduce the onboarding redesign page and add a layout-level toggle to hide the header search field so onboarding can use a cleaner, focused header without CSS workarounds. Co-authored-by: Cursor --- packages/shared/src/components/MainLayout.tsx | 2 + .../components/layout/MainLayoutHeader.tsx | 5 +- packages/webapp/pages/onboarding-v2.tsx | 1414 +++++++++++++++++ 3 files changed, 1420 insertions(+), 1 deletion(-) create mode 100644 packages/webapp/pages/onboarding-v2.tsx diff --git a/packages/shared/src/components/MainLayout.tsx b/packages/shared/src/components/MainLayout.tsx index 20442a4e97..e73df9112b 100644 --- a/packages/shared/src/components/MainLayout.tsx +++ b/packages/shared/src/components/MainLayout.tsx @@ -71,6 +71,7 @@ function MainLayoutComponent({ isNavItemsButton, customBanner, additionalButtons, + hideSearchField, screenCentered = true, showSidebar = true, className, @@ -192,6 +193,7 @@ function MainLayoutComponent({ hasBanner={isBannerAvailable} sidebarRendered={sidebarRendered} additionalButtons={additionalButtons} + hideSearchField={hideSearchField} onLogoClick={onLogoClick} />
unknown; + hideSearchField?: boolean; } const SearchPanel = dynamic( @@ -39,6 +40,7 @@ function MainLayoutHeader({ sidebarRendered, additionalButtons, onLogoClick, + hideSearchField, }: MainLayoutHeaderProps): ReactElement { const { loadedSettings } = useSettingsContext(); const { streak, isStreaksEnabled } = useReadingStreak(); @@ -56,6 +58,7 @@ function MainLayoutHeader({ const RenderSearchPanel = useCallback( () => + !hideSearchField && loadedSettings && ( ), - [loadedSettings, isSearchPage, hasBanner], + [loadedSettings, isSearchPage, hasBanner, hideSearchField], ); if (loadedSettings && !isLaptop) { diff --git a/packages/webapp/pages/onboarding-v2.tsx b/packages/webapp/pages/onboarding-v2.tsx new file mode 100644 index 0000000000..0213e24c61 --- /dev/null +++ b/packages/webapp/pages/onboarding-v2.tsx @@ -0,0 +1,1414 @@ +import type { ReactElement, ReactNode } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import classNames from 'classnames'; +import type { NextSeoProps } from 'next-seo'; +import MainFeedLayout from '@dailydotdev/shared/src/components/MainFeedLayout'; +import MainLayout from '@dailydotdev/shared/src/components/MainLayout'; +import type { MainLayoutProps } from '@dailydotdev/shared/src/components/MainLayout'; +import { FeedLayoutProvider } from '@dailydotdev/shared/src/contexts/FeedContext'; +import { + ThemeMode, + useSettingsContext, +} from '@dailydotdev/shared/src/contexts/SettingsContext'; +import { getLayout as getFooterNavBarLayout } from '../components/layouts/FooterNavBarLayout'; +import { getTemplatedTitle } from '../components/layouts/utils'; +import { defaultOpenGraph, defaultSeo } from '../next-seo'; + +const seo: NextSeoProps = { + title: getTemplatedTitle('Onboarding V2'), + openGraph: { ...defaultOpenGraph }, + ...defaultSeo, + noindex: true, + nofollow: true, +}; + +const TOPICS_ROW_1 = [ + 'React', + 'TypeScript', + 'System Design', + 'AI & ML', + 'PostgreSQL', + 'Next.js', + 'Docker', + 'GraphQL', + 'Kubernetes', + 'Python', + 'Web Performance', + 'Microservices', + 'AWS', + 'Redis', + 'Svelte', + 'Deno', +]; + +const TOPICS_ROW_2 = [ + 'Open Source', + 'DevOps', + 'Rust', + 'Career Growth', + 'Node.js', + 'Go', + 'Cloud Native', + 'Testing', + 'Security', + 'CSS', + 'Linux', + 'API Design', + 'CI / CD', + 'Terraform', + 'MongoDB', + 'WebAssembly', +]; + +const MARQUEE_COPIES = [0, 1, 2]; + +const SELECTABLE_TOPICS = [ + { label: 'React', color: 'water' as const }, + { label: 'TypeScript', color: 'water' as const }, + { label: 'Python', color: 'onion' as const }, + { label: 'Node.js', color: 'avocado' as const }, + { label: 'Next.js', color: 'water' as const }, + { label: 'AI & ML', color: 'cheese' as const }, + { label: 'System Design', color: 'cabbage' as const }, + { label: 'Docker', color: 'onion' as const }, + { label: 'AWS', color: 'cheese' as const }, + { label: 'GraphQL', color: 'cabbage' as const }, + { label: 'Rust', color: 'ketchup' as const }, + { label: 'Go', color: 'water' as const }, + { label: 'DevOps', color: 'onion' as const }, + { label: 'Kubernetes', color: 'onion' as const }, + { label: 'PostgreSQL', color: 'water' as const }, + { label: 'Security', color: 'ketchup' as const }, + { label: 'Testing', color: 'avocado' as const }, + { label: 'CSS', color: 'bacon' as const }, + { label: 'Open Source', color: 'cabbage' as const }, + { label: 'API Design', color: 'cabbage' as const }, +]; + +const TOPIC_SELECTED_STYLES = + 'border-white/[0.12] bg-white/[0.10] text-text-primary'; + +const TopicPill = ({ label }: { label: string }): ReactElement => ( + + {label} + +); + +const OnboardingV2Page = (): ReactElement => { + const { + applyThemeMode, + loadedSettings, + sidebarExpanded, + toggleSidebarExpanded, + } = useSettingsContext(); + const [showSignupPrompt, setShowSignupPrompt] = useState(false); + const [mounted, setMounted] = useState(false); + const [feedVisible, setFeedVisible] = useState(false); + const [panelVisible, setPanelVisible] = useState(false); + const [panelStageProgress, setPanelStageProgress] = useState(0); + const [selectedTopics, setSelectedTopics] = useState>(new Set()); + const [aiPrompt, setAiPrompt] = useState(''); + const [signupContext, setSignupContext] = useState< + 'topics' | 'github' | 'ai' | 'manual' | null + >(null); + const didSetSidebarDefault = useRef(false); + const panelSentinelRef = useRef(null); + const panelStageRef = useRef(null); + const heroRef = useRef(null); + const panelBoxRef = useRef(null); + const scrollY = useRef(0); + + const toggleTopic = useCallback((topic: string) => { + setSelectedTopics((prev) => { + const next = new Set(prev); + if (next.has(topic)) { + next.delete(topic); + } else { + next.add(topic); + } + return next; + }); + }, []); + + const openSignup = useCallback( + (context: 'topics' | 'github' | 'ai' | 'manual') => { + setSignupContext(context); + setShowSignupPrompt(true); + }, + [], + ); + + useEffect(() => { + applyThemeMode(ThemeMode.Dark); + return () => { + applyThemeMode(); + }; + }, [applyThemeMode]); + + useEffect(() => { + if (!loadedSettings || didSetSidebarDefault.current) { + return; + } + didSetSidebarDefault.current = true; + if (!sidebarExpanded) { + void toggleSidebarExpanded(); + } + }, [loadedSettings, sidebarExpanded, toggleSidebarExpanded]); + + useEffect(() => { + const raf = requestAnimationFrame(() => setMounted(true)); + return () => cancelAnimationFrame(raf); + }, []); + + useEffect(() => { + if (!mounted) { + return undefined; + } + + // Keep intro order stable: hero settles before feed animates in. + const timer = window.setTimeout(() => { + setFeedVisible(true); + }, 700); + + return () => { + window.clearTimeout(timer); + }; + }, [mounted]); + + useEffect(() => { + const node = panelSentinelRef.current; + if (!node) { + return undefined; + } + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setPanelVisible(true); + } + }, + { rootMargin: '0px 0px 200px 0px', threshold: 0 }, + ); + observer.observe(node); + return () => observer.disconnect(); + }, []); + + useEffect(() => { + const stage = panelStageRef.current; + if (!stage) { + return undefined; + } + + const prefersReduced = window.matchMedia( + '(prefers-reduced-motion: reduce)', + ).matches; + if (prefersReduced) { + setPanelStageProgress(1); + return undefined; + } + + let frame = 0; + const update = () => { + frame = 0; + const rect = stage.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + const start = viewportHeight * 0.82; + const end = viewportHeight * 0.34; + const raw = (start - rect.top) / (start - end); + const clamped = Math.min(1, Math.max(0, raw)); + setPanelStageProgress((prev) => + Math.abs(prev - clamped) > 0.01 ? clamped : prev, + ); + }; + + const onScrollOrResize = () => { + if (frame) { + return; + } + frame = requestAnimationFrame(update); + }; + + update(); + window.addEventListener('scroll', onScrollOrResize, { passive: true }); + window.addEventListener('resize', onScrollOrResize); + + return () => { + if (frame) { + cancelAnimationFrame(frame); + } + window.removeEventListener('scroll', onScrollOrResize); + window.removeEventListener('resize', onScrollOrResize); + }; + }, []); + + // Parallax scroll: shift hero layers at different speeds + useEffect(() => { + const prefersReduced = window.matchMedia( + '(prefers-reduced-motion: reduce)', + ).matches; + if (prefersReduced) return undefined; + + let ticking = false; + const onScroll = () => { + scrollY.current = window.scrollY; + if (!ticking) { + ticking = true; + requestAnimationFrame(() => { + const hero = heroRef.current; + if (hero) { + const y = scrollY.current; + hero.style.setProperty('--scroll-y', `${y}`); + } + ticking = false; + }); + } + }; + window.addEventListener('scroll', onScroll, { passive: true }); + return () => window.removeEventListener('scroll', onScroll); + }, []); + + // Scroll-reveal: stagger feed articles as they enter viewport + useEffect(() => { + if (!feedVisible) return undefined; + + const prefersReduced = window.matchMedia( + '(prefers-reduced-motion: reduce)', + ).matches; + if (prefersReduced) return undefined; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + (entry.target as HTMLElement).classList.add('onb-revealed'); + observer.unobserve(entry.target); + } + }); + }, + { rootMargin: '0px 0px -40px 0px', threshold: 0.05 }, + ); + + const observeFeedArticles = () => { + document.querySelectorAll('article').forEach((el, i) => { + const article = el as HTMLElement; + + if (!article.dataset.onbRevealDelay) { + article.style.setProperty('--reveal-delay', `${Math.min(i * 60, 400)}ms`); + article.dataset.onbRevealDelay = 'true'; + } + + if (article.classList.contains('onb-revealed')) { + return; + } + + observer.observe(article); + }); + }; + + observeFeedArticles(); + + const mutationObserver = new MutationObserver(() => { + observeFeedArticles(); + }); + mutationObserver.observe(document.body, { + childList: true, + subtree: true, + }); + + return () => { + mutationObserver.disconnect(); + observer.disconnect(); + }; + }, [feedVisible]); + + // Cursor-tracking glow on personalization panel + useEffect(() => { + const box = panelBoxRef.current; + if (!box) return undefined; + + const onMove = (e: MouseEvent) => { + const rect = box.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + box.style.setProperty('--mouse-x', `${x}px`); + box.style.setProperty('--mouse-y', `${y}px`); + }; + box.addEventListener('mousemove', onMove); + return () => box.removeEventListener('mousemove', onMove); + }, []); + + const recommendedTopics = useMemo(() => { + if (!aiPrompt.trim()) { + return []; + } + + const lower = aiPrompt.toLowerCase(); + + return SELECTABLE_TOPICS.map((topic) => { + const labelLower = topic.label.toLowerCase(); + const keywords = labelLower.split(/[\s&/.+-]+/).filter(Boolean); + const hasDirect = lower.includes(labelLower); + const score = keywords.reduce((acc, kw) => { + if (kw.length < 3) return acc; + return lower.includes(kw) ? acc + kw.length : acc; + }, hasDirect ? 100 : 0); + + return { ...topic, score }; + }) + .filter((topic) => topic.score > 0 && !selectedTopics.has(topic.label)) + .sort((a, b) => b.score - a.score) + .slice(0, 6); + }, [aiPrompt, selectedTopics]); + + const panelLift = Math.round(panelStageProgress * 60); + const panelBackdropOpacity = Math.min(0.6, panelStageProgress * 0.75); + const panelShadowOpacity = 0.12 + panelStageProgress * 0.2; + const panelRevealOffset = panelVisible ? 40 : 120; + + return ( +
+
+ Explore +
+ {/* ── Hero ── */} +
+ {/* Dot grid — shifts subtly with scroll */} +
+ + {/* Floating particles */} +
+
+
+
+
+
+
+
+ + {/* Ambient glows */} +
+
+
+
+ + {/* Centered text content */} +
+ {/* Live indicator */} +
+ + + + + + 1M+ developers · live now + +
+ + {/* Headline */} +
+

+ + Join top dev community. + +
+ + Build your feed identity. + +

+
+ + {/* Subtext */} +
+

+ Tap into live signals from the global dev community, then + lock your feed to your stack with GitHub import or manual setup. +

+
+ + {/* Hero CTA group */} +
+
+
+ + +
+
+
+ + {/* ── Topic marquee with gradient backdrop ── */} +
+ + {/* ── Feed ── */} +
+ + {/* Scroll sentinel — triggers panel at ~50% of feed */} +
+ + {/* ── Personalization Panel ── */} +
+
+ {/* Local backdrop wash over feed cards */} +
+
+ +
+ {/* Panel ambient glow */} +
+ {/* Top edge glow line */} +
+
+ {/* Section title */} +
+

+ You just explored the global feed. +

+

+ Now build a feed that is truly yours +

+
+ + {/* Two-path layout */} +
+ {/* ── Path A: GitHub ── */} +
+ {/* Animated rings icon */} +
+
+
+
+
+ + + +
+ +

+ One-click setup +

+

+ We'll read your repos & stars to instantly + build a feed that matches your stack. +

+ +
+
+ +
+

+ Read-only access · No special permissions +

+
+ + {/* ── Path B: Manual ── */} +
+
+
+ + + +
+
+

+ Describe your stack +

+

+ Manual · pick your own topics +

+
+
+ + {/* Textarea */} +
+